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