@contractspec/module.ai-chat 1.57.0 → 1.59.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 (120) hide show
  1. package/dist/ai-chat.capability.d.ts +2 -0
  2. package/dist/ai-chat.capability.d.ts.map +1 -0
  3. package/dist/ai-chat.feature.d.ts +1 -6
  4. package/dist/ai-chat.feature.d.ts.map +1 -1
  5. package/dist/ai-chat.operations.d.ts +217 -223
  6. package/dist/ai-chat.operations.d.ts.map +1 -1
  7. package/dist/browser/context/index.js +415 -0
  8. package/dist/browser/core/index.js +336 -0
  9. package/dist/browser/index.js +2291 -0
  10. package/dist/browser/presentation/components/index.js +974 -0
  11. package/dist/browser/presentation/hooks/index.js +556 -0
  12. package/dist/browser/presentation/index.js +1520 -0
  13. package/dist/browser/providers/index.js +51 -0
  14. package/dist/context/chat.test.d.ts +2 -0
  15. package/dist/context/chat.test.d.ts.map +1 -0
  16. package/dist/context/context-builder.d.ts +37 -36
  17. package/dist/context/context-builder.d.ts.map +1 -1
  18. package/dist/context/file-operations.d.ts +64 -67
  19. package/dist/context/file-operations.d.ts.map +1 -1
  20. package/dist/context/index.d.ts +7 -4
  21. package/dist/context/index.d.ts.map +1 -0
  22. package/dist/context/index.js +409 -4
  23. package/dist/context/workspace-context.d.ts +84 -87
  24. package/dist/context/workspace-context.d.ts.map +1 -1
  25. package/dist/core/chat-service.d.ts +56 -60
  26. package/dist/core/chat-service.d.ts.map +1 -1
  27. package/dist/core/conversation-store.d.ts +60 -61
  28. package/dist/core/conversation-store.d.ts.map +1 -1
  29. package/dist/core/index.d.ts +7 -4
  30. package/dist/core/index.d.ts.map +1 -0
  31. package/dist/core/index.js +330 -3
  32. package/dist/core/message-types.d.ts +94 -97
  33. package/dist/core/message-types.d.ts.map +1 -1
  34. package/dist/docs/ai-chat.docblock.d.ts +2 -0
  35. package/dist/docs/ai-chat.docblock.d.ts.map +1 -0
  36. package/dist/docs/index.d.ts +7 -0
  37. package/dist/docs/index.d.ts.map +1 -0
  38. package/dist/events.d.ts +103 -109
  39. package/dist/events.d.ts.map +1 -1
  40. package/dist/index.d.ts +16 -21
  41. package/dist/index.d.ts.map +1 -0
  42. package/dist/index.js +2286 -23
  43. package/dist/node/context/index.js +410 -0
  44. package/dist/node/core/index.js +331 -0
  45. package/dist/node/index.js +2286 -0
  46. package/dist/node/presentation/components/index.js +969 -0
  47. package/dist/node/presentation/hooks/index.js +551 -0
  48. package/dist/node/presentation/index.js +1515 -0
  49. package/dist/node/providers/index.js +46 -0
  50. package/dist/presentation/components/ChatContainer.d.ts +7 -16
  51. package/dist/presentation/components/ChatContainer.d.ts.map +1 -1
  52. package/dist/presentation/components/ChatInput.d.ts +17 -30
  53. package/dist/presentation/components/ChatInput.d.ts.map +1 -1
  54. package/dist/presentation/components/ChatMessage.d.ts +9 -19
  55. package/dist/presentation/components/ChatMessage.d.ts.map +1 -1
  56. package/dist/presentation/components/CodePreview.d.ts +20 -35
  57. package/dist/presentation/components/CodePreview.d.ts.map +1 -1
  58. package/dist/presentation/components/ContextIndicator.d.ts +11 -21
  59. package/dist/presentation/components/ContextIndicator.d.ts.map +1 -1
  60. package/dist/presentation/components/ModelPicker.d.ts +21 -32
  61. package/dist/presentation/components/ModelPicker.d.ts.map +1 -1
  62. package/dist/presentation/components/index.d.ts +10 -7
  63. package/dist/presentation/components/index.d.ts.map +1 -0
  64. package/dist/presentation/components/index.js +968 -7
  65. package/dist/presentation/hooks/index.d.ts +6 -3
  66. package/dist/presentation/hooks/index.d.ts.map +1 -0
  67. package/dist/presentation/hooks/index.js +550 -3
  68. package/dist/presentation/hooks/use-chat.test.d.ts +2 -0
  69. package/dist/presentation/hooks/use-chat.test.d.ts.map +1 -0
  70. package/dist/presentation/hooks/useChat.d.ts +50 -54
  71. package/dist/presentation/hooks/useChat.d.ts.map +1 -1
  72. package/dist/presentation/hooks/useProviders.d.ts +21 -25
  73. package/dist/presentation/hooks/useProviders.d.ts.map +1 -1
  74. package/dist/presentation/index.d.ts +8 -11
  75. package/dist/presentation/index.d.ts.map +1 -0
  76. package/dist/presentation/index.js +1515 -12
  77. package/dist/providers/chat-utilities.d.ts +18 -7
  78. package/dist/providers/chat-utilities.d.ts.map +1 -1
  79. package/dist/providers/index.d.ts +8 -3
  80. package/dist/providers/index.d.ts.map +1 -0
  81. package/dist/providers/index.js +45 -3
  82. package/dist/schema.d.ts +195 -200
  83. package/dist/schema.d.ts.map +1 -1
  84. package/package.json +123 -34
  85. package/dist/ai-chat.feature.js +0 -102
  86. package/dist/ai-chat.feature.js.map +0 -1
  87. package/dist/ai-chat.operations.js +0 -172
  88. package/dist/ai-chat.operations.js.map +0 -1
  89. package/dist/context/context-builder.js +0 -148
  90. package/dist/context/context-builder.js.map +0 -1
  91. package/dist/context/file-operations.js +0 -175
  92. package/dist/context/file-operations.js.map +0 -1
  93. package/dist/context/workspace-context.js +0 -124
  94. package/dist/context/workspace-context.js.map +0 -1
  95. package/dist/core/chat-service.js +0 -227
  96. package/dist/core/chat-service.js.map +0 -1
  97. package/dist/core/conversation-store.js +0 -109
  98. package/dist/core/conversation-store.js.map +0 -1
  99. package/dist/events.js +0 -98
  100. package/dist/events.js.map +0 -1
  101. package/dist/presentation/components/ChatContainer.js +0 -63
  102. package/dist/presentation/components/ChatContainer.js.map +0 -1
  103. package/dist/presentation/components/ChatInput.js +0 -149
  104. package/dist/presentation/components/ChatInput.js.map +0 -1
  105. package/dist/presentation/components/ChatMessage.js +0 -136
  106. package/dist/presentation/components/ChatMessage.js.map +0 -1
  107. package/dist/presentation/components/CodePreview.js +0 -127
  108. package/dist/presentation/components/CodePreview.js.map +0 -1
  109. package/dist/presentation/components/ContextIndicator.js +0 -97
  110. package/dist/presentation/components/ContextIndicator.js.map +0 -1
  111. package/dist/presentation/components/ModelPicker.js +0 -202
  112. package/dist/presentation/components/ModelPicker.js.map +0 -1
  113. package/dist/presentation/hooks/useChat.js +0 -172
  114. package/dist/presentation/hooks/useChat.js.map +0 -1
  115. package/dist/presentation/hooks/useProviders.js +0 -41
  116. package/dist/presentation/hooks/useProviders.js.map +0 -1
  117. package/dist/providers/chat-utilities.js +0 -17
  118. package/dist/providers/chat-utilities.js.map +0 -1
  119. package/dist/schema.js +0 -100
  120. package/dist/schema.js.map +0 -1
