@contractspec/module.ai-chat 4.0.2 → 4.1.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 (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 +1138 -21
  5. package/dist/browser/index.js +2816 -651
  6. package/dist/browser/presentation/components/index.js +3143 -358
  7. package/dist/browser/presentation/hooks/index.js +961 -43
  8. package/dist/browser/presentation/index.js +2784 -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 +1138 -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 +2816 -651
  30. package/dist/node/core/index.js +1138 -21
  31. package/dist/node/index.js +2816 -651
  32. package/dist/node/presentation/components/index.js +3143 -358
  33. package/dist/node/presentation/hooks/index.js +961 -43
  34. package/dist/node/presentation/index.js +2787 -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 +3143 -358
  45. package/dist/presentation/hooks/index.d.ts +2 -0
  46. package/dist/presentation/hooks/index.js +961 -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 +2787 -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,504 @@ 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 { validatePatchProposal } from "@contractspec/lib.surface-runtime/spec/validate-surface-patch";
526
+ import { buildSurfacePatchProposal } from "@contractspec/lib.surface-runtime/runtime/planner-tools";
527
+ var VALID_OPS = [
528
+ "insert-node",
529
+ "replace-node",
530
+ "remove-node",
531
+ "move-node",
532
+ "resize-panel",
533
+ "set-layout",
534
+ "reveal-field",
535
+ "hide-field",
536
+ "promote-action",
537
+ "set-focus"
538
+ ];
539
+ var DEFAULT_NODE_KINDS = [
540
+ "entity-section",
541
+ "entity-card",
542
+ "data-view",
543
+ "assistant-panel",
544
+ "chat-thread",
545
+ "action-bar",
546
+ "timeline",
547
+ "table",
548
+ "rich-doc",
549
+ "form",
550
+ "chart",
551
+ "custom-widget"
552
+ ];
553
+ function collectSlotIdsFromRegion(node) {
554
+ const ids = [];
555
+ if (node.type === "slot") {
556
+ ids.push(node.slotId);
557
+ }
558
+ if (node.type === "panel-group" || node.type === "stack") {
559
+ for (const child of node.children) {
560
+ ids.push(...collectSlotIdsFromRegion(child));
561
+ }
562
+ }
563
+ if (node.type === "tabs") {
564
+ for (const tab of node.tabs) {
565
+ ids.push(...collectSlotIdsFromRegion(tab.child));
566
+ }
567
+ }
568
+ if (node.type === "floating") {
569
+ ids.push(node.anchorSlotId);
570
+ ids.push(...collectSlotIdsFromRegion(node.child));
571
+ }
572
+ return ids;
573
+ }
574
+ function deriveConstraints(plan) {
575
+ const slotIds = collectSlotIdsFromRegion(plan.layoutRoot);
576
+ const uniqueSlots = [...new Set(slotIds)];
577
+ return {
578
+ allowedOps: VALID_OPS,
579
+ allowedSlots: uniqueSlots.length > 0 ? uniqueSlots : ["assistant", "primary"],
580
+ allowedNodeKinds: DEFAULT_NODE_KINDS
581
+ };
582
+ }
583
+ var ProposePatchInputSchema = z3.object({
584
+ proposalId: z3.string().describe("Unique proposal identifier"),
585
+ ops: z3.array(z3.object({
586
+ op: z3.enum([
587
+ "insert-node",
588
+ "replace-node",
589
+ "remove-node",
590
+ "move-node",
591
+ "resize-panel",
592
+ "set-layout",
593
+ "reveal-field",
594
+ "hide-field",
595
+ "promote-action",
596
+ "set-focus"
597
+ ]),
598
+ slotId: z3.string().optional(),
599
+ nodeId: z3.string().optional(),
600
+ toSlotId: z3.string().optional(),
601
+ index: z3.number().optional(),
602
+ node: z3.object({
603
+ nodeId: z3.string(),
604
+ kind: z3.string(),
605
+ title: z3.string().optional(),
606
+ props: z3.record(z3.string(), z3.unknown()).optional(),
607
+ children: z3.array(z3.unknown()).optional()
608
+ }).optional(),
609
+ persistKey: z3.string().optional(),
610
+ sizes: z3.array(z3.number()).optional(),
611
+ layoutId: z3.string().optional(),
612
+ fieldId: z3.string().optional(),
613
+ actionId: z3.string().optional(),
614
+ placement: z3.enum(["header", "inline", "context", "assistant"]).optional(),
615
+ targetId: z3.string().optional()
616
+ }))
617
+ });
618
+ function createSurfacePlannerTools(config) {
619
+ const { plan, constraints, onPatchProposal } = config;
620
+ const resolvedConstraints = constraints ?? deriveConstraints(plan);
621
+ const proposePatchTool = tool3({
622
+ description: "Propose surface patches (layout changes, node insertions, etc.) for user approval. " + "Only use allowed ops, slots, and node kinds from the planner context.",
623
+ inputSchema: ProposePatchInputSchema,
624
+ execute: async (input) => {
625
+ const ops = input.ops;
626
+ try {
627
+ validatePatchProposal(ops, resolvedConstraints);
628
+ const proposal = buildSurfacePatchProposal(input.proposalId, ops);
629
+ onPatchProposal?.(proposal);
630
+ return {
631
+ success: true,
632
+ proposalId: proposal.proposalId,
633
+ opsCount: proposal.ops.length,
634
+ message: "Patch proposal validated; awaiting user approval"
635
+ };
636
+ } catch (err) {
637
+ return {
638
+ success: false,
639
+ error: err instanceof Error ? err.message : String(err),
640
+ proposalId: input.proposalId
641
+ };
642
+ }
643
+ }
644
+ });
645
+ return {
646
+ "propose-patch": proposePatchTool
647
+ };
648
+ }
649
+ function buildPlannerPromptInput(plan) {
650
+ const constraints = deriveConstraints(plan);
651
+ return {
652
+ bundleMeta: {
653
+ key: plan.bundleKey,
654
+ version: "0.0.0",
655
+ title: plan.bundleKey
656
+ },
657
+ surfaceId: plan.surfaceId,
658
+ allowedPatchOps: constraints.allowedOps,
659
+ allowedSlots: [...constraints.allowedSlots],
660
+ allowedNodeKinds: [...constraints.allowedNodeKinds],
661
+ actions: plan.actions.map((a) => ({ actionId: a.actionId, title: a.title })),
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
+
124
674
  // src/core/chat-service.ts
675
+ import { compilePlannerPrompt } from "@contractspec/lib.surface-runtime/runtime/planner-prompt";
125
676
  var DEFAULT_SYSTEM_PROMPT = `You are ContractSpec AI, an expert coding assistant specialized in ContractSpec development.
126
677
 
127
678
  Your capabilities:
@@ -136,6 +687,9 @@ Guidelines:
136
687
  - Reference relevant ContractSpec concepts and patterns
137
688
  - Ask clarifying questions when the user's intent is unclear
138
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.`;
139
693
 
140
694
  class ChatService {
141
695
  provider;
@@ -145,19 +699,93 @@ class ChatService {
145
699
  maxHistoryMessages;
146
700
  onUsage;
147
701
  tools;
702
+ thinkingLevel;
148
703
  sendReasoning;
149
704
  sendSources;
705
+ modelSelector;
150
706
  constructor(config) {
151
707
  this.provider = config.provider;
152
708
  this.context = config.context;
153
709
  this.store = config.store ?? new InMemoryConversationStore;
154
- this.systemPrompt = config.systemPrompt ?? DEFAULT_SYSTEM_PROMPT;
710
+ this.systemPrompt = this.buildSystemPrompt(config);
155
711
  this.maxHistoryMessages = config.maxHistoryMessages ?? 20;
156
712
  this.onUsage = config.onUsage;
157
- this.tools = config.tools;
158
- 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");
159
717
  this.sendSources = config.sendSources ?? false;
160
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
+ }
161
789
  async send(options) {
162
790
  let conversation;
163
791
  if (options.conversationId) {
@@ -175,20 +803,25 @@ class ChatService {
175
803
  workspacePath: this.context?.workspacePath
176
804
  });
177
805
  }
178
- await this.store.appendMessage(conversation.id, {
179
- role: "user",
180
- content: options.content,
181
- status: "completed",
182
- attachments: options.attachments
183
- });
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;
184
815
  const messages = this.buildMessages(conversation, options);
185
- const model = this.provider.getModel();
816
+ const { model, providerName } = await this.resolveModel();
817
+ const providerOptions = getProviderOptions(this.thinkingLevel, providerName);
186
818
  try {
187
819
  const result = await generateText({
188
820
  model,
189
821
  messages,
190
822
  system: this.systemPrompt,
191
- tools: this.tools
823
+ tools: this.tools,
824
+ providerOptions: Object.keys(providerOptions).length > 0 ? providerOptions : undefined
192
825
  });
193
826
  const assistantMessage = await this.store.appendMessage(conversation.id, {
194
827
  role: "assistant",
@@ -233,23 +866,27 @@ class ChatService {
233
866
  workspacePath: this.context?.workspacePath
234
867
  });
235
868
  }
236
- await this.store.appendMessage(conversation.id, {
237
- role: "user",
238
- content: options.content,
239
- status: "completed",
240
- attachments: options.attachments
241
- });
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;
242
878
  const assistantMessage = await this.store.appendMessage(conversation.id, {
243
879
  role: "assistant",
244
880
  content: "",
245
881
  status: "streaming"
246
882
  });
247
883
  const messages = this.buildMessages(conversation, options);
248
- const model = this.provider.getModel();
884
+ const { model, providerName } = await this.resolveModel();
249
885
  const systemPrompt = this.systemPrompt;
250
886
  const tools = this.tools;
251
887
  const store = this.store;
252
888
  const onUsage = this.onUsage;
889
+ const streamProviderOptions = getProviderOptions(this.thinkingLevel, providerName);
253
890
  async function* streamGenerator() {
254
891
  let fullContent = "";
255
892
  let fullReasoning = "";
@@ -260,7 +897,8 @@ class ChatService {
260
897
  model,
261
898
  messages,
262
899
  system: systemPrompt,
263
- tools
900
+ tools,
901
+ providerOptions: Object.keys(streamProviderOptions).length > 0 ? streamProviderOptions : undefined
264
902
  });
265
903
  for await (const part of result.fullStream) {
266
904
  if (part.type === "text-delta") {
@@ -375,6 +1013,18 @@ class ChatService {
375
1013
  ...options
376
1014
  });
377
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
+ }
378
1028
  async deleteConversation(conversationId) {
379
1029
  return this.store.delete(conversationId);
380
1030
  }
@@ -445,9 +1095,9 @@ import {
445
1095
  function toolsToToolSet(defs) {
446
1096
  const result = {};
447
1097
  for (const def of defs) {
448
- result[def.name] = tool({
1098
+ result[def.name] = tool4({
449
1099
  description: def.description ?? def.name,
450
- inputSchema: z.object({}).passthrough(),
1100
+ inputSchema: z4.object({}).passthrough(),
451
1101
  execute: async () => ({})
452
1102
  });
453
1103
  }
@@ -461,21 +1111,63 @@ function useChat(options = {}) {
461
1111
  apiKey,
462
1112
  proxyUrl,
463
1113
  conversationId: initialConversationId,
1114
+ store,
464
1115
  systemPrompt,
465
1116
  streaming = true,
466
1117
  onSend,
467
1118
  onResponse,
468
1119
  onError,
469
1120
  onUsage,
470
- tools: toolsDefs
1121
+ tools: toolsDefs,
1122
+ thinkingLevel,
1123
+ workflowToolsConfig,
1124
+ modelSelector,
1125
+ contractsContext,
1126
+ surfacePlanConfig,
1127
+ mcpServers,
1128
+ agentMode
471
1129
  } = options;
472
1130
  const [messages, setMessages] = React.useState([]);
1131
+ const [mcpTools, setMcpTools] = React.useState(null);
1132
+ const mcpCleanupRef = React.useRef(null);
473
1133
  const [conversation, setConversation] = React.useState(null);
474
1134
  const [isLoading, setIsLoading] = React.useState(false);
475
1135
  const [error, setError] = React.useState(null);
476
1136
  const [conversationId, setConversationId] = React.useState(initialConversationId ?? null);
477
1137
  const abortControllerRef = React.useRef(null);
478
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]);
479
1171
  React.useEffect(() => {
480
1172
  const chatProvider = createProvider({
481
1173
  provider,
@@ -485,9 +1177,16 @@ function useChat(options = {}) {
485
1177
  });
486
1178
  chatServiceRef.current = new ChatService({
487
1179
  provider: chatProvider,
1180
+ store,
488
1181
  systemPrompt,
489
1182
  onUsage,
490
- 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
491
1190
  });
492
1191
  }, [
493
1192
  provider,
@@ -495,9 +1194,16 @@ function useChat(options = {}) {
495
1194
  model,
496
1195
  apiKey,
497
1196
  proxyUrl,
1197
+ store,
498
1198
  systemPrompt,
499
1199
  onUsage,
500
- toolsDefs
1200
+ toolsDefs,
1201
+ thinkingLevel,
1202
+ workflowToolsConfig,
1203
+ modelSelector,
1204
+ contractsContext,
1205
+ surfacePlanConfig,
1206
+ mcpTools
501
1207
  ]);
502
1208
  React.useEffect(() => {
503
1209
  if (!conversationId || !chatServiceRef.current)
@@ -513,7 +1219,90 @@ function useChat(options = {}) {
513
1219
  };
514
1220
  loadConversation().catch(console.error);
515
1221
  }, [conversationId]);
516
- 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
+ }
517
1306
  if (!chatServiceRef.current) {
518
1307
  throw new Error("Chat service not initialized");
519
1308
  }
@@ -521,25 +1310,28 @@ function useChat(options = {}) {
521
1310
  setError(null);
522
1311
  abortControllerRef.current = new AbortController;
523
1312
  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);
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
+ }
536
1327
  if (streaming) {
537
1328
  const result = await chatServiceRef.current.stream({
538
1329
  conversationId: conversationId ?? undefined,
539
1330
  content,
540
- attachments
1331
+ attachments,
1332
+ skipUserAppend: opts?.skipUserAppend
541
1333
  });
542
- if (!conversationId) {
1334
+ if (!conversationId && !opts?.skipUserAppend) {
543
1335
  setConversationId(result.conversationId);
544
1336
  }
545
1337
  const assistantMessage = {
@@ -620,7 +1412,8 @@ function useChat(options = {}) {
620
1412
  const result = await chatServiceRef.current.send({
621
1413
  conversationId: conversationId ?? undefined,
622
1414
  content,
623
- attachments
1415
+ attachments,
1416
+ skipUserAppend: opts?.skipUserAppend
624
1417
  });
625
1418
  setConversation(result.conversation);
626
1419
  setMessages(result.conversation.messages);
@@ -637,7 +1430,7 @@ function useChat(options = {}) {
637
1430
  setIsLoading(false);
638
1431
  abortControllerRef.current = null;
639
1432
  }
640
- }, [conversationId, streaming, onSend, onResponse, onError, messages]);
1433
+ }, [conversationId, streaming, onSend, onResponse, onError, onUsage, messages, agentMode, store]);
641
1434
  const clearConversation = React.useCallback(() => {
642
1435
  setMessages([]);
643
1436
  setConversation(null);
@@ -658,6 +1451,44 @@ function useChat(options = {}) {
658
1451
  abortControllerRef.current?.abort();
659
1452
  setIsLoading(false);
660
1453
  }, []);
1454
+ const createNewConversation = clearConversation;
1455
+ const editMessage = React.useCallback(async (messageId, newContent) => {
1456
+ if (!chatServiceRef.current || !conversationId)
1457
+ return;
1458
+ const msg = messages.find((m) => m.id === messageId);
1459
+ if (!msg || msg.role !== "user")
1460
+ return;
1461
+ await chatServiceRef.current.updateMessage(conversationId, messageId, { content: newContent });
1462
+ const truncated = await chatServiceRef.current.truncateAfter(conversationId, messageId);
1463
+ if (truncated) {
1464
+ setMessages(truncated.messages);
1465
+ }
1466
+ await sendMessage(newContent, undefined, { skipUserAppend: true });
1467
+ }, [conversationId, messages, sendMessage]);
1468
+ const forkConversation = React.useCallback(async (upToMessageId) => {
1469
+ if (!chatServiceRef.current)
1470
+ return null;
1471
+ const idToFork = conversationId ?? conversation?.id;
1472
+ if (!idToFork)
1473
+ return null;
1474
+ try {
1475
+ const forked = await chatServiceRef.current.forkConversation(idToFork, upToMessageId);
1476
+ setConversationId(forked.id);
1477
+ setConversation(forked);
1478
+ setMessages(forked.messages);
1479
+ return forked.id;
1480
+ } catch {
1481
+ return null;
1482
+ }
1483
+ }, [conversationId, conversation]);
1484
+ const updateConversationFn = React.useCallback(async (updates) => {
1485
+ if (!chatServiceRef.current || !conversationId)
1486
+ return null;
1487
+ const updated = await chatServiceRef.current.updateConversation(conversationId, updates);
1488
+ if (updated)
1489
+ setConversation(updated);
1490
+ return updated;
1491
+ }, [conversationId]);
661
1492
  const addToolApprovalResponse = React.useCallback((_toolCallId, _result) => {
662
1493
  throw new Error(`addToolApprovalResponse: Tool approval requires server route with toUIMessageStreamResponse. ` + `Use createChatRoute and @ai-sdk/react useChat for tools with requireApproval.`);
663
1494
  }, []);
@@ -672,6 +1503,10 @@ function useChat(options = {}) {
672
1503
  setConversationId,
673
1504
  regenerate,
674
1505
  stop,
1506
+ createNewConversation,
1507
+ editMessage,
1508
+ forkConversation,
1509
+ updateConversation: updateConversationFn,
675
1510
  ...hasApprovalTools && { addToolApprovalResponse }
676
1511
  };
677
1512
  }
@@ -715,11 +1550,94 @@ function useProviders() {
715
1550
  refresh: loadProviders
716
1551
  };
717
1552
  }
1553
+ // src/presentation/hooks/useMessageSelection.ts
1554
+ import * as React3 from "react";
1555
+ "use client";
1556
+ function useMessageSelection(messageIds) {
1557
+ const [selectedIds, setSelectedIds] = React3.useState(() => new Set);
1558
+ const idSet = React3.useMemo(() => new Set(messageIds), [messageIds.join(",")]);
1559
+ React3.useEffect(() => {
1560
+ setSelectedIds((prev) => {
1561
+ const next = new Set;
1562
+ for (const id of prev) {
1563
+ if (idSet.has(id))
1564
+ next.add(id);
1565
+ }
1566
+ return next.size === prev.size ? prev : next;
1567
+ });
1568
+ }, [idSet]);
1569
+ const toggle = React3.useCallback((id) => {
1570
+ setSelectedIds((prev) => {
1571
+ const next = new Set(prev);
1572
+ if (next.has(id))
1573
+ next.delete(id);
1574
+ else
1575
+ next.add(id);
1576
+ return next;
1577
+ });
1578
+ }, []);
1579
+ const selectAll = React3.useCallback(() => {
1580
+ setSelectedIds(new Set(messageIds));
1581
+ }, [messageIds.join(",")]);
1582
+ const clearSelection = React3.useCallback(() => {
1583
+ setSelectedIds(new Set);
1584
+ }, []);
1585
+ const isSelected = React3.useCallback((id) => selectedIds.has(id), [selectedIds]);
1586
+ const selectedCount = selectedIds.size;
1587
+ return {
1588
+ selectedIds,
1589
+ toggle,
1590
+ selectAll,
1591
+ clearSelection,
1592
+ isSelected,
1593
+ selectedCount
1594
+ };
1595
+ }
1596
+ // src/presentation/hooks/useConversations.ts
1597
+ import * as React4 from "react";
1598
+ "use client";
1599
+ function useConversations(options) {
1600
+ const { store, projectId, tags, limit = 50 } = options;
1601
+ const [conversations, setConversations] = React4.useState([]);
1602
+ const [isLoading, setIsLoading] = React4.useState(true);
1603
+ const refresh = React4.useCallback(async () => {
1604
+ setIsLoading(true);
1605
+ try {
1606
+ const list = await store.list({
1607
+ status: "active",
1608
+ projectId,
1609
+ tags,
1610
+ limit
1611
+ });
1612
+ setConversations(list);
1613
+ } finally {
1614
+ setIsLoading(false);
1615
+ }
1616
+ }, [store, projectId, tags, limit]);
1617
+ React4.useEffect(() => {
1618
+ refresh();
1619
+ }, [refresh]);
1620
+ const deleteConversation = React4.useCallback(async (id) => {
1621
+ const ok = await store.delete(id);
1622
+ if (ok) {
1623
+ setConversations((prev) => prev.filter((c) => c.id !== id));
1624
+ }
1625
+ return ok;
1626
+ }, [store]);
1627
+ return {
1628
+ conversations,
1629
+ isLoading,
1630
+ refresh,
1631
+ deleteConversation
1632
+ };
1633
+ }
718
1634
 
719
1635
  // src/presentation/hooks/index.ts
720
1636
  import { useCompletion } from "@ai-sdk/react";
721
1637
  export {
722
1638
  useProviders,
1639
+ useMessageSelection,
1640
+ useConversations,
723
1641
  useCompletion,
724
1642
  useChat
725
1643
  };