@contractspec/module.ai-chat 4.0.3 → 4.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/README.md +130 -10
  2. package/dist/adapters/ai-sdk-bundle-adapter.d.ts +18 -0
  3. package/dist/adapters/index.d.ts +4 -0
  4. package/dist/browser/core/index.js +1143 -21
  5. package/dist/browser/index.js +2813 -631
  6. package/dist/browser/presentation/components/index.js +3160 -358
  7. package/dist/browser/presentation/hooks/index.js +978 -43
  8. package/dist/browser/presentation/index.js +2801 -666
  9. package/dist/core/agent-adapter.d.ts +53 -0
  10. package/dist/core/agent-tools-adapter.d.ts +12 -0
  11. package/dist/core/chat-service.d.ts +49 -1
  12. package/dist/core/contracts-context.d.ts +46 -0
  13. package/dist/core/contracts-context.test.d.ts +1 -0
  14. package/dist/core/conversation-store.d.ts +16 -2
  15. package/dist/core/create-chat-route.d.ts +3 -0
  16. package/dist/core/export-formatters.d.ts +29 -0
  17. package/dist/core/export-formatters.test.d.ts +1 -0
  18. package/dist/core/index.d.ts +8 -0
  19. package/dist/core/index.js +1143 -21
  20. package/dist/core/local-storage-conversation-store.d.ts +33 -0
  21. package/dist/core/message-types.d.ts +6 -0
  22. package/dist/core/surface-planner-tools.d.ts +23 -0
  23. package/dist/core/surface-planner-tools.test.d.ts +1 -0
  24. package/dist/core/thinking-levels.d.ts +38 -0
  25. package/dist/core/thinking-levels.test.d.ts +1 -0
  26. package/dist/core/workflow-tools.d.ts +18 -0
  27. package/dist/core/workflow-tools.test.d.ts +1 -0
  28. package/dist/index.d.ts +4 -2
  29. package/dist/index.js +2813 -631
  30. package/dist/node/core/index.js +1143 -21
  31. package/dist/node/index.js +2813 -631
  32. package/dist/node/presentation/components/index.js +3160 -358
  33. package/dist/node/presentation/hooks/index.js +978 -43
  34. package/dist/node/presentation/index.js +2804 -669
  35. package/dist/presentation/components/ChatContainer.d.ts +3 -1
  36. package/dist/presentation/components/ChatExportToolbar.d.ts +25 -0
  37. package/dist/presentation/components/ChatMessage.d.ts +16 -1
  38. package/dist/presentation/components/ChatSidebar.d.ts +26 -0
  39. package/dist/presentation/components/ChatWithExport.d.ts +34 -0
  40. package/dist/presentation/components/ChatWithSidebar.d.ts +19 -0
  41. package/dist/presentation/components/ThinkingLevelPicker.d.ts +16 -0
  42. package/dist/presentation/components/ToolResultRenderer.d.ts +33 -0
  43. package/dist/presentation/components/index.d.ts +6 -0
  44. package/dist/presentation/components/index.js +3160 -358
  45. package/dist/presentation/hooks/index.d.ts +2 -0
  46. package/dist/presentation/hooks/index.js +978 -43
  47. package/dist/presentation/hooks/useChat.d.ts +44 -2
  48. package/dist/presentation/hooks/useConversations.d.ts +18 -0
  49. package/dist/presentation/hooks/useMessageSelection.d.ts +13 -0
  50. package/dist/presentation/index.js +2804 -669
  51. package/package.json +14 -18
@@ -3,8 +3,8 @@ var __require = import.meta.require;
3
3
 
4
4
  // src/presentation/hooks/useChat.tsx
5
5
  import * as React from "react";
6
- import { tool } from "ai";
7
- import { z } from "zod";
6
+ import { tool as tool4 } from "ai";
7
+ import { z as z4 } from "zod";
8
8
 
9
9
  // src/core/chat-service.ts
10
10
  import { generateText, streamText } from "ai";
@@ -86,11 +86,65 @@ class InMemoryConversationStore {
86
86
  if (options?.status) {
87
87
  results = results.filter((c) => c.status === options.status);
88
88
  }
89
+ if (options?.projectId) {
90
+ results = results.filter((c) => c.projectId === options.projectId);
91
+ }
92
+ if (options?.tags && options.tags.length > 0) {
93
+ const tagSet = new Set(options.tags);
94
+ results = results.filter((c) => c.tags && c.tags.some((t) => tagSet.has(t)));
95
+ }
89
96
  results.sort((a, b) => b.updatedAt.getTime() - a.updatedAt.getTime());
90
97
  const offset = options?.offset ?? 0;
91
98
  const limit = options?.limit ?? 100;
92
99
  return results.slice(offset, offset + limit);
93
100
  }