@@ -0,0 +1,1520 @@
1
+ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
2
+ get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
3
+ }) : x)(function(x) {
4
+ if (typeof require !== "undefined")
5
+ return require.apply(this, arguments);
6
+ throw Error('Dynamic require of "' + x + '" is not supported');
7
+ });
8
+
9
+ // src/presentation/components/ChatContainer.tsx
10
+ import * as React from "react";
11
+ import { ScrollArea } from "@contractspec/lib.ui-kit-web/ui/scroll-area";
12
+ import { cn } from "@contractspec/lib.ui-kit-web/ui/utils";
13
+ import { jsxDEV } from "react/jsx-dev-runtime";
14
+ "use client";
15
+ function ChatContainer({
16
+ children,
17
+ className,
18
+ showScrollButton = true
19
+ }) {
20
+ const scrollRef = React.useRef(null);
21
+ const [showScrollDown, setShowScrollDown] = React.useState(false);
22
+ React.useEffect(() => {
23
+ const container = scrollRef.current;
24
+ if (!container)
25
+ return;
26
+ const isAtBottom = container.scrollHeight - container.scrollTop <= container.clientHeight + 100;
27
+ if (isAtBottom) {
28
+ container.scrollTop = container.scrollHeight;
29
+ }
30
+ }, [children]);
31
+ const handleScroll = React.useCallback((event) => {
32
+ const container = event.currentTarget;
33
+ const isAtBottom = container.scrollHeight - container.scrollTop <= container.clientHeight + 100;
34
+ setShowScrollDown(!isAtBottom);
35
+ }, []);
36
+ const scrollToBottom = React.useCallback(() => {
37
+ const container = scrollRef.current;
38
+ if (container) {
39
+ container.scrollTo({
40
+ top: container.scrollHeight,
41
+ behavior: "smooth"
42
+ });
43
+ }
44
+ }, []);
45
+ return /* @__PURE__ */ jsxDEV("div", {
46
+ className: cn("relative flex flex-1 flex-col", className),
47
+ children: [
48
+ /* @__PURE__ */ jsxDEV(ScrollArea, {
49
+ ref: scrollRef,
50
+ className: "flex-1",
51
+ onScroll: handleScroll,
52
+ children: /* @__PURE__ */ jsxDEV("div", {
53
+ className: "flex flex-col gap-4 p-4",
54
+ children
55
+ }, undefined, false, undefined, this)
56
+ }, undefined, false, undefined, this),
57
+ showScrollButton && showScrollDown && /* @__PURE__ */ jsxDEV("button", {
58
+ onClick: scrollToBottom,
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"),
60
+ "aria-label": "Scroll to bottom",
61
+ children: [
62
+ /* @__PURE__ */ jsxDEV("svg", {
63
+ xmlns: "http://www.w3.org/2000/svg",
64
+ width: "16",
65
+ height: "16",
66
+ viewBox: "0 0 24 24",
67
+ fill: "none",
68
+ stroke: "currentColor",
69
+ strokeWidth: "2",
70
+ strokeLinecap: "round",
71
+ strokeLinejoin: "round",
72
+ children: /* @__PURE__ */ jsxDEV("path", {
73
+ d: "m6 9 6 6 6-6"
74
+ }, undefined, false, undefined, this)
75
+ }, undefined, false, undefined, this),
76
+ "New messages"
77
+ ]
78
+ }, undefined, true, undefined, this)
79
+ ]
80
+ }, undefined, true, undefined, this);
81
+ }
82
+ // src/presentation/components/ChatMessage.tsx
83
+ import * as React3 from "react";
84
+ import { cn as cn3 } from "@contractspec/lib.ui-kit-web/ui/utils";
85
+ import { Avatar, AvatarFallback } from "@contractspec/lib.ui-kit-web/ui/avatar";
86
+ import { Skeleton } from "@contractspec/lib.ui-kit-web/ui/skeleton";
87
+ import { Bot, User, AlertCircle, Copy as Copy2, Check as Check2 } from "lucide-react";
88
+ import { Button as Button2 } from "@contractspec/lib.design-system";
89
+
90
+ // src/presentation/components/CodePreview.tsx
91
+ import * as React2 from "react";
92
+ import { cn as cn2 } from "@contractspec/lib.ui-kit-web/ui/utils";
93
+ import { Button } from "@contractspec/lib.design-system";
94
+ import { Copy, Check, Play, Download } from "lucide-react";
95
+ import { jsxDEV as jsxDEV2 } from "react/jsx-dev-runtime";
96
+ "use client";
97
+ var LANGUAGE_NAMES = {
98
+ ts: "TypeScript",
99
+ tsx: "TypeScript (React)",
100
+ typescript: "TypeScript",
101
+ js: "JavaScript",
102
+ jsx: "JavaScript (React)",
103
+ javascript: "JavaScript",
104
+ json: "JSON",
105
+ md: "Markdown",
106
+ yaml: "YAML",
107
+ yml: "YAML",
108
+ bash: "Bash",
109
+ sh: "Shell",
110
+ sql: "SQL",
111
+ py: "Python",
112
+ python: "Python",
113
+ go: "Go",
114
+ rust: "Rust",
115
+ rs: "Rust"
116
+ };
117
+ function CodePreview({
118
+ code,
119
+ language = "text",
120
+ filename,
121
+ className,
122
+ showCopy = true,
123
+ showExecute = false,
124
+ onExecute,
125
+ showDownload = false,
126
+ maxHeight = 400
127
+ }) {
128
+ const [copied, setCopied] = React2.useState(false);
129
+ const displayLanguage = LANGUAGE_NAMES[language.toLowerCase()] ?? language;
130
+ const lines = code.split(`
131
+ `);
132
+ const handleCopy = React2.useCallback(async () => {
133
+ await navigator.clipboard.writeText(code);
134
+ setCopied(true);
135
+ setTimeout(() => setCopied(false), 2000);
136
+ }, [code]);
137
+ const handleDownload = React2.useCallback(() => {
138
+ const blob = new Blob([code], { type: "text/plain" });
139
+ const url = URL.createObjectURL(blob);
140
+ const a = document.createElement("a");
141
+ a.href = url;
142
+ a.download = filename ?? `code.${language}`;
143
+ document.body.appendChild(a);
144
+ a.click();
145
+ document.body.removeChild(a);
146
+ URL.revokeObjectURL(url);
147
+ }, [code, filename, language]);
148
+ return /* @__PURE__ */ jsxDEV2("div", {
149
+ className: cn2("overflow-hidden rounded-lg border", "bg-muted/50", className),
150
+ children: [
151
+ /* @__PURE__ */ jsxDEV2("div", {
152
+ className: cn2("flex items-center justify-between px-3 py-1.5", "bg-muted/80 border-b"),
153
+ children: [
154
+ /* @__PURE__ */ jsxDEV2("div", {
155
+ className: "flex items-center gap-2 text-sm",
156
+ children: [
157
+ filename && /* @__PURE__ */ jsxDEV2("span", {
158
+ className: "text-foreground font-mono",
159
+ children: filename
160
+ }, undefined, false, undefined, this),
161
+ /* @__PURE__ */ jsxDEV2("span", {
162
+ className: "text-muted-foreground",
163
+ children: displayLanguage
164
+ }, undefined, false, undefined, this)
165
+ ]
166
+ }, undefined, true, undefined, this),
167
+ /* @__PURE__ */ jsxDEV2("div", {
168
+ className: "flex items-center gap-1",
169
+ children: [
170
+ showExecute && onExecute && /* @__PURE__ */ jsxDEV2(Button, {
171
+ variant: "ghost",
172
+ size: "sm",
173
+ onPress: () => onExecute(code),
174
+ className: "h-7 w-7 p-0",
175
+ "aria-label": "Execute code",
176
+ children: /* @__PURE__ */ jsxDEV2(Play, {
177
+ className: "h-3.5 w-3.5"
178
+ }, undefined, false, undefined, this)
179
+ }, undefined, false, undefined, this),
180
+ showDownload && /* @__PURE__ */ jsxDEV2(Button, {
181
+ variant: "ghost",
182
+ size: "sm",
183
+ onPress: handleDownload,
184
+ className: "h-7 w-7 p-0",
185
+ "aria-label": "Download code",
186
+ children: /* @__PURE__ */ jsxDEV2(Download, {
187
+ className: "h-3.5 w-3.5"
188
+ }, undefined, false, undefined, this)
189
+ }, undefined, false, undefined, this),
190
+ showCopy && /* @__PURE__ */ jsxDEV2(Button, {
191
+ variant: "ghost",
192
+ size: "sm",
193
+ onPress: handleCopy,
194
+ className: "h-7 w-7 p-0",
195
+ "aria-label": copied ? "Copied" : "Copy code",
196
+ children: copied ? /* @__PURE__ */ jsxDEV2(Check, {
197
+ className: "h-3.5 w-3.5 text-green-500"
198
+ }, undefined, false, undefined, this) : /* @__PURE__ */ jsxDEV2(Copy, {
199
+ className: "h-3.5 w-3.5"
200
+ }, undefined, false, undefined, this)
201
+ }, undefined, false, undefined, this)
202
+ ]
203
+ }, undefined, true, undefined, this)
204
+ ]
205
+ }, undefined, true, undefined, this),
206
+ /* @__PURE__ */ jsxDEV2("div", {
207
+ className: "overflow-auto",
208
+ style: { maxHeight },
209
+ children: /* @__PURE__ */ jsxDEV2("pre", {
210
+ className: "p-3",
211
+ children: /* @__PURE__ */ jsxDEV2("code", {
212
+ className: "text-sm",
213
+ children: lines.map((line, i) => /* @__PURE__ */ jsxDEV2("div", {
214
+ className: "flex",
215
+ children: [
216
+ /* @__PURE__ */ jsxDEV2("span", {
217
+ className: "text-muted-foreground mr-4 w-8 text-right select-none",
218
+ children: i + 1
219
+ }, undefined, false, undefined, this),
220
+ /* @__PURE__ */ jsxDEV2("span", {
221
+ className: "flex-1",
222
+ children: line || " "
223
+ }, undefined, false, undefined, this)
224
+ ]
225
+ }, i, true, undefined, this))
226
+ }, undefined, false, undefined, this)
227
+ }, undefined, false, undefined, this)
228
+ }, undefined, false, undefined, this)
229
+ ]
230
+ }, undefined, true, undefined, this);
231
+ }
232
+
233
+ // src/presentation/components/ChatMessage.tsx
234
+ import { jsxDEV as jsxDEV3, Fragment } from "react/jsx-dev-runtime";
235
+ "use client";
236
+ function extractCodeBlocks(content) {
237
+ const codeBlockRegex = /```(\w+)?\n([\s\S]*?)```/g;
238
+ const blocks = [];
239
+ let match;
240
+ while ((match = codeBlockRegex.exec(content)) !== null) {
241
+ blocks.push({
242
+ language: match[1] ?? "text",
243
+ code: match[2] ?? "",
244
+ raw: match[0]
245
+ });
246
+ }
247
+ return blocks;
248
+ }
249
+ function MessageContent({ content }) {
250
+ const codeBlocks = extractCodeBlocks(content);
251
+ if (codeBlocks.length === 0) {
252
+ return /* @__PURE__ */ jsxDEV3("p", {
253
+ className: "whitespace-pre-wrap",
254
+ children: content
255
+ }, undefined, false, undefined, this);
256
+ }
257
+ let remaining = content;
258
+ const parts = [];
259
+ let key = 0;
260
+ for (const block of codeBlocks) {
261
+ const [before, after] = remaining.split(block.raw);
262
+ if (before) {
263
+ parts.push(/* @__PURE__ */ jsxDEV3("p", {
264
+ className: "whitespace-pre-wrap",
265
+ children: before.trim()
266
+ }, key++, false, undefined, this));
267
+ }
268
+ parts.push(/* @__PURE__ */ jsxDEV3(CodePreview, {
269
+ code: block.code,
270
+ language: block.language,
271
+ className: "my-2"
272
+ }, key++, false, undefined, this));
273
+ remaining = after ?? "";
274
+ }
275
+ if (remaining.trim()) {
276
+ parts.push(/* @__PURE__ */ jsxDEV3("p", {
277
+ className: "whitespace-pre-wrap",
278
+ children: remaining.trim()
279
+ }, key++, false, undefined, this));
280
+ }
281
+ return /* @__PURE__ */ jsxDEV3(Fragment, {
282
+ children: parts
283
+ }, undefined, false, undefined, this);
284
+ }
285
+ function ChatMessage({
286
+ message,
287
+ className,
288
+ showCopy = true,
289
+ showAvatar = true
290
+ }) {
291
+ const [copied, setCopied] = React3.useState(false);
292
+ const isUser = message.role === "user";
293
+ const isError = message.status === "error";
294
+ const isStreaming = message.status === "streaming";
295
+ const handleCopy = React3.useCallback(async () => {
296
+ await navigator.clipboard.writeText(message.content);
297
+ setCopied(true);
298
+ setTimeout(() => setCopied(false), 2000);
299
+ }, [message.content]);
300
+ return /* @__PURE__ */ jsxDEV3("div", {
301
+ className: cn3("group flex gap-3", isUser && "flex-row-reverse", className),
302
+ children: [
303
+ showAvatar && /* @__PURE__ */ jsxDEV3(Avatar, {
304
+ className: "h-8 w-8 shrink-0",
305
+ children: /* @__PURE__ */ jsxDEV3(AvatarFallback, {
306
+ className: cn3(isUser ? "bg-primary text-primary-foreground" : "bg-muted"),
307
+ children: isUser ? /* @__PURE__ */ jsxDEV3(User, {
308
+ className: "h-4 w-4"
309
+ }, undefined, false, undefined, this) : /* @__PURE__ */ jsxDEV3(Bot, {
310
+ className: "h-4 w-4"
311
+ }, undefined, false, undefined, this)
312
+ }, undefined, false, undefined, this)
313
+ }, undefined, false, undefined, this),
314
+ /* @__PURE__ */ jsxDEV3("div", {
315
+ className: cn3("flex max-w-[80%] flex-col gap-1", isUser && "items-end"),
316
+ children: [
317
+ /* @__PURE__ */ jsxDEV3("div", {
318
+ 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"),
319
+ children: isError && message.error ? /* @__PURE__ */ jsxDEV3("div", {
320
+ className: "flex items-start gap-2",
321
+ children: [
322
+ /* @__PURE__ */ jsxDEV3(AlertCircle, {
323
+ className: "text-destructive mt-0.5 h-4 w-4 shrink-0"
324
+ }, undefined, false, undefined, this),
325
+ /* @__PURE__ */ jsxDEV3("div", {
326
+ children: [
327
+ /* @__PURE__ */ jsxDEV3("p", {
328
+ className: "text-destructive font-medium",
329
+ children: message.error.code
330
+ }, undefined, false, undefined, this),
331
+ /* @__PURE__ */ jsxDEV3("p", {
332
+ className: "text-muted-foreground text-sm",
333
+ children: message.error.message
334
+ }, undefined, false, undefined, this)
335
+ ]
336
+ }, undefined, true, undefined, this)
337
+ ]
338
+ }, undefined, true, undefined, this) : isStreaming && !message.content ? /* @__PURE__ */ jsxDEV3("div", {
339
+ className: "flex flex-col gap-2",
340
+ children: [
341
+ /* @__PURE__ */ jsxDEV3(Skeleton, {
342
+ className: "h-4 w-48"
343
+ }, undefined, false, undefined, this),
344
+ /* @__PURE__ */ jsxDEV3(Skeleton, {
345
+ className: "h-4 w-32"
346
+ }, undefined, false, undefined, this)
347
+ ]
348
+ }, undefined, true, undefined, this) : /* @__PURE__ */ jsxDEV3(MessageContent, {
349
+ content: message.content
350
+ }, undefined, false, undefined, this)
351
+ }, undefined, false, undefined, this),
352
+ /* @__PURE__ */ jsxDEV3("div", {
353
+ className: cn3("flex items-center gap-2 text-xs", "text-muted-foreground opacity-0 transition-opacity", "group-hover:opacity-100"),
354
+ children: [
355
+ /* @__PURE__ */ jsxDEV3("span", {
356
+ children: new Date(message.createdAt).toLocaleTimeString([], {
357
+ hour: "2-digit",
358
+ minute: "2-digit"
359
+ })
360
+ }, undefined, false, undefined, this),
361
+ message.usage && /* @__PURE__ */ jsxDEV3("span", {
362
+ children: [
363
+ message.usage.inputTokens + message.usage.outputTokens,
364
+ " tokens"
365
+ ]
366
+ }, undefined, true, undefined, this),
367
+ showCopy && !isUser && message.content && /* @__PURE__ */ jsxDEV3(Button2, {
368
+ variant: "ghost",
369
+ size: "sm",
370
+ className: "h-6 w-6 p-0",
371
+ onPress: handleCopy,
372
+ "aria-label": copied ? "Copied" : "Copy message",
373
+ children: copied ? /* @__PURE__ */ jsxDEV3(Check2, {
374
+ className: "h-3 w-3"
375
+ }, undefined, false, undefined, this) : /* @__PURE__ */ jsxDEV3(Copy2, {
376
+ className: "h-3 w-3"
377
+ }, undefined, false, undefined, this)
378
+ }, undefined, false, undefined, this)
379
+ ]
380
+ }, undefined, true, undefined, this),
381
+ message.reasoning && /* @__PURE__ */ jsxDEV3("details", {
382
+ className: "text-muted-foreground mt-2 text-sm",
383
+ children: [
384
+ /* @__PURE__ */ jsxDEV3("summary", {
385
+ className: "cursor-pointer hover:underline",
386
+ children: "View reasoning"
387
+ }, undefined, false, undefined, this),
388
+ /* @__PURE__ */ jsxDEV3("div", {
389
+ className: "bg-muted mt-1 rounded-md p-2",
390
+ children: /* @__PURE__ */ jsxDEV3("p", {
391
+ className: "whitespace-pre-wrap",
392
+ children: message.reasoning
393
+ }, undefined, false, undefined, this)
394
+ }, undefined, false, undefined, this)
395
+ ]
396
+ }, undefined, true, undefined, this)
397
+ ]
398
+ }, undefined, true, undefined, this)
399
+ ]
400
+ }, undefined, true, undefined, this);
401
+ }
402
+ // src/presentation/components/ChatInput.tsx
403
+ import * as React4 from "react";
404
+ import { cn as cn4 } from "@contractspec/lib.ui-kit-web/ui/utils";
405
+ import { Textarea } from "@contractspec/lib.design-system";
406
+ import { Button as Button3 } from "@contractspec/lib.design-system";
407
+ import { Send, Paperclip, X, Loader2, FileText, Code } from "lucide-react";
408
+ import { jsxDEV as jsxDEV4, Fragment as Fragment2 } from "react/jsx-dev-runtime";
409
+ "use client";
410
+ function ChatInput({
411
+ onSend,
412
+ disabled = false,
413
+ isLoading = false,
414
+ placeholder = "Type a message...",
415
+ className,
416
+ showAttachments = true,
417
+ maxAttachments = 5
418
+ }) {
419
+ const [content, setContent] = React4.useState("");
420
+ const [attachments, setAttachments] = React4.useState([]);
421
+ const textareaRef = React4.useRef(null);
422
+ const fileInputRef = React4.useRef(null);
423
+ const canSend = content.trim().length > 0 || attachments.length > 0;
424
+ const handleSubmit = React4.useCallback((e) => {
425
+ e?.preventDefault();
426
+ if (!canSend || disabled || isLoading)
427
+ return;
428
+ onSend(content.trim(), attachments.length > 0 ? attachments : undefined);
429
+ setContent("");
430
+ setAttachments([]);
431
+ textareaRef.current?.focus();
432
+ }, [canSend, content, attachments, disabled, isLoading, onSend]);
433
+ const handleKeyDown = React4.useCallback((e) => {
434
+ if (e.key === "Enter" && !e.shiftKey) {
435
+ e.preventDefault();
436
+ handleSubmit();
437
+ }
438
+ }, [handleSubmit]);
439
+ const handleFileSelect = React4.useCallback(async (e) => {
440
+ const files = e.target.files;
441
+ if (!files)
442
+ return;
443
+ const newAttachments = [];
444
+ for (const file of Array.from(files)) {
445
+ if (attachments.length + newAttachments.length >= maxAttachments)
446
+ break;
447
+ const content2 = await file.text();
448
+ const extension = file.name.split(".").pop()?.toLowerCase() ?? "";
449
+ const isCode = [
450
+ "ts",
451
+ "tsx",
452
+ "js",
453
+ "jsx",
454
+ "py",
455
+ "go",
456
+ "rs",
457
+ "java"
458
+ ].includes(extension);
459
+ newAttachments.push({
460
+ id: `att_${Date.now()}_${Math.random().toString(36).slice(2, 9)}`,
461
+ type: isCode ? "code" : "file",
462
+ name: file.name,
463
+ content: content2,
464
+ mimeType: file.type,
465
+ size: file.size
466
+ });
467
+ }
468
+ setAttachments((prev) => [...prev, ...newAttachments]);
469
+ e.target.value = "";
470
+ }, [attachments.length, maxAttachments]);
471
+ const removeAttachment = React4.useCallback((id) => {
472
+ setAttachments((prev) => prev.filter((a) => a.id !== id));
473
+ }, []);
474
+ return /* @__PURE__ */ jsxDEV4("div", {
475
+ className: cn4("flex flex-col gap-2", className),
476
+ children: [
477
+ attachments.length > 0 && /* @__PURE__ */ jsxDEV4("div", {
478
+ className: "flex flex-wrap gap-2",
479
+ children: attachments.map((attachment) => /* @__PURE__ */ jsxDEV4("div", {
480
+ className: cn4("flex items-center gap-1.5 rounded-md px-2 py-1", "bg-muted text-muted-foreground text-sm"),
481
+ children: [
482
+ attachment.type === "code" ? /* @__PURE__ */ jsxDEV4(Code, {
483
+ className: "h-3.5 w-3.5"
484
+ }, undefined, false, undefined, this) : /* @__PURE__ */ jsxDEV4(FileText, {
485
+ className: "h-3.5 w-3.5"
486
+ }, undefined, false, undefined, this),
487
+ /* @__PURE__ */ jsxDEV4("span", {
488
+ className: "max-w-[150px] truncate",
489
+ children: attachment.name
490
+ }, undefined, false, undefined, this),
491
+ /* @__PURE__ */ jsxDEV4("button", {
492
+ type: "button",
493
+ onClick: () => removeAttachment(attachment.id),
494
+ className: "hover:text-foreground",
495
+ "aria-label": `Remove ${attachment.name}`,
496
+ children: /* @__PURE__ */ jsxDEV4(X, {
497
+ className: "h-3.5 w-3.5"
498
+ }, undefined, false, undefined, this)
499
+ }, undefined, false, undefined, this)
500
+ ]
501
+ }, attachment.id, true, undefined, this))
502
+ }, undefined, false, undefined, this),
503
+ /* @__PURE__ */ jsxDEV4("form", {
504
+ onSubmit: handleSubmit,
505
+ className: "flex items-end gap-2",
506
+ children: [
507
+ showAttachments && /* @__PURE__ */ jsxDEV4(Fragment2, {
508
+ children: [
509
+ /* @__PURE__ */ jsxDEV4("input", {
510
+ ref: fileInputRef,
511
+ type: "file",
512
+ multiple: true,
513
+ accept: ".ts,.tsx,.js,.jsx,.json,.md,.txt,.py,.go,.rs,.java,.yaml,.yml",
514
+ onChange: handleFileSelect,
515
+ className: "hidden",
516
+ "aria-label": "Attach files"
517
+ }, undefined, false, undefined, this),
518
+ /* @__PURE__ */ jsxDEV4(Button3, {
519
+ type: "button",
520
+ variant: "ghost",
521
+ size: "sm",
522
+ onPress: () => fileInputRef.current?.click(),
523
+ disabled: disabled || attachments.length >= maxAttachments,
524
+ "aria-label": "Attach files",
525
+ children: /* @__PURE__ */ jsxDEV4(Paperclip, {
526
+ className: "h-4 w-4"
527
+ }, undefined, false, undefined, this)
528
+ }, undefined, false, undefined, this)
529
+ ]
530
+ }, undefined, true, undefined, this),
531
+ /* @__PURE__ */ jsxDEV4("div", {
532
+ className: "relative flex-1",
533
+ children: /* @__PURE__ */ jsxDEV4(Textarea, {
534
+ value: content,
535
+ onChange: (e) => setContent(e.target.value),
536
+ onKeyDown: handleKeyDown,
537
+ placeholder,
538
+ disabled,
539
+ className: cn4("max-h-[200px] min-h-[44px] resize-none pr-12", "focus-visible:ring-1"),
540
+ rows: 1,
541
+ "aria-label": "Chat message"
542
+ }, undefined, false, undefined, this)
543
+ }, undefined, false, undefined, this),
544
+ /* @__PURE__ */ jsxDEV4(Button3, {
545
+ type: "submit",
546
+ disabled: !canSend || disabled || isLoading,
547
+ size: "sm",
548
+ "aria-label": isLoading ? "Sending..." : "Send message",
549
+ children: isLoading ? /* @__PURE__ */ jsxDEV4(Loader2, {
550
+ className: "h-4 w-4 animate-spin"
551
+ }, undefined, false, undefined, this) : /* @__PURE__ */ jsxDEV4(Send, {
552
+ className: "h-4 w-4"
553
+ }, undefined, false, undefined, this)
554
+ }, undefined, false, undefined, this)
555
+ ]
556
+ }, undefined, true, undefined, this),
557
+ /* @__PURE__ */ jsxDEV4("p", {
558
+ className: "text-muted-foreground text-xs",
559
+ children: "Press Enter to send, Shift+Enter for new line"
560
+ }, undefined, false, undefined, this)
561
+ ]
562
+ }, undefined, true, undefined, this);
563
+ }
564
+ // src/presentation/components/ModelPicker.tsx
565
+ import * as React5 from "react";
566
+ import { cn as cn5 } from "@contractspec/lib.ui-kit-web/ui/utils";
567
+ import { Button as Button4 } from "@contractspec/lib.design-system";
568
+ import {
569
+ Select,
570
+ SelectContent,
571
+ SelectItem,
572
+ SelectTrigger,
573
+ SelectValue
574
+ } from "@contractspec/lib.ui-kit-web/ui/select";
575
+ import { Badge } from "@contractspec/lib.ui-kit-web/ui/badge";
576
+ import { Label } from "@contractspec/lib.ui-kit-web/ui/label";
577
+ import { Bot as Bot2, Cloud, Cpu, Sparkles } from "lucide-react";
578
+ import {
579
+ getModelsForProvider
580
+ } from "@contractspec/lib.ai-providers";
581
+ import { jsxDEV as jsxDEV5 } from "react/jsx-dev-runtime";
582
+ "use client";
583
+ var PROVIDER_ICONS = {
584
+ ollama: /* @__PURE__ */ jsxDEV5(Cpu, {
585
+ className: "h-4 w-4"
586
+ }, undefined, false, undefined, this),
587
+ openai: /* @__PURE__ */ jsxDEV5(Bot2, {
588
+ className: "h-4 w-4"
589
+ }, undefined, false, undefined, this),
590
+ anthropic: /* @__PURE__ */ jsxDEV5(Sparkles, {
591
+ className: "h-4 w-4"
592
+ }, undefined, false, undefined, this),
593
+ mistral: /* @__PURE__ */ jsxDEV5(Cloud, {
594
+ className: "h-4 w-4"
595
+ }, undefined, false, undefined, this),
596
+ gemini: /* @__PURE__ */ jsxDEV5(Sparkles, {
597
+ className: "h-4 w-4"
598
+ }, undefined, false, undefined, this)
599
+ };
600
+ var PROVIDER_NAMES = {
601
+ ollama: "Ollama (Local)",
602
+ openai: "OpenAI",
603
+ anthropic: "Anthropic",
604
+ mistral: "Mistral",
605
+ gemini: "Google Gemini"
606
+ };
607
+ var MODE_BADGES = {
608
+ local: { label: "Local", variant: "secondary" },
609
+ byok: { label: "BYOK", variant: "outline" },
610
+ managed: { label: "Managed", variant: "default" }
611
+ };
612
+ function ModelPicker({
613
+ value,
614
+ onChange,
615
+ availableProviders,
616
+ className,
617
+ compact = false
618
+ }) {
619
+ const providers = availableProviders ?? [
620
+ { provider: "ollama", available: true, mode: "local" },
621
+ { provider: "openai", available: true, mode: "byok" },
622
+ { provider: "anthropic", available: true, mode: "byok" },
623
+ { provider: "mistral", available: true, mode: "byok" },
624
+ { provider: "gemini", available: true, mode: "byok" }
625
+ ];
626
+ const models = getModelsForProvider(value.provider);
627
+ const selectedModel = models.find((m) => m.id === value.model);
628
+ const handleProviderChange = React5.useCallback((providerName) => {
629
+ const provider = providerName;
630
+ const providerInfo = providers.find((p) => p.provider === provider);
631
+ const providerModels = getModelsForProvider(provider);
632
+ const defaultModel = providerModels[0]?.id ?? "";
633
+ onChange({
634
+ provider,
635
+ model: defaultModel,
636
+ mode: providerInfo?.mode ?? "byok"
637
+ });
638
+ }, [onChange, providers]);
639
+ const handleModelChange = React5.useCallback((modelId) => {
640
+ onChange({
641
+ ...value,
642
+ model: modelId
643
+ });
644
+ }, [onChange, value]);
645
+ if (compact) {
646
+ return /* @__PURE__ */ jsxDEV5("div", {
647
+ className: cn5("flex items-center gap-2", className),
648
+ children: [
649
+ /* @__PURE__ */ jsxDEV5(Select, {
650
+ value: value.provider,
651
+ onValueChange: handleProviderChange,
652
+ children: [
653
+ /* @__PURE__ */ jsxDEV5(SelectTrigger, {
654
+ className: "w-[140px]",
655
+ children: /* @__PURE__ */ jsxDEV5(SelectValue, {}, undefined, false, undefined, this)
656
+ }, undefined, false, undefined, this),
657
+ /* @__PURE__ */ jsxDEV5(SelectContent, {
658
+ children: providers.map((p) => /* @__PURE__ */ jsxDEV5(SelectItem, {
659
+ value: p.provider,
660
+ disabled: !p.available,
661
+ children: /* @__PURE__ */ jsxDEV5("div", {
662
+ className: "flex items-center gap-2",
663
+ children: [
664
+ PROVIDER_ICONS[p.provider],
665
+ /* @__PURE__ */ jsxDEV5("span", {
666
+ children: PROVIDER_NAMES[p.provider]
667
+ }, undefined, false, undefined, this)
668
+ ]
669
+ }, undefined, true, undefined, this)
670
+ }, p.provider, false, undefined, this))
671
+ }, undefined, false, undefined, this)
672
+ ]
673
+ }, undefined, true, undefined, this),
674
+ /* @__PURE__ */ jsxDEV5(Select, {
675
+ value: value.model,
676
+ onValueChange: handleModelChange,
677
+ children: [
678
+ /* @__PURE__ */ jsxDEV5(SelectTrigger, {
679
+ className: "w-[160px]",
680
+ children: /* @__PURE__ */ jsxDEV5(SelectValue, {}, undefined, false, undefined, this)
681
+ }, undefined, false, undefined, this),
682
+ /* @__PURE__ */ jsxDEV5(SelectContent, {
683
+ children: models.map((m) => /* @__PURE__ */ jsxDEV5(SelectItem, {
684
+ value: m.id,
685
+ children: m.name
686
+ }, m.id, false, undefined, this))
687
+ }, undefined, false, undefined, this)
688
+ ]
689
+ }, undefined, true, undefined, this)
690
+ ]
691
+ }, undefined, true, undefined, this);
692
+ }
693
+ return /* @__PURE__ */ jsxDEV5("div", {
694
+ className: cn5("flex flex-col gap-3", className),
695
+ children: [
696
+ /* @__PURE__ */ jsxDEV5("div", {
697
+ className: "flex flex-col gap-1.5",
698
+ children: [
699
+ /* @__PURE__ */ jsxDEV5(Label, {
700
+ htmlFor: "provider-selection",
701
+ className: "text-sm font-medium",
702
+ children: "Provider"
703
+ }, undefined, false, undefined, this),
704
+ /* @__PURE__ */ jsxDEV5("div", {
705
+ className: "flex flex-wrap gap-2",
706
+ id: "provider-selection",
707
+ children: providers.map((p) => /* @__PURE__ */ jsxDEV5(Button4, {
708
+ variant: value.provider === p.provider ? "default" : "outline",
709
+ size: "sm",
710
+ onPress: () => p.available && handleProviderChange(p.provider),
711
+ disabled: !p.available,
712
+ className: cn5(!p.available && "opacity-50"),
713
+ children: [
714
+ PROVIDER_ICONS[p.provider],
715
+ /* @__PURE__ */ jsxDEV5("span", {
716
+ children: PROVIDER_NAMES[p.provider]
717
+ }, undefined, false, undefined, this),
718
+ /* @__PURE__ */ jsxDEV5(Badge, {
719
+ variant: MODE_BADGES[p.mode].variant,
720
+ className: "ml-1",
721
+ children: MODE_BADGES[p.mode].label
722
+ }, undefined, false, undefined, this)
723
+ ]
724
+ }, p.provider, true, undefined, this))
725
+ }, undefined, false, undefined, this)
726
+ ]
727
+ }, undefined, true, undefined, this),
728
+ /* @__PURE__ */ jsxDEV5("div", {
729
+ className: "flex flex-col gap-1.5",
730
+ children: [
731
+ /* @__PURE__ */ jsxDEV5(Label, {
732
+ htmlFor: "model-picker",
733
+ className: "text-sm font-medium",
734
+ children: "Model"
735
+ }, undefined, false, undefined, this),
736
+ /* @__PURE__ */ jsxDEV5(Select, {
737
+ name: "model-picker",
738
+ value: value.model,
739
+ onValueChange: handleModelChange,
740
+ children: [
741
+ /* @__PURE__ */ jsxDEV5(SelectTrigger, {
742
+ children: /* @__PURE__ */ jsxDEV5(SelectValue, {
743
+ placeholder: "Select a model"
744
+ }, undefined, false, undefined, this)
745
+ }, undefined, false, undefined, this),
746
+ /* @__PURE__ */ jsxDEV5(SelectContent, {
747
+ children: models.map((m) => /* @__PURE__ */ jsxDEV5(SelectItem, {
748
+ value: m.id,
749
+ children: /* @__PURE__ */ jsxDEV5("div", {
750
+ className: "flex items-center gap-2",
751
+ children: [
752
+ /* @__PURE__ */ jsxDEV5("span", {
753
+ children: m.name
754
+ }, undefined, false, undefined, this),
755
+ /* @__PURE__ */ jsxDEV5("span", {
756
+ className: "text-muted-foreground text-xs",
757
+ children: [
758
+ Math.round(m.contextWindow / 1000),
759
+ "K"
760
+ ]
761
+ }, undefined, true, undefined, this),
762
+ m.capabilities.vision && /* @__PURE__ */ jsxDEV5(Badge, {
763
+ variant: "outline",
764
+ className: "text-xs",
765
+ children: "Vision"
766
+ }, undefined, false, undefined, this),
767
+ m.capabilities.reasoning && /* @__PURE__ */ jsxDEV5(Badge, {
768
+ variant: "outline",
769
+ className: "text-xs",
770
+ children: "Reasoning"
771
+ }, undefined, false, undefined, this)
772
+ ]
773
+ }, undefined, true, undefined, this)
774
+ }, m.id, false, undefined, this))
775
+ }, undefined, false, undefined, this)
776
+ ]
777
+ }, undefined, true, undefined, this)
778
+ ]
779
+ }, undefined, true, undefined, this),
780
+ selectedModel && /* @__PURE__ */ jsxDEV5("div", {
781
+ className: "text-muted-foreground flex flex-wrap gap-2 text-xs",
782
+ children: [
783
+ /* @__PURE__ */ jsxDEV5("span", {
784
+ children: [
785
+ "Context: ",
786
+ Math.round(selectedModel.contextWindow / 1000),
787
+ "K tokens"
788
+ ]
789
+ }, undefined, true, undefined, this),
790
+ selectedModel.capabilities.vision && /* @__PURE__ */ jsxDEV5("span", {
791
+ children: "• Vision"
792
+ }, undefined, false, undefined, this),
793
+ selectedModel.capabilities.tools && /* @__PURE__ */ jsxDEV5("span", {
794
+ children: "• Tools"
795
+ }, undefined, false, undefined, this),
796
+ selectedModel.capabilities.reasoning && /* @__PURE__ */ jsxDEV5("span", {
797
+ children: "• Reasoning"
798
+ }, undefined, false, undefined, this)
799
+ ]
800
+ }, undefined, true, undefined, this)
801
+ ]
802
+ }, undefined, true, undefined, this);
803
+ }
804
+ // src/presentation/components/ContextIndicator.tsx
805
+ import { cn as cn6 } from "@contractspec/lib.ui-kit-web/ui/utils";
806
+ import { Badge as Badge2 } from "@contractspec/lib.ui-kit-web/ui/badge";
807
+ import {
808
+ Tooltip,
809
+ TooltipContent,
810
+ TooltipProvider,
811
+ TooltipTrigger
812
+ } from "@contractspec/lib.ui-kit-web/ui/tooltip";
813
+ import { FolderOpen, FileCode, Zap, Info } from "lucide-react";
814
+ import { jsxDEV as jsxDEV6, Fragment as Fragment3 } from "react/jsx-dev-runtime";
815
+ "use client";
816
+ function ContextIndicator({
817
+ summary,
818
+ active = false,
819
+ className,
820
+ showDetails = true
821
+ }) {
822
+ if (!summary && !active) {
823
+ return /* @__PURE__ */ jsxDEV6("div", {
824
+ className: cn6("flex items-center gap-1.5 text-sm", "text-muted-foreground", className),
825
+ children: [
826
+ /* @__PURE__ */ jsxDEV6(Info, {
827
+ className: "h-4 w-4"
828
+ }, undefined, false, undefined, this),
829
+ /* @__PURE__ */ jsxDEV6("span", {
830
+ children: "No workspace context"
831
+ }, undefined, false, undefined, this)
832
+ ]
833
+ }, undefined, true, undefined, this);
834
+ }
835
+ const content = /* @__PURE__ */ jsxDEV6("div", {
836
+ className: cn6("flex items-center gap-2", active ? "text-foreground" : "text-muted-foreground", className),
837
+ children: [
838
+ /* @__PURE__ */ jsxDEV6(Badge2, {
839
+ variant: active ? "default" : "secondary",
840
+ className: "flex items-center gap-1",
841
+ children: [
842
+ /* @__PURE__ */ jsxDEV6(Zap, {
843
+ className: "h-3 w-3"
844
+ }, undefined, false, undefined, this),
845
+ "Context"
846
+ ]
847
+ }, undefined, true, undefined, this),
848
+ summary && showDetails && /* @__PURE__ */ jsxDEV6(Fragment3, {
849
+ children: [
850
+ /* @__PURE__ */ jsxDEV6("div", {
851
+ className: "flex items-center gap-1 text-xs",
852
+ children: [
853
+ /* @__PURE__ */ jsxDEV6(FolderOpen, {
854
+ className: "h-3.5 w-3.5"
855
+ }, undefined, false, undefined, this),
856
+ /* @__PURE__ */ jsxDEV6("span", {
857
+ children: summary.name
858
+ }, undefined, false, undefined, this)
859
+ ]
860
+ }, undefined, true, undefined, this),
861
+ /* @__PURE__ */ jsxDEV6("div", {
862
+ className: "flex items-center gap-1 text-xs",
863
+ children: [
864
+ /* @__PURE__ */ jsxDEV6(FileCode, {
865
+ className: "h-3.5 w-3.5"
866
+ }, undefined, false, undefined, this),
867
+ /* @__PURE__ */ jsxDEV6("span", {
868
+ children: [
869
+ summary.specs.total,
870
+ " specs"
871
+ ]
872
+ }, undefined, true, undefined, this)
873
+ ]
874
+ }, undefined, true, undefined, this)
875
+ ]
876
+ }, undefined, true, undefined, this)
877
+ ]
878
+ }, undefined, true, undefined, this);
879
+ if (!summary) {
880
+ return content;
881
+ }
882
+ return /* @__PURE__ */ jsxDEV6(TooltipProvider, {
883
+ children: /* @__PURE__ */ jsxDEV6(Tooltip, {
884
+ children: [
885
+ /* @__PURE__ */ jsxDEV6(TooltipTrigger, {
886
+ asChild: true,
887
+ children: content
888
+ }, undefined, false, undefined, this),
889
+ /* @__PURE__ */ jsxDEV6(TooltipContent, {
890
+ side: "bottom",
891
+ className: "max-w-[300px]",
892
+ children: /* @__PURE__ */ jsxDEV6("div", {
893
+ className: "flex flex-col gap-2 text-sm",
894
+ children: [
895
+ /* @__PURE__ */ jsxDEV6("div", {
896
+ className: "font-medium",
897
+ children: summary.name
898
+ }, undefined, false, undefined, this),
899
+ /* @__PURE__ */ jsxDEV6("div", {
900
+ className: "text-muted-foreground text-xs",
901
+ children: summary.path
902
+ }, undefined, false, undefined, this),
903
+ /* @__PURE__ */ jsxDEV6("div", {
904
+ className: "border-t pt-2",
905
+ children: /* @__PURE__ */ jsxDEV6("div", {
906
+ className: "grid grid-cols-2 gap-1 text-xs",
907
+ children: [
908
+ /* @__PURE__ */ jsxDEV6("span", {
909
+ children: "Commands:"
910
+ }, undefined, false, undefined, this),
911
+ /* @__PURE__ */ jsxDEV6("span", {
912
+ className: "text-right",
913
+ children: summary.specs.commands
914
+ }, undefined, false, undefined, this),
915
+ /* @__PURE__ */ jsxDEV6("span", {
916
+ children: "Queries:"
917
+ }, undefined, false, undefined, this),
918
+ /* @__PURE__ */ jsxDEV6("span", {
919
+ className: "text-right",
920
+ children: summary.specs.queries
921
+ }, undefined, false, undefined, this),
922
+ /* @__PURE__ */ jsxDEV6("span", {
923
+ children: "Events:"
924
+ }, undefined, false, undefined, this),
925
+ /* @__PURE__ */ jsxDEV6("span", {
926
+ className: "text-right",
927
+ children: summary.specs.events
928
+ }, undefined, false, undefined, this),
929
+ /* @__PURE__ */ jsxDEV6("span", {
930
+ children: "Presentations:"
931
+ }, undefined, false, undefined, this),
932
+ /* @__PURE__ */ jsxDEV6("span", {
933
+ className: "text-right",
934
+ children: summary.specs.presentations
935
+ }, undefined, false, undefined, this)
936
+ ]
937
+ }, undefined, true, undefined, this)
938
+ }, undefined, false, undefined, this),
939
+ /* @__PURE__ */ jsxDEV6("div", {
940
+ className: "border-t pt-2 text-xs",
941
+ children: [
942
+ /* @__PURE__ */ jsxDEV6("span", {
943
+ children: [
944
+ summary.files.total,
945
+ " files"
946
+ ]
947
+ }, undefined, true, undefined, this),
948
+ /* @__PURE__ */ jsxDEV6("span", {
949
+ className: "mx-1",
950
+ children: "•"
951
+ }, undefined, false, undefined, this),
952
+ /* @__PURE__ */ jsxDEV6("span", {
953
+ children: [
954
+ summary.files.specFiles,
955
+ " spec files"
956
+ ]
957
+ }, undefined, true, undefined, this)
958
+ ]
959
+ }, undefined, true, undefined, this)
960
+ ]
961
+ }, undefined, true, undefined, this)
962
+ }, undefined, false, undefined, this)
963
+ ]
964
+ }, undefined, true, undefined, this)
965
+ }, undefined, false, undefined, this);
966
+ }
967
+ // src/presentation/hooks/useChat.tsx
968
+ import * as React6 from "react";
969
+
970
+ // src/core/chat-service.ts
971
+ import { generateText, streamText } from "ai";
972
+
973
+ // src/core/conversation-store.ts
974
+ function generateId(prefix) {
975
+ return `${prefix}_${Date.now()}_${Math.random().toString(36).slice(2, 11)}`;
976
+ }
977
+
978
+ class InMemoryConversationStore {
979
+ conversations = new Map;
980
+ async get(conversationId) {
981
+ return this.conversations.get(conversationId) ?? null;
982
+ }
983
+ async create(conversation) {
984
+ const now = new Date;
985
+ const fullConversation = {
986
+ ...conversation,
987
+ id: generateId("conv"),
988
+ createdAt: now,
989
+ updatedAt: now
990
+ };
991
+ this.conversations.set(fullConversation.id, fullConversation);
992
+ return fullConversation;
993
+ }
994
+ async update(conversationId, updates) {
995
+ const conversation = this.conversations.get(conversationId);
996
+ if (!conversation)
997
+ return null;
998
+ const updated = {
999
+ ...conversation,
1000
+ ...updates,
1001
+ updatedAt: new Date
1002
+ };
1003
+ this.conversations.set(conversationId, updated);
1004
+ return updated;
1005
+ }
1006
+ async appendMessage(conversationId, message) {
1007
+ const conversation = this.conversations.get(conversationId);
1008
+ if (!conversation) {
1009
+ throw new Error(`Conversation ${conversationId} not found`);
1010
+ }
1011
+ const now = new Date;
1012
+ const fullMessage = {
1013
+ ...message,
1014
+ id: generateId("msg"),
1015
+ conversationId,
1016
+ createdAt: now,
1017
+ updatedAt: now
1018
+ };
1019
+ conversation.messages.push(fullMessage);
1020
+ conversation.updatedAt = now;
1021
+ return fullMessage;
1022
+ }
1023
+ async updateMessage(conversationId, messageId, updates) {
1024
+ const conversation = this.conversations.get(conversationId);
1025
+ if (!conversation)
1026
+ return null;
1027
+ const messageIndex = conversation.messages.findIndex((m) => m.id === messageId);
1028
+ if (messageIndex === -1)
1029
+ return null;
1030
+ const message = conversation.messages[messageIndex];
1031
+ if (!message)
1032
+ return null;
1033
+ const updated = {
1034
+ ...message,
1035
+ ...updates,
1036
+ updatedAt: new Date
1037
+ };
1038
+ conversation.messages[messageIndex] = updated;
1039
+ conversation.updatedAt = new Date;
1040
+ return updated;
1041
+ }
1042
+ async delete(conversationId) {
1043
+ return this.conversations.delete(conversationId);
1044
+ }
1045
+ async list(options) {
1046
+ let results = Array.from(this.conversations.values());
1047
+ if (options?.status) {
1048
+ results = results.filter((c) => c.status === options.status);
1049
+ }
1050
+ results.sort((a, b) => b.updatedAt.getTime() - a.updatedAt.getTime());
1051
+ const offset = options?.offset ?? 0;
1052
+ const limit = options?.limit ?? 100;
1053
+ return results.slice(offset, offset + limit);
1054
+ }
1055
+ async search(query, limit = 20) {
1056
+ const lowerQuery = query.toLowerCase();
1057
+ const results = [];
1058
+ for (const conversation of this.conversations.values()) {
1059
+ if (conversation.title?.toLowerCase().includes(lowerQuery)) {
1060
+ results.push(conversation);
1061
+ continue;
1062
+ }
1063
+ const hasMatch = conversation.messages.some((m) => m.content.toLowerCase().includes(lowerQuery));
1064
+ if (hasMatch) {
1065
+ results.push(conversation);
1066
+ }
1067
+ if (results.length >= limit)
1068
+ break;
1069
+ }
1070
+ return results;
1071
+ }
1072
+ clear() {
1073
+ this.conversations.clear();
1074
+ }
1075
+ }
1076
+ function createInMemoryConversationStore() {
1077
+ return new InMemoryConversationStore;
1078
+ }
1079
+
1080
+ // src/core/chat-service.ts
1081
+ var DEFAULT_SYSTEM_PROMPT = `You are ContractSpec AI, an expert coding assistant specialized in ContractSpec development.
1082
+
1083
+ Your capabilities:
1084
+ - Help users create, modify, and understand ContractSpec specifications
1085
+ - Generate code that follows ContractSpec patterns and best practices
1086
+ - Explain concepts from the ContractSpec documentation
1087
+ - Suggest improvements and identify issues in specs and implementations
1088
+
1089
+ Guidelines:
1090
+ - Be concise but thorough
1091
+ - Provide code examples when helpful
1092
+ - Reference relevant ContractSpec concepts and patterns
1093
+ - Ask clarifying questions when the user's intent is unclear
1094
+ - When suggesting code changes, explain the rationale`;
1095
+
1096
+ class ChatService {
1097
+ provider;
1098
+ context;
1099
+ store;
1100
+ systemPrompt;
1101
+ maxHistoryMessages;
1102
+ onUsage;
1103
+ constructor(config) {
1104
+ this.provider = config.provider;
1105
+ this.context = config.context;
1106
+ this.store = config.store ?? new InMemoryConversationStore;
1107
+ this.systemPrompt = config.systemPrompt ?? DEFAULT_SYSTEM_PROMPT;
1108
+ this.maxHistoryMessages = config.maxHistoryMessages ?? 20;
1109
+ this.onUsage = config.onUsage;
1110
+ }
1111
+ async send(options) {
1112
+ let conversation;
1113
+ if (options.conversationId) {
1114
+ const existing = await this.store.get(options.conversationId);
1115
+ if (!existing) {
1116
+ throw new Error(`Conversation ${options.conversationId} not found`);
1117
+ }
1118
+ conversation = existing;
1119
+ } else {
1120
+ conversation = await this.store.create({
1121
+ status: "active",
1122
+ provider: this.provider.name,
1123
+ model: this.provider.model,
1124
+ messages: [],
1125
+ workspacePath: this.context?.workspacePath
1126
+ });
1127
+ }
1128
+ await this.store.appendMessage(conversation.id, {
1129
+ role: "user",
1130
+ content: options.content,
1131
+ status: "completed",
1132
+ attachments: options.attachments
1133
+ });
1134
+ const prompt = this.buildPrompt(conversation, options);
1135
+ const model = this.provider.getModel();
1136
+ try {
1137
+ const result = await generateText({
1138
+ model,
1139
+ prompt,
1140
+ system: this.systemPrompt
1141
+ });
1142
+ const assistantMessage = await this.store.appendMessage(conversation.id, {
1143
+ role: "assistant",
1144
+ content: result.text,
1145
+ status: "completed"
1146
+ });
1147
+ const updatedConversation = await this.store.get(conversation.id);
1148
+ if (!updatedConversation) {
1149
+ throw new Error("Conversation lost after update");
1150
+ }
1151
+ return {
1152
+ message: assistantMessage,
1153
+ conversation: updatedConversation
1154
+ };
1155
+ } catch (error) {
1156
+ await this.store.appendMessage(conversation.id, {
1157
+ role: "assistant",
1158
+ content: "",
1159
+ status: "error",
1160
+ error: {
1161
+ code: "generation_failed",
1162
+ message: error instanceof Error ? error.message : String(error)
1163
+ }
1164
+ });
1165
+ throw error;
1166
+ }
1167
+ }
1168
+ async stream(options) {
1169
+ let conversation;
1170
+ if (options.conversationId) {
1171
+ const existing = await this.store.get(options.conversationId);
1172
+ if (!existing) {
1173
+ throw new Error(`Conversation ${options.conversationId} not found`);
1174
+ }
1175
+ conversation = existing;
1176
+ } else {
1177
+ conversation = await this.store.create({
1178
+ status: "active",
1179
+ provider: this.provider.name,
1180
+ model: this.provider.model,
1181
+ messages: [],
1182
+ workspacePath: this.context?.workspacePath
1183
+ });
1184
+ }
1185
+ await this.store.appendMessage(conversation.id, {
1186
+ role: "user",
1187
+ content: options.content,
1188
+ status: "completed",
1189
+ attachments: options.attachments
1190
+ });
1191
+ const assistantMessage = await this.store.appendMessage(conversation.id, {
1192
+ role: "assistant",
1193
+ content: "",
1194
+ status: "streaming"
1195
+ });
1196
+ const prompt = this.buildPrompt(conversation, options);
1197
+ const model = this.provider.getModel();
1198
+ const self = {
1199
+ systemPrompt: this.systemPrompt,
1200
+ store: this.store
1201
+ };
1202
+ async function* streamGenerator() {
1203
+ let fullContent = "";
1204
+ try {
1205
+ const result = streamText({
1206
+ model,
1207
+ prompt,
1208
+ system: self.systemPrompt
1209
+ });
1210
+ for await (const chunk of result.textStream) {
1211
+ fullContent += chunk;
1212
+ yield { type: "text", content: chunk };
1213
+ }
1214
+ await self.store.updateMessage(conversation.id, assistantMessage.id, {
1215
+ content: fullContent,
1216
+ status: "completed"
1217
+ });
1218
+ yield {
1219
+ type: "done"
1220
+ };
1221
+ } catch (error) {
1222
+ await self.store.updateMessage(conversation.id, assistantMessage.id, {
1223
+ content: fullContent,
1224
+ status: "error",
1225
+ error: {
1226
+ code: "stream_failed",
1227
+ message: error instanceof Error ? error.message : String(error)
1228
+ }
1229
+ });
1230
+ yield {
1231
+ type: "error",
1232
+ error: {
1233
+ code: "stream_failed",
1234
+ message: error instanceof Error ? error.message : String(error)
1235
+ }
1236
+ };
1237
+ }
1238
+ }
1239
+ return {
1240
+ conversationId: conversation.id,
1241
+ messageId: assistantMessage.id,
1242
+ stream: streamGenerator()
1243
+ };
1244
+ }
1245
+ async getConversation(conversationId) {
1246
+ return this.store.get(conversationId);
1247
+ }
1248
+ async listConversations(options) {
1249
+ return this.store.list({
1250
+ status: "active",
1251
+ ...options
1252
+ });
1253
+ }
1254
+ async deleteConversation(conversationId) {
1255
+ return this.store.delete(conversationId);
1256
+ }
1257
+ buildPrompt(conversation, options) {
1258
+ let prompt = "";
1259
+ const historyStart = Math.max(0, conversation.messages.length - this.maxHistoryMessages);
1260
+ for (let i = historyStart;i < conversation.messages.length; i++) {
1261
+ const msg = conversation.messages[i];
1262
+ if (!msg)
1263
+ continue;
1264
+ if (msg.role === "user" || msg.role === "assistant") {
1265
+ prompt += `${msg.role === "user" ? "User" : "Assistant"}: ${msg.content}
1266
+
1267
+ `;
1268
+ }
1269
+ }
1270
+ let content = options.content;
1271
+ if (options.attachments?.length) {
1272
+ const attachmentInfo = options.attachments.map((a) => {
1273
+ if (a.type === "file" || a.type === "code") {
1274
+ return `
1275
+
1276
+ ### ${a.name}
1277
+ \`\`\`
1278
+ ${a.content}
1279
+ \`\`\``;
1280
+ }
1281
+ return `
1282
+
1283
+ [Attachment: ${a.name}]`;
1284
+ }).join("");
1285
+ content += attachmentInfo;
1286
+ }
1287
+ prompt += `User: ${content}
1288
+
1289
+ Assistant:`;
1290
+ return prompt;
1291
+ }
1292
+ }
1293
+ function createChatService(config) {
1294
+ return new ChatService(config);
1295
+ }
1296
+
1297
+ // src/presentation/hooks/useChat.tsx
1298
+ import {
1299
+ createProvider
1300
+ } from "@contractspec/lib.ai-providers";
1301
+ "use client";
1302
+ function useChat(options = {}) {
1303
+ const {
1304
+ provider = "openai",
1305
+ mode = "byok",
1306
+ model,
1307
+ apiKey,
1308
+ proxyUrl,
1309
+ conversationId: initialConversationId,
1310
+ systemPrompt,
1311
+ streaming = true,
1312
+ onSend,
1313
+ onResponse,
1314
+ onError,
1315
+ onUsage
1316
+ } = options;
1317
+ const [messages, setMessages] = React6.useState([]);
1318
+ const [conversation, setConversation] = React6.useState(null);
1319
+ const [isLoading, setIsLoading] = React6.useState(false);
1320
+ const [error, setError] = React6.useState(null);
1321
+ const [conversationId, setConversationId] = React6.useState(initialConversationId ?? null);
1322
+ const abortControllerRef = React6.useRef(null);
1323
+ const chatServiceRef = React6.useRef(null);
1324
+ React6.useEffect(() => {
1325
+ const chatProvider = createProvider({
1326
+ provider,
1327
+ model,
1328
+ apiKey,
1329
+ proxyUrl
1330
+ });
1331
+ chatServiceRef.current = new ChatService({
1332
+ provider: chatProvider,
1333
+ systemPrompt,
1334
+ onUsage
1335
+ });
1336
+ }, [provider, mode, model, apiKey, proxyUrl, systemPrompt, onUsage]);
1337
+ React6.useEffect(() => {
1338
+ if (!conversationId || !chatServiceRef.current)
1339
+ return;
1340
+ const loadConversation = async () => {
1341
+ if (!chatServiceRef.current)
1342
+ return;
1343
+ const conv = await chatServiceRef.current.getConversation(conversationId);
1344
+ if (conv) {
1345
+ setConversation(conv);
1346
+ setMessages(conv.messages);
1347
+ }
1348
+ };
1349
+ loadConversation().catch(console.error);
1350
+ }, [conversationId]);
1351
+ const sendMessage = React6.useCallback(async (content, attachments) => {
1352
+ if (!chatServiceRef.current) {
1353
+ throw new Error("Chat service not initialized");
1354
+ }
1355
+ setIsLoading(true);
1356
+ setError(null);
1357
+ abortControllerRef.current = new AbortController;
1358
+ try {
1359
+ const userMessage = {
1360
+ id: `msg_${Date.now()}`,
1361
+ conversationId: conversationId ?? "",
1362
+ role: "user",
1363
+ content,
1364
+ status: "completed",
1365
+ createdAt: new Date,
1366
+ updatedAt: new Date,
1367
+ attachments
1368
+ };
1369
+ setMessages((prev) => [...prev, userMessage]);
1370
+ onSend?.(userMessage);
1371
+ if (streaming) {
1372
+ const result = await chatServiceRef.current.stream({
1373
+ conversationId: conversationId ?? undefined,
1374
+ content,
1375
+ attachments
1376
+ });
1377
+ if (!conversationId) {
1378
+ setConversationId(result.conversationId);
1379
+ }
1380
+ const assistantMessage = {
1381
+ id: result.messageId,
1382
+ conversationId: result.conversationId,
1383
+ role: "assistant",
1384
+ content: "",
1385
+ status: "streaming",
1386
+ createdAt: new Date,
1387
+ updatedAt: new Date
1388
+ };
1389
+ setMessages((prev) => [...prev, assistantMessage]);
1390
+ let fullContent = "";
1391
+ for await (const chunk of result.stream) {
1392
+ if (chunk.type === "text" && chunk.content) {
1393
+ fullContent += chunk.content;
1394
+ setMessages((prev) => prev.map((m) => m.id === result.messageId ? { ...m, content: fullContent } : m));
1395
+ } else if (chunk.type === "done") {
1396
+ setMessages((prev) => prev.map((m) => m.id === result.messageId ? {
1397
+ ...m,
1398
+ status: "completed",
1399
+ usage: chunk.usage,
1400
+ updatedAt: new Date
1401
+ } : m));
1402
+ onResponse?.(messages.find((m) => m.id === result.messageId) ?? assistantMessage);
1403
+ } else if (chunk.type === "error") {
1404
+ setMessages((prev) => prev.map((m) => m.id === result.messageId ? {
1405
+ ...m,
1406
+ status: "error",
1407
+ error: chunk.error,
1408
+ updatedAt: new Date
1409
+ } : m));
1410
+ if (chunk.error) {
1411
+ const err = new Error(chunk.error.message);
1412
+ setError(err);
1413
+ onError?.(err);
1414
+ }
1415
+ }
1416
+ }
1417
+ } else {
1418
+ const result = await chatServiceRef.current.send({
1419
+ conversationId: conversationId ?? undefined,
1420
+ content,
1421
+ attachments
1422
+ });
1423
+ setConversation(result.conversation);
1424
+ setMessages(result.conversation.messages);
1425
+ if (!conversationId) {
1426
+ setConversationId(result.conversation.id);
1427
+ }
1428
+ onResponse?.(result.message);
1429
+ }
1430
+ } catch (err) {
1431
+ const error2 = err instanceof Error ? err : new Error(String(err));
1432
+ setError(error2);
1433
+ onError?.(error2);
1434
+ } finally {
1435
+ setIsLoading(false);
1436
+ abortControllerRef.current = null;
1437
+ }
1438
+ }, [conversationId, streaming, onSend, onResponse, onError, messages]);
1439
+ const clearConversation = React6.useCallback(() => {
1440
+ setMessages([]);
1441
+ setConversation(null);
1442
+ setConversationId(null);
1443
+ setError(null);
1444
+ }, []);
1445
+ const regenerate = React6.useCallback(async () => {
1446
+ const lastUserMessageIndex = messages.findLastIndex((m) => m.role === "user");
1447
+ if (lastUserMessageIndex === -1)
1448
+ return;
1449
+ const lastUserMessage = messages[lastUserMessageIndex];
1450
+ if (!lastUserMessage)
1451
+ return;
1452
+ setMessages((prev) => prev.slice(0, lastUserMessageIndex + 1));
1453
+ await sendMessage(lastUserMessage.content, lastUserMessage.attachments);
1454
+ }, [messages, sendMessage]);
1455
+ const stop = React6.useCallback(() => {
1456
+ abortControllerRef.current?.abort();
1457
+ setIsLoading(false);
1458
+ }, []);
1459
+ return {
1460
+ messages,
1461
+ conversation,
1462
+ isLoading,
1463
+ error,
1464
+ sendMessage,
1465
+ clearConversation,
1466
+ setConversationId,
1467
+ regenerate,
1468
+ stop
1469
+ };
1470
+ }
1471
+ // src/presentation/hooks/useProviders.tsx
1472
+ import * as React7 from "react";
1473
+ import {
1474
+ getAvailableProviders,
1475
+ getModelsForProvider as getModelsForProvider2
1476
+ } from "@contractspec/lib.ai-providers";
1477
+ "use client";
1478
+ function useProviders() {
1479
+ const [providers, setProviders] = React7.useState([]);
1480
+ const [isLoading, setIsLoading] = React7.useState(true);
1481
+ const loadProviders = React7.useCallback(async () => {
1482
+ setIsLoading(true);
1483
+ try {
1484
+ const available = getAvailableProviders();
1485
+ const providersWithModels = available.map((p) => ({
1486
+ ...p,
1487
+ models: getModelsForProvider2(p.provider)
1488
+ }));
1489
+ setProviders(providersWithModels);
1490
+ } catch (error) {
1491
+ console.error("Failed to load providers:", error);
1492
+ } finally {
1493
+ setIsLoading(false);
1494
+ }
1495
+ }, []);
1496
+ React7.useEffect(() => {
1497
+ loadProviders();
1498
+ }, [loadProviders]);
1499
+ const availableProviders = React7.useMemo(() => providers.filter((p) => p.available), [providers]);
1500
+ const isAvailable = React7.useCallback((provider) => providers.some((p) => p.provider === provider && p.available), [providers]);
1501
+ const getModelsCallback = React7.useCallback((provider) => providers.find((p) => p.provider === provider)?.models ?? [], [providers]);
1502
+ return {
1503
+ providers,
1504
+ availableProviders,
1505
+ isAvailable,
1506
+ getModels: getModelsCallback,
1507
+ isLoading,
1508
+ refresh: loadProviders
1509
+ };
1510
+ }
1511
+ export {
1512
+ useProviders,
1513
+ useChat,
1514
+ ModelPicker,
1515
+ ContextIndicator,
1516
+ CodePreview,
1517
+ ChatMessage,
1518
+ ChatInput,
1519
+ ChatContainer
1520
+ };