@contractspec/module.ai-chat 1.56.1 → 1.58.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 -7
  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 -37
  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 -61
  26. package/dist/core/chat-service.d.ts.map +1 -1
  27. package/dist/core/conversation-store.d.ts +60 -62
  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 -55
  71. package/dist/presentation/hooks/useChat.d.ts.map +1 -1
  72. package/dist/presentation/hooks/useProviders.d.ts +21 -26
  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 -8
  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 +126 -37
  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,2291 @@
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/context/workspace-context.ts
10
+ class WorkspaceContext {
11
+ workspacePath;
12
+ allowWrites;
13
+ specs = [];
14
+ files = [];
15
+ initialized = false;
16
+ constructor(config) {
17
+ this.workspacePath = config.workspacePath;
18
+ this.allowWrites = config.allowWrites ?? false;
19
+ }
20
+ async initialize() {
21
+ if (this.initialized)
22
+ return;
23
+ this.initialized = true;
24
+ }
25
+ getSpecs() {
26
+ return this.specs;
27
+ }
28
+ getFiles() {
29
+ return this.files;
30
+ }
31
+ addSpecs(specs) {
32
+ this.specs.push(...specs);
33
+ }
34
+ addFiles(files) {
35
+ this.files.push(...files);
36
+ }
37
+ getSummary() {
38
+ const commands = this.specs.filter((s) => s.type === "command").length;
39
+ const queries = this.specs.filter((s) => s.type === "query").length;
40
+ const events = this.specs.filter((s) => s.type === "event").length;
41
+ const presentations = this.specs.filter((s) => s.type === "presentation").length;
42
+ const tsFiles = this.files.filter((f) => f.extension === ".ts").length;
43
+ const specFiles = this.files.filter((f) => f.isSpec).length;
44
+ return {
45
+ name: this.workspacePath.split("/").pop() ?? "workspace",
46
+ path: this.workspacePath,
47
+ specs: {
48
+ total: this.specs.length,
49
+ commands,
50
+ queries,
51
+ events,
52
+ presentations
53
+ },
54
+ files: {
55
+ total: this.files.length,
56
+ typescript: tsFiles,
57
+ specFiles
58
+ }
59
+ };
60
+ }
61
+ getContextSummary() {
62
+ const summary = this.getSummary();
63
+ const parts = [
64
+ `Workspace: ${summary.name}`,
65
+ `Path: ${summary.path}`,
66
+ "",
67
+ "### Specs",
68
+ `- Commands: ${summary.specs.commands}`,
69
+ `- Queries: ${summary.specs.queries}`,
70
+ `- Events: ${summary.specs.events}`,
71
+ `- Presentations: ${summary.specs.presentations}`
72
+ ];
73
+ if (this.specs.length > 0) {
74
+ parts.push("", "### Available Specs");
75
+ for (const spec of this.specs.slice(0, 20)) {
76
+ parts.push(`- ${spec.name} (${spec.type})`);
77
+ }
78
+ if (this.specs.length > 20) {
79
+ parts.push(`- ... and ${this.specs.length - 20} more`);
80
+ }
81
+ }
82
+ return parts.join(`
83
+ `);
84
+ }
85
+ findSpecs(query) {
86
+ const lowerQuery = query.toLowerCase();
87
+ return this.specs.filter((s) => s.name.toLowerCase().includes(lowerQuery) || s.description?.toLowerCase().includes(lowerQuery) || s.tags?.some((t) => t.toLowerCase().includes(lowerQuery)));
88
+ }
89
+ findFiles(query) {
90
+ const lowerQuery = query.toLowerCase();
91
+ return this.files.filter((f) => f.path.toLowerCase().includes(lowerQuery) || f.name.toLowerCase().includes(lowerQuery));
92
+ }
93
+ }
94
+ async function createWorkspaceContext(path, options) {
95
+ const context = new WorkspaceContext({
96
+ workspacePath: path,
97
+ ...options
98
+ });
99
+ await context.initialize();
100
+ return context;
101
+ }
102
+ // src/context/context-builder.ts
103
+ function estimateTokens(text) {
104
+ return Math.ceil(text.length / 4);
105
+ }
106
+ function scoreSpec(spec, query) {
107
+ if (!query)
108
+ return 0.5;
109
+ const lowerQuery = query.toLowerCase();
110
+ let score = 0;
111
+ if (spec.name.toLowerCase().includes(lowerQuery)) {
112
+ score += 0.4;
113
+ }
114
+ if (spec.description?.toLowerCase().includes(lowerQuery)) {
115
+ score += 0.3;
116
+ }
117
+ if (spec.tags?.some((t) => t.toLowerCase().includes(lowerQuery))) {
118
+ score += 0.2;
119
+ }
120
+ return Math.min(score, 1);
121
+ }
122
+ function scoreFile(file, query) {
123
+ if (!query)
124
+ return 0.5;
125
+ const lowerQuery = query.toLowerCase();
126
+ let score = 0;
127
+ if (file.path.toLowerCase().includes(lowerQuery)) {
128
+ score += 0.5;
129
+ }
130
+ if (file.name.toLowerCase().includes(lowerQuery)) {
131
+ score += 0.3;
132
+ }
133
+ if (file.isSpec) {
134
+ score += 0.2;
135
+ }
136
+ return Math.min(score, 1);
137
+ }
138
+
139
+ class ContextBuilder {
140
+ context;
141
+ constructor(context) {
142
+ this.context = context;
143
+ }
144
+ build(options = {}) {
145
+ const maxTokens = options.maxTokens ?? 4000;
146
+ const entries = [];
147
+ let totalTokens = 0;
148
+ if (options.includeSpecs?.length) {
149
+ for (const specName of options.includeSpecs) {
150
+ const spec = this.context.getSpecs().find((s) => s.name === specName);
151
+ if (spec) {
152
+ const entry = {
153
+ type: "spec",
154
+ path: spec.path,
155
+ summary: `${spec.type}: ${spec.name}${spec.description ? ` - ${spec.description}` : ""}`,
156
+ relevance: 1
157
+ };
158
+ entries.push(entry);
159
+ totalTokens += estimateTokens(entry.summary ?? "");
160
+ }
161
+ }
162
+ }
163
+ if (options.includeFiles?.length) {
164
+ for (const filePath of options.includeFiles) {
165
+ const file = this.context.getFiles().find((f) => f.path === filePath);
166
+ if (file) {
167
+ const entry = {
168
+ type: "file",
169
+ path: file.path,
170
+ summary: `File: ${file.relativePath}`,
171
+ relevance: 1
172
+ };
173
+ entries.push(entry);
174
+ totalTokens += estimateTokens(entry.summary ?? "");
175
+ }
176
+ }
177
+ }
178
+ if (options.query) {
179
+ const scoredSpecs = this.context.getSpecs().map((spec) => ({ spec, score: scoreSpec(spec, options.query) })).filter(({ score }) => score > 0.2).sort((a, b) => b.score - a.score);
180
+ for (const { spec, score } of scoredSpecs) {
181
+ if (totalTokens >= maxTokens)
182
+ break;
183
+ if (entries.some((e) => e.path === spec.path))
184
+ continue;
185
+ const entry = {
186
+ type: "spec",
187
+ path: spec.path,
188
+ summary: `${spec.type}: ${spec.name}${spec.description ? ` - ${spec.description}` : ""}`,
189
+ relevance: score
190
+ };
191
+ entries.push(entry);
192
+ totalTokens += estimateTokens(entry.summary ?? "");
193
+ }
194
+ }
195
+ if (options.query) {
196
+ const scoredFiles = this.context.getFiles().map((file) => ({ file, score: scoreFile(file, options.query) })).filter(({ score }) => score > 0.2).sort((a, b) => b.score - a.score);
197
+ for (const { file, score } of scoredFiles) {
198
+ if (totalTokens >= maxTokens)
199
+ break;
200
+ if (entries.some((e) => e.path === file.path))
201
+ continue;
202
+ const entry = {
203
+ type: "file",
204
+ path: file.path,
205
+ summary: `File: ${file.relativePath}`,
206
+ relevance: score
207
+ };
208
+ entries.push(entry);
209
+ totalTokens += estimateTokens(entry.summary ?? "");
210
+ }
211
+ }
212
+ const summary = this.buildSummary(entries);
213
+ return {
214
+ entries,
215
+ summary,
216
+ totalTokensEstimate: totalTokens + estimateTokens(summary)
217
+ };
218
+ }
219
+ buildSummary(entries) {
220
+ if (entries.length === 0) {
221
+ return this.context.getContextSummary();
222
+ }
223
+ const parts = [];
224
+ const workspaceSummary = this.context.getSummary();
225
+ parts.push(`Workspace: ${workspaceSummary.name}`);
226
+ parts.push("");
227
+ const specs = entries.filter((e) => e.type === "spec");
228
+ if (specs.length > 0) {
229
+ parts.push("### Relevant Specs");
230
+ for (const entry of specs) {
231
+ parts.push(`- ${entry.summary}`);
232
+ }
233
+ parts.push("");
234
+ }
235
+ const files = entries.filter((e) => e.type === "file");
236
+ if (files.length > 0) {
237
+ parts.push("### Relevant Files");
238
+ for (const entry of files) {
239
+ parts.push(`- ${entry.summary}`);
240
+ }
241
+ }
242
+ return parts.join(`
243
+ `);
244
+ }
245
+ }
246
+ function createContextBuilder(context) {
247
+ return new ContextBuilder(context);
248
+ }
249
+ // src/context/file-operations.ts
250
+ class FileOperations {
251
+ fs;
252
+ workspacePath;
253
+ allowWrites;
254
+ constructor(fs, workspacePath, allowWrites = false) {
255
+ this.fs = fs;
256
+ this.workspacePath = workspacePath;
257
+ this.allowWrites = allowWrites;
258
+ }
259
+ async read(relativePath) {
260
+ const fullPath = this.resolvePath(relativePath);
261
+ try {
262
+ const content = await this.fs.readFile(fullPath);
263
+ return { success: true, path: relativePath, content };
264
+ } catch (error) {
265
+ return {
266
+ success: false,
267
+ path: relativePath,
268
+ error: error instanceof Error ? error.message : String(error)
269
+ };
270
+ }
271
+ }
272
+ async write(relativePath, content) {
273
+ if (!this.allowWrites) {
274
+ return {
275
+ success: false,
276
+ path: relativePath,
277
+ error: "File writes are not enabled"
278
+ };
279
+ }
280
+ const fullPath = this.resolvePath(relativePath);
281
+ try {
282
+ await this.fs.writeFile(fullPath, content);
283
+ return { success: true, path: relativePath };
284
+ } catch (error) {
285
+ return {
286
+ success: false,
287
+ path: relativePath,
288
+ error: error instanceof Error ? error.message : String(error)
289
+ };
290
+ }
291
+ }
292
+ async execute(operations) {
293
+ const results = [];
294
+ for (const operation of operations) {
295
+ let result;
296
+ switch (operation.type) {
297
+ case "read": {
298
+ const readResult = await this.read(operation.path);
299
+ result = {
300
+ operation,
301
+ success: readResult.success,
302
+ content: readResult.content,
303
+ error: readResult.error
304
+ };
305
+ break;
306
+ }
307
+ case "write":
308
+ case "create": {
309
+ if (!operation.content) {
310
+ result = {
311
+ operation,
312
+ success: false,
313
+ error: "Content is required for write operations"
314
+ };
315
+ } else {
316
+ const writeResult = await this.write(operation.path, operation.content);
317
+ result = {
318
+ operation,
319
+ success: writeResult.success,
320
+ error: writeResult.error
321
+ };
322
+ }
323
+ break;
324
+ }
325
+ case "delete": {
326
+ if (!this.allowWrites) {
327
+ result = {
328
+ operation,
329
+ success: false,
330
+ error: "File writes are not enabled"
331
+ };
332
+ } else {
333
+ try {
334
+ await this.fs.deleteFile(this.resolvePath(operation.path));
335
+ result = { operation, success: true };
336
+ } catch (error) {
337
+ result = {
338
+ operation,
339
+ success: false,
340
+ error: error instanceof Error ? error.message : String(error)
341
+ };
342
+ }
343
+ }
344
+ break;
345
+ }
346
+ default:
347
+ result = {
348
+ operation,
349
+ success: false,
350
+ error: `Unknown operation type: ${operation.type}`
351
+ };
352
+ }
353
+ results.push(result);
354
+ }
355
+ return results;
356
+ }
357
+ resolvePath(relativePath) {
358
+ const normalized = relativePath.replace(/\.\./g, "").replace(/^\//, "");
359
+ return `${this.workspacePath}/${normalized}`;
360
+ }
361
+ }
362
+ function createNodeFileOperations(workspacePath, allowWrites = false) {
363
+ const fs = {
364
+ async readFile(path) {
365
+ const { readFile } = await import("node:fs/promises");
366
+ return readFile(path, "utf-8");
367
+ },
368
+ async writeFile(path, content) {
369
+ const { writeFile, mkdir } = await import("node:fs/promises");
370
+ const { dirname } = await import("node:path");
371
+ await mkdir(dirname(path), { recursive: true });
372
+ await writeFile(path, content, "utf-8");
373
+ },
374
+ async exists(path) {
375
+ const { access } = await import("node:fs/promises");
376
+ try {
377
+ await access(path);
378
+ return true;
379
+ } catch {
380
+ return false;
381
+ }
382
+ },
383
+ async deleteFile(path) {
384
+ const { unlink } = await import("node:fs/promises");
385
+ await unlink(path);
386
+ },
387
+ async listFiles(directory, options) {
388
+ const { readdir } = await import("node:fs/promises");
389
+ const { join } = await import("node:path");
390
+ const files = [];
391
+ const entries = await readdir(directory, { withFileTypes: true });
392
+ for (const entry of entries) {
393
+ const fullPath = join(directory, entry.name);
394
+ if (entry.isDirectory() && options?.recursive) {
395
+ const subFiles = await this.listFiles(fullPath, options);
396
+ files.push(...subFiles);
397
+ } else if (entry.isFile()) {
398
+ if (!options?.pattern || entry.name.match(new RegExp(options.pattern))) {
399
+ files.push(fullPath);
400
+ }
401
+ }
402
+ }
403
+ return files;
404
+ }
405
+ };
406
+ return new FileOperations(fs, workspacePath, allowWrites);
407
+ }
408
+ // src/presentation/components/ChatContainer.tsx
409
+ import * as React from "react";
410
+ import { ScrollArea } from "@contractspec/lib.ui-kit-web/ui/scroll-area";
411
+ import { cn } from "@contractspec/lib.ui-kit-web/ui/utils";
412
+ import { jsxDEV } from "react/jsx-dev-runtime";
413
+ "use client";
414
+ function ChatContainer({
415
+ children,
416
+ className,
417
+ showScrollButton = true
418
+ }) {
419
+ const scrollRef = React.useRef(null);
420
+ const [showScrollDown, setShowScrollDown] = React.useState(false);
421
+ React.useEffect(() => {
422
+ const container = scrollRef.current;
423
+ if (!container)
424
+ return;
425
+ const isAtBottom = container.scrollHeight - container.scrollTop <= container.clientHeight + 100;
426
+ if (isAtBottom) {
427
+ container.scrollTop = container.scrollHeight;
428
+ }
429
+ }, [children]);
430
+ const handleScroll = React.useCallback((event) => {
431
+ const container = event.currentTarget;
432
+ const isAtBottom = container.scrollHeight - container.scrollTop <= container.clientHeight + 100;
433
+ setShowScrollDown(!isAtBottom);
434
+ }, []);
435
+ const scrollToBottom = React.useCallback(() => {
436
+ const container = scrollRef.current;
437
+ if (container) {
438
+ container.scrollTo({
439
+ top: container.scrollHeight,
440
+ behavior: "smooth"
441
+ });
442
+ }
443
+ }, []);
444
+ return /* @__PURE__ */ jsxDEV("div", {
445
+ className: cn("relative flex flex-1 flex-col", className),
446
+ children: [
447
+ /* @__PURE__ */ jsxDEV(ScrollArea, {
448
+ ref: scrollRef,
449
+ className: "flex-1",
450
+ onScroll: handleScroll,
451
+ children: /* @__PURE__ */ jsxDEV("div", {
452
+ className: "flex flex-col gap-4 p-4",
453
+ children
454
+ }, undefined, false, undefined, this)
455
+ }, undefined, false, undefined, this),
456
+ showScrollButton && showScrollDown && /* @__PURE__ */ jsxDEV("button", {
457
+ onClick: scrollToBottom,
458
+ 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"),
459
+ "aria-label": "Scroll to bottom",
460
+ children: [
461
+ /* @__PURE__ */ jsxDEV("svg", {
462
+ xmlns: "http://www.w3.org/2000/svg",
463
+ width: "16",
464
+ height: "16",
465
+ viewBox: "0 0 24 24",
466
+ fill: "none",
467
+ stroke: "currentColor",
468
+ strokeWidth: "2",
469
+ strokeLinecap: "round",
470
+ strokeLinejoin: "round",
471
+ children: /* @__PURE__ */ jsxDEV("path", {
472
+ d: "m6 9 6 6 6-6"
473
+ }, undefined, false, undefined, this)
474
+ }, undefined, false, undefined, this),
475
+ "New messages"
476
+ ]
477
+ }, undefined, true, undefined, this)
478
+ ]
479
+ }, undefined, true, undefined, this);
480
+ }
481
+ // src/presentation/components/ChatMessage.tsx
482
+ import * as React3 from "react";
483
+ import { cn as cn3 } from "@contractspec/lib.ui-kit-web/ui/utils";
484
+ import { Avatar, AvatarFallback } from "@contractspec/lib.ui-kit-web/ui/avatar";
485
+ import { Skeleton } from "@contractspec/lib.ui-kit-web/ui/skeleton";
486
+ import { Bot, User, AlertCircle, Copy as Copy2, Check as Check2 } from "lucide-react";
487
+ import { Button as Button2 } from "@contractspec/lib.design-system";
488
+
489
+ // src/presentation/components/CodePreview.tsx
490
+ import * as React2 from "react";
491
+ import { cn as cn2 } from "@contractspec/lib.ui-kit-web/ui/utils";
492
+ import { Button } from "@contractspec/lib.design-system";
493
+ import { Copy, Check, Play, Download } from "lucide-react";
494
+ import { jsxDEV as jsxDEV2 } from "react/jsx-dev-runtime";
495
+ "use client";
496
+ var LANGUAGE_NAMES = {
497
+ ts: "TypeScript",
498
+ tsx: "TypeScript (React)",
499
+ typescript: "TypeScript",
500
+ js: "JavaScript",
501
+ jsx: "JavaScript (React)",
502
+ javascript: "JavaScript",
503
+ json: "JSON",
504
+ md: "Markdown",
505
+ yaml: "YAML",
506
+ yml: "YAML",
507
+ bash: "Bash",
508
+ sh: "Shell",
509
+ sql: "SQL",
510
+ py: "Python",
511
+ python: "Python",
512
+ go: "Go",
513
+ rust: "Rust",
514
+ rs: "Rust"
515
+ };
516
+ function CodePreview({
517
+ code,
518
+ language = "text",
519
+ filename,
520
+ className,
521
+ showCopy = true,
522
+ showExecute = false,
523
+ onExecute,
524
+ showDownload = false,
525
+ maxHeight = 400
526
+ }) {
527
+ const [copied, setCopied] = React2.useState(false);
528
+ const displayLanguage = LANGUAGE_NAMES[language.toLowerCase()] ?? language;
529
+ const lines = code.split(`
530
+ `);
531
+ const handleCopy = React2.useCallback(async () => {
532
+ await navigator.clipboard.writeText(code);
533
+ setCopied(true);
534
+ setTimeout(() => setCopied(false), 2000);
535
+ }, [code]);
536
+ const handleDownload = React2.useCallback(() => {
537
+ const blob = new Blob([code], { type: "text/plain" });
538
+ const url = URL.createObjectURL(blob);
539
+ const a = document.createElement("a");
540
+ a.href = url;
541
+ a.download = filename ?? `code.${language}`;
542
+ document.body.appendChild(a);
543
+ a.click();
544
+ document.body.removeChild(a);
545
+ URL.revokeObjectURL(url);
546
+ }, [code, filename, language]);
547
+ return /* @__PURE__ */ jsxDEV2("div", {
548
+ className: cn2("overflow-hidden rounded-lg border", "bg-muted/50", className),
549
+ children: [
550
+ /* @__PURE__ */ jsxDEV2("div", {
551
+ className: cn2("flex items-center justify-between px-3 py-1.5", "bg-muted/80 border-b"),
552
+ children: [
553
+ /* @__PURE__ */ jsxDEV2("div", {
554
+ className: "flex items-center gap-2 text-sm",
555
+ children: [
556
+ filename && /* @__PURE__ */ jsxDEV2("span", {
557
+ className: "text-foreground font-mono",
558
+ children: filename
559
+ }, undefined, false, undefined, this),
560
+ /* @__PURE__ */ jsxDEV2("span", {
561
+ className: "text-muted-foreground",
562
+ children: displayLanguage
563
+ }, undefined, false, undefined, this)
564
+ ]
565
+ }, undefined, true, undefined, this),
566
+ /* @__PURE__ */ jsxDEV2("div", {
567
+ className: "flex items-center gap-1",
568
+ children: [
569
+ showExecute && onExecute && /* @__PURE__ */ jsxDEV2(Button, {
570
+ variant: "ghost",
571
+ size: "sm",
572
+ onPress: () => onExecute(code),
573
+ className: "h-7 w-7 p-0",
574
+ "aria-label": "Execute code",
575
+ children: /* @__PURE__ */ jsxDEV2(Play, {
576
+ className: "h-3.5 w-3.5"
577
+ }, undefined, false, undefined, this)
578
+ }, undefined, false, undefined, this),
579
+ showDownload && /* @__PURE__ */ jsxDEV2(Button, {
580
+ variant: "ghost",
581
+ size: "sm",
582
+ onPress: handleDownload,
583
+ className: "h-7 w-7 p-0",
584
+ "aria-label": "Download code",
585
+ children: /* @__PURE__ */ jsxDEV2(Download, {
586
+ className: "h-3.5 w-3.5"
587
+ }, undefined, false, undefined, this)
588
+ }, undefined, false, undefined, this),
589
+ showCopy && /* @__PURE__ */ jsxDEV2(Button, {
590
+ variant: "ghost",
591
+ size: "sm",
592
+ onPress: handleCopy,
593
+ className: "h-7 w-7 p-0",
594
+ "aria-label": copied ? "Copied" : "Copy code",
595
+ children: copied ? /* @__PURE__ */ jsxDEV2(Check, {
596
+ className: "h-3.5 w-3.5 text-green-500"
597
+ }, undefined, false, undefined, this) : /* @__PURE__ */ jsxDEV2(Copy, {
598
+ className: "h-3.5 w-3.5"
599
+ }, undefined, false, undefined, this)
600
+ }, undefined, false, undefined, this)
601
+ ]
602
+ }, undefined, true, undefined, this)
603
+ ]
604
+ }, undefined, true, undefined, this),
605
+ /* @__PURE__ */ jsxDEV2("div", {
606
+ className: "overflow-auto",
607
+ style: { maxHeight },
608
+ children: /* @__PURE__ */ jsxDEV2("pre", {
609
+ className: "p-3",
610
+ children: /* @__PURE__ */ jsxDEV2("code", {
611
+ className: "text-sm",
612
+ children: lines.map((line, i) => /* @__PURE__ */ jsxDEV2("div", {
613
+ className: "flex",
614
+ children: [
615
+ /* @__PURE__ */ jsxDEV2("span", {
616
+ className: "text-muted-foreground mr-4 w-8 text-right select-none",
617
+ children: i + 1
618
+ }, undefined, false, undefined, this),
619
+ /* @__PURE__ */ jsxDEV2("span", {
620
+ className: "flex-1",
621
+ children: line || " "
622
+ }, undefined, false, undefined, this)
623
+ ]
624
+ }, i, true, undefined, this))
625
+ }, undefined, false, undefined, this)
626
+ }, undefined, false, undefined, this)
627
+ }, undefined, false, undefined, this)
628
+ ]
629
+ }, undefined, true, undefined, this);
630
+ }
631
+
632
+ // src/presentation/components/ChatMessage.tsx
633
+ import { jsxDEV as jsxDEV3, Fragment } from "react/jsx-dev-runtime";
634
+ "use client";
635
+ function extractCodeBlocks(content) {
636
+ const codeBlockRegex = /```(\w+)?\n([\s\S]*?)```/g;
637
+ const blocks = [];
638
+ let match;
639
+ while ((match = codeBlockRegex.exec(content)) !== null) {
640
+ blocks.push({
641
+ language: match[1] ?? "text",
642
+ code: match[2] ?? "",
643
+ raw: match[0]
644
+ });
645
+ }
646
+ return blocks;
647
+ }
648
+ function MessageContent({ content }) {
649
+ const codeBlocks = extractCodeBlocks(content);
650
+ if (codeBlocks.length === 0) {
651
+ return /* @__PURE__ */ jsxDEV3("p", {
652
+ className: "whitespace-pre-wrap",
653
+ children: content
654
+ }, undefined, false, undefined, this);
655
+ }
656
+ let remaining = content;
657
+ const parts = [];
658
+ let key = 0;
659
+ for (const block of codeBlocks) {
660
+ const [before, after] = remaining.split(block.raw);
661
+ if (before) {
662
+ parts.push(/* @__PURE__ */ jsxDEV3("p", {
663
+ className: "whitespace-pre-wrap",
664
+ children: before.trim()
665
+ }, key++, false, undefined, this));
666
+ }
667
+ parts.push(/* @__PURE__ */ jsxDEV3(CodePreview, {
668
+ code: block.code,
669
+ language: block.language,
670
+ className: "my-2"
671
+ }, key++, false, undefined, this));
672
+ remaining = after ?? "";
673
+ }
674
+ if (remaining.trim()) {
675
+ parts.push(/* @__PURE__ */ jsxDEV3("p", {
676
+ className: "whitespace-pre-wrap",
677
+ children: remaining.trim()
678
+ }, key++, false, undefined, this));
679
+ }
680
+ return /* @__PURE__ */ jsxDEV3(Fragment, {
681
+ children: parts
682
+ }, undefined, false, undefined, this);
683
+ }
684
+ function ChatMessage({
685
+ message,
686
+ className,
687
+ showCopy = true,
688
+ showAvatar = true
689
+ }) {
690
+ const [copied, setCopied] = React3.useState(false);
691
+ const isUser = message.role === "user";
692
+ const isError = message.status === "error";
693
+ const isStreaming = message.status === "streaming";
694
+ const handleCopy = React3.useCallback(async () => {
695
+ await navigator.clipboard.writeText(message.content);
696
+ setCopied(true);
697
+ setTimeout(() => setCopied(false), 2000);
698
+ }, [message.content]);
699
+ return /* @__PURE__ */ jsxDEV3("div", {
700
+ className: cn3("group flex gap-3", isUser && "flex-row-reverse", className),
701
+ children: [
702
+ showAvatar && /* @__PURE__ */ jsxDEV3(Avatar, {
703
+ className: "h-8 w-8 shrink-0",
704
+ children: /* @__PURE__ */ jsxDEV3(AvatarFallback, {
705
+ className: cn3(isUser ? "bg-primary text-primary-foreground" : "bg-muted"),
706
+ children: isUser ? /* @__PURE__ */ jsxDEV3(User, {
707
+ className: "h-4 w-4"
708
+ }, undefined, false, undefined, this) : /* @__PURE__ */ jsxDEV3(Bot, {
709
+ className: "h-4 w-4"
710
+ }, undefined, false, undefined, this)
711
+ }, undefined, false, undefined, this)
712
+ }, undefined, false, undefined, this),
713
+ /* @__PURE__ */ jsxDEV3("div", {
714
+ className: cn3("flex max-w-[80%] flex-col gap-1", isUser && "items-end"),
715
+ children: [
716
+ /* @__PURE__ */ jsxDEV3("div", {
717
+ 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"),
718
+ children: isError && message.error ? /* @__PURE__ */ jsxDEV3("div", {
719
+ className: "flex items-start gap-2",
720
+ children: [
721
+ /* @__PURE__ */ jsxDEV3(AlertCircle, {
722
+ className: "text-destructive mt-0.5 h-4 w-4 shrink-0"
723
+ }, undefined, false, undefined, this),
724
+ /* @__PURE__ */ jsxDEV3("div", {
725
+ children: [
726
+ /* @__PURE__ */ jsxDEV3("p", {
727
+ className: "text-destructive font-medium",
728
+ children: message.error.code
729
+ }, undefined, false, undefined, this),
730
+ /* @__PURE__ */ jsxDEV3("p", {
731
+ className: "text-muted-foreground text-sm",
732
+ children: message.error.message
733
+ }, undefined, false, undefined, this)
734
+ ]
735
+ }, undefined, true, undefined, this)
736
+ ]
737
+ }, undefined, true, undefined, this) : isStreaming && !message.content ? /* @__PURE__ */ jsxDEV3("div", {
738
+ className: "flex flex-col gap-2",
739
+ children: [
740
+ /* @__PURE__ */ jsxDEV3(Skeleton, {
741
+ className: "h-4 w-48"
742
+ }, undefined, false, undefined, this),
743
+ /* @__PURE__ */ jsxDEV3(Skeleton, {
744
+ className: "h-4 w-32"
745
+ }, undefined, false, undefined, this)
746
+ ]
747
+ }, undefined, true, undefined, this) : /* @__PURE__ */ jsxDEV3(MessageContent, {
748
+ content: message.content
749
+ }, undefined, false, undefined, this)
750
+ }, undefined, false, undefined, this),
751
+ /* @__PURE__ */ jsxDEV3("div", {
752
+ className: cn3("flex items-center gap-2 text-xs", "text-muted-foreground opacity-0 transition-opacity", "group-hover:opacity-100"),
753
+ children: [
754
+ /* @__PURE__ */ jsxDEV3("span", {
755
+ children: new Date(message.createdAt).toLocaleTimeString([], {
756
+ hour: "2-digit",
757
+ minute: "2-digit"
758
+ })
759
+ }, undefined, false, undefined, this),
760
+ message.usage && /* @__PURE__ */ jsxDEV3("span", {
761
+ children: [
762
+ message.usage.inputTokens + message.usage.outputTokens,
763
+ " tokens"
764
+ ]
765
+ }, undefined, true, undefined, this),
766
+ showCopy && !isUser && message.content && /* @__PURE__ */ jsxDEV3(Button2, {
767
+ variant: "ghost",
768
+ size: "sm",
769
+ className: "h-6 w-6 p-0",
770
+ onPress: handleCopy,
771
+ "aria-label": copied ? "Copied" : "Copy message",
772
+ children: copied ? /* @__PURE__ */ jsxDEV3(Check2, {
773
+ className: "h-3 w-3"
774
+ }, undefined, false, undefined, this) : /* @__PURE__ */ jsxDEV3(Copy2, {
775
+ className: "h-3 w-3"
776
+ }, undefined, false, undefined, this)
777
+ }, undefined, false, undefined, this)
778
+ ]
779
+ }, undefined, true, undefined, this),
780
+ message.reasoning && /* @__PURE__ */ jsxDEV3("details", {
781
+ className: "text-muted-foreground mt-2 text-sm",
782
+ children: [
783
+ /* @__PURE__ */ jsxDEV3("summary", {
784
+ className: "cursor-pointer hover:underline",
785
+ children: "View reasoning"
786
+ }, undefined, false, undefined, this),
787
+ /* @__PURE__ */ jsxDEV3("div", {
788
+ className: "bg-muted mt-1 rounded-md p-2",
789
+ children: /* @__PURE__ */ jsxDEV3("p", {
790
+ className: "whitespace-pre-wrap",
791
+ children: message.reasoning
792
+ }, undefined, false, undefined, this)
793
+ }, undefined, false, undefined, this)
794
+ ]
795
+ }, undefined, true, undefined, this)
796
+ ]
797
+ }, undefined, true, undefined, this)
798
+ ]
799
+ }, undefined, true, undefined, this);
800
+ }
801
+ // src/presentation/components/ChatInput.tsx
802
+ import * as React4 from "react";
803
+ import { cn as cn4 } from "@contractspec/lib.ui-kit-web/ui/utils";
804
+ import { Textarea } from "@contractspec/lib.design-system";
805
+ import { Button as Button3 } from "@contractspec/lib.design-system";
806
+ import { Send, Paperclip, X, Loader2, FileText, Code } from "lucide-react";
807
+ import { jsxDEV as jsxDEV4, Fragment as Fragment2 } from "react/jsx-dev-runtime";
808
+ "use client";
809
+ function ChatInput({
810
+ onSend,
811
+ disabled = false,
812
+ isLoading = false,
813
+ placeholder = "Type a message...",
814
+ className,
815
+ showAttachments = true,
816
+ maxAttachments = 5
817
+ }) {
818
+ const [content, setContent] = React4.useState("");
819
+ const [attachments, setAttachments] = React4.useState([]);
820
+ const textareaRef = React4.useRef(null);
821
+ const fileInputRef = React4.useRef(null);
822
+ const canSend = content.trim().length > 0 || attachments.length > 0;
823
+ const handleSubmit = React4.useCallback((e) => {
824
+ e?.preventDefault();
825
+ if (!canSend || disabled || isLoading)
826
+ return;
827
+ onSend(content.trim(), attachments.length > 0 ? attachments : undefined);
828
+ setContent("");
829
+ setAttachments([]);
830
+ textareaRef.current?.focus();
831
+ }, [canSend, content, attachments, disabled, isLoading, onSend]);
832
+ const handleKeyDown = React4.useCallback((e) => {
833
+ if (e.key === "Enter" && !e.shiftKey) {
834
+ e.preventDefault();
835
+ handleSubmit();
836
+ }
837
+ }, [handleSubmit]);
838
+ const handleFileSelect = React4.useCallback(async (e) => {
839
+ const files = e.target.files;
840
+ if (!files)
841
+ return;
842
+ const newAttachments = [];
843
+ for (const file of Array.from(files)) {
844
+ if (attachments.length + newAttachments.length >= maxAttachments)
845
+ break;
846
+ const content2 = await file.text();
847
+ const extension = file.name.split(".").pop()?.toLowerCase() ?? "";
848
+ const isCode = [
849
+ "ts",
850
+ "tsx",
851
+ "js",
852
+ "jsx",
853
+ "py",
854
+ "go",
855
+ "rs",
856
+ "java"
857
+ ].includes(extension);
858
+ newAttachments.push({
859
+ id: `att_${Date.now()}_${Math.random().toString(36).slice(2, 9)}`,
860
+ type: isCode ? "code" : "file",
861
+ name: file.name,
862
+ content: content2,
863
+ mimeType: file.type,
864
+ size: file.size
865
+ });
866
+ }
867
+ setAttachments((prev) => [...prev, ...newAttachments]);
868
+ e.target.value = "";
869
+ }, [attachments.length, maxAttachments]);
870
+ const removeAttachment = React4.useCallback((id) => {
871
+ setAttachments((prev) => prev.filter((a) => a.id !== id));
872
+ }, []);
873
+ return /* @__PURE__ */ jsxDEV4("div", {
874
+ className: cn4("flex flex-col gap-2", className),
875
+ children: [
876
+ attachments.length > 0 && /* @__PURE__ */ jsxDEV4("div", {
877
+ className: "flex flex-wrap gap-2",
878
+ children: attachments.map((attachment) => /* @__PURE__ */ jsxDEV4("div", {
879
+ className: cn4("flex items-center gap-1.5 rounded-md px-2 py-1", "bg-muted text-muted-foreground text-sm"),
880
+ children: [
881
+ attachment.type === "code" ? /* @__PURE__ */ jsxDEV4(Code, {
882
+ className: "h-3.5 w-3.5"
883
+ }, undefined, false, undefined, this) : /* @__PURE__ */ jsxDEV4(FileText, {
884
+ className: "h-3.5 w-3.5"
885
+ }, undefined, false, undefined, this),
886
+ /* @__PURE__ */ jsxDEV4("span", {
887
+ className: "max-w-[150px] truncate",
888
+ children: attachment.name
889
+ }, undefined, false, undefined, this),
890
+ /* @__PURE__ */ jsxDEV4("button", {
891
+ type: "button",
892
+ onClick: () => removeAttachment(attachment.id),
893
+ className: "hover:text-foreground",
894
+ "aria-label": `Remove ${attachment.name}`,
895
+ children: /* @__PURE__ */ jsxDEV4(X, {
896
+ className: "h-3.5 w-3.5"
897
+ }, undefined, false, undefined, this)
898
+ }, undefined, false, undefined, this)
899
+ ]
900
+ }, attachment.id, true, undefined, this))
901
+ }, undefined, false, undefined, this),
902
+ /* @__PURE__ */ jsxDEV4("form", {
903
+ onSubmit: handleSubmit,
904
+ className: "flex items-end gap-2",
905
+ children: [
906
+ showAttachments && /* @__PURE__ */ jsxDEV4(Fragment2, {
907
+ children: [
908
+ /* @__PURE__ */ jsxDEV4("input", {
909
+ ref: fileInputRef,
910
+ type: "file",
911
+ multiple: true,
912
+ accept: ".ts,.tsx,.js,.jsx,.json,.md,.txt,.py,.go,.rs,.java,.yaml,.yml",
913
+ onChange: handleFileSelect,
914
+ className: "hidden",
915
+ "aria-label": "Attach files"
916
+ }, undefined, false, undefined, this),
917
+ /* @__PURE__ */ jsxDEV4(Button3, {
918
+ type: "button",
919
+ variant: "ghost",
920
+ size: "sm",
921
+ onPress: () => fileInputRef.current?.click(),
922
+ disabled: disabled || attachments.length >= maxAttachments,
923
+ "aria-label": "Attach files",
924
+ children: /* @__PURE__ */ jsxDEV4(Paperclip, {
925
+ className: "h-4 w-4"
926
+ }, undefined, false, undefined, this)
927
+ }, undefined, false, undefined, this)
928
+ ]
929
+ }, undefined, true, undefined, this),
930
+ /* @__PURE__ */ jsxDEV4("div", {
931
+ className: "relative flex-1",
932
+ children: /* @__PURE__ */ jsxDEV4(Textarea, {
933
+ value: content,
934
+ onChange: (e) => setContent(e.target.value),
935
+ onKeyDown: handleKeyDown,
936
+ placeholder,
937
+ disabled,
938
+ className: cn4("max-h-[200px] min-h-[44px] resize-none pr-12", "focus-visible:ring-1"),
939
+ rows: 1,
940
+ "aria-label": "Chat message"
941
+ }, undefined, false, undefined, this)
942
+ }, undefined, false, undefined, this),
943
+ /* @__PURE__ */ jsxDEV4(Button3, {
944
+ type: "submit",
945
+ disabled: !canSend || disabled || isLoading,
946
+ size: "sm",
947
+ "aria-label": isLoading ? "Sending..." : "Send message",
948
+ children: isLoading ? /* @__PURE__ */ jsxDEV4(Loader2, {
949
+ className: "h-4 w-4 animate-spin"
950
+ }, undefined, false, undefined, this) : /* @__PURE__ */ jsxDEV4(Send, {
951
+ className: "h-4 w-4"
952
+ }, undefined, false, undefined, this)
953
+ }, undefined, false, undefined, this)
954
+ ]
955
+ }, undefined, true, undefined, this),
956
+ /* @__PURE__ */ jsxDEV4("p", {
957
+ className: "text-muted-foreground text-xs",
958
+ children: "Press Enter to send, Shift+Enter for new line"
959
+ }, undefined, false, undefined, this)
960
+ ]
961
+ }, undefined, true, undefined, this);
962
+ }
963
+ // src/presentation/components/ModelPicker.tsx
964
+ import * as React5 from "react";
965
+ import { cn as cn5 } from "@contractspec/lib.ui-kit-web/ui/utils";
966
+ import { Button as Button4 } from "@contractspec/lib.design-system";
967
+ import {
968
+ Select,
969
+ SelectContent,
970
+ SelectItem,
971
+ SelectTrigger,
972
+ SelectValue
973
+ } from "@contractspec/lib.ui-kit-web/ui/select";
974
+ import { Badge } from "@contractspec/lib.ui-kit-web/ui/badge";
975
+ import { Label } from "@contractspec/lib.ui-kit-web/ui/label";
976
+ import { Bot as Bot2, Cloud, Cpu, Sparkles } from "lucide-react";
977
+ import {
978
+ getModelsForProvider
979
+ } from "@contractspec/lib.ai-providers";
980
+ import { jsxDEV as jsxDEV5 } from "react/jsx-dev-runtime";
981
+ "use client";
982
+ var PROVIDER_ICONS = {
983
+ ollama: /* @__PURE__ */ jsxDEV5(Cpu, {
984
+ className: "h-4 w-4"
985
+ }, undefined, false, undefined, this),
986
+ openai: /* @__PURE__ */ jsxDEV5(Bot2, {
987
+ className: "h-4 w-4"
988
+ }, undefined, false, undefined, this),
989
+ anthropic: /* @__PURE__ */ jsxDEV5(Sparkles, {
990
+ className: "h-4 w-4"
991
+ }, undefined, false, undefined, this),
992
+ mistral: /* @__PURE__ */ jsxDEV5(Cloud, {
993
+ className: "h-4 w-4"
994
+ }, undefined, false, undefined, this),
995
+ gemini: /* @__PURE__ */ jsxDEV5(Sparkles, {
996
+ className: "h-4 w-4"
997
+ }, undefined, false, undefined, this)
998
+ };
999
+ var PROVIDER_NAMES = {
1000
+ ollama: "Ollama (Local)",
1001
+ openai: "OpenAI",
1002
+ anthropic: "Anthropic",
1003
+ mistral: "Mistral",
1004
+ gemini: "Google Gemini"
1005
+ };
1006
+ var MODE_BADGES = {
1007
+ local: { label: "Local", variant: "secondary" },
1008
+ byok: { label: "BYOK", variant: "outline" },
1009
+ managed: { label: "Managed", variant: "default" }
1010
+ };
1011
+ function ModelPicker({
1012
+ value,
1013
+ onChange,
1014
+ availableProviders,
1015
+ className,
1016
+ compact = false
1017
+ }) {
1018
+ const providers = availableProviders ?? [
1019
+ { provider: "ollama", available: true, mode: "local" },
1020
+ { provider: "openai", available: true, mode: "byok" },
1021
+ { provider: "anthropic", available: true, mode: "byok" },
1022
+ { provider: "mistral", available: true, mode: "byok" },
1023
+ { provider: "gemini", available: true, mode: "byok" }
1024
+ ];
1025
+ const models = getModelsForProvider(value.provider);
1026
+ const selectedModel = models.find((m) => m.id === value.model);
1027
+ const handleProviderChange = React5.useCallback((providerName) => {
1028
+ const provider = providerName;
1029
+ const providerInfo = providers.find((p) => p.provider === provider);
1030
+ const providerModels = getModelsForProvider(provider);
1031
+ const defaultModel = providerModels[0]?.id ?? "";
1032
+ onChange({
1033
+ provider,
1034
+ model: defaultModel,
1035
+ mode: providerInfo?.mode ?? "byok"
1036
+ });
1037
+ }, [onChange, providers]);
1038
+ const handleModelChange = React5.useCallback((modelId) => {
1039
+ onChange({
1040
+ ...value,
1041
+ model: modelId
1042
+ });
1043
+ }, [onChange, value]);
1044
+ if (compact) {
1045
+ return /* @__PURE__ */ jsxDEV5("div", {
1046
+ className: cn5("flex items-center gap-2", className),
1047
+ children: [
1048
+ /* @__PURE__ */ jsxDEV5(Select, {
1049
+ value: value.provider,
1050
+ onValueChange: handleProviderChange,
1051
+ children: [
1052
+ /* @__PURE__ */ jsxDEV5(SelectTrigger, {
1053
+ className: "w-[140px]",
1054
+ children: /* @__PURE__ */ jsxDEV5(SelectValue, {}, undefined, false, undefined, this)
1055
+ }, undefined, false, undefined, this),
1056
+ /* @__PURE__ */ jsxDEV5(SelectContent, {
1057
+ children: providers.map((p) => /* @__PURE__ */ jsxDEV5(SelectItem, {
1058
+ value: p.provider,
1059
+ disabled: !p.available,
1060
+ children: /* @__PURE__ */ jsxDEV5("div", {
1061
+ className: "flex items-center gap-2",
1062
+ children: [
1063
+ PROVIDER_ICONS[p.provider],
1064
+ /* @__PURE__ */ jsxDEV5("span", {
1065
+ children: PROVIDER_NAMES[p.provider]
1066
+ }, undefined, false, undefined, this)
1067
+ ]
1068
+ }, undefined, true, undefined, this)
1069
+ }, p.provider, false, undefined, this))
1070
+ }, undefined, false, undefined, this)
1071
+ ]
1072
+ }, undefined, true, undefined, this),
1073
+ /* @__PURE__ */ jsxDEV5(Select, {
1074
+ value: value.model,
1075
+ onValueChange: handleModelChange,
1076
+ children: [
1077
+ /* @__PURE__ */ jsxDEV5(SelectTrigger, {
1078
+ className: "w-[160px]",
1079
+ children: /* @__PURE__ */ jsxDEV5(SelectValue, {}, undefined, false, undefined, this)
1080
+ }, undefined, false, undefined, this),
1081
+ /* @__PURE__ */ jsxDEV5(SelectContent, {
1082
+ children: models.map((m) => /* @__PURE__ */ jsxDEV5(SelectItem, {
1083
+ value: m.id,
1084
+ children: m.name
1085
+ }, m.id, false, undefined, this))
1086
+ }, undefined, false, undefined, this)
1087
+ ]
1088
+ }, undefined, true, undefined, this)
1089
+ ]
1090
+ }, undefined, true, undefined, this);
1091
+ }
1092
+ return /* @__PURE__ */ jsxDEV5("div", {
1093
+ className: cn5("flex flex-col gap-3", className),
1094
+ children: [
1095
+ /* @__PURE__ */ jsxDEV5("div", {
1096
+ className: "flex flex-col gap-1.5",
1097
+ children: [
1098
+ /* @__PURE__ */ jsxDEV5(Label, {
1099
+ htmlFor: "provider-selection",
1100
+ className: "text-sm font-medium",
1101
+ children: "Provider"
1102
+ }, undefined, false, undefined, this),
1103
+ /* @__PURE__ */ jsxDEV5("div", {
1104
+ className: "flex flex-wrap gap-2",
1105
+ id: "provider-selection",
1106
+ children: providers.map((p) => /* @__PURE__ */ jsxDEV5(Button4, {
1107
+ variant: value.provider === p.provider ? "default" : "outline",
1108
+ size: "sm",
1109
+ onPress: () => p.available && handleProviderChange(p.provider),
1110
+ disabled: !p.available,
1111
+ className: cn5(!p.available && "opacity-50"),
1112
+ children: [
1113
+ PROVIDER_ICONS[p.provider],
1114
+ /* @__PURE__ */ jsxDEV5("span", {
1115
+ children: PROVIDER_NAMES[p.provider]
1116
+ }, undefined, false, undefined, this),
1117
+ /* @__PURE__ */ jsxDEV5(Badge, {
1118
+ variant: MODE_BADGES[p.mode].variant,
1119
+ className: "ml-1",
1120
+ children: MODE_BADGES[p.mode].label
1121
+ }, undefined, false, undefined, this)
1122
+ ]
1123
+ }, p.provider, true, undefined, this))
1124
+ }, undefined, false, undefined, this)
1125
+ ]
1126
+ }, undefined, true, undefined, this),
1127
+ /* @__PURE__ */ jsxDEV5("div", {
1128
+ className: "flex flex-col gap-1.5",
1129
+ children: [
1130
+ /* @__PURE__ */ jsxDEV5(Label, {
1131
+ htmlFor: "model-picker",
1132
+ className: "text-sm font-medium",
1133
+ children: "Model"
1134
+ }, undefined, false, undefined, this),
1135
+ /* @__PURE__ */ jsxDEV5(Select, {
1136
+ name: "model-picker",
1137
+ value: value.model,
1138
+ onValueChange: handleModelChange,
1139
+ children: [
1140
+ /* @__PURE__ */ jsxDEV5(SelectTrigger, {
1141
+ children: /* @__PURE__ */ jsxDEV5(SelectValue, {
1142
+ placeholder: "Select a model"
1143
+ }, undefined, false, undefined, this)
1144
+ }, undefined, false, undefined, this),
1145
+ /* @__PURE__ */ jsxDEV5(SelectContent, {
1146
+ children: models.map((m) => /* @__PURE__ */ jsxDEV5(SelectItem, {
1147
+ value: m.id,
1148
+ children: /* @__PURE__ */ jsxDEV5("div", {
1149
+ className: "flex items-center gap-2",
1150
+ children: [
1151
+ /* @__PURE__ */ jsxDEV5("span", {
1152
+ children: m.name
1153
+ }, undefined, false, undefined, this),
1154
+ /* @__PURE__ */ jsxDEV5("span", {
1155
+ className: "text-muted-foreground text-xs",
1156
+ children: [
1157
+ Math.round(m.contextWindow / 1000),
1158
+ "K"
1159
+ ]
1160
+ }, undefined, true, undefined, this),
1161
+ m.capabilities.vision && /* @__PURE__ */ jsxDEV5(Badge, {
1162
+ variant: "outline",
1163
+ className: "text-xs",
1164
+ children: "Vision"
1165
+ }, undefined, false, undefined, this),
1166
+ m.capabilities.reasoning && /* @__PURE__ */ jsxDEV5(Badge, {
1167
+ variant: "outline",
1168
+ className: "text-xs",
1169
+ children: "Reasoning"
1170
+ }, undefined, false, undefined, this)
1171
+ ]
1172
+ }, undefined, true, undefined, this)
1173
+ }, m.id, false, undefined, this))
1174
+ }, undefined, false, undefined, this)
1175
+ ]
1176
+ }, undefined, true, undefined, this)
1177
+ ]
1178
+ }, undefined, true, undefined, this),
1179
+ selectedModel && /* @__PURE__ */ jsxDEV5("div", {
1180
+ className: "text-muted-foreground flex flex-wrap gap-2 text-xs",
1181
+ children: [
1182
+ /* @__PURE__ */ jsxDEV5("span", {
1183
+ children: [
1184
+ "Context: ",
1185
+ Math.round(selectedModel.contextWindow / 1000),
1186
+ "K tokens"
1187
+ ]
1188
+ }, undefined, true, undefined, this),
1189
+ selectedModel.capabilities.vision && /* @__PURE__ */ jsxDEV5("span", {
1190
+ children: "• Vision"
1191
+ }, undefined, false, undefined, this),
1192
+ selectedModel.capabilities.tools && /* @__PURE__ */ jsxDEV5("span", {
1193
+ children: "• Tools"
1194
+ }, undefined, false, undefined, this),
1195
+ selectedModel.capabilities.reasoning && /* @__PURE__ */ jsxDEV5("span", {
1196
+ children: "• Reasoning"
1197
+ }, undefined, false, undefined, this)
1198
+ ]
1199
+ }, undefined, true, undefined, this)
1200
+ ]
1201
+ }, undefined, true, undefined, this);
1202
+ }
1203
+ // src/presentation/components/ContextIndicator.tsx
1204
+ import { cn as cn6 } from "@contractspec/lib.ui-kit-web/ui/utils";
1205
+ import { Badge as Badge2 } from "@contractspec/lib.ui-kit-web/ui/badge";
1206
+ import {
1207
+ Tooltip,
1208
+ TooltipContent,
1209
+ TooltipProvider,
1210
+ TooltipTrigger
1211
+ } from "@contractspec/lib.ui-kit-web/ui/tooltip";
1212
+ import { FolderOpen, FileCode, Zap, Info } from "lucide-react";
1213
+ import { jsxDEV as jsxDEV6, Fragment as Fragment3 } from "react/jsx-dev-runtime";
1214
+ "use client";
1215
+ function ContextIndicator({
1216
+ summary,
1217
+ active = false,
1218
+ className,
1219
+ showDetails = true
1220
+ }) {
1221
+ if (!summary && !active) {
1222
+ return /* @__PURE__ */ jsxDEV6("div", {
1223
+ className: cn6("flex items-center gap-1.5 text-sm", "text-muted-foreground", className),
1224
+ children: [
1225
+ /* @__PURE__ */ jsxDEV6(Info, {
1226
+ className: "h-4 w-4"
1227
+ }, undefined, false, undefined, this),
1228
+ /* @__PURE__ */ jsxDEV6("span", {
1229
+ children: "No workspace context"
1230
+ }, undefined, false, undefined, this)
1231
+ ]
1232
+ }, undefined, true, undefined, this);
1233
+ }
1234
+ const content = /* @__PURE__ */ jsxDEV6("div", {
1235
+ className: cn6("flex items-center gap-2", active ? "text-foreground" : "text-muted-foreground", className),
1236
+ children: [
1237
+ /* @__PURE__ */ jsxDEV6(Badge2, {
1238
+ variant: active ? "default" : "secondary",
1239
+ className: "flex items-center gap-1",
1240
+ children: [
1241
+ /* @__PURE__ */ jsxDEV6(Zap, {
1242
+ className: "h-3 w-3"
1243
+ }, undefined, false, undefined, this),
1244
+ "Context"
1245
+ ]
1246
+ }, undefined, true, undefined, this),
1247
+ summary && showDetails && /* @__PURE__ */ jsxDEV6(Fragment3, {
1248
+ children: [
1249
+ /* @__PURE__ */ jsxDEV6("div", {
1250
+ className: "flex items-center gap-1 text-xs",
1251
+ children: [
1252
+ /* @__PURE__ */ jsxDEV6(FolderOpen, {
1253
+ className: "h-3.5 w-3.5"
1254
+ }, undefined, false, undefined, this),
1255
+ /* @__PURE__ */ jsxDEV6("span", {
1256
+ children: summary.name
1257
+ }, undefined, false, undefined, this)
1258
+ ]
1259
+ }, undefined, true, undefined, this),
1260
+ /* @__PURE__ */ jsxDEV6("div", {
1261
+ className: "flex items-center gap-1 text-xs",
1262
+ children: [
1263
+ /* @__PURE__ */ jsxDEV6(FileCode, {
1264
+ className: "h-3.5 w-3.5"
1265
+ }, undefined, false, undefined, this),
1266
+ /* @__PURE__ */ jsxDEV6("span", {
1267
+ children: [
1268
+ summary.specs.total,
1269
+ " specs"
1270
+ ]
1271
+ }, undefined, true, undefined, this)
1272
+ ]
1273
+ }, undefined, true, undefined, this)
1274
+ ]
1275
+ }, undefined, true, undefined, this)
1276
+ ]
1277
+ }, undefined, true, undefined, this);
1278
+ if (!summary) {
1279
+ return content;
1280
+ }
1281
+ return /* @__PURE__ */ jsxDEV6(TooltipProvider, {
1282
+ children: /* @__PURE__ */ jsxDEV6(Tooltip, {
1283
+ children: [
1284
+ /* @__PURE__ */ jsxDEV6(TooltipTrigger, {
1285
+ asChild: true,
1286
+ children: content
1287
+ }, undefined, false, undefined, this),
1288
+ /* @__PURE__ */ jsxDEV6(TooltipContent, {
1289
+ side: "bottom",
1290
+ className: "max-w-[300px]",
1291
+ children: /* @__PURE__ */ jsxDEV6("div", {
1292
+ className: "flex flex-col gap-2 text-sm",
1293
+ children: [
1294
+ /* @__PURE__ */ jsxDEV6("div", {
1295
+ className: "font-medium",
1296
+ children: summary.name
1297
+ }, undefined, false, undefined, this),
1298
+ /* @__PURE__ */ jsxDEV6("div", {
1299
+ className: "text-muted-foreground text-xs",
1300
+ children: summary.path
1301
+ }, undefined, false, undefined, this),
1302
+ /* @__PURE__ */ jsxDEV6("div", {
1303
+ className: "border-t pt-2",
1304
+ children: /* @__PURE__ */ jsxDEV6("div", {
1305
+ className: "grid grid-cols-2 gap-1 text-xs",
1306
+ children: [
1307
+ /* @__PURE__ */ jsxDEV6("span", {
1308
+ children: "Commands:"
1309
+ }, undefined, false, undefined, this),
1310
+ /* @__PURE__ */ jsxDEV6("span", {
1311
+ className: "text-right",
1312
+ children: summary.specs.commands
1313
+ }, undefined, false, undefined, this),
1314
+ /* @__PURE__ */ jsxDEV6("span", {
1315
+ children: "Queries:"
1316
+ }, undefined, false, undefined, this),
1317
+ /* @__PURE__ */ jsxDEV6("span", {
1318
+ className: "text-right",
1319
+ children: summary.specs.queries
1320
+ }, undefined, false, undefined, this),
1321
+ /* @__PURE__ */ jsxDEV6("span", {
1322
+ children: "Events:"
1323
+ }, undefined, false, undefined, this),
1324
+ /* @__PURE__ */ jsxDEV6("span", {
1325
+ className: "text-right",
1326
+ children: summary.specs.events
1327
+ }, undefined, false, undefined, this),
1328
+ /* @__PURE__ */ jsxDEV6("span", {
1329
+ children: "Presentations:"
1330
+ }, undefined, false, undefined, this),
1331
+ /* @__PURE__ */ jsxDEV6("span", {
1332
+ className: "text-right",
1333
+ children: summary.specs.presentations
1334
+ }, undefined, false, undefined, this)
1335
+ ]
1336
+ }, undefined, true, undefined, this)
1337
+ }, undefined, false, undefined, this),
1338
+ /* @__PURE__ */ jsxDEV6("div", {
1339
+ className: "border-t pt-2 text-xs",
1340
+ children: [
1341
+ /* @__PURE__ */ jsxDEV6("span", {
1342
+ children: [
1343
+ summary.files.total,
1344
+ " files"
1345
+ ]
1346
+ }, undefined, true, undefined, this),
1347
+ /* @__PURE__ */ jsxDEV6("span", {
1348
+ className: "mx-1",
1349
+ children: "•"
1350
+ }, undefined, false, undefined, this),
1351
+ /* @__PURE__ */ jsxDEV6("span", {
1352
+ children: [
1353
+ summary.files.specFiles,
1354
+ " spec files"
1355
+ ]
1356
+ }, undefined, true, undefined, this)
1357
+ ]
1358
+ }, undefined, true, undefined, this)
1359
+ ]
1360
+ }, undefined, true, undefined, this)
1361
+ }, undefined, false, undefined, this)
1362
+ ]
1363
+ }, undefined, true, undefined, this)
1364
+ }, undefined, false, undefined, this);
1365
+ }
1366
+ // src/presentation/hooks/useChat.tsx
1367
+ import * as React6 from "react";
1368
+
1369
+ // src/core/chat-service.ts
1370
+ import { generateText, streamText } from "ai";
1371
+
1372
+ // src/core/conversation-store.ts
1373
+ function generateId(prefix) {
1374
+ return `${prefix}_${Date.now()}_${Math.random().toString(36).slice(2, 11)}`;
1375
+ }
1376
+
1377
+ class InMemoryConversationStore {
1378
+ conversations = new Map;
1379
+ async get(conversationId) {
1380
+ return this.conversations.get(conversationId) ?? null;
1381
+ }
1382
+ async create(conversation) {
1383
+ const now = new Date;
1384
+ const fullConversation = {
1385
+ ...conversation,
1386
+ id: generateId("conv"),
1387
+ createdAt: now,
1388
+ updatedAt: now
1389
+ };
1390
+ this.conversations.set(fullConversation.id, fullConversation);
1391
+ return fullConversation;
1392
+ }
1393
+ async update(conversationId, updates) {
1394
+ const conversation = this.conversations.get(conversationId);
1395
+ if (!conversation)
1396
+ return null;
1397
+ const updated = {
1398
+ ...conversation,
1399
+ ...updates,
1400
+ updatedAt: new Date
1401
+ };
1402
+ this.conversations.set(conversationId, updated);
1403
+ return updated;
1404
+ }
1405
+ async appendMessage(conversationId, message) {
1406
+ const conversation = this.conversations.get(conversationId);
1407
+ if (!conversation) {
1408
+ throw new Error(`Conversation ${conversationId} not found`);
1409
+ }
1410
+ const now = new Date;
1411
+ const fullMessage = {
1412
+ ...message,
1413
+ id: generateId("msg"),
1414
+ conversationId,
1415
+ createdAt: now,
1416
+ updatedAt: now
1417
+ };
1418
+ conversation.messages.push(fullMessage);
1419
+ conversation.updatedAt = now;
1420
+ return fullMessage;
1421
+ }
1422
+ async updateMessage(conversationId, messageId, updates) {
1423
+ const conversation = this.conversations.get(conversationId);
1424
+ if (!conversation)
1425
+ return null;
1426
+ const messageIndex = conversation.messages.findIndex((m) => m.id === messageId);
1427
+ if (messageIndex === -1)
1428
+ return null;
1429
+ const message = conversation.messages[messageIndex];
1430
+ if (!message)
1431
+ return null;
1432
+ const updated = {
1433
+ ...message,
1434
+ ...updates,
1435
+ updatedAt: new Date
1436
+ };
1437
+ conversation.messages[messageIndex] = updated;
1438
+ conversation.updatedAt = new Date;
1439
+ return updated;
1440
+ }
1441
+ async delete(conversationId) {
1442
+ return this.conversations.delete(conversationId);
1443
+ }
1444
+ async list(options) {
1445
+ let results = Array.from(this.conversations.values());
1446
+ if (options?.status) {
1447
+ results = results.filter((c) => c.status === options.status);
1448
+ }
1449
+ results.sort((a, b) => b.updatedAt.getTime() - a.updatedAt.getTime());
1450
+ const offset = options?.offset ?? 0;
1451
+ const limit = options?.limit ?? 100;
1452
+ return results.slice(offset, offset + limit);
1453
+ }
1454
+ async search(query, limit = 20) {
1455
+ const lowerQuery = query.toLowerCase();
1456
+ const results = [];
1457
+ for (const conversation of this.conversations.values()) {
1458
+ if (conversation.title?.toLowerCase().includes(lowerQuery)) {
1459
+ results.push(conversation);
1460
+ continue;
1461
+ }
1462
+ const hasMatch = conversation.messages.some((m) => m.content.toLowerCase().includes(lowerQuery));
1463
+ if (hasMatch) {
1464
+ results.push(conversation);
1465
+ }
1466
+ if (results.length >= limit)
1467
+ break;
1468
+ }
1469
+ return results;
1470
+ }
1471
+ clear() {
1472
+ this.conversations.clear();
1473
+ }
1474
+ }
1475
+ function createInMemoryConversationStore() {
1476
+ return new InMemoryConversationStore;
1477
+ }
1478
+
1479
+ // src/core/chat-service.ts
1480
+ var DEFAULT_SYSTEM_PROMPT = `You are ContractSpec AI, an expert coding assistant specialized in ContractSpec development.
1481
+
1482
+ Your capabilities:
1483
+ - Help users create, modify, and understand ContractSpec specifications
1484
+ - Generate code that follows ContractSpec patterns and best practices
1485
+ - Explain concepts from the ContractSpec documentation
1486
+ - Suggest improvements and identify issues in specs and implementations
1487
+
1488
+ Guidelines:
1489
+ - Be concise but thorough
1490
+ - Provide code examples when helpful
1491
+ - Reference relevant ContractSpec concepts and patterns
1492
+ - Ask clarifying questions when the user's intent is unclear
1493
+ - When suggesting code changes, explain the rationale`;
1494
+
1495
+ class ChatService {
1496
+ provider;
1497
+ context;
1498
+ store;
1499
+ systemPrompt;
1500
+ maxHistoryMessages;
1501
+ onUsage;
1502
+ constructor(config) {
1503
+ this.provider = config.provider;
1504
+ this.context = config.context;
1505
+ this.store = config.store ?? new InMemoryConversationStore;
1506
+ this.systemPrompt = config.systemPrompt ?? DEFAULT_SYSTEM_PROMPT;
1507
+ this.maxHistoryMessages = config.maxHistoryMessages ?? 20;
1508
+ this.onUsage = config.onUsage;
1509
+ }
1510
+ async send(options) {
1511
+ let conversation;
1512
+ if (options.conversationId) {
1513
+ const existing = await this.store.get(options.conversationId);
1514
+ if (!existing) {
1515
+ throw new Error(`Conversation ${options.conversationId} not found`);
1516
+ }
1517
+ conversation = existing;
1518
+ } else {
1519
+ conversation = await this.store.create({
1520
+ status: "active",
1521
+ provider: this.provider.name,
1522
+ model: this.provider.model,
1523
+ messages: [],
1524
+ workspacePath: this.context?.workspacePath
1525
+ });
1526
+ }
1527
+ await this.store.appendMessage(conversation.id, {
1528
+ role: "user",
1529
+ content: options.content,
1530
+ status: "completed",
1531
+ attachments: options.attachments
1532
+ });
1533
+ const prompt = this.buildPrompt(conversation, options);
1534
+ const model = this.provider.getModel();
1535
+ try {
1536
+ const result = await generateText({
1537
+ model,
1538
+ prompt,
1539
+ system: this.systemPrompt
1540
+ });
1541
+ const assistantMessage = await this.store.appendMessage(conversation.id, {
1542
+ role: "assistant",
1543
+ content: result.text,
1544
+ status: "completed"
1545
+ });
1546
+ const updatedConversation = await this.store.get(conversation.id);
1547
+ if (!updatedConversation) {
1548
+ throw new Error("Conversation lost after update");
1549
+ }
1550
+ return {
1551
+ message: assistantMessage,
1552
+ conversation: updatedConversation
1553
+ };
1554
+ } catch (error) {
1555
+ await this.store.appendMessage(conversation.id, {
1556
+ role: "assistant",
1557
+ content: "",
1558
+ status: "error",
1559
+ error: {
1560
+ code: "generation_failed",
1561
+ message: error instanceof Error ? error.message : String(error)
1562
+ }
1563
+ });
1564
+ throw error;
1565
+ }
1566
+ }
1567
+ async stream(options) {
1568
+ let conversation;
1569
+ if (options.conversationId) {
1570
+ const existing = await this.store.get(options.conversationId);
1571
+ if (!existing) {
1572
+ throw new Error(`Conversation ${options.conversationId} not found`);
1573
+ }
1574
+ conversation = existing;
1575
+ } else {
1576
+ conversation = await this.store.create({
1577
+ status: "active",
1578
+ provider: this.provider.name,
1579
+ model: this.provider.model,
1580
+ messages: [],
1581
+ workspacePath: this.context?.workspacePath
1582
+ });
1583
+ }
1584
+ await this.store.appendMessage(conversation.id, {
1585
+ role: "user",
1586
+ content: options.content,
1587
+ status: "completed",
1588
+ attachments: options.attachments
1589
+ });
1590
+ const assistantMessage = await this.store.appendMessage(conversation.id, {
1591
+ role: "assistant",
1592
+ content: "",
1593
+ status: "streaming"
1594
+ });
1595
+ const prompt = this.buildPrompt(conversation, options);
1596
+ const model = this.provider.getModel();
1597
+ const self = {
1598
+ systemPrompt: this.systemPrompt,
1599
+ store: this.store
1600
+ };
1601
+ async function* streamGenerator() {
1602
+ let fullContent = "";
1603
+ try {
1604
+ const result = streamText({
1605
+ model,
1606
+ prompt,
1607
+ system: self.systemPrompt
1608
+ });
1609
+ for await (const chunk of result.textStream) {
1610
+ fullContent += chunk;
1611
+ yield { type: "text", content: chunk };
1612
+ }
1613
+ await self.store.updateMessage(conversation.id, assistantMessage.id, {
1614
+ content: fullContent,
1615
+ status: "completed"
1616
+ });
1617
+ yield {
1618
+ type: "done"
1619
+ };
1620
+ } catch (error) {
1621
+ await self.store.updateMessage(conversation.id, assistantMessage.id, {
1622
+ content: fullContent,
1623
+ status: "error",
1624
+ error: {
1625
+ code: "stream_failed",
1626
+ message: error instanceof Error ? error.message : String(error)
1627
+ }
1628
+ });
1629
+ yield {
1630
+ type: "error",
1631
+ error: {
1632
+ code: "stream_failed",
1633
+ message: error instanceof Error ? error.message : String(error)
1634
+ }
1635
+ };
1636
+ }
1637
+ }
1638
+ return {
1639
+ conversationId: conversation.id,
1640
+ messageId: assistantMessage.id,
1641
+ stream: streamGenerator()
1642
+ };
1643
+ }
1644
+ async getConversation(conversationId) {
1645
+ return this.store.get(conversationId);
1646
+ }
1647
+ async listConversations(options) {
1648
+ return this.store.list({
1649
+ status: "active",
1650
+ ...options
1651
+ });
1652
+ }
1653
+ async deleteConversation(conversationId) {
1654
+ return this.store.delete(conversationId);
1655
+ }
1656
+ buildPrompt(conversation, options) {
1657
+ let prompt = "";
1658
+ const historyStart = Math.max(0, conversation.messages.length - this.maxHistoryMessages);
1659
+ for (let i = historyStart;i < conversation.messages.length; i++) {
1660
+ const msg = conversation.messages[i];
1661
+ if (!msg)
1662
+ continue;
1663
+ if (msg.role === "user" || msg.role === "assistant") {
1664
+ prompt += `${msg.role === "user" ? "User" : "Assistant"}: ${msg.content}
1665
+
1666
+ `;
1667
+ }
1668
+ }
1669
+ let content = options.content;
1670
+ if (options.attachments?.length) {
1671
+ const attachmentInfo = options.attachments.map((a) => {
1672
+ if (a.type === "file" || a.type === "code") {
1673
+ return `
1674
+
1675
+ ### ${a.name}
1676
+ \`\`\`
1677
+ ${a.content}
1678
+ \`\`\``;
1679
+ }
1680
+ return `
1681
+
1682
+ [Attachment: ${a.name}]`;
1683
+ }).join("");
1684
+ content += attachmentInfo;
1685
+ }
1686
+ prompt += `User: ${content}
1687
+
1688
+ Assistant:`;
1689
+ return prompt;
1690
+ }
1691
+ }
1692
+ function createChatService(config) {
1693
+ return new ChatService(config);
1694
+ }
1695
+
1696
+ // src/presentation/hooks/useChat.tsx
1697
+ import {
1698
+ createProvider
1699
+ } from "@contractspec/lib.ai-providers";
1700
+ "use client";
1701
+ function useChat(options = {}) {
1702
+ const {
1703
+ provider = "openai",
1704
+ mode = "byok",
1705
+ model,
1706
+ apiKey,
1707
+ proxyUrl,
1708
+ conversationId: initialConversationId,
1709
+ systemPrompt,
1710
+ streaming = true,
1711
+ onSend,
1712
+ onResponse,
1713
+ onError,
1714
+ onUsage
1715
+ } = options;
1716
+ const [messages, setMessages] = React6.useState([]);
1717
+ const [conversation, setConversation] = React6.useState(null);
1718
+ const [isLoading, setIsLoading] = React6.useState(false);
1719
+ const [error, setError] = React6.useState(null);
1720
+ const [conversationId, setConversationId] = React6.useState(initialConversationId ?? null);
1721
+ const abortControllerRef = React6.useRef(null);
1722
+ const chatServiceRef = React6.useRef(null);
1723
+ React6.useEffect(() => {
1724
+ const chatProvider = createProvider({
1725
+ provider,
1726
+ model,
1727
+ apiKey,
1728
+ proxyUrl
1729
+ });
1730
+ chatServiceRef.current = new ChatService({
1731
+ provider: chatProvider,
1732
+ systemPrompt,
1733
+ onUsage
1734
+ });
1735
+ }, [provider, mode, model, apiKey, proxyUrl, systemPrompt, onUsage]);
1736
+ React6.useEffect(() => {
1737
+ if (!conversationId || !chatServiceRef.current)
1738
+ return;
1739
+ const loadConversation = async () => {
1740
+ if (!chatServiceRef.current)
1741
+ return;
1742
+ const conv = await chatServiceRef.current.getConversation(conversationId);
1743
+ if (conv) {
1744
+ setConversation(conv);
1745
+ setMessages(conv.messages);
1746
+ }
1747
+ };
1748
+ loadConversation().catch(console.error);
1749
+ }, [conversationId]);
1750
+ const sendMessage = React6.useCallback(async (content, attachments) => {
1751
+ if (!chatServiceRef.current) {
1752
+ throw new Error("Chat service not initialized");
1753
+ }
1754
+ setIsLoading(true);
1755
+ setError(null);
1756
+ abortControllerRef.current = new AbortController;
1757
+ try {
1758
+ const userMessage = {
1759
+ id: `msg_${Date.now()}`,
1760
+ conversationId: conversationId ?? "",
1761
+ role: "user",
1762
+ content,
1763
+ status: "completed",
1764
+ createdAt: new Date,
1765
+ updatedAt: new Date,
1766
+ attachments
1767
+ };
1768
+ setMessages((prev) => [...prev, userMessage]);
1769
+ onSend?.(userMessage);
1770
+ if (streaming) {
1771
+ const result = await chatServiceRef.current.stream({
1772
+ conversationId: conversationId ?? undefined,
1773
+ content,
1774
+ attachments
1775
+ });
1776
+ if (!conversationId) {
1777
+ setConversationId(result.conversationId);
1778
+ }
1779
+ const assistantMessage = {
1780
+ id: result.messageId,
1781
+ conversationId: result.conversationId,
1782
+ role: "assistant",
1783
+ content: "",
1784
+ status: "streaming",
1785
+ createdAt: new Date,
1786
+ updatedAt: new Date
1787
+ };
1788
+ setMessages((prev) => [...prev, assistantMessage]);
1789
+ let fullContent = "";
1790
+ for await (const chunk of result.stream) {
1791
+ if (chunk.type === "text" && chunk.content) {
1792
+ fullContent += chunk.content;
1793
+ setMessages((prev) => prev.map((m) => m.id === result.messageId ? { ...m, content: fullContent } : m));
1794
+ } else if (chunk.type === "done") {
1795
+ setMessages((prev) => prev.map((m) => m.id === result.messageId ? {
1796
+ ...m,
1797
+ status: "completed",
1798
+ usage: chunk.usage,
1799
+ updatedAt: new Date
1800
+ } : m));
1801
+ onResponse?.(messages.find((m) => m.id === result.messageId) ?? assistantMessage);
1802
+ } else if (chunk.type === "error") {
1803
+ setMessages((prev) => prev.map((m) => m.id === result.messageId ? {
1804
+ ...m,
1805
+ status: "error",
1806
+ error: chunk.error,
1807
+ updatedAt: new Date
1808
+ } : m));
1809
+ if (chunk.error) {
1810
+ const err = new Error(chunk.error.message);
1811
+ setError(err);
1812
+ onError?.(err);
1813
+ }
1814
+ }
1815
+ }
1816
+ } else {
1817
+ const result = await chatServiceRef.current.send({
1818
+ conversationId: conversationId ?? undefined,
1819
+ content,
1820
+ attachments
1821
+ });
1822
+ setConversation(result.conversation);
1823
+ setMessages(result.conversation.messages);
1824
+ if (!conversationId) {
1825
+ setConversationId(result.conversation.id);
1826
+ }
1827
+ onResponse?.(result.message);
1828
+ }
1829
+ } catch (err) {
1830
+ const error2 = err instanceof Error ? err : new Error(String(err));
1831
+ setError(error2);
1832
+ onError?.(error2);
1833
+ } finally {
1834
+ setIsLoading(false);
1835
+ abortControllerRef.current = null;
1836
+ }
1837
+ }, [conversationId, streaming, onSend, onResponse, onError, messages]);
1838
+ const clearConversation = React6.useCallback(() => {
1839
+ setMessages([]);
1840
+ setConversation(null);
1841
+ setConversationId(null);
1842
+ setError(null);
1843
+ }, []);
1844
+ const regenerate = React6.useCallback(async () => {
1845
+ const lastUserMessageIndex = messages.findLastIndex((m) => m.role === "user");
1846
+ if (lastUserMessageIndex === -1)
1847
+ return;
1848
+ const lastUserMessage = messages[lastUserMessageIndex];
1849
+ if (!lastUserMessage)
1850
+ return;
1851
+ setMessages((prev) => prev.slice(0, lastUserMessageIndex + 1));
1852
+ await sendMessage(lastUserMessage.content, lastUserMessage.attachments);
1853
+ }, [messages, sendMessage]);
1854
+ const stop = React6.useCallback(() => {
1855
+ abortControllerRef.current?.abort();
1856
+ setIsLoading(false);
1857
+ }, []);
1858
+ return {
1859
+ messages,
1860
+ conversation,
1861
+ isLoading,
1862
+ error,
1863
+ sendMessage,
1864
+ clearConversation,
1865
+ setConversationId,
1866
+ regenerate,
1867
+ stop
1868
+ };
1869
+ }
1870
+ // src/presentation/hooks/useProviders.tsx
1871
+ import * as React7 from "react";
1872
+ import {
1873
+ getAvailableProviders,
1874
+ getModelsForProvider as getModelsForProvider2
1875
+ } from "@contractspec/lib.ai-providers";
1876
+ "use client";
1877
+ function useProviders() {
1878
+ const [providers, setProviders] = React7.useState([]);
1879
+ const [isLoading, setIsLoading] = React7.useState(true);
1880
+ const loadProviders = React7.useCallback(async () => {
1881
+ setIsLoading(true);
1882
+ try {
1883
+ const available = getAvailableProviders();
1884
+ const providersWithModels = available.map((p) => ({
1885
+ ...p,
1886
+ models: getModelsForProvider2(p.provider)
1887
+ }));
1888
+ setProviders(providersWithModels);
1889
+ } catch (error) {
1890
+ console.error("Failed to load providers:", error);
1891
+ } finally {
1892
+ setIsLoading(false);
1893
+ }
1894
+ }, []);
1895
+ React7.useEffect(() => {
1896
+ loadProviders();
1897
+ }, [loadProviders]);
1898
+ const availableProviders = React7.useMemo(() => providers.filter((p) => p.available), [providers]);
1899
+ const isAvailable = React7.useCallback((provider) => providers.some((p) => p.provider === provider && p.available), [providers]);
1900
+ const getModelsCallback = React7.useCallback((provider) => providers.find((p) => p.provider === provider)?.models ?? [], [providers]);
1901
+ return {
1902
+ providers,
1903
+ availableProviders,
1904
+ isAvailable,
1905
+ getModels: getModelsCallback,
1906
+ isLoading,
1907
+ refresh: loadProviders
1908
+ };
1909
+ }
1910
+ // src/providers/index.ts
1911
+ import {
1912
+ createProvider as createProvider2,
1913
+ createProviderFromEnv,
1914
+ getAvailableProviders as getAvailableProviders2,
1915
+ DEFAULT_MODELS,
1916
+ MODELS,
1917
+ getModelsForProvider as getModelsForProvider3,
1918
+ getModelInfo,
1919
+ getRecommendedModels,
1920
+ getDefaultModel,
1921
+ validateProvider,
1922
+ hasCredentials,
1923
+ getEnvVarName,
1924
+ isOllamaRunning,
1925
+ listOllamaModels
1926
+ } from "@contractspec/lib.ai-providers";
1927
+
1928
+ // src/providers/chat-utilities.ts
1929
+ function supportsLocalMode(provider) {
1930
+ return provider === "ollama";
1931
+ }
1932
+ function isStudioAvailable(provider) {
1933
+ return provider !== "ollama";
1934
+ }
1935
+ // src/ai-chat.feature.ts
1936
+ import { defineFeature } from "@contractspec/lib.contracts";
1937
+ var AiChatFeature = defineFeature({
1938
+ meta: {
1939
+ key: "ai-chat",
1940
+ version: "1.0.0",
1941
+ title: "AI Vibe Coding Chat",
1942
+ description: "AI-powered conversational coding assistant with full workspace context",
1943
+ domain: "platform",
1944
+ owners: ["@platform.ai"],
1945
+ tags: ["ai", "chat", "llm", "vibe-coding", "assistant"],
1946
+ stability: "experimental"
1947
+ },
1948
+ operations: [
1949
+ { key: "ai-chat.send", version: "1.0.0" },
1950
+ { key: "ai-chat.stream", version: "1.0.0" },
1951
+ { key: "ai-chat.conversations.list", version: "1.0.0" },
1952
+ { key: "ai-chat.conversations.get", version: "1.0.0" },
1953
+ { key: "ai-chat.conversations.delete", version: "1.0.0" },
1954
+ { key: "ai-chat.providers.list", version: "1.0.0" },
1955
+ { key: "ai-chat.context.scan", version: "1.0.0" }
1956
+ ],
1957
+ events: [
1958
+ { key: "ai-chat.message.sent", version: "1.0.0" },
1959
+ { key: "ai-chat.message.received", version: "1.0.0" },
1960
+ { key: "ai-chat.conversation.created", version: "1.0.0" },
1961
+ { key: "ai-chat.conversation.deleted", version: "1.0.0" },
1962
+ { key: "ai-chat.error", version: "1.0.0" }
1963
+ ],
1964
+ presentations: [],
1965
+ opToPresentation: [],
1966
+ presentationsTargets: [],
1967
+ capabilities: {
1968
+ provides: [{ key: "ai-chat", version: "1.0.0" }],
1969
+ requires: [
1970
+ { key: "identity", version: "1.0.0" },
1971
+ { key: "metering", version: "1.0.0" }
1972
+ ]
1973
+ }
1974
+ });
1975
+ // src/schema.ts
1976
+ import { defineSchemaModel, ScalarTypeEnum } from "@contractspec/lib.schema";
1977
+ var ChatMessageModel = defineSchemaModel({
1978
+ name: "ChatMessage",
1979
+ fields: {
1980
+ id: { type: ScalarTypeEnum.String_unsecure(), isOptional: false },
1981
+ role: { type: ScalarTypeEnum.String_unsecure(), isOptional: false },
1982
+ content: { type: ScalarTypeEnum.String_unsecure(), isOptional: false },
1983
+ status: { type: ScalarTypeEnum.String_unsecure(), isOptional: false },
1984
+ createdAt: { type: ScalarTypeEnum.DateTime(), isOptional: false }
1985
+ }
1986
+ });
1987
+ var ChatConversationModel = defineSchemaModel({
1988
+ name: "ChatConversation",
1989
+ fields: {
1990
+ id: { type: ScalarTypeEnum.String_unsecure(), isOptional: false },
1991
+ title: { type: ScalarTypeEnum.String_unsecure(), isOptional: true },
1992
+ status: { type: ScalarTypeEnum.String_unsecure(), isOptional: false },
1993
+ messages: { type: ChatMessageModel, isArray: true, isOptional: false },
1994
+ provider: { type: ScalarTypeEnum.String_unsecure(), isOptional: false },
1995
+ model: { type: ScalarTypeEnum.String_unsecure(), isOptional: false }
1996
+ }
1997
+ });
1998
+ var SendMessageInputModel = defineSchemaModel({
1999
+ name: "SendMessageInput",
2000
+ fields: {
2001
+ conversationId: {
2002
+ type: ScalarTypeEnum.String_unsecure(),
2003
+ isOptional: true
2004
+ },
2005
+ content: { type: ScalarTypeEnum.String_unsecure(), isOptional: false },
2006
+ stream: { type: ScalarTypeEnum.Boolean(), isOptional: true }
2007
+ }
2008
+ });
2009
+ var SendMessageOutputModel = defineSchemaModel({
2010
+ name: "SendMessageOutput",
2011
+ fields: {
2012
+ message: { type: ChatMessageModel, isOptional: false },
2013
+ conversation: { type: ChatConversationModel, isOptional: false }
2014
+ }
2015
+ });
2016
+ var ListConversationsOutputModel = defineSchemaModel({
2017
+ name: "ListConversationsOutput",
2018
+ fields: {
2019
+ conversations: {
2020
+ type: ChatConversationModel,
2021
+ isArray: true,
2022
+ isOptional: false
2023
+ }
2024
+ }
2025
+ });
2026
+ // src/ai-chat.operations.ts
2027
+ import { defineCommand, defineQuery } from "@contractspec/lib.contracts";
2028
+ import { ScalarTypeEnum as ScalarTypeEnum2, defineSchemaModel as defineSchemaModel2 } from "@contractspec/lib.schema";
2029
+ var SendMessageContract = defineCommand({
2030
+ meta: {
2031
+ key: "ai-chat.send",
2032
+ version: "1.0.0",
2033
+ owners: ["@ai-team"],
2034
+ stability: "experimental",
2035
+ description: "Send a message to the AI chat.",
2036
+ tags: ["chat", "send"],
2037
+ goal: "Send message",
2038
+ context: "Chat UI"
2039
+ },
2040
+ io: { input: SendMessageInputModel, output: SendMessageOutputModel },
2041
+ policy: { auth: "user" }
2042
+ });
2043
+ var StreamMessageOutputModel = defineSchemaModel2({
2044
+ name: "StreamMessageOutput",
2045
+ fields: {
2046
+ stream: { type: ScalarTypeEnum2.String_unsecure(), isOptional: false }
2047
+ }
2048
+ });
2049
+ var StreamMessageContract = defineCommand({
2050
+ meta: {
2051
+ key: "ai-chat.stream",
2052
+ version: "1.0.0",
2053
+ owners: ["@ai-team"],
2054
+ stability: "experimental",
2055
+ description: "Stream a message response from the AI chat.",
2056
+ tags: ["chat", "stream"],
2057
+ goal: "Stream response",
2058
+ context: "Chat UI"
2059
+ },
2060
+ io: { input: SendMessageInputModel, output: StreamMessageOutputModel },
2061
+ policy: { auth: "user" }
2062
+ });
2063
+ var ListConversationsContract = defineQuery({
2064
+ meta: {
2065
+ key: "ai-chat.conversations.list",
2066
+ version: "1.0.0",
2067
+ owners: ["@ai-team"],
2068
+ stability: "experimental",
2069
+ description: "List user conversations.",
2070
+ tags: ["chat", "list"],
2071
+ goal: "List conversations",
2072
+ context: "Chat History"
2073
+ },
2074
+ io: {
2075
+ input: defineSchemaModel2({ name: "VoidInput", fields: {} }),
2076
+ output: ListConversationsOutputModel
2077
+ },
2078
+ policy: { auth: "user" }
2079
+ });
2080
+ var GetConversationContract = defineQuery({
2081
+ meta: {
2082
+ key: "ai-chat.conversations.get",
2083
+ version: "1.0.0",
2084
+ owners: ["@ai-team"],
2085
+ stability: "experimental",
2086
+ description: "Get a specific conversation.",
2087
+ tags: ["chat", "get"],
2088
+ goal: "Get conversation",
2089
+ context: "Chat UI"
2090
+ },
2091
+ io: {
2092
+ input: defineSchemaModel2({
2093
+ name: "GetConversationInput",
2094
+ fields: {
2095
+ id: { type: ScalarTypeEnum2.String_unsecure(), isOptional: false }
2096
+ }
2097
+ }),
2098
+ output: ChatConversationModel
2099
+ },
2100
+ policy: { auth: "user" }
2101
+ });
2102
+ var DeleteConversationContract = defineCommand({
2103
+ meta: {
2104
+ key: "ai-chat.conversations.delete",
2105
+ version: "1.0.0",
2106
+ owners: ["@ai-team"],
2107
+ stability: "experimental",
2108
+ description: "Delete a conversation.",
2109
+ tags: ["chat", "delete"],
2110
+ goal: "Delete conversation",
2111
+ context: "Chat History"
2112
+ },
2113
+ io: {
2114
+ input: defineSchemaModel2({
2115
+ name: "DeleteConversationInput",
2116
+ fields: {
2117
+ id: { type: ScalarTypeEnum2.String_unsecure(), isOptional: false }
2118
+ }
2119
+ }),
2120
+ output: defineSchemaModel2({ name: "VoidOutput", fields: {} })
2121
+ },
2122
+ policy: { auth: "user" }
2123
+ });
2124
+ var ListProvidersContract = defineQuery({
2125
+ meta: {
2126
+ key: "ai-chat.providers.list",
2127
+ version: "1.0.0",
2128
+ owners: ["@ai-team"],
2129
+ stability: "experimental",
2130
+ description: "List available AI providers.",
2131
+ tags: ["chat", "providers"],
2132
+ goal: "List providers",
2133
+ context: "Settings"
2134
+ },
2135
+ io: {
2136
+ input: defineSchemaModel2({ name: "VoidInput2", fields: {} }),
2137
+ output: defineSchemaModel2({
2138
+ name: "ProviderList",
2139
+ fields: {
2140
+ providers: {
2141
+ type: ScalarTypeEnum2.String_unsecure(),
2142
+ isArray: true,
2143
+ isOptional: false
2144
+ }
2145
+ }
2146
+ })
2147
+ },
2148
+ policy: { auth: "user" }
2149
+ });
2150
+ var ScanContextContract = defineCommand({
2151
+ meta: {
2152
+ key: "ai-chat.context.scan",
2153
+ version: "1.0.0",
2154
+ owners: ["@ai-team"],
2155
+ stability: "experimental",
2156
+ description: "Scan workspace context.",
2157
+ tags: ["chat", "context"],
2158
+ goal: "Scan context",
2159
+ context: "Background"
2160
+ },
2161
+ io: {
2162
+ input: defineSchemaModel2({
2163
+ name: "ScanContextInput",
2164
+ fields: {
2165
+ path: { type: ScalarTypeEnum2.String_unsecure(), isOptional: false }
2166
+ }
2167
+ }),
2168
+ output: defineSchemaModel2({ name: "VoidOutput2", fields: {} })
2169
+ },
2170
+ policy: { auth: "user" }
2171
+ });
2172
+ // src/events.ts
2173
+ import { defineEvent } from "@contractspec/lib.contracts";
2174
+ import { defineSchemaModel as defineSchemaModel3, ScalarTypeEnum as ScalarTypeEnum3 } from "@contractspec/lib.schema";
2175
+ var MessageSentEvent = defineEvent({
2176
+ meta: {
2177
+ key: "ai-chat.message.sent",
2178
+ version: "1.0.0",
2179
+ description: "Message sent by user",
2180
+ stability: "stable",
2181
+ owners: ["@ai-chat"],
2182
+ tags: ["ai-chat", "message", "sent"]
2183
+ },
2184
+ payload: ChatMessageModel
2185
+ });
2186
+ var MessageReceivedEvent = defineEvent({
2187
+ meta: {
2188
+ key: "ai-chat.message.received",
2189
+ version: "1.0.0",
2190
+ description: "Message received from AI",
2191
+ stability: "stable",
2192
+ owners: ["@ai-chat"],
2193
+ tags: ["ai-chat", "message", "received"]
2194
+ },
2195
+ payload: ChatMessageModel
2196
+ });
2197
+ var ConversationCreatedEvent = defineEvent({
2198
+ meta: {
2199
+ key: "ai-chat.conversation.created",
2200
+ version: "1.0.0",
2201
+ description: "New conversation created",
2202
+ stability: "stable",
2203
+ owners: ["@ai-chat"],
2204
+ tags: ["ai-chat", "conversation", "created"]
2205
+ },
2206
+ payload: ChatConversationModel
2207
+ });
2208
+ var ConversationDeletedEvent = defineEvent({
2209
+ meta: {
2210
+ key: "ai-chat.conversation.deleted",
2211
+ version: "1.0.0",
2212
+ description: "Conversation deleted",
2213
+ stability: "stable",
2214
+ owners: ["@ai-chat"],
2215
+ tags: ["ai-chat", "conversation", "deleted"]
2216
+ },
2217
+ payload: defineSchemaModel3({
2218
+ name: "ConversationDeletedPayload",
2219
+ fields: {
2220
+ id: { type: ScalarTypeEnum3.String_unsecure(), isOptional: false }
2221
+ }
2222
+ })
2223
+ });
2224
+ var ChatErrorEvent = defineEvent({
2225
+ meta: {
2226
+ key: "ai-chat.error",
2227
+ version: "1.0.0",
2228
+ description: "Chat error occurred",
2229
+ stability: "stable",
2230
+ owners: ["@ai-chat"],
2231
+ tags: ["ai-chat", "error"]
2232
+ },
2233
+ payload: defineSchemaModel3({
2234
+ name: "ChatErrorPayload",
2235
+ fields: {
2236
+ code: { type: ScalarTypeEnum3.String_unsecure(), isOptional: false },
2237
+ message: { type: ScalarTypeEnum3.String_unsecure(), isOptional: false }
2238
+ }
2239
+ })
2240
+ });
2241
+ export {
2242
+ validateProvider,
2243
+ useProviders,
2244
+ useChat,
2245
+ supportsLocalMode,
2246
+ listOllamaModels,
2247
+ isStudioAvailable,
2248
+ isOllamaRunning,
2249
+ hasCredentials,
2250
+ getRecommendedModels,
2251
+ getModelsForProvider3 as getModelsForProvider,
2252
+ getModelInfo,
2253
+ getEnvVarName,
2254
+ getDefaultModel,
2255
+ getAvailableProviders2 as getAvailableProviders,
2256
+ createWorkspaceContext,
2257
+ createProviderFromEnv,
2258
+ createProvider2 as createProvider,
2259
+ createNodeFileOperations,
2260
+ createContextBuilder,
2261
+ WorkspaceContext,
2262
+ StreamMessageContract,
2263
+ SendMessageOutputModel,
2264
+ SendMessageInputModel,
2265
+ SendMessageContract,
2266
+ ScanContextContract,
2267
+ ModelPicker,
2268
+ MessageSentEvent,
2269
+ MessageReceivedEvent,
2270
+ MODELS,
2271
+ ListProvidersContract,
2272
+ ListConversationsOutputModel,
2273
+ ListConversationsContract,
2274
+ GetConversationContract,
2275
+ FileOperations,
2276
+ DeleteConversationContract,
2277
+ DEFAULT_MODELS,
2278
+ ConversationDeletedEvent,
2279
+ ConversationCreatedEvent,
2280
+ ContextIndicator,
2281
+ ContextBuilder,
2282
+ CodePreview,
2283
+ ChatMessageModel,
2284
+ ChatMessage as ChatMessageComponent,
2285
+ ChatMessage,
2286
+ ChatInput,
2287
+ ChatErrorEvent,
2288
+ ChatConversationModel,
2289
+ ChatContainer,
2290
+ AiChatFeature
2291
+ };