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