@contractspec/module.ai-chat 4.0.2 → 4.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/README.md +130 -10
  2. package/dist/adapters/ai-sdk-bundle-adapter.d.ts +18 -0
  3. package/dist/adapters/index.d.ts +4 -0
  4. package/dist/browser/core/index.js +1138 -21
  5. package/dist/browser/index.js +2816 -651
  6. package/dist/browser/presentation/components/index.js +3143 -358
  7. package/dist/browser/presentation/hooks/index.js +961 -43
  8. package/dist/browser/presentation/index.js +2784 -666
  9. package/dist/core/agent-adapter.d.ts +53 -0
  10. package/dist/core/agent-tools-adapter.d.ts +12 -0
  11. package/dist/core/chat-service.d.ts +49 -1
  12. package/dist/core/contracts-context.d.ts +46 -0
  13. package/dist/core/contracts-context.test.d.ts +1 -0
  14. package/dist/core/conversation-store.d.ts +16 -2
  15. package/dist/core/create-chat-route.d.ts +3 -0
  16. package/dist/core/export-formatters.d.ts +29 -0
  17. package/dist/core/export-formatters.test.d.ts +1 -0
  18. package/dist/core/index.d.ts +8 -0
  19. package/dist/core/index.js +1138 -21
  20. package/dist/core/local-storage-conversation-store.d.ts +33 -0
  21. package/dist/core/message-types.d.ts +6 -0
  22. package/dist/core/surface-planner-tools.d.ts +23 -0
  23. package/dist/core/surface-planner-tools.test.d.ts +1 -0
  24. package/dist/core/thinking-levels.d.ts +38 -0
  25. package/dist/core/thinking-levels.test.d.ts +1 -0
  26. package/dist/core/workflow-tools.d.ts +18 -0
  27. package/dist/core/workflow-tools.test.d.ts +1 -0
  28. package/dist/index.d.ts +4 -2
  29. package/dist/index.js +2816 -651
  30. package/dist/node/core/index.js +1138 -21
  31. package/dist/node/index.js +2816 -651
  32. package/dist/node/presentation/components/index.js +3143 -358
  33. package/dist/node/presentation/hooks/index.js +961 -43
  34. package/dist/node/presentation/index.js +2787 -669
  35. package/dist/presentation/components/ChatContainer.d.ts +3 -1
  36. package/dist/presentation/components/ChatExportToolbar.d.ts +25 -0
  37. package/dist/presentation/components/ChatMessage.d.ts +16 -1
  38. package/dist/presentation/components/ChatSidebar.d.ts +26 -0
  39. package/dist/presentation/components/ChatWithExport.d.ts +34 -0
  40. package/dist/presentation/components/ChatWithSidebar.d.ts +19 -0
  41. package/dist/presentation/components/ThinkingLevelPicker.d.ts +16 -0
  42. package/dist/presentation/components/ToolResultRenderer.d.ts +33 -0
  43. package/dist/presentation/components/index.d.ts +6 -0
  44. package/dist/presentation/components/index.js +3143 -358
  45. package/dist/presentation/hooks/index.d.ts +2 -0
  46. package/dist/presentation/hooks/index.js +961 -43
  47. package/dist/presentation/hooks/useChat.d.ts +44 -2
  48. package/dist/presentation/hooks/useConversations.d.ts +18 -0
  49. package/dist/presentation/hooks/useMessageSelection.d.ts +13 -0
  50. package/dist/presentation/index.js +2787 -669
  51. package/package.json +14 -18
@@ -5,12 +5,13 @@ var __require = /* @__PURE__ */ createRequire(import.meta.url);
5
5
  import * as React from "react";
6
6
  import { ScrollArea } from "@contractspec/lib.ui-kit-web/ui/scroll-area";
7
7
  import { cn } from "@contractspec/lib.ui-kit-web/ui/utils";
8
- import { jsxDEV } from "react/jsx-dev-runtime";
8
+ import { jsx, jsxs } from "react/jsx-runtime";
9
9
  "use client";
10
10
  function ChatContainer({
11
11
  children,
12
12
  className,
13
- showScrollButton = true
13
+ showScrollButton = true,
14
+ headerContent
14
15
  }) {
15
16
  const scrollRef = React.useRef(null);
16
17
  const [showScrollDown, setShowScrollDown] = React.useState(false);
@@ -37,24 +38,28 @@ function ChatContainer({
37
38
  });
38
39
  }
39
40
  }, []);