101
+ async fork(conversationId, upToMessageId) {
102
+ const source = this.conversations.get(conversationId);
103
+ if (!source) {
104
+ throw new Error(`Conversation ${conversationId} not found`);
105
+ }
106
+ let messagesToCopy = source.messages;
107
+ if (upToMessageId) {
108
+ const idx = source.messages.findIndex((m) => m.id === upToMessageId);
109
+ if (idx === -1) {
110
+ throw new Error(`Message ${upToMessageId} not found`);
111
+ }
112
+ messagesToCopy = source.messages.slice(0, idx + 1);
113
+ }
114
+ const now = new Date;
115
+ const forkedMessages = messagesToCopy.map((m) => ({
116
+ ...m,
117
+ id: generateId("msg"),
118
+ conversationId: "",
119
+ createdAt: new Date(m.createdAt),
120
+ updatedAt: new Date(m.updatedAt)
121
+ }));
122
+ const forked = {
123
+ ...source,
124
+ id: generateId("conv"),
125
+ title: source.title ? `${source.title} (fork)` : undefined,
126
+ forkedFromId: source.id,
127
+ createdAt: now,
128
+ updatedAt: now,
129
+ messages: forkedMessages
130
+ };
131
+ for (const m of forked.messages) {
132
+ m.conversationId = forked.id;
133
+ }
134
+ this.conversations.set(forked.id, forked);
135
+ return forked;
136
+ }
137
+ async truncateAfter(conversationId, messageId) {
138
+ const conv = this.conversations.get(conversationId);
139
+ if (!conv)
140
+ return null;
141
+ const idx = conv.messages.findIndex((m) => m.id === messageId);
142
+ if (idx === -1)
143
+ return null;
144
+ conv.messages = conv.messages.slice(0, idx + 1);
145
+ conv.updatedAt = new Date;
146
+ return conv;
147
+ }
94
148
  async search(query, limit = 20) {
95
149
  const lowerQuery = query.toLowerCase();
96
150
  const results = [];
@@ -116,7 +170,509 @@ function createInMemoryConversationStore() {
116
170
  return new InMemoryConversationStore;
117
171
  }
118
172
 
173
+ // src/core/thinking-levels.ts
174
+ var THINKING_LEVEL_LABELS = {
175
+ instant: "Instant",
176
+ thinking: "Thinking",
177
+ extra_thinking: "Extra Thinking",
178
+ max: "Max"
179
+ };
180
+ var THINKING_LEVEL_DESCRIPTIONS = {
181
+ instant: "Fast responses, minimal reasoning",
182
+ thinking: "Standard reasoning depth",
183
+ extra_thinking: "More thorough reasoning",
184
+ max: "Maximum reasoning depth"
185
+ };
186
+ function getProviderOptions(level, providerName) {
187
+ if (!level || level === "instant") {
188
+ return {};
189
+ }
190
+ switch (providerName) {
191
+ case "anthropic": {
192
+ const budgetMap = {
193
+ thinking: 8000,
194
+ extra_thinking: 16000,
195
+ max: 32000
196
+ };
197
+ return {
198
+ anthropic: {
199
+ thinking: { type: "enabled", budgetTokens: budgetMap[level] }
200
+ }
201
+ };
202
+ }
203
+ case "openai": {
204
+ const effortMap = {
205
+ thinking: "low",
206
+ extra_thinking: "medium",
207
+ max: "high"
208
+ };
209
+ return {
210
+ openai: {
211
+ reasoningEffort: effortMap[level]
212
+ }
213
+ };
214
+ }
215
+ case "ollama":
216
+ case "mistral":
217
+ case "gemini":
218
+ return {};
219
+ default:
220
+ return {};
221
+ }
222
+ }
223
+
224
+ // src/core/workflow-tools.ts
225
+ import { tool } from "ai";
226
+ import { z } from "zod";
227
+ import {
228
+ WorkflowComposer,
229
+ validateExtension
230
+ } from "@contractspec/lib.workflow-composer";
231
+ var StepTypeSchema = z.enum(["human", "automation", "decision"]);
232
+ var StepActionSchema = z.object({
233
+ operation: z.object({
234
+ name: z.string(),
235
+ version: z.number()
236
+ }).optional(),
237
+ form: z.object({
238
+ key: z.string(),
239
+ version: z.number()
240
+ }).optional()
241
+ }).optional();
242
+ var StepSchema = z.object({
243
+ id: z.string(),
244
+ type: StepTypeSchema,
245
+ label: z.string(),
246
+ description: z.string().optional(),
247
+ action: StepActionSchema
248
+ });
249
+ var StepInjectionSchema = z.object({
250
+ after: z.string().optional(),
251
+ before: z.string().optional(),
252
+ inject: StepSchema,
253
+ transitionTo: z.string().optional(),
254
+ transitionFrom: z.string().optional(),
255
+ when: z.string().optional()
256
+ });
257
+ var WorkflowExtensionInputSchema = z.object({
258
+ workflow: z.string(),
259
+ tenantId: z.string().optional(),
260
+ role: z.string().optional(),
261
+ priority: z.number().optional(),
262
+ customSteps: z.array(StepInjectionSchema).optional(),
263
+ hiddenSteps: z.array(z.string()).optional()
264
+ });
265
+ function createWorkflowTools(config) {
266
+ const { baseWorkflows, composer } = config;
267
+ const baseByKey = new Map(baseWorkflows.map((b) => [b.meta.key, b]));
268
+ const createWorkflowExtensionTool = tool({
269
+ description: "Create or validate a workflow extension. Use when the user asks to add steps, modify a workflow, or create a tenant-specific extension. The extension targets an existing base workflow.",
270
+ inputSchema: WorkflowExtensionInputSchema,
271
+ execute: async (input) => {
272
+ const extension = {
273
+ workflow: input.workflow,
274
+ tenantId: input.tenantId,
275
+ role: input.role,
276
+ priority: input.priority,
277
+ customSteps: input.customSteps,
278
+ hiddenSteps: input.hiddenSteps
279
+ };
280
+ const base = baseByKey.get(input.workflow);
281
+ if (!base) {
282
+ return {
283
+ success: false,
284
+ error: `Base workflow "${input.workflow}" not found. Available: ${Array.from(baseByKey.keys()).join(", ")}`,
285
+ extension
286
+ };
287
+ }
288
+ try {
289
+ validateExtension(extension, base);
290
+ return {
291
+ success: true,
292
+ message: "Extension validated successfully",
293
+ extension
294
+ };
295
+ } catch (err) {
296
+ return {
297
+ success: false,
298
+ error: err instanceof Error ? err.message : String(err),
299
+ extension
300
+ };
301
+ }
302
+ }
303
+ });
304
+ const composeWorkflowInputSchema = z.object({
305
+ workflowKey: z.string().describe("Base workflow meta.key"),
306
+ tenantId: z.string().optional(),
307
+ role: z.string().optional(),
308
+ extensions: z.array(WorkflowExtensionInputSchema).optional().describe("Extensions to register before composing")
309
+ });
310
+ const composeWorkflowTool = tool({
311
+ description: "Compose a workflow by applying registered extensions to a base workflow. Returns the composed WorkflowSpec.",
312
+ inputSchema: composeWorkflowInputSchema,
313
+ execute: async (input) => {
314
+ const base = baseByKey.get(input.workflowKey);
315
+ if (!base) {
316
+ return {
317
+ success: false,
318
+ error: `Base workflow "${input.workflowKey}" not found. Available: ${Array.from(baseByKey.keys()).join(", ")}`
319
+ };
320
+ }
321
+ const comp = composer ?? new WorkflowComposer;
322
+ if (input.extensions?.length) {
323
+ for (const ext of input.extensions) {
324
+ comp.register({
325
+ workflow: ext.workflow,
326
+ tenantId: ext.tenantId,
327
+ role: ext.role,
328
+ priority: ext.priority,
329
+ customSteps: ext.customSteps,
330
+ hiddenSteps: ext.hiddenSteps
331
+ });
332
+ }
333
+ }
334
+ try {
335
+ const composed = comp.compose({
336
+ base,
337
+ tenantId: input.tenantId,
338
+ role: input.role
339
+ });
340
+ return {
341
+ success: true,
342
+ workflow: composed,
343
+ meta: composed.meta,
344
+ stepIds: composed.definition.steps.map((s) => s.id)
345
+ };
346
+ } catch (err) {
347
+ return {
348
+ success: false,
349
+ error: err instanceof Error ? err.message : String(err)
350
+ };
351
+ }
352
+ }
353
+ });
354
+ const generateWorkflowSpecCodeInputSchema = z.object({
355
+ workflowKey: z.string().describe("Workflow meta.key"),
356
+ composedSteps: z.array(z.object({
357
+ id: z.string(),
358
+ type: z.enum(["human", "automation", "decision"]),
359
+ label: z.string(),
360
+ description: z.string().optional()
361
+ })).optional().describe("Steps to include; if omitted, uses the base workflow")
362
+ });
363
+ const generateWorkflowSpecCodeTool = tool({
364
+ description: "Generate TypeScript code for a workflow spec. Use after composing a workflow to output the spec as code the user can save.",
365
+ inputSchema: generateWorkflowSpecCodeInputSchema,
366
+ execute: async (input) => {
367
+ const base = baseByKey.get(input.workflowKey);
368
+ if (!base) {
369
+ return {
370
+ success: false,
371
+ error: `Base workflow "${input.workflowKey}" not found. Available: ${Array.from(baseByKey.keys()).join(", ")}`,
372
+ code: null
373
+ };
374
+ }
375
+ const steps = input.composedSteps ?? base.definition.steps;
376
+ const specVarName = toPascalCase((base.meta.key.split(".").pop() ?? "Workflow") + "") + "Workflow";
377
+ const stepsCode = steps.map((s) => ` {
378
+ id: '${s.id}',
379
+ type: '${s.type}',
380
+ label: '${escapeString(s.label)}',${s.description ? `
381
+ description: '${escapeString(s.description)}',` : ""}
382
+ }`).join(`,
383
+ `);
384
+ const meta = base.meta;
385
+ const transitionsJson = JSON.stringify(base.definition.transitions, null, 6);
386
+ const code = `import type { WorkflowSpec } from '@contractspec/lib.contracts-spec/workflow';
387
+
388
+ /**
389
+ * Workflow: ${base.meta.key}
390
+ * Generated via AI chat workflow tools.
391
+ */
392
+ export const ${specVarName}: WorkflowSpec = {
393
+ meta: {
394
+ key: '${base.meta.key}',
395
+ version: '${String(base.meta.version)}',
396
+ title: '${escapeString(meta.title ?? base.meta.key)}',
397
+ description: '${escapeString(meta.description ?? "")}',
398
+ },
399
+ definition: {
400
+ entryStepId: '${base.definition.entryStepId ?? base.definition.steps[0]?.id ?? ""}',
401
+ steps: [
402
+ ${stepsCode}
403
+ ],
404
+ transitions: ${transitionsJson},
405
+ },
406
+ };
407
+ `;
408
+ return {
409
+ success: true,
410
+ code,
411
+ workflowKey: input.workflowKey
412
+ };
413
+ }
414
+ });
415
+ return {
416
+ create_workflow_extension: createWorkflowExtensionTool,
417
+ compose_workflow: composeWorkflowTool,
418
+ generate_workflow_spec_code: generateWorkflowSpecCodeTool
419
+ };
420
+ }
421
+ function toPascalCase(value) {
422
+ return value.split(/[-_.]/).filter(Boolean).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join("");
423
+ }
424
+ function escapeString(value) {
425
+ return value.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
426
+ }
427
+
428
+ // src/core/contracts-context.ts
429
+ function buildContractsContextPrompt(config) {
430
+ const parts = [];
431
+ if (!config.agentSpecs?.length && !config.dataViewSpecs?.length && !config.formSpecs?.length && !config.presentationSpecs?.length && !config.operationRefs?.length) {
432
+ return "";
433
+ }
434
+ parts.push(`
435
+
436
+ ## Available resources`);
437
+ if (config.agentSpecs?.length) {
438
+ parts.push(`
439
+ ### Agent tools`);
440
+ for (const agent of config.agentSpecs) {
441
+ const toolNames = agent.tools?.map((t) => t.name).join(", ") ?? "none";
442
+ parts.push(`- **${agent.key}**: tools: ${toolNames}`);
443
+ }
444
+ }
445
+ if (config.dataViewSpecs?.length) {
446
+ parts.push(`
447
+ ### Data views`);
448
+ for (const dv of config.dataViewSpecs) {
449
+ parts.push(`- **${dv.key}**: ${dv.meta.title ?? dv.key}`);
450
+ }
451
+ }
452
+ if (config.formSpecs?.length) {
453
+ parts.push(`
454
+ ### Forms`);
455
+ for (const form of config.formSpecs) {
456
+ parts.push(`- **${form.key}**: ${form.meta.title ?? form.key}`);
457
+ }
458
+ }
459
+ if (config.presentationSpecs?.length) {
460
+ parts.push(`
461
+ ### Presentations`);
462
+ for (const pres of config.presentationSpecs) {
463
+ parts.push(`- **${pres.key}**: ${pres.meta.title ?? pres.key} (targets: ${pres.targets?.join(", ") ?? "react"})`);
464
+ }
465
+ }
466
+ if (config.operationRefs?.length) {
467
+ parts.push(`
468
+ ### Operations`);
469
+ for (const op of config.operationRefs) {
470
+ parts.push(`- **${op.key}@${op.version}**`);
471
+ }
472
+ }
473
+ parts.push(`
474
+ Use the available tools to invoke operations, query data views, or propose surface changes when appropriate.`);
475
+ return parts.join(`
476
+ `);
477
+ }
478
+
479
+ // src/core/agent-tools-adapter.ts
480
+ import { tool as tool2 } from "ai";
481
+ import { z as z2 } from "zod";
482
+ function getInputSchema(_schema) {
483
+ return z2.object({}).passthrough();
484
+ }
485
+ function agentToolConfigsToToolSet(configs, handlers) {
486
+ const result = {};
487
+ for (const config of configs) {
488
+ const handler = handlers?.[config.name];
489
+ const inputSchema = getInputSchema(config.schema);
490
+ result[config.name] = tool2({
491
+ description: config.description ?? config.name,
492
+ inputSchema,
493
+ execute: async (input) => {
494
+ if (!handler) {
495
+ return {
496
+ status: "unimplemented",
497
+ message: "Wire handler in host",
498
+ toolName: config.name
499
+ };
500
+ }
501
+ try {
502
+ const output = await Promise.resolve(handler(input));
503
+ return typeof output === "string" ? output : output;
504
+ } catch (err) {
505
+ return {
506
+ status: "error",
507
+ error: err instanceof Error ? err.message : String(err),
508
+ toolName: config.name
509
+ };
510
+ }
511
+ }
512
+ });
513
+ }
514
+ return result;
515
+ }
516
+
517
+ // src/core/surface-planner-tools.ts
518
+ import { tool as tool3 } from "ai";
519
+ import { z as z3 } from "zod";
520
+ import {
521
+ validatePatchProposal
522
+ } from "@contractspec/lib.surface-runtime/spec/validate-surface-patch";
523
+ import { buildSurfacePatchProposal } from "@contractspec/lib.surface-runtime/runtime/planner-tools";
524
+ var VALID_OPS = [
525
+ "insert-node",
526
+ "replace-node",
527
+ "remove-node",
528
+ "move-node",
529
+ "resize-panel",
530
+ "set-layout",
531
+ "reveal-field",
532
+ "hide-field",
533
+ "promote-action",
534
+ "set-focus"
535
+ ];
536
+ var DEFAULT_NODE_KINDS = [
537
+ "entity-section",
538
+ "entity-card",
539
+ "data-view",
540
+ "assistant-panel",
541
+ "chat-thread",
542
+ "action-bar",
543
+ "timeline",
544
+ "table",
545
+ "rich-doc",
546
+ "form",
547
+ "chart",
548
+ "custom-widget"
549
+ ];
550
+ function collectSlotIdsFromRegion(node) {
551
+ const ids = [];
552
+ if (node.type === "slot") {
553
+ ids.push(node.slotId);
554
+ }
555
+ if (node.type === "panel-group" || node.type === "stack") {
556
+ for (const child of node.children) {
557
+ ids.push(...collectSlotIdsFromRegion(child));
558
+ }
559
+ }
560
+ if (node.type === "tabs") {
561
+ for (const tab of node.tabs) {
562
+ ids.push(...collectSlotIdsFromRegion(tab.child));
563
+ }
564
+ }
565
+ if (node.type === "floating") {
566
+ ids.push(node.anchorSlotId);
567
+ ids.push(...collectSlotIdsFromRegion(node.child));
568
+ }
569
+ return ids;
570
+ }
571
+ function deriveConstraints(plan) {
572
+ const slotIds = collectSlotIdsFromRegion(plan.layoutRoot);
573
+ const uniqueSlots = [...new Set(slotIds)];
574
+ return {
575
+ allowedOps: VALID_OPS,
576
+ allowedSlots: uniqueSlots.length > 0 ? uniqueSlots : ["assistant", "primary"],
577
+ allowedNodeKinds: DEFAULT_NODE_KINDS
578
+ };
579
+ }
580
+ var ProposePatchInputSchema = z3.object({
581
+ proposalId: z3.string().describe("Unique proposal identifier"),
582
+ ops: z3.array(z3.object({
583
+ op: z3.enum([
584
+ "insert-node",
585
+ "replace-node",
586
+ "remove-node",
587
+ "move-node",
588
+ "resize-panel",
589
+ "set-layout",
590
+ "reveal-field",
591
+ "hide-field",
592
+ "promote-action",
593
+ "set-focus"
594
+ ]),
595
+ slotId: z3.string().optional(),
596
+ nodeId: z3.string().optional(),
597
+ toSlotId: z3.string().optional(),
598
+ index: z3.number().optional(),
599
+ node: z3.object({
600
+ nodeId: z3.string(),
601
+ kind: z3.string(),
602
+ title: z3.string().optional(),
603
+ props: z3.record(z3.string(), z3.unknown()).optional(),
604
+ children: z3.array(z3.unknown()).optional()
605
+ }).optional(),
606
+ persistKey: z3.string().optional(),
607
+ sizes: z3.array(z3.number()).optional(),
608
+ layoutId: z3.string().optional(),
609
+ fieldId: z3.string().optional(),
610
+ actionId: z3.string().optional(),
611
+ placement: z3.enum(["header", "inline", "context", "assistant"]).optional(),
612
+ targetId: z3.string().optional()
613
+ }))
614
+ });
615
+ function createSurfacePlannerTools(config) {
616
+ const { plan, constraints, onPatchProposal } = config;
617
+ const resolvedConstraints = constraints ?? deriveConstraints(plan);
618
+ const proposePatchTool = tool3({
619
+ description: "Propose surface patches (layout changes, node insertions, etc.) for user approval. " + "Only use allowed ops, slots, and node kinds from the planner context.",
620
+ inputSchema: ProposePatchInputSchema,
621
+ execute: async (input) => {
622
+ const ops = input.ops;
623
+ try {
624
+ validatePatchProposal(ops, resolvedConstraints);
625
+ const proposal = buildSurfacePatchProposal(input.proposalId, ops);
626
+ onPatchProposal?.(proposal);
627
+ return {
628
+ success: true,
629
+ proposalId: proposal.proposalId,
630
+ opsCount: proposal.ops.length,
631
+ message: "Patch proposal validated; awaiting user approval"
632
+ };
633
+ } catch (err) {
634
+ return {
635
+ success: false,
636
+ error: err instanceof Error ? err.message : String(err),
637
+ proposalId: input.proposalId
638
+ };
639
+ }
640
+ }
641
+ });
642
+ return {
643
+ "propose-patch": proposePatchTool
644
+ };
645
+ }
646
+ function buildPlannerPromptInput(plan) {
647
+ const constraints = deriveConstraints(plan);
648
+ return {
649
+ bundleMeta: {
650
+ key: plan.bundleKey,
651
+ version: "0.0.0",
652
+ title: plan.bundleKey
653
+ },
654
+ surfaceId: plan.surfaceId,
655
+ allowedPatchOps: constraints.allowedOps,
656
+ allowedSlots: [...constraints.allowedSlots],
657
+ allowedNodeKinds: [...constraints.allowedNodeKinds],
658
+ actions: plan.actions.map((a) => ({
659
+ actionId: a.actionId,
660
+ title: a.title
661
+ })),
662
+ preferences: {
663
+ guidance: "hints",
664
+ density: "standard",
665
+ dataDepth: "detailed",
666
+ control: "standard",
667
+ media: "text",
668
+ pace: "balanced",
669
+ narrative: "top-down"
670
+ }
671
+ };
672
+ }
673
+
119
674
  // src/core/chat-service.ts
675
+ import { compilePlannerPrompt } from "@contractspec/lib.surface-runtime/runtime/planner-prompt";
120
676
  var DEFAULT_SYSTEM_PROMPT = `You are ContractSpec AI, an expert coding assistant specialized in ContractSpec development.
121
677
 
122
678
  Your capabilities:
@@ -131,6 +687,9 @@ Guidelines:
131
687
  - Reference relevant ContractSpec concepts and patterns
132
688
  - Ask clarifying questions when the user's intent is unclear
133
689
  - When suggesting code changes, explain the rationale`;
690
+ var WORKFLOW_TOOLS_PROMPT = `
691
+
692
+ Workflow creation: You can create and modify workflows. Use create_workflow_extension when the user asks to add steps, change a workflow, or create a tenant-specific extension. Use compose_workflow to apply extensions to a base workflow. Use generate_workflow_spec_code to output TypeScript for the user to save.`;
134
693
 
135
694
  class ChatService {
136
695
  provider;
@@ -140,19 +699,93 @@ class ChatService {
140
699
  maxHistoryMessages;
141
700
  onUsage;
142
701
  tools;
702
+ thinkingLevel;
143
703
  sendReasoning;
144
704
  sendSources;
705
+ modelSelector;
145
706
  constructor(config) {
146
707
  this.provider = config.provider;
147
708
  this.context = config.context;
148
709
  this.store = config.store ?? new InMemoryConversationStore;
149
- this.systemPrompt = config.systemPrompt ?? DEFAULT_SYSTEM_PROMPT;
710
+ this.systemPrompt = this.buildSystemPrompt(config);
150
711
  this.maxHistoryMessages = config.maxHistoryMessages ?? 20;
151
712
  this.onUsage = config.onUsage;
152
- this.tools = config.tools;
153
- this.sendReasoning = config.sendReasoning ?? false;
713
+ this.tools = this.mergeTools(config);
714
+ this.thinkingLevel = config.thinkingLevel;
715
+ this.modelSelector = config.modelSelector;
716
+ this.sendReasoning = config.sendReasoning ?? (config.thinkingLevel != null && config.thinkingLevel !== "instant");
154
717
  this.sendSources = config.sendSources ?? false;
155
718
  }
719
+ buildSystemPrompt(config) {
720
+ let base = config.systemPrompt ?? DEFAULT_SYSTEM_PROMPT;
721
+ if (config.workflowToolsConfig?.baseWorkflows?.length) {
722
+ base += WORKFLOW_TOOLS_PROMPT;
723
+ }
724
+ const contractsPrompt = buildContractsContextPrompt(config.contractsContext ?? {});
725
+ if (contractsPrompt) {
726
+ base += contractsPrompt;
727
+ }
728
+ if (config.surfacePlanConfig?.plan) {
729
+ const plannerInput = buildPlannerPromptInput(config.surfacePlanConfig.plan);
730
+ base += `
731
+
732
+ ` + compilePlannerPrompt(plannerInput);
733
+ }
734
+ return base;
735
+ }
736
+ mergeTools(config) {
737
+ let merged = config.tools ?? {};
738
+ const wfConfig = config.workflowToolsConfig;
739
+ if (wfConfig?.baseWorkflows?.length) {
740
+ const workflowTools = createWorkflowTools({
741
+ baseWorkflows: wfConfig.baseWorkflows,
742
+ composer: wfConfig.composer
743
+ });
744
+ merged = { ...merged, ...workflowTools };
745
+ }
746
+ const contractsCtx = config.contractsContext;
747
+ if (contractsCtx?.agentSpecs?.length) {
748
+ const allTools = [];
749
+ for (const agent of contractsCtx.agentSpecs) {
750
+ if (agent.tools?.length)
751
+ allTools.push(...agent.tools);
752
+ }
753
+ if (allTools.length > 0) {
754
+ const agentTools = agentToolConfigsToToolSet(allTools);
755
+ merged = { ...merged, ...agentTools };
756
+ }
757
+ }
758
+ const surfaceConfig = config.surfacePlanConfig;
759
+ if (surfaceConfig?.plan) {
760
+ const plannerTools = createSurfacePlannerTools({
761
+ plan: surfaceConfig.plan,
762
+ onPatchProposal: surfaceConfig.onPatchProposal
763
+ });
764
+ merged = { ...merged, ...plannerTools };
765
+ }
766
+ if (config.mcpTools && Object.keys(config.mcpTools).length > 0) {
767
+ merged = { ...merged, ...config.mcpTools };
768
+ }
769
+ return Object.keys(merged).length > 0 ? merged : undefined;
770
+ }
771
+ async resolveModel() {
772
+ if (this.modelSelector) {
773
+ const dimension = this.thinkingLevelToDimension(this.thinkingLevel);
774
+ const { model, selection } = await this.modelSelector.selectAndCreate({
775
+ taskDimension: dimension
776
+ });
777
+ return { model, providerName: selection.providerKey };
778
+ }
779
+ return {
780
+ model: this.provider.getModel(),
781
+ providerName: this.provider.name
782
+ };
783
+ }
784
+ thinkingLevelToDimension(level) {
785
+ if (!level || level === "instant")
786
+ return "latency";
787
+ return "reasoning";
788
+ }
156
789
  async send(options) {
157
790
  let conversation;
158
791
  if (options.conversationId) {
@@ -170,20 +803,25 @@ class ChatService {
170
803
  workspacePath: this.context?.workspacePath
171
804
  });
172
805
  }
173
- await this.store.appendMessage(conversation.id, {
174
- role: "user",
175
- content: options.content,
176
- status: "completed",
177
- attachments: options.attachments
178
- });
806
+ if (!options.skipUserAppend) {
807
+ await this.store.appendMessage(conversation.id, {
808
+ role: "user",
809
+ content: options.content,
810
+ status: "completed",
811
+ attachments: options.attachments
812
+ });
813
+ }
814
+ conversation = await this.store.get(conversation.id) ?? conversation;
179
815
  const messages = this.buildMessages(conversation, options);
180
- const model = this.provider.getModel();
816
+ const { model, providerName } = await this.resolveModel();
817
+ const providerOptions = getProviderOptions(this.thinkingLevel, providerName);
181
818
  try {
182
819
  const result = await generateText({
183
820
  model,
184
821
  messages,
185
822
  system: this.systemPrompt,
186
- tools: this.tools
823
+ tools: this.tools,
824
+ providerOptions: Object.keys(providerOptions).length > 0 ? providerOptions : undefined
187
825
  });
188
826
  const assistantMessage = await this.store.appendMessage(conversation.id, {
189
827
  role: "assistant",
@@ -228,23 +866,27 @@ class ChatService {
228
866
  workspacePath: this.context?.workspacePath
229
867
  });
230
868
  }
231
- await this.store.appendMessage(conversation.id, {
232
- role: "user",
233
- content: options.content,
234
- status: "completed",
235
- attachments: options.attachments
236
- });
869
+ if (!options.skipUserAppend) {
870
+ await this.store.appendMessage(conversation.id, {
871
+ role: "user",
872
+ content: options.content,
873
+ status: "completed",
874
+ attachments: options.attachments
875
+ });
876
+ }
877
+ conversation = await this.store.get(conversation.id) ?? conversation;
237
878
  const assistantMessage = await this.store.appendMessage(conversation.id, {
238
879
  role: "assistant",
239
880
  content: "",
240
881
  status: "streaming"
241
882
  });
242
883
  const messages = this.buildMessages(conversation, options);
243
- const model = this.provider.getModel();
884
+ const { model, providerName } = await this.resolveModel();
244
885
  const systemPrompt = this.systemPrompt;
245
886
  const tools = this.tools;
246
887
  const store = this.store;
247
888
  const onUsage = this.onUsage;
889
+ const streamProviderOptions = getProviderOptions(this.thinkingLevel, providerName);
248
890
  async function* streamGenerator() {
249
891
  let fullContent = "";
250
892
  let fullReasoning = "";
@@ -255,7 +897,8 @@ class ChatService {
255
897
  model,
256
898
  messages,
257
899
  system: systemPrompt,
258
- tools
900
+ tools,
901
+ providerOptions: Object.keys(streamProviderOptions).length > 0 ? streamProviderOptions : undefined
259
902
  });
260
903
  for await (const part of result.fullStream) {
261
904
  if (part.type === "text-delta") {
@@ -370,6 +1013,18 @@ class ChatService {
370
1013
  ...options
371
1014
  });
372
1015
  }
1016
+ async updateConversation(conversationId, updates) {
1017
+ return this.store.update(conversationId, updates);
1018
+ }
1019
+ async forkConversation(conversationId, upToMessageId) {
1020
+ return this.store.fork(conversationId, upToMessageId);
1021
+ }
1022
+ async updateMessage(conversationId, messageId, updates) {
1023
+ return this.store.updateMessage(conversationId, messageId, updates);
1024
+ }
1025
+ async truncateAfter(conversationId, messageId) {
1026
+ return this.store.truncateAfter(conversationId, messageId);
1027
+ }
373
1028
  async deleteConversation(conversationId) {
374
1029
  return this.store.delete(conversationId);
375
1030
  }
@@ -440,9 +1095,9 @@ import {
440
1095
  function toolsToToolSet(defs) {
441
1096
  const result = {};
442
1097
  for (const def of defs) {
443
- result[def.name] = tool({
1098
+ result[def.name] = tool4({
444
1099
  description: def.description ?? def.name,
445
- inputSchema: z.object({}).passthrough(),
1100
+ inputSchema: z4.object({}).passthrough(),
446
1101
  execute: async () => ({})
447
1102
  });
448
1103
  }
@@ -456,21 +1111,63 @@ function useChat(options = {}) {
456
1111
  apiKey,
457
1112
  proxyUrl,
458
1113
  conversationId: initialConversationId,
1114
+ store,
459
1115
  systemPrompt,
460
1116
  streaming = true,
461
1117
  onSend,
462
1118
  onResponse,
463
1119
  onError,
464
1120
  onUsage,
465
- tools: toolsDefs
1121
+ tools: toolsDefs,
1122
+ thinkingLevel,
1123
+ workflowToolsConfig,
1124
+ modelSelector,
1125
+ contractsContext,
1126
+ surfacePlanConfig,
1127
+ mcpServers,
1128
+ agentMode
466
1129
  } = options;
467
1130
  const [messages, setMessages] = React.useState([]);
1131
+ const [mcpTools, setMcpTools] = React.useState(null);
1132
+ const mcpCleanupRef = React.useRef(null);
468
1133
  const [conversation, setConversation] = React.useState(null);
469
1134
  const [isLoading, setIsLoading] = React.useState(false);
470
1135
  const [error, setError] = React.useState(null);
471
1136
  const [conversationId, setConversationId] = React.useState(initialConversationId ?? null);
472
1137
  const abortControllerRef = React.useRef(null);
473
1138
  const chatServiceRef = React.useRef(null);
1139
+ React.useEffect(() => {
1140
+ if (!mcpServers?.length) {
1141
+ setMcpTools(null);
1142
+ return;
1143
+ }
1144
+ let cancelled = false;
1145
+ import("@contractspec/lib.ai-agent/tools/mcp-client").then(({ createMcpToolsets }) => {
1146
+ createMcpToolsets(mcpServers).then(({ tools, cleanup }) => {
1147
+ if (!cancelled) {
1148
+ setMcpTools(tools);
1149
+ mcpCleanupRef.current = cleanup;
1150
+ } else {
1151
+ cleanup().catch(() => {
1152
+ return;
1153
+ });
1154
+ }
1155
+ }).catch(() => {
1156
+ if (!cancelled)
1157
+ setMcpTools(null);
1158
+ });
1159
+ });
1160
+ return () => {
1161
+ cancelled = true;
1162
+ const cleanup = mcpCleanupRef.current;
1163
+ mcpCleanupRef.current = null;
1164
+ if (cleanup)
1165
+ cleanup().catch(() => {
1166
+ return;
1167
+ });
1168
+ setMcpTools(null);
1169
+ };
1170
+ }, [mcpServers]);
474
1171
  React.useEffect(() => {
475
1172
  const chatProvider = createProvider({
476
1173
  provider,
@@ -480,9 +1177,16 @@ function useChat(options = {}) {
480
1177
  });
481
1178
  chatServiceRef.current = new ChatService({
482
1179
  provider: chatProvider,
1180
+ store,
483
1181
  systemPrompt,
484
1182
  onUsage,
485
- tools: toolsDefs?.length ? toolsToToolSet(toolsDefs) : undefined
1183
+ tools: toolsDefs?.length ? toolsToToolSet(toolsDefs) : undefined,
1184
+ thinkingLevel,
1185
+ workflowToolsConfig,
1186
+ modelSelector,
1187
+ contractsContext,
1188
+ surfacePlanConfig,
1189
+ mcpTools
486
1190
  });
487
1191
  }, [
488
1192
  provider,
@@ -490,9 +1194,16 @@ function useChat(options = {}) {
490
1194
  model,
491
1195
  apiKey,
492
1196
  proxyUrl,
1197
+ store,
493
1198
  systemPrompt,
494
1199
  onUsage,
495
- toolsDefs
1200
+ toolsDefs,
1201
+ thinkingLevel,
1202
+ workflowToolsConfig,
1203
+ modelSelector,
1204
+ contractsContext,
1205
+ surfacePlanConfig,
1206
+ mcpTools
496
1207
  ]);
497
1208
  React.useEffect(() => {
498
1209
  if (!conversationId || !chatServiceRef.current)
@@ -508,7 +1219,90 @@ function useChat(options = {}) {
508
1219
  };
509
1220
  loadConversation().catch(console.error);
510
1221
  }, [conversationId]);
511
- const sendMessage = React.useCallback(async (content, attachments) => {
1222
+ const sendMessage = React.useCallback(async (content, attachments, opts) => {
1223
+ if (agentMode?.agent) {
1224
+ setIsLoading(true);
1225
+ setError(null);
1226
+ abortControllerRef.current = new AbortController;
1227
+ try {
1228
+ if (!opts?.skipUserAppend) {
1229
+ const userMessage = {
1230
+ id: `msg_${Date.now()}`,
1231
+ conversationId: conversationId ?? "",
1232
+ role: "user",
1233
+ content,
1234
+ status: "completed",
1235
+ createdAt: new Date,
1236
+ updatedAt: new Date,
1237
+ attachments
1238
+ };
1239
+ setMessages((prev) => [...prev, userMessage]);
1240
+ onSend?.(userMessage);
1241
+ }
1242
+ const result = await agentMode.agent.generate({
1243
+ prompt: content,
1244
+ signal: abortControllerRef.current.signal
1245
+ });
1246
+ const toolCallsMap = new Map;
1247
+ for (const tc of result.toolCalls ?? []) {
1248
+ const tr = result.toolResults?.find((r) => r.toolCallId === tc.toolCallId);
1249
+ toolCallsMap.set(tc.toolCallId, {
1250
+ id: tc.toolCallId,
1251
+ name: tc.toolName,
1252
+ args: tc.args ?? {},
1253
+ result: tr?.output,
1254
+ status: "completed"
1255
+ });
1256
+ }
1257
+ const assistantMessage = {
1258
+ id: `msg_${Date.now()}_a`,
1259
+ conversationId: conversationId ?? "",
1260
+ role: "assistant",
1261
+ content: result.text,
1262
+ status: "completed",
1263
+ createdAt: new Date,
1264
+ updatedAt: new Date,
1265
+ toolCalls: toolCallsMap.size ? Array.from(toolCallsMap.values()) : undefined,
1266
+ usage: result.usage
1267
+ };
1268
+ setMessages((prev) => [...prev, assistantMessage]);
1269
+ onResponse?.(assistantMessage);
1270
+ onUsage?.(result.usage ?? { inputTokens: 0, outputTokens: 0 });
1271
+ if (store && !conversationId) {
1272
+ const conv = await store.create({
1273
+ status: "active",
1274
+ provider: "agent",
1275
+ model: "agent",
1276
+ messages: []
1277
+ });
1278
+ if (!opts?.skipUserAppend) {
1279
+ await store.appendMessage(conv.id, {
1280
+ role: "user",
1281
+ content,
1282
+ status: "completed",
1283
+ attachments
1284
+ });
1285
+ }
1286
+ await store.appendMessage(conv.id, {
1287
+ role: "assistant",
1288
+ content: result.text,
1289
+ status: "completed",
1290
+ toolCalls: assistantMessage.toolCalls,
1291
+ usage: result.usage
1292
+ });
1293
+ const updated = await store.get(conv.id);
1294
+ if (updated)
1295
+ setConversation(updated);
1296
+ setConversationId(conv.id);
1297
+ }
1298
+ } catch (err) {
1299
+ setError(err instanceof Error ? err : new Error(String(err)));
1300
+ onError?.(err instanceof Error ? err : new Error(String(err)));
1301
+ } finally {
1302
+ setIsLoading(false);
1303
+ }
1304
+ return;
1305
+ }
512
1306
  if (!chatServiceRef.current) {
513
1307
  throw new Error("Chat service not initialized");
514
1308
  }
@@ -516,25 +1310,28 @@ function useChat(options = {}) {
516
1310
  setError(null);
517
1311
  abortControllerRef.current = new AbortController;
518
1312
  try {
519
- const userMessage = {
520
- id: `msg_${Date.now()}`,
521
- conversationId: conversationId ?? "",
522
- role: "user",
523
- content,
524
- status: "completed",
525
- createdAt: new Date,
526
- updatedAt: new Date,
527
- attachments
528
- };
529
- setMessages((prev) => [...prev, userMessage]);
530
- onSend?.(userMessage);
1313
+ if (!opts?.skipUserAppend) {
1314
+ const userMessage = {
1315
+ id: `msg_${Date.now()}`,
1316
+ conversationId: conversationId ?? "",
1317
+ role: "user",
1318
+ content,
1319
+ status: "completed",
1320
+ createdAt: new Date,
1321
+ updatedAt: new Date,
1322
+ attachments
1323
+ };
1324
+ setMessages((prev) => [...prev, userMessage]);
1325
+ onSend?.(userMessage);
1326
+ }
531
1327
  if (streaming) {
532
1328
  const result = await chatServiceRef.current.stream({
533
1329
  conversationId: conversationId ?? undefined,
534
1330
  content,
535
- attachments
1331
+ attachments,
1332
+ skipUserAppend: opts?.skipUserAppend
536
1333
  });
537
- if (!conversationId) {
1334
+ if (!conversationId && !opts?.skipUserAppend) {
538
1335
  setConversationId(result.conversationId);
539
1336
  }
540
1337
  const assistantMessage = {
@@ -615,7 +1412,8 @@ function useChat(options = {}) {
615
1412
  const result = await chatServiceRef.current.send({
616
1413
  conversationId: conversationId ?? undefined,
617
1414
  content,
618
- attachments
1415
+ attachments,
1416
+ skipUserAppend: opts?.skipUserAppend
619
1417
  });
620
1418
  setConversation(result.conversation);
621
1419
  setMessages(result.conversation.messages);
@@ -632,7 +1430,17 @@ function useChat(options = {}) {
632
1430
  setIsLoading(false);
633
1431
  abortControllerRef.current = null;
634
1432
  }
635
- }, [conversationId, streaming, onSend, onResponse, onError, messages]);
1433
+ }, [
1434
+ conversationId,
1435
+ streaming,
1436
+ onSend,
1437
+ onResponse,
1438
+ onError,
1439
+ onUsage,
1440
+ messages,
1441
+ agentMode,
1442
+ store
1443
+ ]);
636
1444
  const clearConversation = React.useCallback(() => {
637
1445
  setMessages([]);
638
1446
  setConversation(null);
@@ -653,6 +1461,46 @@ function useChat(options = {}) {
653
1461
  abortControllerRef.current?.abort();
654
1462
  setIsLoading(false);
655
1463
  }, []);
1464
+ const createNewConversation = clearConversation;
1465
+ const editMessage = React.useCallback(async (messageId, newContent) => {
1466
+ if (!chatServiceRef.current || !conversationId)
1467
+ return;
1468
+ const msg = messages.find((m) => m.id === messageId);
1469
+ if (!msg || msg.role !== "user")
1470
+ return;
1471
+ await chatServiceRef.current.updateMessage(conversationId, messageId, {
1472
+ content: newContent
1473
+ });
1474
+ const truncated = await chatServiceRef.current.truncateAfter(conversationId, messageId);
1475
+ if (truncated) {
1476
+ setMessages(truncated.messages);
1477
+ }
1478
+ await sendMessage(newContent, undefined, { skipUserAppend: true });
1479
+ }, [conversationId, messages, sendMessage]);
1480
+ const forkConversation = React.useCallback(async (upToMessageId) => {
1481
+ if (!chatServiceRef.current)
1482
+ return null;
1483
+ const idToFork = conversationId ?? conversation?.id;
1484
+ if (!idToFork)
1485
+ return null;
1486
+ try {
1487
+ const forked = await chatServiceRef.current.forkConversation(idToFork, upToMessageId);
1488
+ setConversationId(forked.id);
1489
+ setConversation(forked);
1490
+ setMessages(forked.messages);
1491
+ return forked.id;
1492
+ } catch {
1493
+ return null;
1494
+ }
1495
+ }, [conversationId, conversation]);
1496
+ const updateConversationFn = React.useCallback(async (updates) => {
1497
+ if (!chatServiceRef.current || !conversationId)
1498
+ return null;
1499
+ const updated = await chatServiceRef.current.updateConversation(conversationId, updates);
1500
+ if (updated)
1501
+ setConversation(updated);
1502
+ return updated;
1503
+ }, [conversationId]);
656
1504
  const addToolApprovalResponse = React.useCallback((_toolCallId, _result) => {
657
1505
  throw new Error(`addToolApprovalResponse: Tool approval requires server route with toUIMessageStreamResponse. ` + `Use createChatRoute and @ai-sdk/react useChat for tools with requireApproval.`);
658
1506
  }, []);
@@ -667,6 +1515,10 @@ function useChat(options = {}) {
667
1515
  setConversationId,
668
1516
  regenerate,
669
1517
  stop,
1518
+ createNewConversation,
1519
+ editMessage,
1520
+ forkConversation,
1521
+ updateConversation: updateConversationFn,
670
1522
  ...hasApprovalTools && { addToolApprovalResponse }
671
1523
  };
672
1524
  }
@@ -710,11 +1562,94 @@ function useProviders() {
710
1562
  refresh: loadProviders
711
1563
  };
712
1564
  }
1565
+ // src/presentation/hooks/useMessageSelection.ts
1566
+ import * as React3 from "react";
1567
+ "use client";
1568
+ function useMessageSelection(messageIds) {
1569
+ const [selectedIds, setSelectedIds] = React3.useState(() => new Set);
1570
+ const idSet = React3.useMemo(() => new Set(messageIds), [messageIds.join(",")]);
1571
+ React3.useEffect(() => {
1572
+ setSelectedIds((prev) => {
1573
+ const next = new Set;
1574
+ for (const id of prev) {
1575
+ if (idSet.has(id))
1576
+ next.add(id);
1577
+ }
1578
+ return next.size === prev.size ? prev : next;
1579
+ });
1580
+ }, [idSet]);
1581
+ const toggle = React3.useCallback((id) => {
1582
+ setSelectedIds((prev) => {
1583
+ const next = new Set(prev);
1584
+ if (next.has(id))
1585
+ next.delete(id);
1586
+ else
1587
+ next.add(id);
1588
+ return next;
1589
+ });
1590
+ }, []);
1591
+ const selectAll = React3.useCallback(() => {
1592
+ setSelectedIds(new Set(messageIds));
1593
+ }, [messageIds.join(",")]);
1594
+ const clearSelection = React3.useCallback(() => {
1595
+ setSelectedIds(new Set);
1596
+ }, []);
1597
+ const isSelected = React3.useCallback((id) => selectedIds.has(id), [selectedIds]);
1598
+ const selectedCount = selectedIds.size;
1599
+ return {
1600
+ selectedIds,
1601
+ toggle,
1602
+ selectAll,
1603
+ clearSelection,
1604
+ isSelected,
1605
+ selectedCount
1606
+ };
1607
+ }
1608
+ // src/presentation/hooks/useConversations.ts
1609
+ import * as React4 from "react";
1610
+ "use client";
1611
+ function useConversations(options) {
1612
+ const { store, projectId, tags, limit = 50 } = options;
1613
+ const [conversations, setConversations] = React4.useState([]);
1614
+ const [isLoading, setIsLoading] = React4.useState(true);
1615
+ const refresh = React4.useCallback(async () => {
1616
+ setIsLoading(true);
1617
+ try {
1618
+ const list = await store.list({
1619
+ status: "active",
1620
+ projectId,
1621
+ tags,
1622
+ limit
1623
+ });
1624
+ setConversations(list);
1625
+ } finally {
1626
+ setIsLoading(false);
1627
+ }
1628
+ }, [store, projectId, tags, limit]);
1629
+ React4.useEffect(() => {
1630
+ refresh();
1631
+ }, [refresh]);
1632
+ const deleteConversation = React4.useCallback(async (id) => {
1633
+ const ok = await store.delete(id);
1634
+ if (ok) {
1635
+ setConversations((prev) => prev.filter((c) => c.id !== id));
1636
+ }
1637
+ return ok;
1638
+ }, [store]);
1639
+ return {
1640
+ conversations,
1641
+ isLoading,
1642
+ refresh,
1643
+ deleteConversation
1644
+ };
1645
+ }
713
1646
 
714
1647
  // src/presentation/hooks/index.ts
715
1648
  import { useCompletion } from "@ai-sdk/react";
716
1649
  export {
717
1650
  useProviders,
1651
+ useMessageSelection,
1652
+ useConversations,
718
1653
  useCompletion,
719
1654
  useChat
720
1655
  };