40
- return /* @__PURE__ */ jsxDEV("div", {
41
+ return /* @__PURE__ */ jsxs("div", {
41
42
  className: cn("relative flex flex-1 flex-col", className),
42
43
  children: [
43
- /* @__PURE__ */ jsxDEV(ScrollArea, {
44
+ headerContent && /* @__PURE__ */ jsx("div", {
45
+ className: "border-border flex shrink-0 items-center justify-end gap-2 border-b px-4 py-2",
46
+ children: headerContent
47
+ }),
48
+ /* @__PURE__ */ jsx(ScrollArea, {
44
49
  ref: scrollRef,
45
50
  className: "flex-1",
46
51
  onScroll: handleScroll,
47
- children: /* @__PURE__ */ jsxDEV("div", {
52
+ children: /* @__PURE__ */ jsx("div", {
48
53
  className: "flex flex-col gap-4 p-4",
49
54
  children
50
- }, undefined, false, undefined, this)
51
- }, undefined, false, undefined, this),
52
- showScrollButton && showScrollDown && /* @__PURE__ */ jsxDEV("button", {
55
+ })
56
+ }),
57
+ showScrollButton && showScrollDown && /* @__PURE__ */ jsxs("button", {
53
58
  onClick: scrollToBottom,
54
59
  className: cn("absolute bottom-4 left-1/2 -translate-x-1/2", "bg-primary text-primary-foreground", "rounded-full px-3 py-1.5 text-sm font-medium shadow-lg", "hover:bg-primary/90 transition-colors", "flex items-center gap-1.5"),
55
60
  "aria-label": "Scroll to bottom",
56
61
  children: [
57
- /* @__PURE__ */ jsxDEV("svg", {
62
+ /* @__PURE__ */ jsx("svg", {
58
63
  xmlns: "http://www.w3.org/2000/svg",
59
64
  width: "16",
60
65
  height: "16",
@@ -64,15 +69,15 @@ function ChatContainer({
64
69
  strokeWidth: "2",
65
70
  strokeLinecap: "round",
66
71
  strokeLinejoin: "round",
67
- children: /* @__PURE__ */ jsxDEV("path", {
72
+ children: /* @__PURE__ */ jsx("path", {
68
73
  d: "m6 9 6 6 6-6"
69
- }, undefined, false, undefined, this)
70
- }, undefined, false, undefined, this),
74
+ })
75
+ }),
71
76
  "New messages"
72
77
  ]
73
- }, undefined, true, undefined, this)
78
+ })
74
79
  ]
75
- }, undefined, true, undefined, this);
80
+ });
76
81
  }
77
82
  // src/presentation/components/ChatMessage.tsx
78
83
  import * as React3 from "react";
@@ -86,16 +91,19 @@ import {
86
91
  Copy as Copy2,
87
92
  Check as Check2,
88
93
  ExternalLink,
89
- Wrench
94
+ Wrench,
95
+ Pencil,
96
+ X
90
97
  } from "lucide-react";
91
98
  import { Button as Button2 } from "@contractspec/lib.design-system";
99
+ import { Checkbox } from "@contractspec/lib.ui-kit-web/ui/checkbox";
92
100
 
93
101
  // src/presentation/components/CodePreview.tsx
94
102
  import * as React2 from "react";
95
103
  import { cn as cn2 } from "@contractspec/lib.ui-kit-web/ui/utils";
96
104
  import { Button } from "@contractspec/lib.design-system";
97
105
  import { Copy, Check, Play, Download } from "lucide-react";
98
- import { jsxDEV as jsxDEV2 } from "react/jsx-dev-runtime";
106
+ import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
99
107
  "use client";
100
108
  var LANGUAGE_NAMES = {
101
109
  ts: "TypeScript",
@@ -148,93 +156,147 @@ function CodePreview({
148
156
  document.body.removeChild(a);
149
157
  URL.revokeObjectURL(url);
150
158
  }, [code, filename, language]);
151
- return /* @__PURE__ */ jsxDEV2("div", {
159
+ return /* @__PURE__ */ jsxs2("div", {
152
160
  className: cn2("overflow-hidden rounded-lg border", "bg-muted/50", className),
153
161
  children: [
154
- /* @__PURE__ */ jsxDEV2("div", {
162
+ /* @__PURE__ */ jsxs2("div", {
155
163
  className: cn2("flex items-center justify-between px-3 py-1.5", "bg-muted/80 border-b"),
156
164
  children: [
157
- /* @__PURE__ */ jsxDEV2("div", {
165
+ /* @__PURE__ */ jsxs2("div", {
158
166
  className: "flex items-center gap-2 text-sm",
159
167
  children: [
160
- filename && /* @__PURE__ */ jsxDEV2("span", {
168
+ filename && /* @__PURE__ */ jsx2("span", {
161
169
  className: "text-foreground font-mono",
162
170
  children: filename
163
- }, undefined, false, undefined, this),
164
- /* @__PURE__ */ jsxDEV2("span", {
171
+ }),
172
+ /* @__PURE__ */ jsx2("span", {
165
173
  className: "text-muted-foreground",
166
174
  children: displayLanguage
167
- }, undefined, false, undefined, this)
175
+ })
168
176
  ]
169
- }, undefined, true, undefined, this),
170
- /* @__PURE__ */ jsxDEV2("div", {
177
+ }),
178
+ /* @__PURE__ */ jsxs2("div", {
171
179
  className: "flex items-center gap-1",
172
180
  children: [
173
- showExecute && onExecute && /* @__PURE__ */ jsxDEV2(Button, {
181
+ showExecute && onExecute && /* @__PURE__ */ jsx2(Button, {
174
182
  variant: "ghost",
175
183
  size: "sm",
176
184
  onPress: () => onExecute(code),
177
185
  className: "h-7 w-7 p-0",
178
186
  "aria-label": "Execute code",
179
- children: /* @__PURE__ */ jsxDEV2(Play, {
187
+ children: /* @__PURE__ */ jsx2(Play, {
180
188
  className: "h-3.5 w-3.5"
181
- }, undefined, false, undefined, this)
182
- }, undefined, false, undefined, this),
183
- showDownload && /* @__PURE__ */ jsxDEV2(Button, {
189
+ })
190
+ }),
191
+ showDownload && /* @__PURE__ */ jsx2(Button, {
184
192
  variant: "ghost",
185
193
  size: "sm",
186
194
  onPress: handleDownload,
187
195
  className: "h-7 w-7 p-0",
188
196
  "aria-label": "Download code",
189
- children: /* @__PURE__ */ jsxDEV2(Download, {
197
+ children: /* @__PURE__ */ jsx2(Download, {
190
198
  className: "h-3.5 w-3.5"
191
- }, undefined, false, undefined, this)
192
- }, undefined, false, undefined, this),
193
- showCopy && /* @__PURE__ */ jsxDEV2(Button, {
199
+ })
200
+ }),
201
+ showCopy && /* @__PURE__ */ jsx2(Button, {
194
202
  variant: "ghost",
195
203
  size: "sm",
196
204
  onPress: handleCopy,
197
205
  className: "h-7 w-7 p-0",
198
206
  "aria-label": copied ? "Copied" : "Copy code",
199
- children: copied ? /* @__PURE__ */ jsxDEV2(Check, {
207
+ children: copied ? /* @__PURE__ */ jsx2(Check, {
200
208
  className: "h-3.5 w-3.5 text-green-500"
201
- }, undefined, false, undefined, this) : /* @__PURE__ */ jsxDEV2(Copy, {
209
+ }) : /* @__PURE__ */ jsx2(Copy, {
202
210
  className: "h-3.5 w-3.5"
203
- }, undefined, false, undefined, this)
204
- }, undefined, false, undefined, this)
211
+ })
212
+ })
205
213
  ]
206
- }, undefined, true, undefined, this)
214
+ })
207
215
  ]
208
- }, undefined, true, undefined, this),
209
- /* @__PURE__ */ jsxDEV2("div", {
216
+ }),
217
+ /* @__PURE__ */ jsx2("div", {
210
218
  className: "overflow-auto",
211
219
  style: { maxHeight },
212
- children: /* @__PURE__ */ jsxDEV2("pre", {
220
+ children: /* @__PURE__ */ jsx2("pre", {
213
221
  className: "p-3",
214
- children: /* @__PURE__ */ jsxDEV2("code", {
222
+ children: /* @__PURE__ */ jsx2("code", {
215
223
  className: "text-sm",
216
- children: lines.map((line, i) => /* @__PURE__ */ jsxDEV2("div", {
224
+ children: lines.map((line, i) => /* @__PURE__ */ jsxs2("div", {
217
225
  className: "flex",
218
226
  children: [
219
- /* @__PURE__ */ jsxDEV2("span", {
227
+ /* @__PURE__ */ jsx2("span", {
220
228
  className: "text-muted-foreground mr-4 w-8 text-right select-none",
221
229
  children: i + 1
222
- }, undefined, false, undefined, this),
223
- /* @__PURE__ */ jsxDEV2("span", {
230
+ }),
231
+ /* @__PURE__ */ jsx2("span", {
224
232
  className: "flex-1",
225
233
  children: line || " "
226
- }, undefined, false, undefined, this)
234
+ })
227
235
  ]
228
- }, i, true, undefined, this))
229
- }, undefined, false, undefined, this)
230
- }, undefined, false, undefined, this)
231
- }, undefined, false, undefined, this)
236
+ }, i))
237
+ })
238
+ })
239
+ })
240
+ ]
241
+ });
242
+ }
243
+
244
+ // src/presentation/components/ToolResultRenderer.tsx
245
+ import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
246
+ "use client";
247
+ function isPresentationToolResult(result) {
248
+ return typeof result === "object" && result !== null && "presentationKey" in result && typeof result.presentationKey === "string";
249
+ }
250
+ function isFormToolResult(result) {
251
+ return typeof result === "object" && result !== null && "formKey" in result && typeof result.formKey === "string";
252
+ }
253
+ function ToolResultRenderer({
254
+ toolName,
255
+ result,
256
+ presentationRenderer,
257
+ formRenderer,
258
+ showRawFallback = true
259
+ }) {
260
+ if (result === undefined || result === null) {
261
+ return null;
262
+ }
263
+ if (isPresentationToolResult(result) && presentationRenderer) {
264
+ const rendered = presentationRenderer(result.presentationKey, result.data);
265
+ if (rendered != null) {
266
+ return /* @__PURE__ */ jsx3("div", {
267
+ className: "mt-2 rounded-md border border-border bg-background/50 p-3",
268
+ children: rendered
269
+ });
270
+ }
271
+ }
272
+ if (isFormToolResult(result) && formRenderer) {
273
+ const rendered = formRenderer(result.formKey, result.defaultValues);
274
+ if (rendered != null) {
275
+ return /* @__PURE__ */ jsx3("div", {
276
+ className: "mt-2 rounded-md border border-border bg-background/50 p-3",
277
+ children: rendered
278
+ });
279
+ }
280
+ }
281
+ if (!showRawFallback) {
282
+ return null;
283
+ }
284
+ return /* @__PURE__ */ jsxs3("div", {
285
+ children: [
286
+ /* @__PURE__ */ jsx3("span", {
287
+ className: "text-muted-foreground font-medium",
288
+ children: "Output:"
289
+ }),
290
+ /* @__PURE__ */ jsx3("pre", {
291
+ className: "bg-background mt-1 overflow-x-auto rounded p-2 text-xs",
292
+ children: typeof result === "object" ? JSON.stringify(result, null, 2) : String(result)
293
+ })
232
294
  ]
233
- }, undefined, true, undefined, this);
295
+ });
234
296
  }
235
297
 
236
298
  // src/presentation/components/ChatMessage.tsx
237
- import { jsxDEV as jsxDEV3, Fragment } from "react/jsx-dev-runtime";
299
+ import { jsx as jsx4, jsxs as jsxs4, Fragment } from "react/jsx-runtime";
238
300
  "use client";
239
301
  function extractCodeBlocks(content) {
240
302
  const codeBlockRegex = /```(\w+)?\n([\s\S]*?)```/g;
@@ -257,33 +319,33 @@ function renderInlineMarkdown(text) {
257
319
  let key = 0;
258
320
  while ((match = linkRegex.exec(text)) !== null) {
259
321
  if (match.index > lastIndex) {
260
- parts.push(/* @__PURE__ */ jsxDEV3("span", {
322
+ parts.push(/* @__PURE__ */ jsx4("span", {
261
323
  children: text.slice(lastIndex, match.index)
262
- }, key++, false, undefined, this));
324
+ }, key++));
263
325
  }
264
- parts.push(/* @__PURE__ */ jsxDEV3("a", {
326
+ parts.push(/* @__PURE__ */ jsx4("a", {
265
327
  href: match[2],
266
328
  target: "_blank",
267
329
  rel: "noopener noreferrer",
268
330
  className: "text-primary underline hover:no-underline",
269
331
  children: match[1]
270
- }, key++, false, undefined, this));
332
+ }, key++));
271
333
  lastIndex = match.index + match[0].length;
272
334
  }
273
335
  if (lastIndex < text.length) {
274
- parts.push(/* @__PURE__ */ jsxDEV3("span", {
336
+ parts.push(/* @__PURE__ */ jsx4("span", {
275
337
  children: text.slice(lastIndex)
276
- }, key++, false, undefined, this));
338
+ }, key++));
277
339
  }
278
340
  return parts.length > 0 ? parts : [text];
279
341
  }
280
342
  function MessageContent({ content }) {
281
343
  const codeBlocks = extractCodeBlocks(content);
282
344
  if (codeBlocks.length === 0) {
283
- return /* @__PURE__ */ jsxDEV3("p", {
345
+ return /* @__PURE__ */ jsx4("p", {
284
346
  className: "whitespace-pre-wrap",
285
347
  children: renderInlineMarkdown(content)
286
- }, undefined, false, undefined, this);
348
+ });
287
349
  }
288
350
  let remaining = content;
289
351
  const parts = [];
@@ -291,33 +353,40 @@ function MessageContent({ content }) {
291
353
  for (const block of codeBlocks) {
292
354
  const [before, after] = remaining.split(block.raw);
293
355
  if (before) {
294
- parts.push(/* @__PURE__ */ jsxDEV3("p", {
356
+ parts.push(/* @__PURE__ */ jsx4("p", {
295
357
  className: "whitespace-pre-wrap",
296
358
  children: renderInlineMarkdown(before.trim())
297
- }, key++, false, undefined, this));
359
+ }, key++));
298
360
  }
299
- parts.push(/* @__PURE__ */ jsxDEV3(CodePreview, {
361
+ parts.push(/* @__PURE__ */ jsx4(CodePreview, {
300
362
  code: block.code,
301
363
  language: block.language,
302
364
  className: "my-2"
303
- }, key++, false, undefined, this));
365
+ }, key++));
304
366
  remaining = after ?? "";
305
367
  }
306
368
  if (remaining.trim()) {
307
- parts.push(/* @__PURE__ */ jsxDEV3("p", {
369
+ parts.push(/* @__PURE__ */ jsx4("p", {
308
370
  className: "whitespace-pre-wrap",
309
371
  children: renderInlineMarkdown(remaining.trim())
310
- }, key++, false, undefined, this));
372
+ }, key++));
311
373
  }
312
- return /* @__PURE__ */ jsxDEV3(Fragment, {
374
+ return /* @__PURE__ */ jsx4(Fragment, {
313
375
  children: parts
314
- }, undefined, false, undefined, this);
376
+ });
315
377
  }
316
378
  function ChatMessage({
317
379
  message,
318
380
  className,
319
381
  showCopy = true,
320
- showAvatar = true
382
+ showAvatar = true,
383
+ selectable = false,
384
+ selected = false,
385
+ onSelect,
386
+ editable = false,
387
+ onEdit,
388
+ presentationRenderer,
389
+ formRenderer
321
390
  }) {
322
391
  const [copied, setCopied] = React3.useState(false);
323
392
  const isUser = message.role === "user";
@@ -328,185 +397,262 @@ function ChatMessage({
328
397
  setCopied(true);
329
398
  setTimeout(() => setCopied(false), 2000);
330
399
  }, [message.content]);
331
- return /* @__PURE__ */ jsxDEV3("div", {
400
+ const handleSelectChange = React3.useCallback((checked) => {
401
+ if (checked !== "indeterminate")
402
+ onSelect?.(message.id);
403
+ }, [message.id, onSelect]);
404
+ const [isEditing, setIsEditing] = React3.useState(false);
405
+ const [editContent, setEditContent] = React3.useState(message.content);
406
+ React3.useEffect(() => {
407
+ setEditContent(message.content);
408
+ }, [message.content]);
409
+ const handleStartEdit = React3.useCallback(() => {
410
+ setEditContent(message.content);
411
+ setIsEditing(true);
412
+ }, [message.content]);
413
+ const handleSaveEdit = React3.useCallback(async () => {
414
+ const trimmed = editContent.trim();
415
+ if (trimmed !== message.content) {
416
+ await onEdit?.(message.id, trimmed);
417
+ }
418
+ setIsEditing(false);
419
+ }, [editContent, message.id, message.content, onEdit]);
420
+ const handleCancelEdit = React3.useCallback(() => {
421
+ setEditContent(message.content);
422
+ setIsEditing(false);
423
+ }, [message.content]);
424
+ return /* @__PURE__ */ jsxs4("div", {
332
425
  className: cn3("group flex gap-3", isUser && "flex-row-reverse", className),
333
426
  children: [
334
- showAvatar && /* @__PURE__ */ jsxDEV3(Avatar, {
427
+ selectable && /* @__PURE__ */ jsx4("div", {
428
+ className: cn3("flex shrink-0 items-start pt-1", "opacity-0 transition-opacity group-hover:opacity-100"),
429
+ children: /* @__PURE__ */ jsx4(Checkbox, {
430
+ checked: selected,
431
+ onCheckedChange: handleSelectChange,
432
+ "aria-label": selected ? "Deselect message" : "Select message"
433
+ })
434
+ }),
435
+ showAvatar && /* @__PURE__ */ jsx4(Avatar, {
335
436
  className: "h-8 w-8 shrink-0",
336
- children: /* @__PURE__ */ jsxDEV3(AvatarFallback, {
437
+ children: /* @__PURE__ */ jsx4(AvatarFallback, {
337
438
  className: cn3(isUser ? "bg-primary text-primary-foreground" : "bg-muted"),
338
- children: isUser ? /* @__PURE__ */ jsxDEV3(User, {
439
+ children: isUser ? /* @__PURE__ */ jsx4(User, {
339
440
  className: "h-4 w-4"
340
- }, undefined, false, undefined, this) : /* @__PURE__ */ jsxDEV3(Bot, {
441
+ }) : /* @__PURE__ */ jsx4(Bot, {
341
442
  className: "h-4 w-4"
342
- }, undefined, false, undefined, this)
343
- }, undefined, false, undefined, this)
344
- }, undefined, false, undefined, this),
345
- /* @__PURE__ */ jsxDEV3("div", {
443
+ })
444
+ })
445
+ }),
446
+ /* @__PURE__ */ jsxs4("div", {
346
447
  className: cn3("flex max-w-[80%] flex-col gap-1", isUser && "items-end"),
347
448
  children: [
348
- /* @__PURE__ */ jsxDEV3("div", {
449
+ /* @__PURE__ */ jsx4("div", {
349
450
  className: cn3("rounded-2xl px-4 py-2", isUser ? "bg-primary text-primary-foreground" : "bg-muted text-foreground", isError && "border-destructive bg-destructive/10 border"),
350
- children: isError && message.error ? /* @__PURE__ */ jsxDEV3("div", {
451
+ children: isError && message.error ? /* @__PURE__ */ jsxs4("div", {
351
452
  className: "flex items-start gap-2",
352
453
  children: [
353
- /* @__PURE__ */ jsxDEV3(AlertCircle, {
454
+ /* @__PURE__ */ jsx4(AlertCircle, {
354
455
  className: "text-destructive mt-0.5 h-4 w-4 shrink-0"
355
- }, undefined, false, undefined, this),
356
- /* @__PURE__ */ jsxDEV3("div", {
456
+ }),
457
+ /* @__PURE__ */ jsxs4("div", {
357
458
  children: [
358
- /* @__PURE__ */ jsxDEV3("p", {
459
+ /* @__PURE__ */ jsx4("p", {
359
460
  className: "text-destructive font-medium",
360
461
  children: message.error.code
361
- }, undefined, false, undefined, this),
362
- /* @__PURE__ */ jsxDEV3("p", {
462
+ }),
463
+ /* @__PURE__ */ jsx4("p", {
363
464
  className: "text-muted-foreground text-sm",
364
465
  children: message.error.message
365
- }, undefined, false, undefined, this)
466
+ })
467
+ ]
468
+ })
469
+ ]
470
+ }) : isEditing ? /* @__PURE__ */ jsxs4("div", {
471
+ className: "flex flex-col gap-2",
472
+ children: [
473
+ /* @__PURE__ */ jsx4("textarea", {
474
+ value: editContent,
475
+ onChange: (e) => setEditContent(e.target.value),
476
+ className: "bg-background/50 min-h-[80px] w-full resize-y rounded-md border px-3 py-2 text-sm",
477
+ rows: 4,
478
+ autoFocus: true
479
+ }),
480
+ /* @__PURE__ */ jsxs4("div", {
481
+ className: "flex gap-2",
482
+ children: [
483
+ /* @__PURE__ */ jsxs4(Button2, {
484
+ variant: "default",
485
+ size: "sm",
486
+ onPress: handleSaveEdit,
487
+ "aria-label": "Save edit",
488
+ children: [
489
+ /* @__PURE__ */ jsx4(Check2, {
490
+ className: "h-3 w-3"
491
+ }),
492
+ "Save"
493
+ ]
494
+ }),
495
+ /* @__PURE__ */ jsxs4(Button2, {
496
+ variant: "ghost",
497
+ size: "sm",
498
+ onPress: handleCancelEdit,
499
+ "aria-label": "Cancel edit",
500
+ children: [
501
+ /* @__PURE__ */ jsx4(X, {
502
+ className: "h-3 w-3"
503
+ }),
504
+ "Cancel"
505
+ ]
506
+ })
366
507
  ]
367
- }, undefined, true, undefined, this)
508
+ })
368
509
  ]
369
- }, undefined, true, undefined, this) : isStreaming && !message.content ? /* @__PURE__ */ jsxDEV3("div", {
510
+ }) : isStreaming && !message.content ? /* @__PURE__ */ jsxs4("div", {
370
511
  className: "flex flex-col gap-2",
371
512
  children: [
372
- /* @__PURE__ */ jsxDEV3(Skeleton, {
513
+ /* @__PURE__ */ jsx4(Skeleton, {
373
514
  className: "h-4 w-48"
374
- }, undefined, false, undefined, this),
375
- /* @__PURE__ */ jsxDEV3(Skeleton, {
515
+ }),
516
+ /* @__PURE__ */ jsx4(Skeleton, {
376
517
  className: "h-4 w-32"
377
- }, undefined, false, undefined, this)
518
+ })
378
519
  ]
379
- }, undefined, true, undefined, this) : /* @__PURE__ */ jsxDEV3(MessageContent, {
520
+ }) : /* @__PURE__ */ jsx4(MessageContent, {
380
521
  content: message.content
381
- }, undefined, false, undefined, this)
382
- }, undefined, false, undefined, this),
383
- /* @__PURE__ */ jsxDEV3("div", {
522
+ })
523
+ }),
524
+ /* @__PURE__ */ jsxs4("div", {
384
525
  className: cn3("flex items-center gap-2 text-xs", "text-muted-foreground opacity-0 transition-opacity", "group-hover:opacity-100"),
385
526
  children: [
386
- /* @__PURE__ */ jsxDEV3("span", {
527
+ /* @__PURE__ */ jsx4("span", {
387
528
  children: new Date(message.createdAt).toLocaleTimeString([], {
388
529
  hour: "2-digit",
389
530
  minute: "2-digit"
390
531
  })
391
- }, undefined, false, undefined, this),
392
- message.usage && /* @__PURE__ */ jsxDEV3("span", {
532
+ }),
533
+ message.usage && /* @__PURE__ */ jsxs4("span", {
393
534
  children: [
394
535
  message.usage.inputTokens + message.usage.outputTokens,
395
536
  " tokens"
396
537
  ]
397
- }, undefined, true, undefined, this),
398
- showCopy && !isUser && message.content && /* @__PURE__ */ jsxDEV3(Button2, {
538
+ }),
539
+ showCopy && !isUser && message.content && /* @__PURE__ */ jsx4(Button2, {
399
540
  variant: "ghost",
400
541
  size: "sm",
401
542
  className: "h-6 w-6 p-0",
402
543
  onPress: handleCopy,
403
544
  "aria-label": copied ? "Copied" : "Copy message",
404
- children: copied ? /* @__PURE__ */ jsxDEV3(Check2, {
545
+ children: copied ? /* @__PURE__ */ jsx4(Check2, {
546
+ className: "h-3 w-3"
547
+ }) : /* @__PURE__ */ jsx4(Copy2, {
405
548
  className: "h-3 w-3"
406
- }, undefined, false, undefined, this) : /* @__PURE__ */ jsxDEV3(Copy2, {
549
+ })
550
+ }),
551
+ editable && isUser && !isEditing && /* @__PURE__ */ jsx4(Button2, {
552
+ variant: "ghost",
553
+ size: "sm",
554
+ className: "h-6 w-6 p-0",
555
+ onPress: handleStartEdit,
556
+ "aria-label": "Edit message",
557
+ children: /* @__PURE__ */ jsx4(Pencil, {
407
558
  className: "h-3 w-3"
408
- }, undefined, false, undefined, this)
409
- }, undefined, false, undefined, this)
559
+ })
560
+ })
410
561
  ]
411
- }, undefined, true, undefined, this),
412
- message.reasoning && /* @__PURE__ */ jsxDEV3("details", {
562
+ }),
563
+ message.reasoning && /* @__PURE__ */ jsxs4("details", {
413
564
  className: "text-muted-foreground mt-2 text-sm",
414
565
  children: [
415
- /* @__PURE__ */ jsxDEV3("summary", {
566
+ /* @__PURE__ */ jsx4("summary", {
416
567
  className: "cursor-pointer hover:underline",
417
568
  children: "View reasoning"
418
- }, undefined, false, undefined, this),
419
- /* @__PURE__ */ jsxDEV3("div", {
569
+ }),
570
+ /* @__PURE__ */ jsx4("div", {
420
571
  className: "bg-muted mt-1 rounded-md p-2",
421
- children: /* @__PURE__ */ jsxDEV3("p", {
572
+ children: /* @__PURE__ */ jsx4("p", {
422
573
  className: "whitespace-pre-wrap",
423
574
  children: message.reasoning
424
- }, undefined, false, undefined, this)
425
- }, undefined, false, undefined, this)
575
+ })
576
+ })
426
577
  ]
427
- }, undefined, true, undefined, this),
428
- message.sources && message.sources.length > 0 && /* @__PURE__ */ jsxDEV3("div", {
578
+ }),
579
+ message.sources && message.sources.length > 0 && /* @__PURE__ */ jsx4("div", {
429
580
  className: "mt-2 flex flex-wrap gap-2",
430
- children: message.sources.map((source) => /* @__PURE__ */ jsxDEV3("a", {
581
+ children: message.sources.map((source) => /* @__PURE__ */ jsxs4("a", {
431
582
  href: source.url ?? "#",
432
583
  target: "_blank",
433
584
  rel: "noopener noreferrer",
434
585
  className: "text-muted-foreground hover:text-foreground bg-muted inline-flex items-center gap-1 rounded-md px-2 py-1 text-xs transition-colors",
435
586
  children: [
436
- /* @__PURE__ */ jsxDEV3(ExternalLink, {
587
+ /* @__PURE__ */ jsx4(ExternalLink, {
437
588
  className: "h-3 w-3"
438
- }, undefined, false, undefined, this),
589
+ }),
439
590
  source.title || source.url || source.id
440
591
  ]
441
- }, source.id, true, undefined, this))
442
- }, undefined, false, undefined, this),
443
- message.toolCalls && message.toolCalls.length > 0 && /* @__PURE__ */ jsxDEV3("div", {
592
+ }, source.id))
593
+ }),
594
+ message.toolCalls && message.toolCalls.length > 0 && /* @__PURE__ */ jsx4("div", {
444
595
  className: "mt-2 space-y-2",
445
- children: message.toolCalls.map((tc) => /* @__PURE__ */ jsxDEV3("details", {
596
+ children: message.toolCalls.map((tc) => /* @__PURE__ */ jsxs4("details", {
446
597
  className: "bg-muted border-border rounded-md border",
447
598
  children: [
448
- /* @__PURE__ */ jsxDEV3("summary", {
599
+ /* @__PURE__ */ jsxs4("summary", {
449
600
  className: "flex cursor-pointer items-center gap-2 px-3 py-2 text-sm font-medium",
450
601
  children: [
451
- /* @__PURE__ */ jsxDEV3(Wrench, {
602
+ /* @__PURE__ */ jsx4(Wrench, {
452
603
  className: "text-muted-foreground h-4 w-4"
453
- }, undefined, false, undefined, this),
604
+ }),
454
605
  tc.name,
455
- /* @__PURE__ */ jsxDEV3("span", {
606
+ /* @__PURE__ */ jsx4("span", {
456
607
  className: cn3("ml-auto rounded px-1.5 py-0.5 text-xs", tc.status === "completed" && "bg-green-500/20 text-green-700 dark:text-green-400", tc.status === "error" && "bg-destructive/20 text-destructive", tc.status === "running" && "bg-blue-500/20 text-blue-700 dark:text-blue-400"),
457
608
  children: tc.status
458
- }, undefined, false, undefined, this)
609
+ })
459
610
  ]
460
- }, undefined, true, undefined, this),
461
- /* @__PURE__ */ jsxDEV3("div", {
611
+ }),
612
+ /* @__PURE__ */ jsxs4("div", {
462
613
  className: "border-border border-t px-3 py-2 text-xs",
463
614
  children: [
464
- Object.keys(tc.args).length > 0 && /* @__PURE__ */ jsxDEV3("div", {
615
+ Object.keys(tc.args).length > 0 && /* @__PURE__ */ jsxs4("div", {
465
616
  className: "mb-2",
466
617
  children: [
467
- /* @__PURE__ */ jsxDEV3("span", {
618
+ /* @__PURE__ */ jsx4("span", {
468
619
  className: "text-muted-foreground font-medium",
469
620
  children: "Input:"
470
- }, undefined, false, undefined, this),
471
- /* @__PURE__ */ jsxDEV3("pre", {
621
+ }),
622
+ /* @__PURE__ */ jsx4("pre", {
472
623
  className: "bg-background mt-1 overflow-x-auto rounded p-2",
473
624
  children: JSON.stringify(tc.args, null, 2)
474
- }, undefined, false, undefined, this)
475
- ]
476
- }, undefined, true, undefined, this),
477
- tc.result !== undefined && /* @__PURE__ */ jsxDEV3("div", {
478
- children: [
479
- /* @__PURE__ */ jsxDEV3("span", {
480
- className: "text-muted-foreground font-medium",
481
- children: "Output:"
482
- }, undefined, false, undefined, this),
483
- /* @__PURE__ */ jsxDEV3("pre", {
484
- className: "bg-background mt-1 overflow-x-auto rounded p-2",
485
- children: typeof tc.result === "object" ? JSON.stringify(tc.result, null, 2) : String(tc.result)
486
- }, undefined, false, undefined, this)
625
+ })
487
626
  ]
488
- }, undefined, true, undefined, this),
489
- tc.error && /* @__PURE__ */ jsxDEV3("p", {
627
+ }),
628
+ tc.result !== undefined && /* @__PURE__ */ jsx4(ToolResultRenderer, {
629
+ toolName: tc.name,
630
+ result: tc.result,
631
+ presentationRenderer,
632
+ formRenderer,
633
+ showRawFallback: true
634
+ }),
635
+ tc.error && /* @__PURE__ */ jsx4("p", {
490
636
  className: "text-destructive mt-1",
491
637
  children: tc.error
492
- }, undefined, false, undefined, this)
638
+ })
493
639
  ]
494
- }, undefined, true, undefined, this)
640
+ })
495
641
  ]
496
- }, tc.id, true, undefined, this))
497
- }, undefined, false, undefined, this)
642
+ }, tc.id))
643
+ })
498
644
  ]
499
- }, undefined, true, undefined, this)
645
+ })
500
646
  ]
501
- }, undefined, true, undefined, this);
647
+ });
502
648
  }
503
649
  // src/presentation/components/ChatInput.tsx
504
650
  import * as React4 from "react";
505
651
  import { cn as cn4 } from "@contractspec/lib.ui-kit-web/ui/utils";
506
652
  import { Textarea } from "@contractspec/lib.design-system";
507
653
  import { Button as Button3 } from "@contractspec/lib.design-system";
508
- import { Send, Paperclip, X, Loader2, FileText, Code } from "lucide-react";
509
- import { jsxDEV as jsxDEV4, Fragment as Fragment2 } from "react/jsx-dev-runtime";
654
+ import { Send, Paperclip, X as X2, Loader2, FileText, Code } from "lucide-react";
655
+ import { jsx as jsx5, jsxs as jsxs5, Fragment as Fragment2 } from "react/jsx-runtime";
510
656
  "use client";
511
657
  function ChatInput({
512
658
  onSend,
@@ -572,42 +718,42 @@ function ChatInput({
572
718
  const removeAttachment = React4.useCallback((id) => {
573
719
  setAttachments((prev) => prev.filter((a) => a.id !== id));
574
720
  }, []);
575
- return /* @__PURE__ */ jsxDEV4("div", {
721
+ return /* @__PURE__ */ jsxs5("div", {
576
722
  className: cn4("flex flex-col gap-2", className),
577
723
  children: [
578
- attachments.length > 0 && /* @__PURE__ */ jsxDEV4("div", {
724
+ attachments.length > 0 && /* @__PURE__ */ jsx5("div", {
579
725
  className: "flex flex-wrap gap-2",
580
- children: attachments.map((attachment) => /* @__PURE__ */ jsxDEV4("div", {
726
+ children: attachments.map((attachment) => /* @__PURE__ */ jsxs5("div", {
581
727
  className: cn4("flex items-center gap-1.5 rounded-md px-2 py-1", "bg-muted text-muted-foreground text-sm"),
582
728
  children: [
583
- attachment.type === "code" ? /* @__PURE__ */ jsxDEV4(Code, {
729
+ attachment.type === "code" ? /* @__PURE__ */ jsx5(Code, {
584
730
  className: "h-3.5 w-3.5"
585
- }, undefined, false, undefined, this) : /* @__PURE__ */ jsxDEV4(FileText, {
731
+ }) : /* @__PURE__ */ jsx5(FileText, {
586
732
  className: "h-3.5 w-3.5"
587
- }, undefined, false, undefined, this),
588
- /* @__PURE__ */ jsxDEV4("span", {
733
+ }),
734
+ /* @__PURE__ */ jsx5("span", {
589
735
  className: "max-w-[150px] truncate",
590
736
  children: attachment.name
591
- }, undefined, false, undefined, this),
592
- /* @__PURE__ */ jsxDEV4("button", {
737
+ }),
738
+ /* @__PURE__ */ jsx5("button", {
593
739
  type: "button",
594
740
  onClick: () => removeAttachment(attachment.id),
595
741
  className: "hover:text-foreground",
596
742
  "aria-label": `Remove ${attachment.name}`,
597
- children: /* @__PURE__ */ jsxDEV4(X, {
743
+ children: /* @__PURE__ */ jsx5(X2, {
598
744
  className: "h-3.5 w-3.5"
599
- }, undefined, false, undefined, this)
600
- }, undefined, false, undefined, this)
745
+ })
746
+ })
601
747
  ]
602
- }, attachment.id, true, undefined, this))
603
- }, undefined, false, undefined, this),
604
- /* @__PURE__ */ jsxDEV4("form", {
748
+ }, attachment.id))
749
+ }),
750
+ /* @__PURE__ */ jsxs5("form", {
605
751
  onSubmit: handleSubmit,
606
752
  className: "flex items-end gap-2",
607
753
  children: [
608
- showAttachments && /* @__PURE__ */ jsxDEV4(Fragment2, {
754
+ showAttachments && /* @__PURE__ */ jsxs5(Fragment2, {
609
755
  children: [
610
- /* @__PURE__ */ jsxDEV4("input", {
756
+ /* @__PURE__ */ jsx5("input", {
611
757
  ref: fileInputRef,
612
758
  type: "file",
613
759
  multiple: true,
@@ -615,23 +761,23 @@ function ChatInput({
615
761
  onChange: handleFileSelect,
616
762
  className: "hidden",
617
763
  "aria-label": "Attach files"
618
- }, undefined, false, undefined, this),
619
- /* @__PURE__ */ jsxDEV4(Button3, {
764
+ }),
765
+ /* @__PURE__ */ jsx5(Button3, {
620
766
  type: "button",
621
767
  variant: "ghost",
622
768
  size: "sm",
623
769
  onPress: () => fileInputRef.current?.click(),
624
770
  disabled: disabled || attachments.length >= maxAttachments,
625
771
  "aria-label": "Attach files",
626
- children: /* @__PURE__ */ jsxDEV4(Paperclip, {
772
+ children: /* @__PURE__ */ jsx5(Paperclip, {
627
773
  className: "h-4 w-4"
628
- }, undefined, false, undefined, this)
629
- }, undefined, false, undefined, this)
774
+ })
775
+ })
630
776
  ]
631
- }, undefined, true, undefined, this),
632
- /* @__PURE__ */ jsxDEV4("div", {
777
+ }),
778
+ /* @__PURE__ */ jsx5("div", {
633
779
  className: "relative flex-1",
634
- children: /* @__PURE__ */ jsxDEV4(Textarea, {
780
+ children: /* @__PURE__ */ jsx5(Textarea, {
635
781
  value: content,
636
782
  onChange: (e) => setContent(e.target.value),
637
783
  onKeyDown: handleKeyDown,
@@ -640,32 +786,418 @@ function ChatInput({
640
786
  className: cn4("max-h-[200px] min-h-[44px] resize-none pr-12", "focus-visible:ring-1"),
641
787
  rows: 1,
642
788
  "aria-label": "Chat message"
643
- }, undefined, false, undefined, this)
644
- }, undefined, false, undefined, this),
645
- /* @__PURE__ */ jsxDEV4(Button3, {
789
+ })
790
+ }),
791
+ /* @__PURE__ */ jsx5(Button3, {
646
792
  type: "submit",
647
793
  disabled: !canSend || disabled || isLoading,
648
794
  size: "sm",
649
795
  "aria-label": isLoading ? "Sending..." : "Send message",
650
- children: isLoading ? /* @__PURE__ */ jsxDEV4(Loader2, {
796
+ children: isLoading ? /* @__PURE__ */ jsx5(Loader2, {
651
797
  className: "h-4 w-4 animate-spin"
652
- }, undefined, false, undefined, this) : /* @__PURE__ */ jsxDEV4(Send, {
798
+ }) : /* @__PURE__ */ jsx5(Send, {
653
799
  className: "h-4 w-4"
654
- }, undefined, false, undefined, this)
655
- }, undefined, false, undefined, this)
800
+ })
801
+ })
656
802
  ]
657
- }, undefined, true, undefined, this),
658
- /* @__PURE__ */ jsxDEV4("p", {
803
+ }),
804
+ /* @__PURE__ */ jsx5("p", {
659
805
  className: "text-muted-foreground text-xs",
660
806
  children: "Press Enter to send, Shift+Enter for new line"
661
- }, undefined, false, undefined, this)
807
+ })
662
808
  ]
663
- }, undefined, true, undefined, this);
809
+ });
664
810
  }
665
- // src/presentation/components/ModelPicker.tsx
811
+ // src/presentation/components/ChatExportToolbar.tsx
666
812
  import * as React5 from "react";
667
- import { cn as cn5 } from "@contractspec/lib.ui-kit-web/ui/utils";
813
+ import { Download as Download2, FileText as FileText2, Copy as Copy3, Check as Check3, Plus, GitFork } from "lucide-react";
668
814
  import { Button as Button4 } from "@contractspec/lib.design-system";
815
+ import {
816
+ DropdownMenu,
817
+ DropdownMenuContent,
818
+ DropdownMenuItem,
819
+ DropdownMenuSeparator,
820
+ DropdownMenuTrigger
821
+ } from "@contractspec/lib.ui-kit-web/ui/dropdown-menu";
822
+
823
+ // src/core/export-formatters.ts
824
+ function formatTimestamp(date) {
825
+ return date.toLocaleTimeString([], {
826
+ hour: "2-digit",
827
+ minute: "2-digit"
828
+ });
829
+ }
830
+ function toIsoString(date) {
831
+ return date.toISOString();
832
+ }
833
+ function messageToJsonSerializable(msg) {
834
+ return {
835
+ id: msg.id,
836
+ conversationId: msg.conversationId,
837
+ role: msg.role,
838
+ content: msg.content,
839
+ status: msg.status,
840
+ createdAt: toIsoString(msg.createdAt),
841
+ updatedAt: toIsoString(msg.updatedAt),
842
+ ...msg.attachments && { attachments: msg.attachments },
843
+ ...msg.codeBlocks && { codeBlocks: msg.codeBlocks },
844
+ ...msg.toolCalls && { toolCalls: msg.toolCalls },
845
+ ...msg.sources && { sources: msg.sources },
846
+ ...msg.reasoning && { reasoning: msg.reasoning },
847
+ ...msg.usage && { usage: msg.usage },
848
+ ...msg.error && { error: msg.error },
849
+ ...msg.metadata && { metadata: msg.metadata }
850
+ };
851
+ }
852
+ function formatSourcesMarkdown(sources) {
853
+ if (sources.length === 0)
854
+ return "";
855
+ return `
856
+
857
+ **Sources:**
858
+ ` + sources.map((s) => `- [${s.title}](${s.url ?? "#"})`).join(`
859
+ `);
860
+ }
861
+ function formatSourcesTxt(sources) {
862
+ if (sources.length === 0)
863
+ return "";
864
+ return `
865
+
866
+ Sources:
867
+ ` + sources.map((s) => `- ${s.title}${s.url ? ` - ${s.url}` : ""}`).join(`
868
+ `);
869
+ }
870
+ function formatToolCallsMarkdown(toolCalls) {
871
+ if (toolCalls.length === 0)
872
+ return "";
873
+ return `
874
+
875
+ **Tool calls:**
876
+ ` + toolCalls.map((tc) => `**${tc.name}** (${tc.status})
877
+ \`\`\`json
878
+ ${JSON.stringify(tc.args, null, 2)}
879
+ \`\`\`` + (tc.result !== undefined ? `
880
+ Output:
881
+ \`\`\`json
882
+ ${typeof tc.result === "object" ? JSON.stringify(tc.result, null, 2) : String(tc.result)}
883
+ \`\`\`` : "") + (tc.error ? `
884
+ Error: ${tc.error}` : "")).join(`
885
+
886
+ `);
887
+ }
888
+ function formatToolCallsTxt(toolCalls) {
889
+ if (toolCalls.length === 0)
890
+ return "";
891
+ return `
892
+
893
+ Tool calls:
894
+ ` + toolCalls.map((tc) => `- ${tc.name} (${tc.status}): ${JSON.stringify(tc.args)}` + (tc.result !== undefined ? ` -> ${typeof tc.result === "object" ? JSON.stringify(tc.result) : String(tc.result)}` : "") + (tc.error ? ` [Error: ${tc.error}]` : "")).join(`
895
+ `);
896
+ }
897
+ function formatUsage(usage) {
898
+ const total = usage.inputTokens + usage.outputTokens;
899
+ return ` (${total} tokens)`;
900
+ }
901
+ function formatMessagesAsMarkdown(messages) {
902
+ const parts = [];
903
+ for (const msg of messages) {
904
+ const roleLabel = msg.role === "user" ? "User" : msg.role === "assistant" ? "Assistant" : "System";
905
+ const header = `## ${roleLabel}`;
906
+ const timestamp = `*${formatTimestamp(msg.createdAt)}*`;
907
+ const usageSuffix = msg.usage ? formatUsage(msg.usage) : "";
908
+ const meta = `${timestamp}${usageSuffix}
909
+
910
+ `;
911
+ let body = msg.content;
912
+ if (msg.error) {
913
+ body += `
914
+
915
+ **Error:** ${msg.error.code} - ${msg.error.message}`;
916
+ }
917
+ if (msg.reasoning) {
918
+ body += `
919
+
920
+ > **Reasoning:**
921
+ > ${msg.reasoning.replace(/\n/g, `
922
+ > `)}`;
923
+ }
924
+ body += formatSourcesMarkdown(msg.sources ?? []);
925
+ body += formatToolCallsMarkdown(msg.toolCalls ?? []);
926
+ parts.push(`${header}
927
+
928
+ ${meta}${body}`);
929
+ }
930
+ return parts.join(`
931
+
932
+ ---
933
+
934
+ `);
935
+ }
936
+ function formatMessagesAsTxt(messages) {
937
+ const parts = [];
938
+ for (const msg of messages) {
939
+ const roleLabel = msg.role === "user" ? "User" : msg.role === "assistant" ? "Assistant" : "System";
940
+ const timestamp = `(${formatTimestamp(msg.createdAt)})`;
941
+ const usageSuffix = msg.usage ? formatUsage(msg.usage) : "";
942
+ const header = `[${roleLabel}] ${timestamp}${usageSuffix}
943
+
944
+ `;
945
+ let body = msg.content;
946
+ if (msg.error) {
947
+ body += `
948
+
949
+ Error: ${msg.error.code} - ${msg.error.message}`;
950
+ }
951
+ if (msg.reasoning) {
952
+ body += `
953
+
954
+ Reasoning: ${msg.reasoning}`;
955
+ }
956
+ body += formatSourcesTxt(msg.sources ?? []);
957
+ body += formatToolCallsTxt(msg.toolCalls ?? []);
958
+ parts.push(`${header}${body}`);
959
+ }
960
+ return parts.join(`
961
+
962
+ ---
963
+
964
+ `);
965
+ }
966
+ function formatMessagesAsJson(messages, conversation) {
967
+ const payload = {
968
+ messages: messages.map(messageToJsonSerializable)
969
+ };
970
+ if (conversation) {
971
+ payload.conversation = {
972
+ id: conversation.id,
973
+ title: conversation.title,
974
+ status: conversation.status,
975
+ createdAt: toIsoString(conversation.createdAt),
976
+ updatedAt: toIsoString(conversation.updatedAt),
977
+ provider: conversation.provider,
978
+ model: conversation.model,
979
+ workspacePath: conversation.workspacePath,
980
+ contextFiles: conversation.contextFiles,
981
+ summary: conversation.summary,
982
+ metadata: conversation.metadata
983
+ };
984
+ }
985
+ return JSON.stringify(payload, null, 2);
986
+ }
987
+ function getExportFilename(format, conversation) {
988
+ const timestamp = new Date().toISOString().replace(/[:.]/g, "-").slice(0, 19);
989
+ const base = conversation?.title ? conversation.title.replace(/[^a-zA-Z0-9-_]/g, "_").slice(0, 40) : "chat-export";
990
+ const ext = format === "markdown" ? "md" : format === "txt" ? "txt" : "json";
991
+ return `${base}-${timestamp}.${ext}`;
992
+ }
993
+ var MIME_TYPES = {
994
+ markdown: "text/markdown",
995
+ txt: "text/plain",
996
+ json: "application/json"
997
+ };
998
+ function downloadAsFile(content, filename, mimeType) {
999
+ const blob = new Blob([content], { type: mimeType });
1000
+ const url = URL.createObjectURL(blob);
1001
+ const a = document.createElement("a");
1002
+ a.href = url;
1003
+ a.download = filename;
1004
+ document.body.appendChild(a);
1005
+ a.click();
1006
+ document.body.removeChild(a);
1007
+ URL.revokeObjectURL(url);
1008
+ }
1009
+ function exportToFile(messages, format, conversation) {
1010
+ let content;
1011
+ if (format === "markdown") {
1012
+ content = formatMessagesAsMarkdown(messages);
1013
+ } else if (format === "txt") {
1014
+ content = formatMessagesAsTxt(messages);
1015
+ } else {
1016
+ content = formatMessagesAsJson(messages, conversation);
1017
+ }
1018
+ const filename = getExportFilename(format, conversation);
1019
+ const mimeType = MIME_TYPES[format];
1020
+ downloadAsFile(content, filename, mimeType);
1021
+ }
1022
+
1023
+ // src/presentation/components/ChatExportToolbar.tsx
1024
+ import { jsx as jsx6, jsxs as jsxs6, Fragment as Fragment3 } from "react/jsx-runtime";
1025
+ "use client";
1026
+ function ChatExportToolbar({
1027
+ messages,
1028
+ conversation,
1029
+ selectedIds,
1030
+ onExported,
1031
+ showSelectionSummary = true,
1032
+ onSelectAll,
1033
+ onClearSelection,
1034
+ selectedCount = selectedIds.size,
1035
+ totalCount = messages.length,
1036
+ onCreateNew,
1037
+ onFork
1038
+ }) {
1039
+ const [copied, setCopied] = React5.useState(false);
1040
+ const toExport = React5.useMemo(() => {
1041
+ if (selectedIds.size > 0) {
1042
+ const idSet = selectedIds;
1043
+ return messages.filter((m) => idSet.has(m.id));
1044
+ }
1045
+ return messages;
1046
+ }, [messages, selectedIds]);
1047
+ const handleExport = React5.useCallback((format) => {
1048
+ exportToFile(toExport, format, conversation);
1049
+ onExported?.(format, toExport.length);
1050
+ }, [toExport, conversation, onExported]);
1051
+ const handleCopy = React5.useCallback(async () => {
1052
+ const content = formatMessagesAsMarkdown(toExport);
1053
+ await navigator.clipboard.writeText(content);
1054
+ setCopied(true);
1055
+ setTimeout(() => setCopied(false), 2000);
1056
+ onExported?.("markdown", toExport.length);
1057
+ }, [toExport, onExported]);
1058
+ const disabled = messages.length === 0;
1059
+ const [forking, setForking] = React5.useState(false);
1060
+ const handleFork = React5.useCallback(async (upToMessageId) => {
1061
+ if (!onFork)
1062
+ return;
1063
+ setForking(true);
1064
+ try {
1065
+ await onFork(upToMessageId);
1066
+ } finally {
1067
+ setForking(false);
1068
+ }
1069
+ }, [onFork]);
1070
+ return /* @__PURE__ */ jsxs6("div", {
1071
+ className: "flex items-center gap-2",
1072
+ children: [
1073
+ onCreateNew && /* @__PURE__ */ jsxs6(Button4, {
1074
+ variant: "outline",
1075
+ size: "sm",
1076
+ onPress: onCreateNew,
1077
+ "aria-label": "New conversation",
1078
+ children: [
1079
+ /* @__PURE__ */ jsx6(Plus, {
1080
+ className: "h-4 w-4"
1081
+ }),
1082
+ "New"
1083
+ ]
1084
+ }),
1085
+ onFork && messages.length > 0 && /* @__PURE__ */ jsxs6(Button4, {
1086
+ variant: "outline",
1087
+ size: "sm",
1088
+ disabled: forking,
1089
+ onPress: () => handleFork(),
1090
+ "aria-label": "Fork conversation",
1091
+ children: [
1092
+ /* @__PURE__ */ jsx6(GitFork, {
1093
+ className: "h-4 w-4"
1094
+ }),
1095
+ "Fork"
1096
+ ]
1097
+ }),
1098
+ showSelectionSummary && selectedCount > 0 && /* @__PURE__ */ jsxs6("span", {
1099
+ className: "text-muted-foreground text-sm",
1100
+ children: [
1101
+ selectedCount,
1102
+ " message",
1103
+ selectedCount !== 1 ? "s" : "",
1104
+ " selected"
1105
+ ]
1106
+ }),
1107
+ onSelectAll && onClearSelection && totalCount > 0 && /* @__PURE__ */ jsxs6(Fragment3, {
1108
+ children: [
1109
+ /* @__PURE__ */ jsx6(Button4, {
1110
+ variant: "ghost",
1111
+ size: "sm",
1112
+ onPress: onSelectAll,
1113
+ className: "text-xs",
1114
+ children: "Select all"
1115
+ }),
1116
+ selectedCount > 0 && /* @__PURE__ */ jsx6(Button4, {
1117
+ variant: "ghost",
1118
+ size: "sm",
1119
+ onPress: onClearSelection,
1120
+ className: "text-xs",
1121
+ children: "Clear"
1122
+ })
1123
+ ]
1124
+ }),
1125
+ /* @__PURE__ */ jsxs6(DropdownMenu, {
1126
+ children: [
1127
+ /* @__PURE__ */ jsx6(DropdownMenuTrigger, {
1128
+ asChild: true,
1129
+ children: /* @__PURE__ */ jsxs6(Button4, {
1130
+ variant: "outline",
1131
+ size: "sm",
1132
+ disabled,
1133
+ "aria-label": selectedCount > 0 ? "Export selected messages" : "Export conversation",
1134
+ children: [
1135
+ /* @__PURE__ */ jsx6(Download2, {
1136
+ className: "h-4 w-4"
1137
+ }),
1138
+ "Export"
1139
+ ]
1140
+ })
1141
+ }),
1142
+ /* @__PURE__ */ jsxs6(DropdownMenuContent, {
1143
+ align: "end",
1144
+ children: [
1145
+ /* @__PURE__ */ jsxs6(DropdownMenuItem, {
1146
+ onSelect: () => handleExport("markdown"),
1147
+ disabled,
1148
+ children: [
1149
+ /* @__PURE__ */ jsx6(FileText2, {
1150
+ className: "h-4 w-4"
1151
+ }),
1152
+ "Export as Markdown (.md)"
1153
+ ]
1154
+ }),
1155
+ /* @__PURE__ */ jsxs6(DropdownMenuItem, {
1156
+ onSelect: () => handleExport("txt"),
1157
+ disabled,
1158
+ children: [
1159
+ /* @__PURE__ */ jsx6(FileText2, {
1160
+ className: "h-4 w-4"
1161
+ }),
1162
+ "Export as Plain Text (.txt)"
1163
+ ]
1164
+ }),
1165
+ /* @__PURE__ */ jsxs6(DropdownMenuItem, {
1166
+ onSelect: () => handleExport("json"),
1167
+ disabled,
1168
+ children: [
1169
+ /* @__PURE__ */ jsx6(FileText2, {
1170
+ className: "h-4 w-4"
1171
+ }),
1172
+ "Export as JSON (.json)"
1173
+ ]
1174
+ }),
1175
+ /* @__PURE__ */ jsx6(DropdownMenuSeparator, {}),
1176
+ /* @__PURE__ */ jsxs6(DropdownMenuItem, {
1177
+ onSelect: () => handleCopy(),
1178
+ disabled,
1179
+ children: [
1180
+ copied ? /* @__PURE__ */ jsx6(Check3, {
1181
+ className: "h-4 w-4 text-green-500"
1182
+ }) : /* @__PURE__ */ jsx6(Copy3, {
1183
+ className: "h-4 w-4"
1184
+ }),
1185
+ copied ? "Copied to clipboard" : "Copy to clipboard"
1186
+ ]
1187
+ })
1188
+ ]
1189
+ })
1190
+ ]
1191
+ })
1192
+ ]
1193
+ });
1194
+ }
1195
+ // src/presentation/components/ChatWithExport.tsx
1196
+ import * as React8 from "react";
1197
+
1198
+ // src/presentation/components/ThinkingLevelPicker.tsx
1199
+ import * as React6 from "react";
1200
+ import { cn as cn5 } from "@contractspec/lib.ui-kit-web/ui/utils";
669
1201
  import {
670
1202
  Select,
671
1203
  SelectContent,
@@ -673,402 +1205,487 @@ import {
673
1205
  SelectTrigger,
674
1206
  SelectValue
675
1207
  } from "@contractspec/lib.ui-kit-web/ui/select";
676
- import { Badge } from "@contractspec/lib.ui-kit-web/ui/badge";
677
1208
  import { Label } from "@contractspec/lib.ui-kit-web/ui/label";
678
- import { Bot as Bot2, Cloud, Cpu, Sparkles } from "lucide-react";
679
- import {
680
- getModelsForProvider
681
- } from "@contractspec/lib.ai-providers";
682
- import { jsxDEV as jsxDEV5 } from "react/jsx-dev-runtime";
683
- "use client";
684
- var PROVIDER_ICONS = {
685
- ollama: /* @__PURE__ */ jsxDEV5(Cpu, {
686
- className: "h-4 w-4"
687
- }, undefined, false, undefined, this),
688
- openai: /* @__PURE__ */ jsxDEV5(Bot2, {
689
- className: "h-4 w-4"
690
- }, undefined, false, undefined, this),
691
- anthropic: /* @__PURE__ */ jsxDEV5(Sparkles, {
692
- className: "h-4 w-4"
693
- }, undefined, false, undefined, this),
694
- mistral: /* @__PURE__ */ jsxDEV5(Cloud, {
695
- className: "h-4 w-4"
696
- }, undefined, false, undefined, this),
697
- gemini: /* @__PURE__ */ jsxDEV5(Sparkles, {
698
- className: "h-4 w-4"
699
- }, undefined, false, undefined, this)
700
- };
701
- var PROVIDER_NAMES = {
702
- ollama: "Ollama (Local)",
703
- openai: "OpenAI",
704
- anthropic: "Anthropic",
705
- mistral: "Mistral",
706
- gemini: "Google Gemini"
1209
+
1210
+ // src/core/thinking-levels.ts
1211
+ var THINKING_LEVEL_LABELS = {
1212
+ instant: "Instant",
1213
+ thinking: "Thinking",
1214
+ extra_thinking: "Extra Thinking",
1215
+ max: "Max"
707
1216
  };
708
- var MODE_BADGES = {
709
- local: { label: "Local", variant: "secondary" },
710
- byok: { label: "BYOK", variant: "outline" },
711
- managed: { label: "Managed", variant: "default" }
1217
+ var THINKING_LEVEL_DESCRIPTIONS = {
1218
+ instant: "Fast responses, minimal reasoning",
1219
+ thinking: "Standard reasoning depth",
1220
+ extra_thinking: "More thorough reasoning",
1221
+ max: "Maximum reasoning depth"
712
1222
  };
713
- function ModelPicker({
1223
+ function getProviderOptions(level, providerName) {
1224
+ if (!level || level === "instant") {
1225
+ return {};
1226
+ }
1227
+ switch (providerName) {
1228
+ case "anthropic": {
1229
+ const budgetMap = {
1230
+ thinking: 8000,
1231
+ extra_thinking: 16000,
1232
+ max: 32000
1233
+ };
1234
+ return {
1235
+ anthropic: {
1236
+ thinking: { type: "enabled", budgetTokens: budgetMap[level] }
1237
+ }
1238
+ };
1239
+ }
1240
+ case "openai": {
1241
+ const effortMap = {
1242
+ thinking: "low",
1243
+ extra_thinking: "medium",
1244
+ max: "high"
1245
+ };
1246
+ return {
1247
+ openai: {
1248
+ reasoningEffort: effortMap[level]
1249
+ }
1250
+ };
1251
+ }
1252
+ case "ollama":
1253
+ case "mistral":
1254
+ case "gemini":
1255
+ return {};
1256
+ default:
1257
+ return {};
1258
+ }
1259
+ }
1260
+
1261
+ // src/presentation/components/ThinkingLevelPicker.tsx
1262
+ import { jsx as jsx7, jsxs as jsxs7 } from "react/jsx-runtime";
1263
+ "use client";
1264
+ var THINKING_LEVELS = [
1265
+ "instant",
1266
+ "thinking",
1267
+ "extra_thinking",
1268
+ "max"
1269
+ ];
1270
+ function ThinkingLevelPicker({
714
1271
  value,
715
1272
  onChange,
716
- availableProviders,
717
1273
  className,
718
1274
  compact = false
719
1275
  }) {
720
- const providers = availableProviders ?? [
721
- { provider: "ollama", available: true, mode: "local" },
722
- { provider: "openai", available: true, mode: "byok" },
723
- { provider: "anthropic", available: true, mode: "byok" },
724
- { provider: "mistral", available: true, mode: "byok" },
725
- { provider: "gemini", available: true, mode: "byok" }
726
- ];
727
- const models = getModelsForProvider(value.provider);
728
- const selectedModel = models.find((m) => m.id === value.model);
729
- const handleProviderChange = React5.useCallback((providerName) => {
730
- const provider = providerName;
731
- const providerInfo = providers.find((p) => p.provider === provider);
732
- const providerModels = getModelsForProvider(provider);
733
- const defaultModel = providerModels[0]?.id ?? "";
734
- onChange({
735
- provider,
736
- model: defaultModel,
737
- mode: providerInfo?.mode ?? "byok"
738
- });
739
- }, [onChange, providers]);
740
- const handleModelChange = React5.useCallback((modelId) => {
741
- onChange({
742
- ...value,
743
- model: modelId
744
- });
745
- }, [onChange, value]);
1276
+ const handleChange = React6.useCallback((v) => {
1277
+ onChange(v);
1278
+ }, [onChange]);
746
1279
  if (compact) {
747
- return /* @__PURE__ */ jsxDEV5("div", {
748
- className: cn5("flex items-center gap-2", className),
1280
+ return /* @__PURE__ */ jsxs7(Select, {
1281
+ value,
1282
+ onValueChange: handleChange,
749
1283
  children: [
750
- /* @__PURE__ */ jsxDEV5(Select, {
751
- value: value.provider,
752
- onValueChange: handleProviderChange,
753
- children: [
754
- /* @__PURE__ */ jsxDEV5(SelectTrigger, {
755
- className: "w-[140px]",
756
- children: /* @__PURE__ */ jsxDEV5(SelectValue, {}, undefined, false, undefined, this)
757
- }, undefined, false, undefined, this),
758
- /* @__PURE__ */ jsxDEV5(SelectContent, {
759
- children: providers.map((p) => /* @__PURE__ */ jsxDEV5(SelectItem, {
760
- value: p.provider,
761
- disabled: !p.available,
762
- children: /* @__PURE__ */ jsxDEV5("div", {
763
- className: "flex items-center gap-2",
764
- children: [
765
- PROVIDER_ICONS[p.provider],
766
- /* @__PURE__ */ jsxDEV5("span", {
767
- children: PROVIDER_NAMES[p.provider]
768
- }, undefined, false, undefined, this)
769
- ]
770
- }, undefined, true, undefined, this)
771
- }, p.provider, false, undefined, this))
772
- }, undefined, false, undefined, this)
773
- ]
774
- }, undefined, true, undefined, this),
775
- /* @__PURE__ */ jsxDEV5(Select, {
776
- value: value.model,
777
- onValueChange: handleModelChange,
778
- children: [
779
- /* @__PURE__ */ jsxDEV5(SelectTrigger, {
780
- className: "w-[160px]",
781
- children: /* @__PURE__ */ jsxDEV5(SelectValue, {}, undefined, false, undefined, this)
782
- }, undefined, false, undefined, this),
783
- /* @__PURE__ */ jsxDEV5(SelectContent, {
784
- children: models.map((m) => /* @__PURE__ */ jsxDEV5(SelectItem, {
785
- value: m.id,
786
- children: m.name
787
- }, m.id, false, undefined, this))
788
- }, undefined, false, undefined, this)
789
- ]
790
- }, undefined, true, undefined, this)
1284
+ /* @__PURE__ */ jsx7(SelectTrigger, {
1285
+ className: cn5("w-[140px]", className),
1286
+ children: /* @__PURE__ */ jsx7(SelectValue, {})
1287
+ }),
1288
+ /* @__PURE__ */ jsx7(SelectContent, {
1289
+ children: THINKING_LEVELS.map((level) => /* @__PURE__ */ jsx7(SelectItem, {
1290
+ value: level,
1291
+ children: THINKING_LEVEL_LABELS[level]
1292
+ }, level))
1293
+ })
791
1294
  ]
792
- }, undefined, true, undefined, this);
1295
+ });
793
1296
  }
794
- return /* @__PURE__ */ jsxDEV5("div", {
795
- className: cn5("flex flex-col gap-3", className),
1297
+ return /* @__PURE__ */ jsxs7("div", {
1298
+ className: cn5("flex flex-col gap-1.5", className),
796
1299
  children: [
797
- /* @__PURE__ */ jsxDEV5("div", {
798
- className: "flex flex-col gap-1.5",
1300
+ /* @__PURE__ */ jsx7(Label, {
1301
+ htmlFor: "thinking-level-picker",
1302
+ className: "text-sm font-medium",
1303
+ children: "Thinking Level"
1304
+ }),
1305
+ /* @__PURE__ */ jsxs7(Select, {
1306
+ name: "thinking-level-picker",
1307
+ value,
1308
+ onValueChange: handleChange,
799
1309
  children: [
800
- /* @__PURE__ */ jsxDEV5(Label, {
801
- htmlFor: "provider-selection",
802
- className: "text-sm font-medium",
803
- children: "Provider"
804
- }, undefined, false, undefined, this),
805
- /* @__PURE__ */ jsxDEV5("div", {
806
- className: "flex flex-wrap gap-2",
807
- id: "provider-selection",
808
- children: providers.map((p) => /* @__PURE__ */ jsxDEV5(Button4, {
809
- variant: value.provider === p.provider ? "default" : "outline",
810
- size: "sm",
811
- onPress: () => p.available && handleProviderChange(p.provider),
812
- disabled: !p.available,
813
- className: cn5(!p.available && "opacity-50"),
814
- children: [
815
- PROVIDER_ICONS[p.provider],
816
- /* @__PURE__ */ jsxDEV5("span", {
817
- children: PROVIDER_NAMES[p.provider]
818
- }, undefined, false, undefined, this),
819
- /* @__PURE__ */ jsxDEV5(Badge, {
820
- variant: MODE_BADGES[p.mode].variant,
821
- className: "ml-1",
822
- children: MODE_BADGES[p.mode].label
823
- }, undefined, false, undefined, this)
824
- ]
825
- }, p.provider, true, undefined, this))
826
- }, undefined, false, undefined, this)
1310
+ /* @__PURE__ */ jsx7(SelectTrigger, {
1311
+ children: /* @__PURE__ */ jsx7(SelectValue, {
1312
+ placeholder: "Select thinking level"
1313
+ })
1314
+ }),
1315
+ /* @__PURE__ */ jsx7(SelectContent, {
1316
+ children: THINKING_LEVELS.map((level) => /* @__PURE__ */ jsx7(SelectItem, {
1317
+ value: level,
1318
+ title: THINKING_LEVEL_DESCRIPTIONS[level],
1319
+ children: THINKING_LEVEL_LABELS[level]
1320
+ }, level))
1321
+ })
827
1322
  ]
828
- }, undefined, true, undefined, this),
829
- /* @__PURE__ */ jsxDEV5("div", {
830
- className: "flex flex-col gap-1.5",
831
- children: [
832
- /* @__PURE__ */ jsxDEV5(Label, {
833
- htmlFor: "model-picker",
834
- className: "text-sm font-medium",
835
- children: "Model"
836
- }, undefined, false, undefined, this),
837
- /* @__PURE__ */ jsxDEV5(Select, {
838
- name: "model-picker",
839
- value: value.model,
840
- onValueChange: handleModelChange,
841
- children: [
842
- /* @__PURE__ */ jsxDEV5(SelectTrigger, {
843
- children: /* @__PURE__ */ jsxDEV5(SelectValue, {
844
- placeholder: "Select a model"
845
- }, undefined, false, undefined, this)
846
- }, undefined, false, undefined, this),
847
- /* @__PURE__ */ jsxDEV5(SelectContent, {
848
- children: models.map((m) => /* @__PURE__ */ jsxDEV5(SelectItem, {
849
- value: m.id,
850
- children: /* @__PURE__ */ jsxDEV5("div", {
851
- className: "flex items-center gap-2",
852
- children: [
853
- /* @__PURE__ */ jsxDEV5("span", {
854
- children: m.name
855
- }, undefined, false, undefined, this),
856
- /* @__PURE__ */ jsxDEV5("span", {
857
- className: "text-muted-foreground text-xs",
858
- children: [
859
- Math.round(m.contextWindow / 1000),
860
- "K"
861
- ]
862
- }, undefined, true, undefined, this),
863
- m.capabilities.vision && /* @__PURE__ */ jsxDEV5(Badge, {
864
- variant: "outline",
865
- className: "text-xs",
866
- children: "Vision"
867
- }, undefined, false, undefined, this),
868
- m.capabilities.reasoning && /* @__PURE__ */ jsxDEV5(Badge, {
869
- variant: "outline",
870
- className: "text-xs",
871
- children: "Reasoning"
872
- }, undefined, false, undefined, this)
873
- ]
874
- }, undefined, true, undefined, this)
875
- }, m.id, false, undefined, this))
876
- }, undefined, false, undefined, this)
877
- ]
878
- }, undefined, true, undefined, this)
879
- ]
880
- }, undefined, true, undefined, this),
881
- selectedModel && /* @__PURE__ */ jsxDEV5("div", {
882
- className: "text-muted-foreground flex flex-wrap gap-2 text-xs",
883
- children: [
884
- /* @__PURE__ */ jsxDEV5("span", {
885
- children: [
886
- "Context: ",
887
- Math.round(selectedModel.contextWindow / 1000),
888
- "K tokens"
889
- ]
890
- }, undefined, true, undefined, this),
891
- selectedModel.capabilities.vision && /* @__PURE__ */ jsxDEV5("span", {
892
- children: "• Vision"
893
- }, undefined, false, undefined, this),
894
- selectedModel.capabilities.tools && /* @__PURE__ */ jsxDEV5("span", {
895
- children: "• Tools"
896
- }, undefined, false, undefined, this),
897
- selectedModel.capabilities.reasoning && /* @__PURE__ */ jsxDEV5("span", {
898
- children: "• Reasoning"
899
- }, undefined, false, undefined, this)
900
- ]
901
- }, undefined, true, undefined, this)
1323
+ })
902
1324
  ]
903
- }, undefined, true, undefined, this);
1325
+ });
904
1326
  }
905
- // src/presentation/components/ContextIndicator.tsx
906
- import { cn as cn6 } from "@contractspec/lib.ui-kit-web/ui/utils";
907
- import { Badge as Badge2 } from "@contractspec/lib.ui-kit-web/ui/badge";
908
- import {
909
- Tooltip,
910
- TooltipContent,
911
- TooltipProvider,
912
- TooltipTrigger
913
- } from "@contractspec/lib.ui-kit-web/ui/tooltip";
914
- import { FolderOpen, FileCode, Zap, Info } from "lucide-react";
915
- import { jsxDEV as jsxDEV6, Fragment as Fragment3 } from "react/jsx-dev-runtime";
1327
+
1328
+ // src/presentation/hooks/useMessageSelection.ts
1329
+ import * as React7 from "react";
916
1330
  "use client";
917
- function ContextIndicator({
918
- summary,
919
- active = false,
1331
+ function useMessageSelection(messageIds) {
1332
+ const [selectedIds, setSelectedIds] = React7.useState(() => new Set);
1333
+ const idSet = React7.useMemo(() => new Set(messageIds), [messageIds.join(",")]);
1334
+ React7.useEffect(() => {
1335
+ setSelectedIds((prev) => {
1336
+ const next = new Set;
1337
+ for (const id of prev) {
1338
+ if (idSet.has(id))
1339
+ next.add(id);
1340
+ }
1341
+ return next.size === prev.size ? prev : next;
1342
+ });
1343
+ }, [idSet]);
1344
+ const toggle = React7.useCallback((id) => {
1345
+ setSelectedIds((prev) => {
1346
+ const next = new Set(prev);
1347
+ if (next.has(id))
1348
+ next.delete(id);
1349
+ else
1350
+ next.add(id);
1351
+ return next;
1352
+ });
1353
+ }, []);
1354
+ const selectAll = React7.useCallback(() => {
1355
+ setSelectedIds(new Set(messageIds));
1356
+ }, [messageIds.join(",")]);
1357
+ const clearSelection = React7.useCallback(() => {
1358
+ setSelectedIds(new Set);
1359
+ }, []);
1360
+ const isSelected = React7.useCallback((id) => selectedIds.has(id), [selectedIds]);
1361
+ const selectedCount = selectedIds.size;
1362
+ return {
1363
+ selectedIds,
1364
+ toggle,
1365
+ selectAll,
1366
+ clearSelection,
1367
+ isSelected,
1368
+ selectedCount
1369
+ };
1370
+ }
1371
+
1372
+ // src/presentation/components/ChatWithExport.tsx
1373
+ import { jsx as jsx8, jsxs as jsxs8, Fragment as Fragment4 } from "react/jsx-runtime";
1374
+ "use client";
1375
+ function ChatWithExport({
1376
+ messages,
1377
+ conversation,
1378
+ children,
920
1379
  className,
921
- showDetails = true
1380
+ showExport = true,
1381
+ showMessageSelection = true,
1382
+ showScrollButton = true,
1383
+ onCreateNew,
1384
+ onFork,
1385
+ onEditMessage,
1386
+ thinkingLevel = "thinking",
1387
+ onThinkingLevelChange,
1388
+ presentationRenderer,
1389
+ formRenderer
922
1390
  }) {
923
- if (!summary && !active) {
924
- return /* @__PURE__ */ jsxDEV6("div", {
925
- className: cn6("flex items-center gap-1.5 text-sm", "text-muted-foreground", className),
926
- children: [
927
- /* @__PURE__ */ jsxDEV6(Info, {
928
- className: "h-4 w-4"
929
- }, undefined, false, undefined, this),
930
- /* @__PURE__ */ jsxDEV6("span", {
931
- children: "No workspace context"
932
- }, undefined, false, undefined, this)
933
- ]
934
- }, undefined, true, undefined, this);
1391
+ const messageIds = React8.useMemo(() => messages.map((m) => m.id), [messages]);
1392
+ const selection = useMessageSelection(messageIds);
1393
+ const hasToolbar = showExport || showMessageSelection;
1394
+ const hasPicker = Boolean(onThinkingLevelChange);
1395
+ const headerContent = hasPicker || hasToolbar ? /* @__PURE__ */ jsxs8(Fragment4, {
1396
+ children: [
1397
+ hasPicker && /* @__PURE__ */ jsx8(ThinkingLevelPicker, {
1398
+ value: thinkingLevel,
1399
+ onChange: onThinkingLevelChange,
1400
+ compact: true
1401
+ }),
1402
+ hasToolbar && /* @__PURE__ */ jsx8(ChatExportToolbar, {
1403
+ messages,
1404
+ conversation,
1405
+ selectedIds: selection.selectedIds,
1406
+ showSelectionSummary: showMessageSelection,
1407
+ onSelectAll: showMessageSelection ? selection.selectAll : undefined,
1408
+ onClearSelection: showMessageSelection ? selection.clearSelection : undefined,
1409
+ selectedCount: selection.selectedCount,
1410
+ totalCount: messages.length,
1411
+ onCreateNew,
1412
+ onFork
1413
+ })
1414
+ ]
1415
+ }) : null;
1416
+ return /* @__PURE__ */ jsxs8(ChatContainer, {
1417
+ className,
1418
+ headerContent,
1419
+ showScrollButton,
1420
+ children: [
1421
+ messages.map((msg) => /* @__PURE__ */ jsx8(ChatMessage, {
1422
+ message: msg,
1423
+ selectable: showMessageSelection,
1424
+ selected: selection.isSelected(msg.id),
1425
+ onSelect: showMessageSelection ? selection.toggle : undefined,
1426
+ editable: msg.role === "user" && !!onEditMessage,
1427
+ onEdit: onEditMessage,
1428
+ presentationRenderer,
1429
+ formRenderer
1430
+ }, msg.id)),
1431
+ children
1432
+ ]
1433
+ });
1434
+ }
1435
+ // src/presentation/components/ChatSidebar.tsx
1436
+ import * as React10 from "react";
1437
+ import { Plus as Plus2, Trash2, MessageSquare } from "lucide-react";
1438
+ import { Button as Button5 } from "@contractspec/lib.design-system";
1439
+ import { cn as cn6 } from "@contractspec/lib.ui-kit-web/ui/utils";
1440
+
1441
+ // src/presentation/hooks/useConversations.ts
1442
+ import * as React9 from "react";
1443
+ "use client";
1444
+ function useConversations(options) {
1445
+ const { store, projectId, tags, limit = 50 } = options;
1446
+ const [conversations, setConversations] = React9.useState([]);
1447
+ const [isLoading, setIsLoading] = React9.useState(true);
1448
+ const refresh = React9.useCallback(async () => {
1449
+ setIsLoading(true);
1450
+ try {
1451
+ const list = await store.list({
1452
+ status: "active",
1453
+ projectId,
1454
+ tags,
1455
+ limit
1456
+ });
1457
+ setConversations(list);
1458
+ } finally {
1459
+ setIsLoading(false);
1460
+ }
1461
+ }, [store, projectId, tags, limit]);
1462
+ React9.useEffect(() => {
1463
+ refresh();
1464
+ }, [refresh]);
1465
+ const deleteConversation = React9.useCallback(async (id) => {
1466
+ const ok = await store.delete(id);
1467
+ if (ok) {
1468
+ setConversations((prev) => prev.filter((c) => c.id !== id));
1469
+ }
1470
+ return ok;
1471
+ }, [store]);
1472
+ return {
1473
+ conversations,
1474
+ isLoading,
1475
+ refresh,
1476
+ deleteConversation
1477
+ };
1478
+ }
1479
+
1480
+ // src/presentation/components/ChatSidebar.tsx
1481
+ import { jsx as jsx9, jsxs as jsxs9, Fragment as Fragment5 } from "react/jsx-runtime";
1482
+ "use client";
1483
+ function formatDate(date) {
1484
+ const d = new Date(date);
1485
+ const now = new Date;
1486
+ const diff = now.getTime() - d.getTime();
1487
+ if (diff < 86400000) {
1488
+ return d.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" });
935
1489
  }
936
- const content = /* @__PURE__ */ jsxDEV6("div", {
937
- className: cn6("flex items-center gap-2", active ? "text-foreground" : "text-muted-foreground", className),
1490
+ if (diff < 604800000) {
1491
+ return d.toLocaleDateString([], { weekday: "short" });
1492
+ }
1493
+ return d.toLocaleDateString([], { month: "short", day: "numeric" });
1494
+ }
1495
+ function ConversationItem({
1496
+ conversation,
1497
+ selected,
1498
+ onSelect,
1499
+ onDelete
1500
+ }) {
1501
+ const title = conversation.title ?? conversation.messages[0]?.content?.slice(0, 50) ?? "New chat";
1502
+ const displayTitle = title.length > 40 ? `${title.slice(0, 40)}…` : title;
1503
+ return /* @__PURE__ */ jsxs9("div", {
1504
+ role: "button",
1505
+ tabIndex: 0,
1506
+ onClick: onSelect,
1507
+ onKeyDown: (e) => {
1508
+ if (e.key === "Enter" || e.key === " ") {
1509
+ e.preventDefault();
1510
+ onSelect();
1511
+ }
1512
+ },
1513
+ className: cn6("group flex cursor-pointer items-center gap-2 rounded-md px-3 py-2 text-sm transition-colors", selected ? "bg-accent text-accent-foreground" : "hover:bg-accent/50"),
938
1514
  children: [
939
- /* @__PURE__ */ jsxDEV6(Badge2, {
940
- variant: active ? "default" : "secondary",
941
- className: "flex items-center gap-1",
942
- children: [
943
- /* @__PURE__ */ jsxDEV6(Zap, {
944
- className: "h-3 w-3"
945
- }, undefined, false, undefined, this),
946
- "Context"
947
- ]
948
- }, undefined, true, undefined, this),
949
- summary && showDetails && /* @__PURE__ */ jsxDEV6(Fragment3, {
1515
+ /* @__PURE__ */ jsx9(MessageSquare, {
1516
+ className: "text-muted-foreground h-4 w-4 shrink-0"
1517
+ }),
1518
+ /* @__PURE__ */ jsxs9("div", {
1519
+ className: "min-w-0 flex-1",
950
1520
  children: [
951
- /* @__PURE__ */ jsxDEV6("div", {
952
- className: "flex items-center gap-1 text-xs",
953
- children: [
954
- /* @__PURE__ */ jsxDEV6(FolderOpen, {
955
- className: "h-3.5 w-3.5"
956
- }, undefined, false, undefined, this),
957
- /* @__PURE__ */ jsxDEV6("span", {
958
- children: summary.name
959
- }, undefined, false, undefined, this)
960
- ]
961
- }, undefined, true, undefined, this),
962
- /* @__PURE__ */ jsxDEV6("div", {
963
- className: "flex items-center gap-1 text-xs",
1521
+ /* @__PURE__ */ jsx9("p", {
1522
+ className: "truncate",
1523
+ children: displayTitle
1524
+ }),
1525
+ /* @__PURE__ */ jsxs9("p", {
1526
+ className: "text-muted-foreground text-xs",
964
1527
  children: [
965
- /* @__PURE__ */ jsxDEV6(FileCode, {
966
- className: "h-3.5 w-3.5"
967
- }, undefined, false, undefined, this),
968
- /* @__PURE__ */ jsxDEV6("span", {
1528
+ formatDate(conversation.updatedAt),
1529
+ conversation.projectName && ` · ${conversation.projectName}`,
1530
+ conversation.tags && conversation.tags.length > 0 && /* @__PURE__ */ jsxs9(Fragment5, {
969
1531
  children: [
970
- summary.specs.total,
971
- " specs"
1532
+ " · ",
1533
+ conversation.tags.slice(0, 2).join(", ")
972
1534
  ]
973
- }, undefined, true, undefined, this)
1535
+ })
974
1536
  ]
975
- }, undefined, true, undefined, this)
1537
+ })
976
1538
  ]
977
- }, undefined, true, undefined, this)
1539
+ }),
1540
+ /* @__PURE__ */ jsx9("span", {
1541
+ onClick: (e) => e.stopPropagation(),
1542
+ children: /* @__PURE__ */ jsx9(Button5, {
1543
+ variant: "ghost",
1544
+ size: "sm",
1545
+ className: "h-6 w-6 shrink-0 p-0 opacity-0 group-hover:opacity-100",
1546
+ onPress: onDelete,
1547
+ "aria-label": "Delete conversation",
1548
+ children: /* @__PURE__ */ jsx9(Trash2, {
1549
+ className: "h-3 w-3"
1550
+ })
1551
+ })
1552
+ })
978
1553
  ]
979
- }, undefined, true, undefined, this);
980
- if (!summary) {
981
- return content;
982
- }
983
- return /* @__PURE__ */ jsxDEV6(TooltipProvider, {
984
- children: /* @__PURE__ */ jsxDEV6(Tooltip, {
985
- children: [
986
- /* @__PURE__ */ jsxDEV6(TooltipTrigger, {
987
- asChild: true,
988
- children: content
989
- }, undefined, false, undefined, this),
990
- /* @__PURE__ */ jsxDEV6(TooltipContent, {
991
- side: "bottom",
992
- className: "max-w-[300px]",
993
- children: /* @__PURE__ */ jsxDEV6("div", {
994
- className: "flex flex-col gap-2 text-sm",
995
- children: [
996
- /* @__PURE__ */ jsxDEV6("div", {
997
- className: "font-medium",
998
- children: summary.name
999
- }, undefined, false, undefined, this),
1000
- /* @__PURE__ */ jsxDEV6("div", {
1001
- className: "text-muted-foreground text-xs",
1002
- children: summary.path
1003
- }, undefined, false, undefined, this),
1004
- /* @__PURE__ */ jsxDEV6("div", {
1005
- className: "border-t pt-2",
1006
- children: /* @__PURE__ */ jsxDEV6("div", {
1007
- className: "grid grid-cols-2 gap-1 text-xs",
1008
- children: [
1009
- /* @__PURE__ */ jsxDEV6("span", {
1010
- children: "Commands:"
1011
- }, undefined, false, undefined, this),
1012
- /* @__PURE__ */ jsxDEV6("span", {
1013
- className: "text-right",
1014
- children: summary.specs.commands
1015
- }, undefined, false, undefined, this),
1016
- /* @__PURE__ */ jsxDEV6("span", {
1017
- children: "Queries:"
1018
- }, undefined, false, undefined, this),
1019
- /* @__PURE__ */ jsxDEV6("span", {
1020
- className: "text-right",
1021
- children: summary.specs.queries
1022
- }, undefined, false, undefined, this),
1023
- /* @__PURE__ */ jsxDEV6("span", {
1024
- children: "Events:"
1025
- }, undefined, false, undefined, this),
1026
- /* @__PURE__ */ jsxDEV6("span", {
1027
- className: "text-right",
1028
- children: summary.specs.events
1029
- }, undefined, false, undefined, this),
1030
- /* @__PURE__ */ jsxDEV6("span", {
1031
- children: "Presentations:"
1032
- }, undefined, false, undefined, this),
1033
- /* @__PURE__ */ jsxDEV6("span", {
1034
- className: "text-right",
1035
- children: summary.specs.presentations
1036
- }, undefined, false, undefined, this)
1037
- ]
1038
- }, undefined, true, undefined, this)
1039
- }, undefined, false, undefined, this),
1040
- /* @__PURE__ */ jsxDEV6("div", {
1041
- className: "border-t pt-2 text-xs",
1042
- children: [
1043
- /* @__PURE__ */ jsxDEV6("span", {
1044
- children: [
1045
- summary.files.total,
1046
- " files"
1047
- ]
1048
- }, undefined, true, undefined, this),
1049
- /* @__PURE__ */ jsxDEV6("span", {
1050
- className: "mx-1",
1051
- children: "•"
1052
- }, undefined, false, undefined, this),
1053
- /* @__PURE__ */ jsxDEV6("span", {
1054
- children: [
1055
- summary.files.specFiles,
1056
- " spec files"
1057
- ]
1058
- }, undefined, true, undefined, this)
1059
- ]
1060
- }, undefined, true, undefined, this)
1061
- ]
1062
- }, undefined, true, undefined, this)
1063
- }, undefined, false, undefined, this)
1064
- ]
1065
- }, undefined, true, undefined, this)
1066
- }, undefined, false, undefined, this);
1554
+ });
1555
+ }
1556
+ function ChatSidebar({
1557
+ store,
1558
+ selectedConversationId,
1559
+ onSelectConversation,
1560
+ onCreateNew,
1561
+ projectId,
1562
+ tags,
1563
+ limit = 50,
1564
+ className,
1565
+ collapsed = false,
1566
+ onUpdateConversation,
1567
+ selectedConversation
1568
+ }) {
1569
+ const { conversations, isLoading, refresh, deleteConversation } = useConversations({ store, projectId, tags, limit });
1570
+ const handleDelete = React10.useCallback(async (id) => {
1571
+ const ok = await deleteConversation(id);
1572
+ if (ok && selectedConversationId === id) {
1573
+ onSelectConversation(null);
1574
+ }
1575
+ }, [deleteConversation, selectedConversationId, onSelectConversation]);
1576
+ if (collapsed)
1577
+ return null;
1578
+ return /* @__PURE__ */ jsxs9("div", {
1579
+ className: cn6("border-border flex w-64 shrink-0 flex-col border-r", className),
1580
+ children: [
1581
+ /* @__PURE__ */ jsxs9("div", {
1582
+ className: "border-border flex shrink-0 items-center justify-between border-b p-2",
1583
+ children: [
1584
+ /* @__PURE__ */ jsx9("span", {
1585
+ className: "text-muted-foreground text-sm font-medium",
1586
+ children: "Conversations"
1587
+ }),
1588
+ /* @__PURE__ */ jsx9(Button5, {
1589
+ variant: "ghost",
1590
+ size: "sm",
1591
+ className: "h-8 w-8 p-0",
1592
+ onPress: onCreateNew,
1593
+ "aria-label": "New conversation",
1594
+ children: /* @__PURE__ */ jsx9(Plus2, {
1595
+ className: "h-4 w-4"
1596
+ })
1597
+ })
1598
+ ]
1599
+ }),
1600
+ /* @__PURE__ */ jsx9("div", {
1601
+ className: "flex-1 overflow-y-auto p-2",
1602
+ children: isLoading ? /* @__PURE__ */ jsx9("div", {
1603
+ className: "text-muted-foreground py-4 text-center text-sm",
1604
+ children: "Loading…"
1605
+ }) : conversations.length === 0 ? /* @__PURE__ */ jsx9("div", {
1606
+ className: "text-muted-foreground py-4 text-center text-sm",
1607
+ children: "No conversations yet"
1608
+ }) : /* @__PURE__ */ jsx9("div", {
1609
+ className: "flex flex-col gap-1",
1610
+ children: conversations.map((conv) => /* @__PURE__ */ jsx9(ConversationItem, {
1611
+ conversation: conv,
1612
+ selected: conv.id === selectedConversationId,
1613
+ onSelect: () => onSelectConversation(conv.id),
1614
+ onDelete: () => handleDelete(conv.id)
1615
+ }, conv.id))
1616
+ })
1617
+ }),
1618
+ selectedConversation && onUpdateConversation && /* @__PURE__ */ jsx9(ConversationMeta, {
1619
+ conversation: selectedConversation,
1620
+ onUpdate: onUpdateConversation
1621
+ })
1622
+ ]
1623
+ });
1624
+ }
1625
+ function ConversationMeta({
1626
+ conversation,
1627
+ onUpdate
1628
+ }) {
1629
+ const [projectName, setProjectName] = React10.useState(conversation.projectName ?? "");
1630
+ const [tagsStr, setTagsStr] = React10.useState(conversation.tags?.join(", ") ?? "");
1631
+ React10.useEffect(() => {
1632
+ setProjectName(conversation.projectName ?? "");
1633
+ setTagsStr(conversation.tags?.join(", ") ?? "");
1634
+ }, [conversation.id, conversation.projectName, conversation.tags]);
1635
+ const handleBlur = React10.useCallback(() => {
1636
+ const tags = tagsStr.split(",").map((t) => t.trim()).filter(Boolean);
1637
+ if (projectName !== (conversation.projectName ?? "") || JSON.stringify(tags) !== JSON.stringify(conversation.tags ?? [])) {
1638
+ onUpdate(conversation.id, {
1639
+ projectName: projectName || undefined,
1640
+ projectId: projectName ? projectName.replace(/\s+/g, "-") : undefined,
1641
+ tags: tags.length > 0 ? tags : undefined
1642
+ });
1643
+ }
1644
+ }, [
1645
+ conversation.id,
1646
+ conversation.projectName,
1647
+ conversation.tags,
1648
+ projectName,
1649
+ tagsStr,
1650
+ onUpdate
1651
+ ]);
1652
+ return /* @__PURE__ */ jsxs9("div", {
1653
+ className: "border-border shrink-0 border-t p-2",
1654
+ children: [
1655
+ /* @__PURE__ */ jsx9("p", {
1656
+ className: "text-muted-foreground mb-1 text-xs font-medium",
1657
+ children: "Project"
1658
+ }),
1659
+ /* @__PURE__ */ jsx9("input", {
1660
+ type: "text",
1661
+ value: projectName,
1662
+ onChange: (e) => setProjectName(e.target.value),
1663
+ onBlur: handleBlur,
1664
+ placeholder: "Project name",
1665
+ className: "border-input bg-background mb-2 w-full rounded px-2 py-1 text-xs"
1666
+ }),
1667
+ /* @__PURE__ */ jsx9("p", {
1668
+ className: "text-muted-foreground mb-1 text-xs font-medium",
1669
+ children: "Tags"
1670
+ }),
1671
+ /* @__PURE__ */ jsx9("input", {
1672
+ type: "text",
1673
+ value: tagsStr,
1674
+ onChange: (e) => setTagsStr(e.target.value),
1675
+ onBlur: handleBlur,
1676
+ placeholder: "tag1, tag2",
1677
+ className: "border-input bg-background w-full rounded px-2 py-1 text-xs"
1678
+ })
1679
+ ]
1680
+ });
1067
1681
  }
1682
+ // src/presentation/components/ChatWithSidebar.tsx
1683
+ import * as React12 from "react";
1684
+
1068
1685
  // src/presentation/hooks/useChat.tsx
1069
- import * as React6 from "react";
1070
- import { tool } from "ai";
1071
- import { z } from "zod";
1686
+ import * as React11 from "react";
1687
+ import { tool as tool4 } from "ai";
1688
+ import { z as z4 } from "zod";
1072
1689
 
1073
1690
  // src/core/chat-service.ts
1074
1691
  import { generateText, streamText } from "ai";
@@ -1150,11 +1767,65 @@ class InMemoryConversationStore {
1150
1767
  if (options?.status) {
1151
1768
  results = results.filter((c) => c.status === options.status);
1152
1769
  }
1770
+ if (options?.projectId) {
1771
+ results = results.filter((c) => c.projectId === options.projectId);
1772
+ }
1773
+ if (options?.tags && options.tags.length > 0) {
1774
+ const tagSet = new Set(options.tags);
1775
+ results = results.filter((c) => c.tags && c.tags.some((t) => tagSet.has(t)));
1776
+ }
1153
1777
  results.sort((a, b) => b.updatedAt.getTime() - a.updatedAt.getTime());
1154
1778
  const offset = options?.offset ?? 0;
1155
1779
  const limit = options?.limit ?? 100;
1156
1780
  return results.slice(offset, offset + limit);
1157
1781
  }
1782
+ async fork(conversationId, upToMessageId) {
1783
+ const source = this.conversations.get(conversationId);
1784
+ if (!source) {
1785
+ throw new Error(`Conversation ${conversationId} not found`);
1786
+ }
1787
+ let messagesToCopy = source.messages;
1788
+ if (upToMessageId) {
1789
+ const idx = source.messages.findIndex((m) => m.id === upToMessageId);
1790
+ if (idx === -1) {
1791
+ throw new Error(`Message ${upToMessageId} not found`);
1792
+ }
1793
+ messagesToCopy = source.messages.slice(0, idx + 1);
1794
+ }
1795
+ const now = new Date;
1796
+ const forkedMessages = messagesToCopy.map((m) => ({
1797
+ ...m,
1798
+ id: generateId("msg"),
1799
+ conversationId: "",
1800
+ createdAt: new Date(m.createdAt),
1801
+ updatedAt: new Date(m.updatedAt)
1802
+ }));
1803
+ const forked = {
1804
+ ...source,
1805
+ id: generateId("conv"),
1806
+ title: source.title ? `${source.title} (fork)` : undefined,
1807
+ forkedFromId: source.id,
1808
+ createdAt: now,
1809
+ updatedAt: now,
1810
+ messages: forkedMessages
1811
+ };
1812
+ for (const m of forked.messages) {
1813
+ m.conversationId = forked.id;
1814
+ }
1815
+ this.conversations.set(forked.id, forked);
1816
+ return forked;
1817
+ }
1818
+ async truncateAfter(conversationId, messageId) {
1819
+ const conv = this.conversations.get(conversationId);
1820
+ if (!conv)
1821
+ return null;
1822
+ const idx = conv.messages.findIndex((m) => m.id === messageId);
1823
+ if (idx === -1)
1824
+ return null;
1825
+ conv.messages = conv.messages.slice(0, idx + 1);
1826
+ conv.updatedAt = new Date;
1827
+ return conv;
1828
+ }
1158
1829
  async search(query, limit = 20) {
1159
1830
  const lowerQuery = query.toLowerCase();
1160
1831
  const results = [];
@@ -1180,43 +1851,566 @@ function createInMemoryConversationStore() {
1180
1851
  return new InMemoryConversationStore;
1181
1852
  }
1182
1853
 
1183
- // src/core/chat-service.ts
1184
- var DEFAULT_SYSTEM_PROMPT = `You are ContractSpec AI, an expert coding assistant specialized in ContractSpec development.
1854
+ // src/core/workflow-tools.ts
1855
+ import { tool } from "ai";
1856
+ import { z } from "zod";
1857
+ import {
1858
+ WorkflowComposer,
1859
+ validateExtension
1860
+ } from "@contractspec/lib.workflow-composer";
1861
+ var StepTypeSchema = z.enum(["human", "automation", "decision"]);
1862
+ var StepActionSchema = z.object({
1863
+ operation: z.object({
1864
+ name: z.string(),
1865
+ version: z.number()
1866
+ }).optional(),
1867
+ form: z.object({
1868
+ key: z.string(),
1869
+ version: z.number()
1870
+ }).optional()
1871
+ }).optional();
1872
+ var StepSchema = z.object({
1873
+ id: z.string(),
1874
+ type: StepTypeSchema,
1875
+ label: z.string(),
1876
+ description: z.string().optional(),
1877
+ action: StepActionSchema
1878
+ });
1879
+ var StepInjectionSchema = z.object({
1880
+ after: z.string().optional(),
1881
+ before: z.string().optional(),
1882
+ inject: StepSchema,
1883
+ transitionTo: z.string().optional(),
1884
+ transitionFrom: z.string().optional(),
1885
+ when: z.string().optional()
1886
+ });
1887
+ var WorkflowExtensionInputSchema = z.object({
1888
+ workflow: z.string(),
1889
+ tenantId: z.string().optional(),
1890
+ role: z.string().optional(),
1891
+ priority: z.number().optional(),
1892
+ customSteps: z.array(StepInjectionSchema).optional(),
1893
+ hiddenSteps: z.array(z.string()).optional()
1894
+ });
1895
+ function createWorkflowTools(config) {
1896
+ const { baseWorkflows, composer } = config;
1897
+ const baseByKey = new Map(baseWorkflows.map((b) => [b.meta.key, b]));
1898
+ const createWorkflowExtensionTool = tool({
1899
+ description: "Create or validate a workflow extension. Use when the user asks to add steps, modify a workflow, or create a tenant-specific extension. The extension targets an existing base workflow.",
1900
+ inputSchema: WorkflowExtensionInputSchema,
1901
+ execute: async (input) => {
1902
+ const extension = {
1903
+ workflow: input.workflow,
1904
+ tenantId: input.tenantId,
1905
+ role: input.role,
1906
+ priority: input.priority,
1907
+ customSteps: input.customSteps,
1908
+ hiddenSteps: input.hiddenSteps
1909
+ };
1910
+ const base = baseByKey.get(input.workflow);
1911
+ if (!base) {
1912
+ return {
1913
+ success: false,
1914
+ error: `Base workflow "${input.workflow}" not found. Available: ${Array.from(baseByKey.keys()).join(", ")}`,
1915
+ extension
1916
+ };
1917
+ }
1918
+ try {
1919
+ validateExtension(extension, base);
1920
+ return {
1921
+ success: true,
1922
+ message: "Extension validated successfully",
1923
+ extension
1924
+ };
1925
+ } catch (err) {
1926
+ return {
1927
+ success: false,
1928
+ error: err instanceof Error ? err.message : String(err),
1929
+ extension
1930
+ };
1931
+ }
1932
+ }
1933
+ });
1934
+ const composeWorkflowInputSchema = z.object({
1935
+ workflowKey: z.string().describe("Base workflow meta.key"),
1936
+ tenantId: z.string().optional(),
1937
+ role: z.string().optional(),
1938
+ extensions: z.array(WorkflowExtensionInputSchema).optional().describe("Extensions to register before composing")
1939
+ });
1940
+ const composeWorkflowTool = tool({
1941
+ description: "Compose a workflow by applying registered extensions to a base workflow. Returns the composed WorkflowSpec.",
1942
+ inputSchema: composeWorkflowInputSchema,
1943
+ execute: async (input) => {
1944
+ const base = baseByKey.get(input.workflowKey);
1945
+ if (!base) {
1946
+ return {
1947
+ success: false,
1948
+ error: `Base workflow "${input.workflowKey}" not found. Available: ${Array.from(baseByKey.keys()).join(", ")}`
1949
+ };
1950
+ }
1951
+ const comp = composer ?? new WorkflowComposer;
1952
+ if (input.extensions?.length) {
1953
+ for (const ext of input.extensions) {
1954
+ comp.register({
1955
+ workflow: ext.workflow,
1956
+ tenantId: ext.tenantId,
1957
+ role: ext.role,
1958
+ priority: ext.priority,
1959
+ customSteps: ext.customSteps,
1960
+ hiddenSteps: ext.hiddenSteps
1961
+ });
1962
+ }
1963
+ }
1964
+ try {
1965
+ const composed = comp.compose({
1966
+ base,
1967
+ tenantId: input.tenantId,
1968
+ role: input.role
1969
+ });
1970
+ return {
1971
+ success: true,
1972
+ workflow: composed,
1973
+ meta: composed.meta,
1974
+ stepIds: composed.definition.steps.map((s) => s.id)
1975
+ };
1976
+ } catch (err) {
1977
+ return {
1978
+ success: false,
1979
+ error: err instanceof Error ? err.message : String(err)
1980
+ };
1981
+ }
1982
+ }
1983
+ });
1984
+ const generateWorkflowSpecCodeInputSchema = z.object({
1985
+ workflowKey: z.string().describe("Workflow meta.key"),
1986
+ composedSteps: z.array(z.object({
1987
+ id: z.string(),
1988
+ type: z.enum(["human", "automation", "decision"]),
1989
+ label: z.string(),
1990
+ description: z.string().optional()
1991
+ })).optional().describe("Steps to include; if omitted, uses the base workflow")
1992
+ });
1993
+ const generateWorkflowSpecCodeTool = tool({
1994
+ description: "Generate TypeScript code for a workflow spec. Use after composing a workflow to output the spec as code the user can save.",
1995
+ inputSchema: generateWorkflowSpecCodeInputSchema,
1996
+ execute: async (input) => {
1997
+ const base = baseByKey.get(input.workflowKey);
1998
+ if (!base) {
1999
+ return {
2000
+ success: false,
2001
+ error: `Base workflow "${input.workflowKey}" not found. Available: ${Array.from(baseByKey.keys()).join(", ")}`,
2002
+ code: null
2003
+ };
2004
+ }
2005
+ const steps = input.composedSteps ?? base.definition.steps;
2006
+ const specVarName = toPascalCase((base.meta.key.split(".").pop() ?? "Workflow") + "") + "Workflow";
2007
+ const stepsCode = steps.map((s) => ` {
2008
+ id: '${s.id}',
2009
+ type: '${s.type}',
2010
+ label: '${escapeString(s.label)}',${s.description ? `
2011
+ description: '${escapeString(s.description)}',` : ""}
2012
+ }`).join(`,
2013
+ `);
2014
+ const meta = base.meta;
2015
+ const transitionsJson = JSON.stringify(base.definition.transitions, null, 6);
2016
+ const code = `import type { WorkflowSpec } from '@contractspec/lib.contracts-spec/workflow';
1185
2017
 
1186
- Your capabilities:
1187
- - Help users create, modify, and understand ContractSpec specifications
1188
- - Generate code that follows ContractSpec patterns and best practices
1189
- - Explain concepts from the ContractSpec documentation
1190
- - Suggest improvements and identify issues in specs and implementations
2018
+ /**
2019
+ * Workflow: ${base.meta.key}
2020
+ * Generated via AI chat workflow tools.
2021
+ */
2022
+ export const ${specVarName}: WorkflowSpec = {
2023
+ meta: {
2024
+ key: '${base.meta.key}',
2025
+ version: '${String(base.meta.version)}',
2026
+ title: '${escapeString(meta.title ?? base.meta.key)}',
2027
+ description: '${escapeString(meta.description ?? "")}',
2028
+ },
2029
+ definition: {
2030
+ entryStepId: '${base.definition.entryStepId ?? base.definition.steps[0]?.id ?? ""}',
2031
+ steps: [
2032
+ ${stepsCode}
2033
+ ],
2034
+ transitions: ${transitionsJson},
2035
+ },
2036
+ };
2037
+ `;
2038
+ return {
2039
+ success: true,
2040
+ code,
2041
+ workflowKey: input.workflowKey
2042
+ };
2043
+ }
2044
+ });
2045
+ return {
2046
+ create_workflow_extension: createWorkflowExtensionTool,
2047
+ compose_workflow: composeWorkflowTool,
2048
+ generate_workflow_spec_code: generateWorkflowSpecCodeTool
2049
+ };
2050
+ }
2051
+ function toPascalCase(value) {
2052
+ return value.split(/[-_.]/).filter(Boolean).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join("");
2053
+ }
2054
+ function escapeString(value) {
2055
+ return value.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
2056
+ }
1191
2057
 
1192
- Guidelines:
1193
- - Be concise but thorough
1194
- - Provide code examples when helpful
1195
- - Reference relevant ContractSpec concepts and patterns
1196
- - Ask clarifying questions when the user's intent is unclear
1197
- - When suggesting code changes, explain the rationale`;
2058
+ // src/core/contracts-context.ts
2059
+ function buildContractsContextPrompt(config) {
2060
+ const parts = [];
2061
+ if (!config.agentSpecs?.length && !config.dataViewSpecs?.length && !config.formSpecs?.length && !config.presentationSpecs?.length && !config.operationRefs?.length) {
2062
+ return "";
2063
+ }
2064
+ parts.push(`
1198
2065
 
1199
- class ChatService {
1200
- provider;
1201
- context;
1202
- store;
1203
- systemPrompt;
1204
- maxHistoryMessages;
1205
- onUsage;
1206
- tools;
1207
- sendReasoning;
1208
- sendSources;
1209
- constructor(config) {
1210
- this.provider = config.provider;
2066
+ ## Available resources`);
2067
+ if (config.agentSpecs?.length) {
2068
+ parts.push(`
2069
+ ### Agent tools`);
2070
+ for (const agent of config.agentSpecs) {
2071
+ const toolNames = agent.tools?.map((t) => t.name).join(", ") ?? "none";
2072
+ parts.push(`- **${agent.key}**: tools: ${toolNames}`);
2073
+ }
2074
+ }
2075
+ if (config.dataViewSpecs?.length) {
2076
+ parts.push(`
2077
+ ### Data views`);
2078
+ for (const dv of config.dataViewSpecs) {
2079
+ parts.push(`- **${dv.key}**: ${dv.meta.title ?? dv.key}`);
2080
+ }
2081
+ }
2082
+ if (config.formSpecs?.length) {
2083
+ parts.push(`
2084
+ ### Forms`);
2085
+ for (const form of config.formSpecs) {
2086
+ parts.push(`- **${form.key}**: ${form.meta.title ?? form.key}`);
2087
+ }
2088
+ }
2089
+ if (config.presentationSpecs?.length) {
2090
+ parts.push(`
2091
+ ### Presentations`);
2092
+ for (const pres of config.presentationSpecs) {
2093
+ parts.push(`- **${pres.key}**: ${pres.meta.title ?? pres.key} (targets: ${pres.targets?.join(", ") ?? "react"})`);
2094
+ }
2095
+ }
2096
+ if (config.operationRefs?.length) {
2097
+ parts.push(`
2098
+ ### Operations`);
2099
+ for (const op of config.operationRefs) {
2100
+ parts.push(`- **${op.key}@${op.version}**`);
2101
+ }
2102
+ }
2103
+ parts.push(`
2104
+ Use the available tools to invoke operations, query data views, or propose surface changes when appropriate.`);
2105
+ return parts.join(`
2106
+ `);
2107
+ }
2108
+
2109
+ // src/core/agent-tools-adapter.ts
2110
+ import { tool as tool2 } from "ai";
2111
+ import { z as z2 } from "zod";
2112
+ function getInputSchema(_schema) {
2113
+ return z2.object({}).passthrough();
2114
+ }
2115
+ function agentToolConfigsToToolSet(configs, handlers) {
2116
+ const result = {};
2117
+ for (const config of configs) {
2118
+ const handler = handlers?.[config.name];
2119
+ const inputSchema = getInputSchema(config.schema);
2120
+ result[config.name] = tool2({
2121
+ description: config.description ?? config.name,
2122
+ inputSchema,
2123
+ execute: async (input) => {
2124
+ if (!handler) {
2125
+ return {
2126
+ status: "unimplemented",
2127
+ message: "Wire handler in host",
2128
+ toolName: config.name
2129
+ };
2130
+ }
2131
+ try {
2132
+ const output = await Promise.resolve(handler(input));
2133
+ return typeof output === "string" ? output : output;
2134
+ } catch (err) {
2135
+ return {
2136
+ status: "error",
2137
+ error: err instanceof Error ? err.message : String(err),
2138
+ toolName: config.name
2139
+ };
2140
+ }
2141
+ }
2142
+ });
2143
+ }
2144
+ return result;
2145
+ }
2146
+
2147
+ // src/core/surface-planner-tools.ts
2148
+ import { tool as tool3 } from "ai";
2149
+ import { z as z3 } from "zod";
2150
+ import { validatePatchProposal } from "@contractspec/lib.surface-runtime/spec/validate-surface-patch";
2151
+ import { buildSurfacePatchProposal } from "@contractspec/lib.surface-runtime/runtime/planner-tools";
2152
+ var VALID_OPS = [
2153
+ "insert-node",
2154
+ "replace-node",
2155
+ "remove-node",
2156
+ "move-node",
2157
+ "resize-panel",
2158
+ "set-layout",
2159
+ "reveal-field",
2160
+ "hide-field",
2161
+ "promote-action",
2162
+ "set-focus"
2163
+ ];
2164
+ var DEFAULT_NODE_KINDS = [
2165
+ "entity-section",
2166
+ "entity-card",
2167
+ "data-view",
2168
+ "assistant-panel",
2169
+ "chat-thread",
2170
+ "action-bar",
2171
+ "timeline",
2172
+ "table",
2173
+ "rich-doc",
2174
+ "form",
2175
+ "chart",
2176
+ "custom-widget"
2177
+ ];
2178
+ function collectSlotIdsFromRegion(node) {
2179
+ const ids = [];
2180
+ if (node.type === "slot") {
2181
+ ids.push(node.slotId);
2182
+ }
2183
+ if (node.type === "panel-group" || node.type === "stack") {
2184
+ for (const child of node.children) {
2185
+ ids.push(...collectSlotIdsFromRegion(child));
2186
+ }
2187
+ }
2188
+ if (node.type === "tabs") {
2189
+ for (const tab of node.tabs) {
2190
+ ids.push(...collectSlotIdsFromRegion(tab.child));
2191
+ }
2192
+ }
2193
+ if (node.type === "floating") {
2194
+ ids.push(node.anchorSlotId);
2195
+ ids.push(...collectSlotIdsFromRegion(node.child));
2196
+ }
2197
+ return ids;
2198
+ }
2199
+ function deriveConstraints(plan) {
2200
+ const slotIds = collectSlotIdsFromRegion(plan.layoutRoot);
2201
+ const uniqueSlots = [...new Set(slotIds)];
2202
+ return {
2203
+ allowedOps: VALID_OPS,
2204
+ allowedSlots: uniqueSlots.length > 0 ? uniqueSlots : ["assistant", "primary"],
2205
+ allowedNodeKinds: DEFAULT_NODE_KINDS
2206
+ };
2207
+ }
2208
+ var ProposePatchInputSchema = z3.object({
2209
+ proposalId: z3.string().describe("Unique proposal identifier"),
2210
+ ops: z3.array(z3.object({
2211
+ op: z3.enum([
2212
+ "insert-node",
2213
+ "replace-node",
2214
+ "remove-node",
2215
+ "move-node",
2216
+ "resize-panel",
2217
+ "set-layout",
2218
+ "reveal-field",
2219
+ "hide-field",
2220
+ "promote-action",
2221
+ "set-focus"
2222
+ ]),
2223
+ slotId: z3.string().optional(),
2224
+ nodeId: z3.string().optional(),
2225
+ toSlotId: z3.string().optional(),
2226
+ index: z3.number().optional(),
2227
+ node: z3.object({
2228
+ nodeId: z3.string(),
2229
+ kind: z3.string(),
2230
+ title: z3.string().optional(),
2231
+ props: z3.record(z3.string(), z3.unknown()).optional(),
2232
+ children: z3.array(z3.unknown()).optional()
2233
+ }).optional(),
2234
+ persistKey: z3.string().optional(),
2235
+ sizes: z3.array(z3.number()).optional(),
2236
+ layoutId: z3.string().optional(),
2237
+ fieldId: z3.string().optional(),
2238
+ actionId: z3.string().optional(),
2239
+ placement: z3.enum(["header", "inline", "context", "assistant"]).optional(),
2240
+ targetId: z3.string().optional()
2241
+ }))
2242
+ });
2243
+ function createSurfacePlannerTools(config) {
2244
+ const { plan, constraints, onPatchProposal } = config;
2245
+ const resolvedConstraints = constraints ?? deriveConstraints(plan);
2246
+ const proposePatchTool = tool3({
2247
+ description: "Propose surface patches (layout changes, node insertions, etc.) for user approval. " + "Only use allowed ops, slots, and node kinds from the planner context.",
2248
+ inputSchema: ProposePatchInputSchema,
2249
+ execute: async (input) => {
2250
+ const ops = input.ops;
2251
+ try {
2252
+ validatePatchProposal(ops, resolvedConstraints);
2253
+ const proposal = buildSurfacePatchProposal(input.proposalId, ops);
2254
+ onPatchProposal?.(proposal);
2255
+ return {
2256
+ success: true,
2257
+ proposalId: proposal.proposalId,
2258
+ opsCount: proposal.ops.length,
2259
+ message: "Patch proposal validated; awaiting user approval"
2260
+ };
2261
+ } catch (err) {
2262
+ return {
2263
+ success: false,
2264
+ error: err instanceof Error ? err.message : String(err),
2265
+ proposalId: input.proposalId
2266
+ };
2267
+ }
2268
+ }
2269
+ });
2270
+ return {
2271
+ "propose-patch": proposePatchTool
2272
+ };
2273
+ }
2274
+ function buildPlannerPromptInput(plan) {
2275
+ const constraints = deriveConstraints(plan);
2276
+ return {
2277
+ bundleMeta: {
2278
+ key: plan.bundleKey,
2279
+ version: "0.0.0",
2280
+ title: plan.bundleKey
2281
+ },
2282
+ surfaceId: plan.surfaceId,
2283
+ allowedPatchOps: constraints.allowedOps,
2284
+ allowedSlots: [...constraints.allowedSlots],
2285
+ allowedNodeKinds: [...constraints.allowedNodeKinds],
2286
+ actions: plan.actions.map((a) => ({ actionId: a.actionId, title: a.title })),
2287
+ preferences: {
2288
+ guidance: "hints",
2289
+ density: "standard",
2290
+ dataDepth: "detailed",
2291
+ control: "standard",
2292
+ media: "text",
2293
+ pace: "balanced",
2294
+ narrative: "top-down"
2295
+ }
2296
+ };
2297
+ }
2298
+
2299
+ // src/core/chat-service.ts
2300
+ import { compilePlannerPrompt } from "@contractspec/lib.surface-runtime/runtime/planner-prompt";
2301
+ var DEFAULT_SYSTEM_PROMPT = `You are ContractSpec AI, an expert coding assistant specialized in ContractSpec development.
2302
+
2303
+ Your capabilities:
2304
+ - Help users create, modify, and understand ContractSpec specifications
2305
+ - Generate code that follows ContractSpec patterns and best practices
2306
+ - Explain concepts from the ContractSpec documentation
2307
+ - Suggest improvements and identify issues in specs and implementations
2308
+
2309
+ Guidelines:
2310
+ - Be concise but thorough
2311
+ - Provide code examples when helpful
2312
+ - Reference relevant ContractSpec concepts and patterns
2313
+ - Ask clarifying questions when the user's intent is unclear
2314
+ - When suggesting code changes, explain the rationale`;
2315
+ var WORKFLOW_TOOLS_PROMPT = `
2316
+
2317
+ Workflow creation: You can create and modify workflows. Use create_workflow_extension when the user asks to add steps, change a workflow, or create a tenant-specific extension. Use compose_workflow to apply extensions to a base workflow. Use generate_workflow_spec_code to output TypeScript for the user to save.`;
2318
+
2319
+ class ChatService {
2320
+ provider;
2321
+ context;
2322
+ store;
2323
+ systemPrompt;
2324
+ maxHistoryMessages;
2325
+ onUsage;
2326
+ tools;
2327
+ thinkingLevel;
2328
+ sendReasoning;
2329
+ sendSources;
2330
+ modelSelector;
2331
+ constructor(config) {
2332
+ this.provider = config.provider;
1211
2333
  this.context = config.context;
1212
2334
  this.store = config.store ?? new InMemoryConversationStore;
1213
- this.systemPrompt = config.systemPrompt ?? DEFAULT_SYSTEM_PROMPT;
2335
+ this.systemPrompt = this.buildSystemPrompt(config);
1214
2336
  this.maxHistoryMessages = config.maxHistoryMessages ?? 20;
1215
2337
  this.onUsage = config.onUsage;
1216
- this.tools = config.tools;
1217
- this.sendReasoning = config.sendReasoning ?? false;
2338
+ this.tools = this.mergeTools(config);
2339
+ this.thinkingLevel = config.thinkingLevel;
2340
+ this.modelSelector = config.modelSelector;
2341
+ this.sendReasoning = config.sendReasoning ?? (config.thinkingLevel != null && config.thinkingLevel !== "instant");
1218
2342
  this.sendSources = config.sendSources ?? false;
1219
2343
  }
2344
+ buildSystemPrompt(config) {
2345
+ let base = config.systemPrompt ?? DEFAULT_SYSTEM_PROMPT;
2346
+ if (config.workflowToolsConfig?.baseWorkflows?.length) {
2347
+ base += WORKFLOW_TOOLS_PROMPT;
2348
+ }
2349
+ const contractsPrompt = buildContractsContextPrompt(config.contractsContext ?? {});
2350
+ if (contractsPrompt) {
2351
+ base += contractsPrompt;
2352
+ }
2353
+ if (config.surfacePlanConfig?.plan) {
2354
+ const plannerInput = buildPlannerPromptInput(config.surfacePlanConfig.plan);
2355
+ base += `
2356
+
2357
+ ` + compilePlannerPrompt(plannerInput);
2358
+ }
2359
+ return base;
2360
+ }
2361
+ mergeTools(config) {
2362
+ let merged = config.tools ?? {};
2363
+ const wfConfig = config.workflowToolsConfig;
2364
+ if (wfConfig?.baseWorkflows?.length) {
2365
+ const workflowTools = createWorkflowTools({
2366
+ baseWorkflows: wfConfig.baseWorkflows,
2367
+ composer: wfConfig.composer
2368
+ });
2369
+ merged = { ...merged, ...workflowTools };
2370
+ }
2371
+ const contractsCtx = config.contractsContext;
2372
+ if (contractsCtx?.agentSpecs?.length) {
2373
+ const allTools = [];
2374
+ for (const agent of contractsCtx.agentSpecs) {
2375
+ if (agent.tools?.length)
2376
+ allTools.push(...agent.tools);
2377
+ }
2378
+ if (allTools.length > 0) {
2379
+ const agentTools = agentToolConfigsToToolSet(allTools);
2380
+ merged = { ...merged, ...agentTools };
2381
+ }
2382
+ }
2383
+ const surfaceConfig = config.surfacePlanConfig;
2384
+ if (surfaceConfig?.plan) {
2385
+ const plannerTools = createSurfacePlannerTools({
2386
+ plan: surfaceConfig.plan,
2387
+ onPatchProposal: surfaceConfig.onPatchProposal
2388
+ });
2389
+ merged = { ...merged, ...plannerTools };
2390
+ }
2391
+ if (config.mcpTools && Object.keys(config.mcpTools).length > 0) {
2392
+ merged = { ...merged, ...config.mcpTools };
2393
+ }
2394
+ return Object.keys(merged).length > 0 ? merged : undefined;
2395
+ }
2396
+ async resolveModel() {
2397
+ if (this.modelSelector) {
2398
+ const dimension = this.thinkingLevelToDimension(this.thinkingLevel);
2399
+ const { model, selection } = await this.modelSelector.selectAndCreate({
2400
+ taskDimension: dimension
2401
+ });
2402
+ return { model, providerName: selection.providerKey };
2403
+ }
2404
+ return {
2405
+ model: this.provider.getModel(),
2406
+ providerName: this.provider.name
2407
+ };
2408
+ }
2409
+ thinkingLevelToDimension(level) {
2410
+ if (!level || level === "instant")
2411
+ return "latency";
2412
+ return "reasoning";
2413
+ }
1220
2414
  async send(options) {
1221
2415
  let conversation;
1222
2416
  if (options.conversationId) {
@@ -1234,20 +2428,25 @@ class ChatService {
1234
2428
  workspacePath: this.context?.workspacePath
1235
2429
  });
1236
2430
  }
1237
- await this.store.appendMessage(conversation.id, {
1238
- role: "user",
1239
- content: options.content,
1240
- status: "completed",
1241
- attachments: options.attachments
1242
- });
2431
+ if (!options.skipUserAppend) {
2432
+ await this.store.appendMessage(conversation.id, {
2433
+ role: "user",
2434
+ content: options.content,
2435
+ status: "completed",
2436
+ attachments: options.attachments
2437
+ });
2438
+ }
2439
+ conversation = await this.store.get(conversation.id) ?? conversation;
1243
2440
  const messages = this.buildMessages(conversation, options);
1244
- const model = this.provider.getModel();
2441
+ const { model, providerName } = await this.resolveModel();
2442
+ const providerOptions = getProviderOptions(this.thinkingLevel, providerName);
1245
2443
  try {
1246
2444
  const result = await generateText({
1247
2445
  model,
1248
2446
  messages,
1249
2447
  system: this.systemPrompt,
1250
- tools: this.tools
2448
+ tools: this.tools,
2449
+ providerOptions: Object.keys(providerOptions).length > 0 ? providerOptions : undefined
1251
2450
  });
1252
2451
  const assistantMessage = await this.store.appendMessage(conversation.id, {
1253
2452
  role: "assistant",
@@ -1292,23 +2491,27 @@ class ChatService {
1292
2491
  workspacePath: this.context?.workspacePath
1293
2492
  });
1294
2493
  }
1295
- await this.store.appendMessage(conversation.id, {
1296
- role: "user",
1297
- content: options.content,
1298
- status: "completed",
1299
- attachments: options.attachments
1300
- });
2494
+ if (!options.skipUserAppend) {
2495
+ await this.store.appendMessage(conversation.id, {
2496
+ role: "user",
2497
+ content: options.content,
2498
+ status: "completed",
2499
+ attachments: options.attachments
2500
+ });
2501
+ }
2502
+ conversation = await this.store.get(conversation.id) ?? conversation;
1301
2503
  const assistantMessage = await this.store.appendMessage(conversation.id, {
1302
2504
  role: "assistant",
1303
2505
  content: "",
1304
2506
  status: "streaming"
1305
2507
  });
1306
2508
  const messages = this.buildMessages(conversation, options);
1307
- const model = this.provider.getModel();
2509
+ const { model, providerName } = await this.resolveModel();
1308
2510
  const systemPrompt = this.systemPrompt;
1309
2511
  const tools = this.tools;
1310
2512
  const store = this.store;
1311
2513
  const onUsage = this.onUsage;
2514
+ const streamProviderOptions = getProviderOptions(this.thinkingLevel, providerName);
1312
2515
  async function* streamGenerator() {
1313
2516
  let fullContent = "";
1314
2517
  let fullReasoning = "";
@@ -1319,7 +2522,8 @@ class ChatService {
1319
2522
  model,
1320
2523
  messages,
1321
2524
  system: systemPrompt,
1322
- tools
2525
+ tools,
2526
+ providerOptions: Object.keys(streamProviderOptions).length > 0 ? streamProviderOptions : undefined
1323
2527
  });
1324
2528
  for await (const part of result.fullStream) {
1325
2529
  if (part.type === "text-delta") {
@@ -1434,6 +2638,18 @@ class ChatService {
1434
2638
  ...options
1435
2639
  });
1436
2640
  }
2641
+ async updateConversation(conversationId, updates) {
2642
+ return this.store.update(conversationId, updates);
2643
+ }
2644
+ async forkConversation(conversationId, upToMessageId) {
2645
+ return this.store.fork(conversationId, upToMessageId);
2646
+ }
2647
+ async updateMessage(conversationId, messageId, updates) {
2648
+ return this.store.updateMessage(conversationId, messageId, updates);
2649
+ }
2650
+ async truncateAfter(conversationId, messageId) {
2651
+ return this.store.truncateAfter(conversationId, messageId);
2652
+ }
1437
2653
  async deleteConversation(conversationId) {
1438
2654
  return this.store.delete(conversationId);
1439
2655
  }
@@ -1504,9 +2720,9 @@ import {
1504
2720
  function toolsToToolSet(defs) {
1505
2721
  const result = {};
1506
2722
  for (const def of defs) {
1507
- result[def.name] = tool({
2723
+ result[def.name] = tool4({
1508
2724
  description: def.description ?? def.name,
1509
- inputSchema: z.object({}).passthrough(),
2725
+ inputSchema: z4.object({}).passthrough(),
1510
2726
  execute: async () => ({})
1511
2727
  });
1512
2728
  }
@@ -1520,22 +2736,64 @@ function useChat(options = {}) {
1520
2736
  apiKey,
1521
2737
  proxyUrl,
1522
2738
  conversationId: initialConversationId,
2739
+ store,
1523
2740
  systemPrompt,
1524
2741
  streaming = true,
1525
2742
  onSend,
1526
2743
  onResponse,
1527
2744
  onError,
1528
2745
  onUsage,
1529
- tools: toolsDefs
2746
+ tools: toolsDefs,
2747
+ thinkingLevel,
2748
+ workflowToolsConfig,
2749
+ modelSelector,
2750
+ contractsContext,
2751
+ surfacePlanConfig,
2752
+ mcpServers,
2753
+ agentMode
1530
2754
  } = options;
1531
- const [messages, setMessages] = React6.useState([]);
1532
- const [conversation, setConversation] = React6.useState(null);
1533
- const [isLoading, setIsLoading] = React6.useState(false);
1534
- const [error, setError] = React6.useState(null);
1535
- const [conversationId, setConversationId] = React6.useState(initialConversationId ?? null);
1536
- const abortControllerRef = React6.useRef(null);
1537
- const chatServiceRef = React6.useRef(null);
1538
- React6.useEffect(() => {
2755
+ const [messages, setMessages] = React11.useState([]);
2756
+ const [mcpTools, setMcpTools] = React11.useState(null);
2757
+ const mcpCleanupRef = React11.useRef(null);
2758
+ const [conversation, setConversation] = React11.useState(null);
2759
+ const [isLoading, setIsLoading] = React11.useState(false);
2760
+ const [error, setError] = React11.useState(null);
2761
+ const [conversationId, setConversationId] = React11.useState(initialConversationId ?? null);
2762
+ const abortControllerRef = React11.useRef(null);
2763
+ const chatServiceRef = React11.useRef(null);
2764
+ React11.useEffect(() => {
2765
+ if (!mcpServers?.length) {
2766
+ setMcpTools(null);
2767
+ return;
2768
+ }
2769
+ let cancelled = false;
2770
+ import("@contractspec/lib.ai-agent/tools/mcp-client").then(({ createMcpToolsets }) => {
2771
+ createMcpToolsets(mcpServers).then(({ tools, cleanup }) => {
2772
+ if (!cancelled) {
2773
+ setMcpTools(tools);
2774
+ mcpCleanupRef.current = cleanup;
2775
+ } else {
2776
+ cleanup().catch(() => {
2777
+ return;
2778
+ });
2779
+ }
2780
+ }).catch(() => {
2781
+ if (!cancelled)
2782
+ setMcpTools(null);
2783
+ });
2784
+ });
2785
+ return () => {
2786
+ cancelled = true;
2787
+ const cleanup = mcpCleanupRef.current;
2788
+ mcpCleanupRef.current = null;
2789
+ if (cleanup)
2790
+ cleanup().catch(() => {
2791
+ return;
2792
+ });
2793
+ setMcpTools(null);
2794
+ };
2795
+ }, [mcpServers]);
2796
+ React11.useEffect(() => {
1539
2797
  const chatProvider = createProvider({
1540
2798
  provider,
1541
2799
  model,
@@ -1544,9 +2802,16 @@ function useChat(options = {}) {
1544
2802
  });
1545
2803
  chatServiceRef.current = new ChatService({
1546
2804
  provider: chatProvider,
2805
+ store,
1547
2806
  systemPrompt,
1548
2807
  onUsage,
1549
- tools: toolsDefs?.length ? toolsToToolSet(toolsDefs) : undefined
2808
+ tools: toolsDefs?.length ? toolsToToolSet(toolsDefs) : undefined,
2809
+ thinkingLevel,
2810
+ workflowToolsConfig,
2811
+ modelSelector,
2812
+ contractsContext,
2813
+ surfacePlanConfig,
2814
+ mcpTools
1550
2815
  });
1551
2816
  }, [
1552
2817
  provider,
@@ -1554,11 +2819,18 @@ function useChat(options = {}) {
1554
2819
  model,
1555
2820
  apiKey,
1556
2821
  proxyUrl,
2822
+ store,
1557
2823
  systemPrompt,
1558
2824
  onUsage,
1559
- toolsDefs
2825
+ toolsDefs,
2826
+ thinkingLevel,
2827
+ workflowToolsConfig,
2828
+ modelSelector,
2829
+ contractsContext,
2830
+ surfacePlanConfig,
2831
+ mcpTools
1560
2832
  ]);
1561
- React6.useEffect(() => {
2833
+ React11.useEffect(() => {
1562
2834
  if (!conversationId || !chatServiceRef.current)
1563
2835
  return;
1564
2836
  const loadConversation = async () => {
@@ -1572,7 +2844,90 @@ function useChat(options = {}) {
1572
2844
  };
1573
2845
  loadConversation().catch(console.error);
1574
2846
  }, [conversationId]);
1575
- const sendMessage = React6.useCallback(async (content, attachments) => {
2847
+ const sendMessage = React11.useCallback(async (content, attachments, opts) => {
2848
+ if (agentMode?.agent) {
2849
+ setIsLoading(true);
2850
+ setError(null);
2851
+ abortControllerRef.current = new AbortController;
2852
+ try {
2853
+ if (!opts?.skipUserAppend) {
2854
+ const userMessage = {
2855
+ id: `msg_${Date.now()}`,
2856
+ conversationId: conversationId ?? "",
2857
+ role: "user",
2858
+ content,
2859
+ status: "completed",
2860
+ createdAt: new Date,
2861
+ updatedAt: new Date,
2862
+ attachments
2863
+ };
2864
+ setMessages((prev) => [...prev, userMessage]);
2865
+ onSend?.(userMessage);
2866
+ }
2867
+ const result = await agentMode.agent.generate({
2868
+ prompt: content,
2869
+ signal: abortControllerRef.current.signal
2870
+ });
2871
+ const toolCallsMap = new Map;
2872
+ for (const tc of result.toolCalls ?? []) {
2873
+ const tr = result.toolResults?.find((r) => r.toolCallId === tc.toolCallId);
2874
+ toolCallsMap.set(tc.toolCallId, {
2875
+ id: tc.toolCallId,
2876
+ name: tc.toolName,
2877
+ args: tc.args ?? {},
2878
+ result: tr?.output,
2879
+ status: "completed"
2880
+ });
2881
+ }
2882
+ const assistantMessage = {
2883
+ id: `msg_${Date.now()}_a`,
2884
+ conversationId: conversationId ?? "",
2885
+ role: "assistant",
2886
+ content: result.text,
2887
+ status: "completed",
2888
+ createdAt: new Date,
2889
+ updatedAt: new Date,
2890
+ toolCalls: toolCallsMap.size ? Array.from(toolCallsMap.values()) : undefined,
2891
+ usage: result.usage
2892
+ };
2893
+ setMessages((prev) => [...prev, assistantMessage]);
2894
+ onResponse?.(assistantMessage);
2895
+ onUsage?.(result.usage ?? { inputTokens: 0, outputTokens: 0 });
2896
+ if (store && !conversationId) {
2897
+ const conv = await store.create({
2898
+ status: "active",
2899
+ provider: "agent",
2900
+ model: "agent",
2901
+ messages: []
2902
+ });
2903
+ if (!opts?.skipUserAppend) {
2904
+ await store.appendMessage(conv.id, {
2905
+ role: "user",
2906
+ content,
2907
+ status: "completed",
2908
+ attachments
2909
+ });
2910
+ }
2911
+ await store.appendMessage(conv.id, {
2912
+ role: "assistant",
2913
+ content: result.text,
2914
+ status: "completed",
2915
+ toolCalls: assistantMessage.toolCalls,
2916
+ usage: result.usage
2917
+ });
2918
+ const updated = await store.get(conv.id);
2919
+ if (updated)
2920
+ setConversation(updated);
2921
+ setConversationId(conv.id);
2922
+ }
2923
+ } catch (err) {
2924
+ setError(err instanceof Error ? err : new Error(String(err)));
2925
+ onError?.(err instanceof Error ? err : new Error(String(err)));
2926
+ } finally {
2927
+ setIsLoading(false);
2928
+ }
2929
+ return;
2930
+ }
1576
2931
  if (!chatServiceRef.current) {
1577
2932
  throw new Error("Chat service not initialized");
1578
2933
  }
@@ -1580,25 +2935,28 @@ function useChat(options = {}) {
1580
2935
  setError(null);
1581
2936
  abortControllerRef.current = new AbortController;
1582
2937
  try {
1583
- const userMessage = {
1584
- id: `msg_${Date.now()}`,
1585
- conversationId: conversationId ?? "",
1586
- role: "user",
1587
- content,
1588
- status: "completed",
1589
- createdAt: new Date,
1590
- updatedAt: new Date,
1591
- attachments
1592
- };
1593
- setMessages((prev) => [...prev, userMessage]);
1594
- onSend?.(userMessage);
2938
+ if (!opts?.skipUserAppend) {
2939
+ const userMessage = {
2940
+ id: `msg_${Date.now()}`,
2941
+ conversationId: conversationId ?? "",
2942
+ role: "user",
2943
+ content,
2944
+ status: "completed",
2945
+ createdAt: new Date,
2946
+ updatedAt: new Date,
2947
+ attachments
2948
+ };
2949
+ setMessages((prev) => [...prev, userMessage]);
2950
+ onSend?.(userMessage);
2951
+ }
1595
2952
  if (streaming) {
1596
2953
  const result = await chatServiceRef.current.stream({
1597
2954
  conversationId: conversationId ?? undefined,
1598
2955
  content,
1599
- attachments
2956
+ attachments,
2957
+ skipUserAppend: opts?.skipUserAppend
1600
2958
  });
1601
- if (!conversationId) {
2959
+ if (!conversationId && !opts?.skipUserAppend) {
1602
2960
  setConversationId(result.conversationId);
1603
2961
  }
1604
2962
  const assistantMessage = {
@@ -1679,7 +3037,8 @@ function useChat(options = {}) {
1679
3037
  const result = await chatServiceRef.current.send({
1680
3038
  conversationId: conversationId ?? undefined,
1681
3039
  content,
1682
- attachments
3040
+ attachments,
3041
+ skipUserAppend: opts?.skipUserAppend
1683
3042
  });
1684
3043
  setConversation(result.conversation);
1685
3044
  setMessages(result.conversation.messages);
@@ -1696,14 +3055,14 @@ function useChat(options = {}) {
1696
3055
  setIsLoading(false);
1697
3056
  abortControllerRef.current = null;
1698
3057
  }
1699
- }, [conversationId, streaming, onSend, onResponse, onError, messages]);
1700
- const clearConversation = React6.useCallback(() => {
3058
+ }, [conversationId, streaming, onSend, onResponse, onError, onUsage, messages, agentMode, store]);
3059
+ const clearConversation = React11.useCallback(() => {
1701
3060
  setMessages([]);
1702
3061
  setConversation(null);
1703
3062
  setConversationId(null);
1704
3063
  setError(null);
1705
3064
  }, []);
1706
- const regenerate = React6.useCallback(async () => {
3065
+ const regenerate = React11.useCallback(async () => {
1707
3066
  const lastUserMessageIndex = messages.findLastIndex((m) => m.role === "user");
1708
3067
  if (lastUserMessageIndex === -1)
1709
3068
  return;
@@ -1713,11 +3072,49 @@ function useChat(options = {}) {
1713
3072
  setMessages((prev) => prev.slice(0, lastUserMessageIndex + 1));
1714
3073
  await sendMessage(lastUserMessage.content, lastUserMessage.attachments);
1715
3074
  }, [messages, sendMessage]);
1716
- const stop = React6.useCallback(() => {
3075
+ const stop = React11.useCallback(() => {
1717
3076
  abortControllerRef.current?.abort();
1718
3077
  setIsLoading(false);
1719
3078
  }, []);
1720
- const addToolApprovalResponse = React6.useCallback((_toolCallId, _result) => {
3079
+ const createNewConversation = clearConversation;
3080
+ const editMessage = React11.useCallback(async (messageId, newContent) => {
3081
+ if (!chatServiceRef.current || !conversationId)
3082
+ return;
3083
+ const msg = messages.find((m) => m.id === messageId);
3084
+ if (!msg || msg.role !== "user")
3085
+ return;
3086
+ await chatServiceRef.current.updateMessage(conversationId, messageId, { content: newContent });
3087
+ const truncated = await chatServiceRef.current.truncateAfter(conversationId, messageId);
3088
+ if (truncated) {
3089
+ setMessages(truncated.messages);
3090
+ }
3091
+ await sendMessage(newContent, undefined, { skipUserAppend: true });
3092
+ }, [conversationId, messages, sendMessage]);
3093
+ const forkConversation = React11.useCallback(async (upToMessageId) => {
3094
+ if (!chatServiceRef.current)
3095
+ return null;
3096
+ const idToFork = conversationId ?? conversation?.id;
3097
+ if (!idToFork)
3098
+ return null;
3099
+ try {
3100
+ const forked = await chatServiceRef.current.forkConversation(idToFork, upToMessageId);
3101
+ setConversationId(forked.id);
3102
+ setConversation(forked);
3103
+ setMessages(forked.messages);
3104
+ return forked.id;
3105
+ } catch {
3106
+ return null;
3107
+ }
3108
+ }, [conversationId, conversation]);
3109
+ const updateConversationFn = React11.useCallback(async (updates) => {
3110
+ if (!chatServiceRef.current || !conversationId)
3111
+ return null;
3112
+ const updated = await chatServiceRef.current.updateConversation(conversationId, updates);
3113
+ if (updated)
3114
+ setConversation(updated);
3115
+ return updated;
3116
+ }, [conversationId]);
3117
+ const addToolApprovalResponse = React11.useCallback((_toolCallId, _result) => {
1721
3118
  throw new Error(`addToolApprovalResponse: Tool approval requires server route with toUIMessageStreamResponse. ` + `Use createChatRoute and @ai-sdk/react useChat for tools with requireApproval.`);
1722
3119
  }, []);
1723
3120
  const hasApprovalTools = toolsDefs?.some((t) => t.requireApproval) ?? false;
@@ -1731,40 +3128,751 @@ function useChat(options = {}) {
1731
3128
  setConversationId,
1732
3129
  regenerate,
1733
3130
  stop,
3131
+ createNewConversation,
3132
+ editMessage,
3133
+ forkConversation,
3134
+ updateConversation: updateConversationFn,
1734
3135
  ...hasApprovalTools && { addToolApprovalResponse }
1735
3136
  };
1736
3137
  }
1737
- // src/presentation/hooks/useProviders.tsx
1738
- import * as React7 from "react";
1739
- import {
1740
- getAvailableProviders,
1741
- getModelsForProvider as getModelsForProvider2
1742
- } from "@contractspec/lib.ai-providers";
1743
- "use client";
1744
- function useProviders() {
1745
- const [providers, setProviders] = React7.useState([]);
1746
- const [isLoading, setIsLoading] = React7.useState(true);
1747
- const loadProviders = React7.useCallback(async () => {
1748
- setIsLoading(true);
1749
- try {
1750
- const available = getAvailableProviders();
1751
- const providersWithModels = available.map((p) => ({
1752
- ...p,
1753
- models: getModelsForProvider2(p.provider)
1754
- }));
1755
- setProviders(providersWithModels);
1756
- } catch (error) {
1757
- console.error("Failed to load providers:", error);
1758
- } finally {
1759
- setIsLoading(false);
3138
+
3139
+ // src/core/local-storage-conversation-store.ts
3140
+ var DEFAULT_KEY = "contractspec:ai-chat:conversations";
3141
+ function generateId2(prefix) {
3142
+ return `${prefix}_${Date.now()}_${Math.random().toString(36).slice(2, 11)}`;
3143
+ }
3144
+ function toSerializable(conv) {
3145
+ return {
3146
+ ...conv,
3147
+ createdAt: conv.createdAt.toISOString(),
3148
+ updatedAt: conv.updatedAt.toISOString(),
3149
+ messages: conv.messages.map((m) => ({
3150
+ ...m,
3151
+ createdAt: m.createdAt.toISOString(),
3152
+ updatedAt: m.updatedAt.toISOString()
3153
+ }))
3154
+ };
3155
+ }
3156
+ function fromSerializable(raw) {
3157
+ const messages = raw.messages?.map((m) => ({
3158
+ ...m,
3159
+ createdAt: new Date(m.createdAt),
3160
+ updatedAt: new Date(m.updatedAt)
3161
+ })) ?? [];
3162
+ return {
3163
+ ...raw,
3164
+ createdAt: new Date(raw.createdAt),
3165
+ updatedAt: new Date(raw.updatedAt),
3166
+ messages
3167
+ };
3168
+ }
3169
+ function loadAll(key) {
3170
+ if (typeof window === "undefined")
3171
+ return new Map;
3172
+ try {
3173
+ const raw = window.localStorage.getItem(key);
3174
+ if (!raw)
3175
+ return new Map;
3176
+ const arr = JSON.parse(raw);
3177
+ const map = new Map;
3178
+ for (const item of arr) {
3179
+ const conv = fromSerializable(item);
3180
+ map.set(conv.id, conv);
1760
3181
  }
1761
- }, []);
1762
- React7.useEffect(() => {
3182
+ return map;
3183
+ } catch {
3184
+ return new Map;
3185
+ }
3186
+ }
3187
+ function saveAll(key, map) {
3188
+ if (typeof window === "undefined")
3189
+ return;
3190
+ try {
3191
+ const arr = Array.from(map.values()).map(toSerializable);
3192
+ window.localStorage.setItem(key, JSON.stringify(arr));
3193
+ } catch {}
3194
+ }
3195
+
3196
+ class LocalStorageConversationStore {
3197
+ key;
3198
+ cache = null;
3199
+ constructor(storageKey = DEFAULT_KEY) {
3200
+ this.key = storageKey;
3201
+ }
3202
+ getMap() {
3203
+ if (!this.cache) {
3204
+ this.cache = loadAll(this.key);
3205
+ }
3206
+ return this.cache;
3207
+ }
3208
+ persist() {
3209
+ saveAll(this.key, this.getMap());
3210
+ }
3211
+ async get(conversationId) {
3212
+ return this.getMap().get(conversationId) ?? null;
3213
+ }
3214
+ async create(conversation) {
3215
+ const now = new Date;
3216
+ const full = {
3217
+ ...conversation,
3218
+ id: generateId2("conv"),
3219
+ createdAt: now,
3220
+ updatedAt: now
3221
+ };
3222
+ this.getMap().set(full.id, full);
3223
+ this.persist();
3224
+ return full;
3225
+ }
3226
+ async update(conversationId, updates) {
3227
+ const conv = this.getMap().get(conversationId);
3228
+ if (!conv)
3229
+ return null;
3230
+ const updated = {
3231
+ ...conv,
3232
+ ...updates,
3233
+ updatedAt: new Date
3234
+ };
3235
+ this.getMap().set(conversationId, updated);
3236
+ this.persist();
3237
+ return updated;
3238
+ }
3239
+ async appendMessage(conversationId, message) {
3240
+ const conv = this.getMap().get(conversationId);
3241
+ if (!conv)
3242
+ throw new Error(`Conversation ${conversationId} not found`);
3243
+ const now = new Date;
3244
+ const fullMessage = {
3245
+ ...message,
3246
+ id: generateId2("msg"),
3247
+ conversationId,
3248
+ createdAt: now,
3249
+ updatedAt: now
3250
+ };
3251
+ conv.messages.push(fullMessage);
3252
+ conv.updatedAt = now;
3253
+ this.persist();
3254
+ return fullMessage;
3255
+ }
3256
+ async updateMessage(conversationId, messageId, updates) {
3257
+ const conv = this.getMap().get(conversationId);
3258
+ if (!conv)
3259
+ return null;
3260
+ const idx = conv.messages.findIndex((m) => m.id === messageId);
3261
+ if (idx === -1)
3262
+ return null;
3263
+ const msg = conv.messages[idx];
3264
+ if (!msg)
3265
+ return null;
3266
+ const updated = {
3267
+ ...msg,
3268
+ ...updates,
3269
+ updatedAt: new Date
3270
+ };
3271
+ conv.messages[idx] = updated;
3272
+ conv.updatedAt = new Date;
3273
+ this.persist();
3274
+ return updated;
3275
+ }
3276
+ async delete(conversationId) {
3277
+ const deleted = this.getMap().delete(conversationId);
3278
+ if (deleted)
3279
+ this.persist();
3280
+ return deleted;
3281
+ }
3282
+ async list(options) {
3283
+ let results = Array.from(this.getMap().values());
3284
+ if (options?.status) {
3285
+ results = results.filter((c) => c.status === options.status);
3286
+ }
3287
+ if (options?.projectId) {
3288
+ results = results.filter((c) => c.projectId === options.projectId);
3289
+ }
3290
+ if (options?.tags && options.tags.length > 0) {
3291
+ const tagSet = new Set(options.tags);
3292
+ results = results.filter((c) => c.tags && c.tags.some((t) => tagSet.has(t)));
3293
+ }
3294
+ results.sort((a, b) => b.updatedAt.getTime() - a.updatedAt.getTime());
3295
+ const offset = options?.offset ?? 0;
3296
+ const limit = options?.limit ?? 100;
3297
+ return results.slice(offset, offset + limit);
3298
+ }
3299
+ async fork(conversationId, upToMessageId) {
3300
+ const source = this.getMap().get(conversationId);
3301
+ if (!source)
3302
+ throw new Error(`Conversation ${conversationId} not found`);
3303
+ let messagesToCopy = source.messages;
3304
+ if (upToMessageId) {
3305
+ const idx = source.messages.findIndex((m) => m.id === upToMessageId);
3306
+ if (idx === -1)
3307
+ throw new Error(`Message ${upToMessageId} not found`);
3308
+ messagesToCopy = source.messages.slice(0, idx + 1);
3309
+ }
3310
+ const now = new Date;
3311
+ const forkedMessages = messagesToCopy.map((m) => ({
3312
+ ...m,
3313
+ id: generateId2("msg"),
3314
+ conversationId: "",
3315
+ createdAt: new Date(m.createdAt),
3316
+ updatedAt: new Date(m.updatedAt)
3317
+ }));
3318
+ const forked = {
3319
+ ...source,
3320
+ id: generateId2("conv"),
3321
+ title: source.title ? `${source.title} (fork)` : undefined,
3322
+ forkedFromId: source.id,
3323
+ createdAt: now,
3324
+ updatedAt: now,
3325
+ messages: forkedMessages
3326
+ };
3327
+ for (const m of forked.messages) {
3328
+ m.conversationId = forked.id;
3329
+ }
3330
+ this.getMap().set(forked.id, forked);
3331
+ this.persist();
3332
+ return forked;
3333
+ }
3334
+ async truncateAfter(conversationId, messageId) {
3335
+ const conv = this.getMap().get(conversationId);
3336
+ if (!conv)
3337
+ return null;
3338
+ const idx = conv.messages.findIndex((m) => m.id === messageId);
3339
+ if (idx === -1)
3340
+ return null;
3341
+ conv.messages = conv.messages.slice(0, idx + 1);
3342
+ conv.updatedAt = new Date;
3343
+ this.persist();
3344
+ return conv;
3345
+ }
3346
+ async search(query, limit = 20) {
3347
+ const lowerQuery = query.toLowerCase();
3348
+ const results = [];
3349
+ for (const conv of this.getMap().values()) {
3350
+ if (conv.title?.toLowerCase().includes(lowerQuery)) {
3351
+ results.push(conv);
3352
+ continue;
3353
+ }
3354
+ if (conv.messages.some((m) => m.content.toLowerCase().includes(lowerQuery))) {
3355
+ results.push(conv);
3356
+ }
3357
+ if (results.length >= limit)
3358
+ break;
3359
+ }
3360
+ return results;
3361
+ }
3362
+ }
3363
+ function createLocalStorageConversationStore(storageKey) {
3364
+ return new LocalStorageConversationStore(storageKey);
3365
+ }
3366
+
3367
+ // src/presentation/components/ChatWithSidebar.tsx
3368
+ import { jsx as jsx10, jsxs as jsxs10 } from "react/jsx-runtime";
3369
+ "use client";
3370
+ var defaultStore = createLocalStorageConversationStore();
3371
+ function ChatWithSidebar({
3372
+ store = defaultStore,
3373
+ projectId,
3374
+ tags,
3375
+ className,
3376
+ thinkingLevel: initialThinkingLevel = "thinking",
3377
+ presentationRenderer,
3378
+ formRenderer,
3379
+ ...useChatOptions
3380
+ }) {
3381
+ const effectiveStore = store;
3382
+ const [thinkingLevel, setThinkingLevel] = React12.useState(initialThinkingLevel);
3383
+ const chat = useChat({
3384
+ ...useChatOptions,
3385
+ store: effectiveStore,
3386
+ thinkingLevel
3387
+ });
3388
+ const {
3389
+ messages,
3390
+ conversation,
3391
+ sendMessage,
3392
+ isLoading,
3393
+ setConversationId,
3394
+ createNewConversation,
3395
+ editMessage,
3396
+ forkConversation,
3397
+ updateConversation
3398
+ } = chat;
3399
+ const selectedConversationId = conversation?.id ?? null;
3400
+ const handleSelectConversation = React12.useCallback((id) => {
3401
+ setConversationId(id);
3402
+ }, [setConversationId]);
3403
+ return /* @__PURE__ */ jsxs10("div", {
3404
+ className: className ?? "flex h-full w-full",
3405
+ children: [
3406
+ /* @__PURE__ */ jsx10(ChatSidebar, {
3407
+ store: effectiveStore,
3408
+ selectedConversationId,
3409
+ onSelectConversation: handleSelectConversation,
3410
+ onCreateNew: createNewConversation,
3411
+ projectId,
3412
+ tags,
3413
+ selectedConversation: conversation,
3414
+ onUpdateConversation: updateConversation ? async (id, updates) => {
3415
+ if (id === selectedConversationId) {
3416
+ await updateConversation(updates);
3417
+ }
3418
+ } : undefined
3419
+ }),
3420
+ /* @__PURE__ */ jsx10("div", {
3421
+ className: "flex min-w-0 flex-1 flex-col",
3422
+ children: /* @__PURE__ */ jsx10(ChatWithExport, {
3423
+ messages,
3424
+ conversation,
3425
+ onCreateNew: createNewConversation,
3426
+ onFork: forkConversation,
3427
+ onEditMessage: editMessage,
3428
+ thinkingLevel,
3429
+ onThinkingLevelChange: setThinkingLevel,
3430
+ presentationRenderer,
3431
+ formRenderer,
3432
+ children: /* @__PURE__ */ jsx10(ChatInput, {
3433
+ onSend: (content, att) => sendMessage(content, att),
3434
+ disabled: isLoading,
3435
+ isLoading
3436
+ })
3437
+ })
3438
+ })
3439
+ ]
3440
+ });
3441
+ }
3442
+ // src/presentation/components/ModelPicker.tsx
3443
+ import * as React13 from "react";
3444
+ import { cn as cn7 } from "@contractspec/lib.ui-kit-web/ui/utils";
3445
+ import { Button as Button6 } from "@contractspec/lib.design-system";
3446
+ import {
3447
+ Select as Select2,
3448
+ SelectContent as SelectContent2,
3449
+ SelectItem as SelectItem2,
3450
+ SelectTrigger as SelectTrigger2,
3451
+ SelectValue as SelectValue2
3452
+ } from "@contractspec/lib.ui-kit-web/ui/select";
3453
+ import { Badge } from "@contractspec/lib.ui-kit-web/ui/badge";
3454
+ import { Label as Label2 } from "@contractspec/lib.ui-kit-web/ui/label";
3455
+ import { Bot as Bot2, Cloud, Cpu, Sparkles } from "lucide-react";
3456
+ import {
3457
+ getModelsForProvider
3458
+ } from "@contractspec/lib.ai-providers";
3459
+ import { jsx as jsx11, jsxs as jsxs11 } from "react/jsx-runtime";
3460
+ "use client";
3461
+ var PROVIDER_ICONS = {
3462
+ ollama: /* @__PURE__ */ jsx11(Cpu, {
3463
+ className: "h-4 w-4"
3464
+ }),
3465
+ openai: /* @__PURE__ */ jsx11(Bot2, {
3466
+ className: "h-4 w-4"
3467
+ }),
3468
+ anthropic: /* @__PURE__ */ jsx11(Sparkles, {
3469
+ className: "h-4 w-4"
3470
+ }),
3471
+ mistral: /* @__PURE__ */ jsx11(Cloud, {
3472
+ className: "h-4 w-4"
3473
+ }),
3474
+ gemini: /* @__PURE__ */ jsx11(Sparkles, {
3475
+ className: "h-4 w-4"
3476
+ })
3477
+ };
3478
+ var PROVIDER_NAMES = {
3479
+ ollama: "Ollama (Local)",
3480
+ openai: "OpenAI",
3481
+ anthropic: "Anthropic",
3482
+ mistral: "Mistral",
3483
+ gemini: "Google Gemini"
3484
+ };
3485
+ var MODE_BADGES = {
3486
+ local: { label: "Local", variant: "secondary" },
3487
+ byok: { label: "BYOK", variant: "outline" },
3488
+ managed: { label: "Managed", variant: "default" }
3489
+ };
3490
+ function ModelPicker({
3491
+ value,
3492
+ onChange,
3493
+ availableProviders,
3494
+ className,
3495
+ compact = false
3496
+ }) {
3497
+ const providers = availableProviders ?? [
3498
+ { provider: "ollama", available: true, mode: "local" },
3499
+ { provider: "openai", available: true, mode: "byok" },
3500
+ { provider: "anthropic", available: true, mode: "byok" },
3501
+ { provider: "mistral", available: true, mode: "byok" },
3502
+ { provider: "gemini", available: true, mode: "byok" }
3503
+ ];
3504
+ const models = getModelsForProvider(value.provider);
3505
+ const selectedModel = models.find((m) => m.id === value.model);
3506
+ const handleProviderChange = React13.useCallback((providerName) => {
3507
+ const provider = providerName;
3508
+ const providerInfo = providers.find((p) => p.provider === provider);
3509
+ const providerModels = getModelsForProvider(provider);
3510
+ const defaultModel = providerModels[0]?.id ?? "";
3511
+ onChange({
3512
+ provider,
3513
+ model: defaultModel,
3514
+ mode: providerInfo?.mode ?? "byok"
3515
+ });
3516
+ }, [onChange, providers]);
3517
+ const handleModelChange = React13.useCallback((modelId) => {
3518
+ onChange({
3519
+ ...value,
3520
+ model: modelId
3521
+ });
3522
+ }, [onChange, value]);
3523
+ if (compact) {
3524
+ return /* @__PURE__ */ jsxs11("div", {
3525
+ className: cn7("flex items-center gap-2", className),
3526
+ children: [
3527
+ /* @__PURE__ */ jsxs11(Select2, {
3528
+ value: value.provider,
3529
+ onValueChange: handleProviderChange,
3530
+ children: [
3531
+ /* @__PURE__ */ jsx11(SelectTrigger2, {
3532
+ className: "w-[140px]",
3533
+ children: /* @__PURE__ */ jsx11(SelectValue2, {})
3534
+ }),
3535
+ /* @__PURE__ */ jsx11(SelectContent2, {
3536
+ children: providers.map((p) => /* @__PURE__ */ jsx11(SelectItem2, {
3537
+ value: p.provider,
3538
+ disabled: !p.available,
3539
+ children: /* @__PURE__ */ jsxs11("div", {
3540
+ className: "flex items-center gap-2",
3541
+ children: [
3542
+ PROVIDER_ICONS[p.provider],
3543
+ /* @__PURE__ */ jsx11("span", {
3544
+ children: PROVIDER_NAMES[p.provider]
3545
+ })
3546
+ ]
3547
+ })
3548
+ }, p.provider))
3549
+ })
3550
+ ]
3551
+ }),
3552
+ /* @__PURE__ */ jsxs11(Select2, {
3553
+ value: value.model,
3554
+ onValueChange: handleModelChange,
3555
+ children: [
3556
+ /* @__PURE__ */ jsx11(SelectTrigger2, {
3557
+ className: "w-[160px]",
3558
+ children: /* @__PURE__ */ jsx11(SelectValue2, {})
3559
+ }),
3560
+ /* @__PURE__ */ jsx11(SelectContent2, {
3561
+ children: models.map((m) => /* @__PURE__ */ jsx11(SelectItem2, {
3562
+ value: m.id,
3563
+ children: m.name
3564
+ }, m.id))
3565
+ })
3566
+ ]
3567
+ })
3568
+ ]
3569
+ });
3570
+ }
3571
+ return /* @__PURE__ */ jsxs11("div", {
3572
+ className: cn7("flex flex-col gap-3", className),
3573
+ children: [
3574
+ /* @__PURE__ */ jsxs11("div", {
3575
+ className: "flex flex-col gap-1.5",
3576
+ children: [
3577
+ /* @__PURE__ */ jsx11(Label2, {
3578
+ htmlFor: "provider-selection",
3579
+ className: "text-sm font-medium",
3580
+ children: "Provider"
3581
+ }),
3582
+ /* @__PURE__ */ jsx11("div", {
3583
+ className: "flex flex-wrap gap-2",
3584
+ id: "provider-selection",
3585
+ children: providers.map((p) => /* @__PURE__ */ jsxs11(Button6, {
3586
+ variant: value.provider === p.provider ? "default" : "outline",
3587
+ size: "sm",
3588
+ onPress: () => p.available && handleProviderChange(p.provider),
3589
+ disabled: !p.available,
3590
+ className: cn7(!p.available && "opacity-50"),
3591
+ children: [
3592
+ PROVIDER_ICONS[p.provider],
3593
+ /* @__PURE__ */ jsx11("span", {
3594
+ children: PROVIDER_NAMES[p.provider]
3595
+ }),
3596
+ /* @__PURE__ */ jsx11(Badge, {
3597
+ variant: MODE_BADGES[p.mode].variant,
3598
+ className: "ml-1",
3599
+ children: MODE_BADGES[p.mode].label
3600
+ })
3601
+ ]
3602
+ }, p.provider))
3603
+ })
3604
+ ]
3605
+ }),
3606
+ /* @__PURE__ */ jsxs11("div", {
3607
+ className: "flex flex-col gap-1.5",
3608
+ children: [
3609
+ /* @__PURE__ */ jsx11(Label2, {
3610
+ htmlFor: "model-picker",
3611
+ className: "text-sm font-medium",
3612
+ children: "Model"
3613
+ }),
3614
+ /* @__PURE__ */ jsxs11(Select2, {
3615
+ name: "model-picker",
3616
+ value: value.model,
3617
+ onValueChange: handleModelChange,
3618
+ children: [
3619
+ /* @__PURE__ */ jsx11(SelectTrigger2, {
3620
+ children: /* @__PURE__ */ jsx11(SelectValue2, {
3621
+ placeholder: "Select a model"
3622
+ })
3623
+ }),
3624
+ /* @__PURE__ */ jsx11(SelectContent2, {
3625
+ children: models.map((m) => /* @__PURE__ */ jsx11(SelectItem2, {
3626
+ value: m.id,
3627
+ children: /* @__PURE__ */ jsxs11("div", {
3628
+ className: "flex items-center gap-2",
3629
+ children: [
3630
+ /* @__PURE__ */ jsx11("span", {
3631
+ children: m.name
3632
+ }),
3633
+ /* @__PURE__ */ jsxs11("span", {
3634
+ className: "text-muted-foreground text-xs",
3635
+ children: [
3636
+ Math.round(m.contextWindow / 1000),
3637
+ "K"
3638
+ ]
3639
+ }),
3640
+ m.capabilities.vision && /* @__PURE__ */ jsx11(Badge, {
3641
+ variant: "outline",
3642
+ className: "text-xs",
3643
+ children: "Vision"
3644
+ }),
3645
+ m.capabilities.reasoning && /* @__PURE__ */ jsx11(Badge, {
3646
+ variant: "outline",
3647
+ className: "text-xs",
3648
+ children: "Reasoning"
3649
+ })
3650
+ ]
3651
+ })
3652
+ }, m.id))
3653
+ })
3654
+ ]
3655
+ })
3656
+ ]
3657
+ }),
3658
+ selectedModel && /* @__PURE__ */ jsxs11("div", {
3659
+ className: "text-muted-foreground flex flex-wrap gap-2 text-xs",
3660
+ children: [
3661
+ /* @__PURE__ */ jsxs11("span", {
3662
+ children: [
3663
+ "Context: ",
3664
+ Math.round(selectedModel.contextWindow / 1000),
3665
+ "K tokens"
3666
+ ]
3667
+ }),
3668
+ selectedModel.capabilities.vision && /* @__PURE__ */ jsx11("span", {
3669
+ children: "• Vision"
3670
+ }),
3671
+ selectedModel.capabilities.tools && /* @__PURE__ */ jsx11("span", {
3672
+ children: "• Tools"
3673
+ }),
3674
+ selectedModel.capabilities.reasoning && /* @__PURE__ */ jsx11("span", {
3675
+ children: "• Reasoning"
3676
+ })
3677
+ ]
3678
+ })
3679
+ ]
3680
+ });
3681
+ }
3682
+ // src/presentation/components/ContextIndicator.tsx
3683
+ import { cn as cn8 } from "@contractspec/lib.ui-kit-web/ui/utils";
3684
+ import { Badge as Badge2 } from "@contractspec/lib.ui-kit-web/ui/badge";
3685
+ import {
3686
+ Tooltip,
3687
+ TooltipContent,
3688
+ TooltipProvider,
3689
+ TooltipTrigger
3690
+ } from "@contractspec/lib.ui-kit-web/ui/tooltip";
3691
+ import { FolderOpen, FileCode, Zap, Info } from "lucide-react";
3692
+ import { jsx as jsx12, jsxs as jsxs12, Fragment as Fragment6 } from "react/jsx-runtime";
3693
+ "use client";
3694
+ function ContextIndicator({
3695
+ summary,
3696
+ active = false,
3697
+ className,
3698
+ showDetails = true
3699
+ }) {
3700
+ if (!summary && !active) {
3701
+ return /* @__PURE__ */ jsxs12("div", {
3702
+ className: cn8("flex items-center gap-1.5 text-sm", "text-muted-foreground", className),
3703
+ children: [
3704
+ /* @__PURE__ */ jsx12(Info, {
3705
+ className: "h-4 w-4"
3706
+ }),
3707
+ /* @__PURE__ */ jsx12("span", {
3708
+ children: "No workspace context"
3709
+ })
3710
+ ]
3711
+ });
3712
+ }
3713
+ const content = /* @__PURE__ */ jsxs12("div", {
3714
+ className: cn8("flex items-center gap-2", active ? "text-foreground" : "text-muted-foreground", className),
3715
+ children: [
3716
+ /* @__PURE__ */ jsxs12(Badge2, {
3717
+ variant: active ? "default" : "secondary",
3718
+ className: "flex items-center gap-1",
3719
+ children: [
3720
+ /* @__PURE__ */ jsx12(Zap, {
3721
+ className: "h-3 w-3"
3722
+ }),
3723
+ "Context"
3724
+ ]
3725
+ }),
3726
+ summary && showDetails && /* @__PURE__ */ jsxs12(Fragment6, {
3727
+ children: [
3728
+ /* @__PURE__ */ jsxs12("div", {
3729
+ className: "flex items-center gap-1 text-xs",
3730
+ children: [
3731
+ /* @__PURE__ */ jsx12(FolderOpen, {
3732
+ className: "h-3.5 w-3.5"
3733
+ }),
3734
+ /* @__PURE__ */ jsx12("span", {
3735
+ children: summary.name
3736
+ })
3737
+ ]
3738
+ }),
3739
+ /* @__PURE__ */ jsxs12("div", {
3740
+ className: "flex items-center gap-1 text-xs",
3741
+ children: [
3742
+ /* @__PURE__ */ jsx12(FileCode, {
3743
+ className: "h-3.5 w-3.5"
3744
+ }),
3745
+ /* @__PURE__ */ jsxs12("span", {
3746
+ children: [
3747
+ summary.specs.total,
3748
+ " specs"
3749
+ ]
3750
+ })
3751
+ ]
3752
+ })
3753
+ ]
3754
+ })
3755
+ ]
3756
+ });
3757
+ if (!summary) {
3758
+ return content;
3759
+ }
3760
+ return /* @__PURE__ */ jsx12(TooltipProvider, {
3761
+ children: /* @__PURE__ */ jsxs12(Tooltip, {
3762
+ children: [
3763
+ /* @__PURE__ */ jsx12(TooltipTrigger, {
3764
+ asChild: true,
3765
+ children: content
3766
+ }),
3767
+ /* @__PURE__ */ jsx12(TooltipContent, {
3768
+ side: "bottom",
3769
+ className: "max-w-[300px]",
3770
+ children: /* @__PURE__ */ jsxs12("div", {
3771
+ className: "flex flex-col gap-2 text-sm",
3772
+ children: [
3773
+ /* @__PURE__ */ jsx12("div", {
3774
+ className: "font-medium",
3775
+ children: summary.name
3776
+ }),
3777
+ /* @__PURE__ */ jsx12("div", {
3778
+ className: "text-muted-foreground text-xs",
3779
+ children: summary.path
3780
+ }),
3781
+ /* @__PURE__ */ jsx12("div", {
3782
+ className: "border-t pt-2",
3783
+ children: /* @__PURE__ */ jsxs12("div", {
3784
+ className: "grid grid-cols-2 gap-1 text-xs",
3785
+ children: [
3786
+ /* @__PURE__ */ jsx12("span", {
3787
+ children: "Commands:"
3788
+ }),
3789
+ /* @__PURE__ */ jsx12("span", {
3790
+ className: "text-right",
3791
+ children: summary.specs.commands
3792
+ }),
3793
+ /* @__PURE__ */ jsx12("span", {
3794
+ children: "Queries:"
3795
+ }),
3796
+ /* @__PURE__ */ jsx12("span", {
3797
+ className: "text-right",
3798
+ children: summary.specs.queries
3799
+ }),
3800
+ /* @__PURE__ */ jsx12("span", {
3801
+ children: "Events:"
3802
+ }),
3803
+ /* @__PURE__ */ jsx12("span", {
3804
+ className: "text-right",
3805
+ children: summary.specs.events
3806
+ }),
3807
+ /* @__PURE__ */ jsx12("span", {
3808
+ children: "Presentations:"
3809
+ }),
3810
+ /* @__PURE__ */ jsx12("span", {
3811
+ className: "text-right",
3812
+ children: summary.specs.presentations
3813
+ })
3814
+ ]
3815
+ })
3816
+ }),
3817
+ /* @__PURE__ */ jsxs12("div", {
3818
+ className: "border-t pt-2 text-xs",
3819
+ children: [
3820
+ /* @__PURE__ */ jsxs12("span", {
3821
+ children: [
3822
+ summary.files.total,
3823
+ " files"
3824
+ ]
3825
+ }),
3826
+ /* @__PURE__ */ jsx12("span", {
3827
+ className: "mx-1",
3828
+ children: "•"
3829
+ }),
3830
+ /* @__PURE__ */ jsxs12("span", {
3831
+ children: [
3832
+ summary.files.specFiles,
3833
+ " spec files"
3834
+ ]
3835
+ })
3836
+ ]
3837
+ })
3838
+ ]
3839
+ })
3840
+ })
3841
+ ]
3842
+ })
3843
+ });
3844
+ }
3845
+ // src/presentation/hooks/useProviders.tsx
3846
+ import * as React14 from "react";
3847
+ import {
3848
+ getAvailableProviders,
3849
+ getModelsForProvider as getModelsForProvider2
3850
+ } from "@contractspec/lib.ai-providers";
3851
+ "use client";
3852
+ function useProviders() {
3853
+ const [providers, setProviders] = React14.useState([]);
3854
+ const [isLoading, setIsLoading] = React14.useState(true);
3855
+ const loadProviders = React14.useCallback(async () => {
3856
+ setIsLoading(true);
3857
+ try {
3858
+ const available = getAvailableProviders();
3859
+ const providersWithModels = available.map((p) => ({
3860
+ ...p,
3861
+ models: getModelsForProvider2(p.provider)
3862
+ }));
3863
+ setProviders(providersWithModels);
3864
+ } catch (error) {
3865
+ console.error("Failed to load providers:", error);
3866
+ } finally {
3867
+ setIsLoading(false);
3868
+ }
3869
+ }, []);
3870
+ React14.useEffect(() => {
1763
3871
  loadProviders();
1764
3872
  }, [loadProviders]);
1765
- const availableProviders = React7.useMemo(() => providers.filter((p) => p.available), [providers]);
1766
- const isAvailable = React7.useCallback((provider) => providers.some((p) => p.provider === provider && p.available), [providers]);
1767
- const getModelsCallback = React7.useCallback((provider) => providers.find((p) => p.provider === provider)?.models ?? [], [providers]);
3873
+ const availableProviders = React14.useMemo(() => providers.filter((p) => p.available), [providers]);
3874
+ const isAvailable = React14.useCallback((provider) => providers.some((p) => p.provider === provider && p.available), [providers]);
3875
+ const getModelsCallback = React14.useCallback((provider) => providers.find((p) => p.provider === provider)?.models ?? [], [providers]);
1768
3876
  return {
1769
3877
  providers,
1770
3878
  availableProviders,
@@ -1779,12 +3887,22 @@ function useProviders() {
1779
3887
  import { useCompletion } from "@ai-sdk/react";
1780
3888
  export {
1781
3889
  useProviders,
3890
+ useMessageSelection,
3891
+ useConversations,
1782
3892
  useCompletion,
1783
3893
  useChat,
3894
+ isPresentationToolResult,
3895
+ isFormToolResult,
3896
+ ToolResultRenderer,
3897
+ ThinkingLevelPicker,
1784
3898
  ModelPicker,
1785
3899
  ContextIndicator,
1786
3900
  CodePreview,
3901
+ ChatWithSidebar,
3902
+ ChatWithExport,
3903
+ ChatSidebar,
1787
3904
  ChatMessage,
1788
3905
  ChatInput,
3906
+ ChatExportToolbar,
1789
3907
  ChatContainer
1790
3908
  };