@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
@@ -3,8 +3,8 @@ var __require = /* @__PURE__ */ createRequire(import.meta.url);
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,504 @@ 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 { validatePatchProposal } from "@contractspec/lib.surface-runtime/spec/validate-surface-patch";
521
+ import { buildSurfacePatchProposal } from "@contractspec/lib.surface-runtime/runtime/planner-tools";
522
+ var VALID_OPS = [
523
+ "insert-node",
524
+ "replace-node",
525
+ "remove-node",
526
+ "move-node",
527
+ "resize-panel",
528
+ "set-layout",
529
+ "reveal-field",
530
+ "hide-field",
531
+ "promote-action",
532
+ "set-focus"
533
+ ];
534
+ var DEFAULT_NODE_KINDS = [
535
+ "entity-section",
536
+ "entity-card",
537
+ "data-view",
538
+ "assistant-panel",
539
+ "chat-thread",
540
+ "action-bar",
541
+ "timeline",
542
+ "table",
543
+ "rich-doc",
544
+ "form",
545
+ "chart",
546
+ "custom-widget"
547
+ ];
548
+ function collectSlotIdsFromRegion(node) {
549
+ const ids = [];
550
+ if (node.type === "slot") {
551
+ ids.push(node.slotId);
552
+ }
553
+ if (node.type === "panel-group" || node.type === "stack") {
554
+ for (const child of node.children) {
555
+ ids.push(...collectSlotIdsFromRegion(child));
556
+ }
557
+ }
558
+ if (node.type === "tabs") {
559
+ for (const tab of node.tabs) {
560
+ ids.push(...collectSlotIdsFromRegion(tab.child));
561
+ }
562
+ }
563
+ if (node.type === "floating") {
564
+ ids.push(node.anchorSlotId);
565
+ ids.push(...collectSlotIdsFromRegion(node.child));
566
+ }
567
+ return ids;
568
+ }
569
+ function deriveConstraints(plan) {
570
+ const slotIds = collectSlotIdsFromRegion(plan.layoutRoot);
571
+ const uniqueSlots = [...new Set(slotIds)];
572
+ return {
573
+ allowedOps: VALID_OPS,
574
+ allowedSlots: uniqueSlots.length > 0 ? uniqueSlots : ["assistant", "primary"],
575
+ allowedNodeKinds: DEFAULT_NODE_KINDS
576
+ };
577
+ }
578
+ var ProposePatchInputSchema = z3.object({
579
+ proposalId: z3.string().describe("Unique proposal identifier"),
580
+ ops: z3.array(z3.object({
581
+ op: z3.enum([
582
+ "insert-node",
583
+ "replace-node",
584
+ "remove-node",
585
+ "move-node",
586
+ "resize-panel",
587
+ "set-layout",
588
+ "reveal-field",
589
+ "hide-field",
590
+ "promote-action",
591
+ "set-focus"
592
+ ]),
593
+ slotId: z3.string().optional(),
594
+ nodeId: z3.string().optional(),
595
+ toSlotId: z3.string().optional(),
596
+ index: z3.number().optional(),
597
+ node: z3.object({
598
+ nodeId: z3.string(),
599
+ kind: z3.string(),
600
+ title: z3.string().optional(),
601
+ props: z3.record(z3.string(), z3.unknown()).optional(),
602
+ children: z3.array(z3.unknown()).optional()
603
+ }).optional(),
604
+ persistKey: z3.string().optional(),
605
+ sizes: z3.array(z3.number()).optional(),
606
+ layoutId: z3.string().optional(),
607
+ fieldId: z3.string().optional(),
608
+ actionId: z3.string().optional(),
609
+ placement: z3.enum(["header", "inline", "context", "assistant"]).optional(),
610
+ targetId: z3.string().optional()
611
+ }))
612
+ });
613
+ function createSurfacePlannerTools(config) {
614
+ const { plan, constraints, onPatchProposal } = config;
615
+ const resolvedConstraints = constraints ?? deriveConstraints(plan);
616
+ const proposePatchTool = tool3({
617
+ description: "Propose surface patches (layout changes, node insertions, etc.) for user approval. " + "Only use allowed ops, slots, and node kinds from the planner context.",
618
+ inputSchema: ProposePatchInputSchema,
619
+ execute: async (input) => {
620
+ const ops = input.ops;
621
+ try {
622
+ validatePatchProposal(ops, resolvedConstraints);
623
+ const proposal = buildSurfacePatchProposal(input.proposalId, ops);
624
+ onPatchProposal?.(proposal);
625
+ return {
626
+ success: true,
627
+ proposalId: proposal.proposalId,
628
+ opsCount: proposal.ops.length,
629
+ message: "Patch proposal validated; awaiting user approval"
630
+ };
631
+ } catch (err) {
632
+ return {
633
+ success: false,
634
+ error: err instanceof Error ? err.message : String(err),
635
+ proposalId: input.proposalId
636
+ };
637
+ }
638
+ }
639
+ });
640
+ return {
641
+ "propose-patch": proposePatchTool
642
+ };
643
+ }
644
+ function buildPlannerPromptInput(plan) {
645
+ const constraints = deriveConstraints(plan);
646
+ return {
647
+ bundleMeta: {
648
+ key: plan.bundleKey,
649
+ version: "0.0.0",
650
+ title: plan.bundleKey
651
+ },
652
+ surfaceId: plan.surfaceId,
653
+ allowedPatchOps: constraints.allowedOps,
654
+ allowedSlots: [...constraints.allowedSlots],
655
+ allowedNodeKinds: [...constraints.allowedNodeKinds],
656
+ actions: plan.actions.map((a) => ({ actionId: a.actionId, title: a.title })),
657
+ preferences: {
658
+ guidance: "hints",
659
+ density: "standard",
660
+ dataDepth: "detailed",
661
+ control: "standard",
662
+ media: "text",
663
+ pace: "balanced",
664
+ narrative: "top-down"
665
+ }
666
+ };
667
+ }
668
+
119
669
  // src/core/chat-service.ts
670
+ import { compilePlannerPrompt } from "@contractspec/lib.surface-runtime/runtime/planner-prompt";
120
671
  var DEFAULT_SYSTEM_PROMPT = `You are ContractSpec AI, an expert coding assistant specialized in ContractSpec development.
121
672
 
122
673
  Your capabilities:
@@ -131,6 +682,9 @@ Guidelines:
131
682
  - Reference relevant ContractSpec concepts and patterns
132
683
  - Ask clarifying questions when the user's intent is unclear
133
684
  - When suggesting code changes, explain the rationale`;
685
+ var WORKFLOW_TOOLS_PROMPT = `
686
+
687
+ 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
688
 
135
689
  class ChatService {
136
690
  provider;
@@ -140,19 +694,93 @@ class ChatService {
140
694
  maxHistoryMessages;
141
695
  onUsage;
142
696
  tools;
697
+ thinkingLevel;
143
698
  sendReasoning;
144
699
  sendSources;
700
+ modelSelector;
145
701
  constructor(config) {
146
702
  this.provider = config.provider;
147
703
  this.context = config.context;
148
704
  this.store = config.store ?? new InMemoryConversationStore;
149
- this.systemPrompt = config.systemPrompt ?? DEFAULT_SYSTEM_PROMPT;
705
+ this.systemPrompt = this.buildSystemPrompt(config);
150
706
  this.maxHistoryMessages = config.maxHistoryMessages ?? 20;
151
707
  this.onUsage = config.onUsage;
152
- this.tools = config.tools;
153
- this.sendReasoning = config.sendReasoning ?? false;
708
+ this.tools = this.mergeTools(config);
709
+ this.thinkingLevel = config.thinkingLevel;
710
+ this.modelSelector = config.modelSelector;
711
+ this.sendReasoning = config.sendReasoning ?? (config.thinkingLevel != null && config.thinkingLevel !== "instant");
154
712
  this.sendSources = config.sendSources ?? false;
155
713
  }
714
+ buildSystemPrompt(config) {
715
+ let base = config.systemPrompt ?? DEFAULT_SYSTEM_PROMPT;
716
+ if (config.workflowToolsConfig?.baseWorkflows?.length) {
717
+ base += WORKFLOW_TOOLS_PROMPT;
718
+ }
719
+ const contractsPrompt = buildContractsContextPrompt(config.contractsContext ?? {});
720
+ if (contractsPrompt) {
721
+ base += contractsPrompt;
722
+ }
723
+ if (config.surfacePlanConfig?.plan) {
724
+ const plannerInput = buildPlannerPromptInput(config.surfacePlanConfig.plan);
725
+ base += `
726
+
727
+ ` + compilePlannerPrompt(plannerInput);
728
+ }
729
+ return base;
730
+ }
731
+ mergeTools(config) {
732
+ let merged = config.tools ?? {};
733
+ const wfConfig = config.workflowToolsConfig;
734
+ if (wfConfig?.baseWorkflows?.length) {
735
+ const workflowTools = createWorkflowTools({
736
+ baseWorkflows: wfConfig.baseWorkflows,
737
+ composer: wfConfig.composer
738
+ });
739
+ merged = { ...merged, ...workflowTools };
740
+ }
741
+ const contractsCtx = config.contractsContext;
742
+ if (contractsCtx?.agentSpecs?.length) {
743
+ const allTools = [];
744
+ for (const agent of contractsCtx.agentSpecs) {
745
+ if (agent.tools?.length)
746
+ allTools.push(...agent.tools);
747
+ }
748
+ if (allTools.length > 0) {
749
+ const agentTools = agentToolConfigsToToolSet(allTools);
750
+ merged = { ...merged, ...agentTools };
751
+ }
752
+ }
753
+ const surfaceConfig = config.surfacePlanConfig;
754
+ if (surfaceConfig?.plan) {
755
+ const plannerTools = createSurfacePlannerTools({
756
+ plan: surfaceConfig.plan,
757
+ onPatchProposal: surfaceConfig.onPatchProposal
758
+ });
759
+ merged = { ...merged, ...plannerTools };
760
+ }
761
+ if (config.mcpTools && Object.keys(config.mcpTools).length > 0) {
762
+ merged = { ...merged, ...config.mcpTools };
763
+ }
764
+ return Object.keys(merged).length > 0 ? merged : undefined;
765
+ }
766
+ async resolveModel() {
767
+ if (this.modelSelector) {
768
+ const dimension = this.thinkingLevelToDimension(this.thinkingLevel);
769
+ const { model, selection } = await this.modelSelector.selectAndCreate({
770
+ taskDimension: dimension
771
+ });
772
+ return { model, providerName: selection.providerKey };
773
+ }
774
+ return {
775
+ model: this.provider.getModel(),
776
+ providerName: this.provider.name
777
+ };
778
+ }
779
+ thinkingLevelToDimension(level) {
780
+ if (!level || level === "instant")
781
+ return "latency";
782
+ return "reasoning";
783
+ }
156
784
  async send(options) {
157
785
  let conversation;
158
786
  if (options.conversationId) {
@@ -170,20 +798,25 @@ class ChatService {
170
798
  workspacePath: this.context?.workspacePath
171
799
  });
172
800
  }
173
- await this.store.appendMessage(conversation.id, {
174
- role: "user",
175
- content: options.content,
176
- status: "completed",
177
- attachments: options.attachments
178
- });
801
+ if (!options.skipUserAppend) {
802
+ await this.store.appendMessage(conversation.id, {
803
+ role: "user",
804
+ content: options.content,
805
+ status: "completed",
806
+ attachments: options.attachments
807
+ });
808
+ }
809
+ conversation = await this.store.get(conversation.id) ?? conversation;
179
810
  const messages = this.buildMessages(conversation, options);
180
- const model = this.provider.getModel();
811
+ const { model, providerName } = await this.resolveModel();
812
+ const providerOptions = getProviderOptions(this.thinkingLevel, providerName);
181
813
  try {
182
814
  const result = await generateText({
183
815
  model,
184
816
  messages,
185
817
  system: this.systemPrompt,
186
- tools: this.tools
818
+ tools: this.tools,
819
+ providerOptions: Object.keys(providerOptions).length > 0 ? providerOptions : undefined
187
820
  });
188
821
  const assistantMessage = await this.store.appendMessage(conversation.id, {
189
822
  role: "assistant",
@@ -228,23 +861,27 @@ class ChatService {
228
861
  workspacePath: this.context?.workspacePath
229
862
  });
230
863
  }
231
- await this.store.appendMessage(conversation.id, {
232
- role: "user",
233
- content: options.content,
234
- status: "completed",
235
- attachments: options.attachments
236
- });
864
+ if (!options.skipUserAppend) {
865
+ await this.store.appendMessage(conversation.id, {
866
+ role: "user",
867
+ content: options.content,
868
+ status: "completed",
869
+ attachments: options.attachments
870
+ });
871
+ }
872
+ conversation = await this.store.get(conversation.id) ?? conversation;
237
873
  const assistantMessage = await this.store.appendMessage(conversation.id, {
238
874
  role: "assistant",
239
875
  content: "",
240
876
  status: "streaming"
241
877
  });
242
878
  const messages = this.buildMessages(conversation, options);
243
- const model = this.provider.getModel();
879
+ const { model, providerName } = await this.resolveModel();
244
880
  const systemPrompt = this.systemPrompt;
245
881
  const tools = this.tools;
246
882
  const store = this.store;
247
883
  const onUsage = this.onUsage;
884
+ const streamProviderOptions = getProviderOptions(this.thinkingLevel, providerName);
248
885
  async function* streamGenerator() {
249
886
  let fullContent = "";
250
887
  let fullReasoning = "";
@@ -255,7 +892,8 @@ class ChatService {
255
892
  model,
256
893
  messages,
257
894
  system: systemPrompt,
258
- tools
895
+ tools,
896
+ providerOptions: Object.keys(streamProviderOptions).length > 0 ? streamProviderOptions : undefined
259
897
  });
260
898
  for await (const part of result.fullStream) {
261
899
  if (part.type === "text-delta") {
@@ -370,6 +1008,18 @@ class ChatService {
370
1008
  ...options
371
1009
  });
372
1010
  }
1011
+ async updateConversation(conversationId, updates) {
1012
+ return this.store.update(conversationId, updates);
1013
+ }
1014
+ async forkConversation(conversationId, upToMessageId) {
1015
+ return this.store.fork(conversationId, upToMessageId);
1016
+ }
1017
+ async updateMessage(conversationId, messageId, updates) {
1018
+ return this.store.updateMessage(conversationId, messageId, updates);
1019
+ }
1020
+ async truncateAfter(conversationId, messageId) {
1021
+ return this.store.truncateAfter(conversationId, messageId);
1022
+ }
373
1023
  async deleteConversation(conversationId) {
374
1024
  return this.store.delete(conversationId);
375
1025
  }
@@ -440,9 +1090,9 @@ import {
440
1090
  function toolsToToolSet(defs) {
441
1091
  const result = {};
442
1092
  for (const def of defs) {
443
- result[def.name] = tool({
1093
+ result[def.name] = tool4({
444
1094
  description: def.description ?? def.name,
445
- inputSchema: z.object({}).passthrough(),
1095
+ inputSchema: z4.object({}).passthrough(),
446
1096
  execute: async () => ({})
447
1097
  });
448
1098
  }
@@ -456,21 +1106,63 @@ function useChat(options = {}) {
456
1106
  apiKey,
457
1107
  proxyUrl,
458
1108
  conversationId: initialConversationId,
1109
+ store,
459
1110
  systemPrompt,
460
1111
  streaming = true,
461
1112
  onSend,
462
1113
  onResponse,
463
1114
  onError,
464
1115
  onUsage,
465
- tools: toolsDefs
1116
+ tools: toolsDefs,
1117
+ thinkingLevel,
1118
+ workflowToolsConfig,
1119
+ modelSelector,
1120
+ contractsContext,
1121
+ surfacePlanConfig,
1122
+ mcpServers,
1123
+ agentMode
466
1124
  } = options;
467
1125
  const [messages, setMessages] = React.useState([]);
1126
+ const [mcpTools, setMcpTools] = React.useState(null);
1127
+ const mcpCleanupRef = React.useRef(null);
468
1128
  const [conversation, setConversation] = React.useState(null);
469
1129
  const [isLoading, setIsLoading] = React.useState(false);
470
1130
  const [error, setError] = React.useState(null);
471
1131
  const [conversationId, setConversationId] = React.useState(initialConversationId ?? null);
472
1132
  const abortControllerRef = React.useRef(null);
473
1133
  const chatServiceRef = React.useRef(null);
1134
+ React.useEffect(() => {
1135
+ if (!mcpServers?.length) {
1136
+ setMcpTools(null);
1137
+ return;
1138
+ }
1139
+ let cancelled = false;
1140
+ import("@contractspec/lib.ai-agent/tools/mcp-client").then(({ createMcpToolsets }) => {
1141
+ createMcpToolsets(mcpServers).then(({ tools, cleanup }) => {
1142
+ if (!cancelled) {
1143
+ setMcpTools(tools);
1144
+ mcpCleanupRef.current = cleanup;
1145
+ } else {
1146
+ cleanup().catch(() => {
1147
+ return;
1148
+ });
1149
+ }
1150
+ }).catch(() => {
1151
+ if (!cancelled)
1152
+ setMcpTools(null);
1153
+ });
1154
+ });
1155
+ return () => {
1156
+ cancelled = true;
1157
+ const cleanup = mcpCleanupRef.current;
1158
+ mcpCleanupRef.current = null;
1159
+ if (cleanup)
1160
+ cleanup().catch(() => {
1161
+ return;
1162
+ });
1163
+ setMcpTools(null);
1164
+ };
1165
+ }, [mcpServers]);
474
1166
  React.useEffect(() => {
475
1167
  const chatProvider = createProvider({
476
1168
  provider,
@@ -480,9 +1172,16 @@ function useChat(options = {}) {
480
1172
  });
481
1173
  chatServiceRef.current = new ChatService({
482
1174
  provider: chatProvider,
1175
+ store,
483
1176
  systemPrompt,
484
1177
  onUsage,
485
- tools: toolsDefs?.length ? toolsToToolSet(toolsDefs) : undefined
1178
+ tools: toolsDefs?.length ? toolsToToolSet(toolsDefs) : undefined,
1179
+ thinkingLevel,
1180
+ workflowToolsConfig,
1181
+ modelSelector,
1182
+ contractsContext,
1183
+ surfacePlanConfig,
1184
+ mcpTools
486
1185
  });
487
1186
  }, [
488
1187
  provider,
@@ -490,9 +1189,16 @@ function useChat(options = {}) {
490
1189
  model,
491
1190
  apiKey,
492
1191
  proxyUrl,
1192
+ store,
493
1193
  systemPrompt,
494
1194
  onUsage,
495
- toolsDefs
1195
+ toolsDefs,
1196
+ thinkingLevel,
1197
+ workflowToolsConfig,
1198
+ modelSelector,
1199
+ contractsContext,
1200
+ surfacePlanConfig,
1201
+ mcpTools
496
1202
  ]);
497
1203
  React.useEffect(() => {
498
1204
  if (!conversationId || !chatServiceRef.current)
@@ -508,7 +1214,90 @@ function useChat(options = {}) {
508
1214
  };
509
1215
  loadConversation().catch(console.error);
510
1216
  }, [conversationId]);
511
- const sendMessage = React.useCallback(async (content, attachments) => {
1217
+ const sendMessage = React.useCallback(async (content, attachments, opts) => {
1218
+ if (agentMode?.agent) {
1219
+ setIsLoading(true);
1220
+ setError(null);
1221
+ abortControllerRef.current = new AbortController;
1222
+ try {
1223
+ if (!opts?.skipUserAppend) {
1224
+ const userMessage = {
1225
+ id: `msg_${Date.now()}`,
1226
+ conversationId: conversationId ?? "",
1227
+ role: "user",
1228
+ content,
1229
+ status: "completed",
1230
+ createdAt: new Date,
1231
+ updatedAt: new Date,
1232
+ attachments
1233
+ };
1234
+ setMessages((prev) => [...prev, userMessage]);
1235
+ onSend?.(userMessage);
1236
+ }
1237
+ const result = await agentMode.agent.generate({
1238
+ prompt: content,
1239
+ signal: abortControllerRef.current.signal
1240
+ });
1241
+ const toolCallsMap = new Map;
1242
+ for (const tc of result.toolCalls ?? []) {
1243
+ const tr = result.toolResults?.find((r) => r.toolCallId === tc.toolCallId);
1244
+ toolCallsMap.set(tc.toolCallId, {
1245
+ id: tc.toolCallId,
1246
+ name: tc.toolName,
1247
+ args: tc.args ?? {},
1248
+ result: tr?.output,
1249
+ status: "completed"
1250
+ });
1251
+ }
1252
+ const assistantMessage = {
1253
+ id: `msg_${Date.now()}_a`,
1254
+ conversationId: conversationId ?? "",
1255
+ role: "assistant",
1256
+ content: result.text,
1257
+ status: "completed",
1258
+ createdAt: new Date,
1259
+ updatedAt: new Date,
1260
+ toolCalls: toolCallsMap.size ? Array.from(toolCallsMap.values()) : undefined,
1261
+ usage: result.usage
1262
+ };
1263
+ setMessages((prev) => [...prev, assistantMessage]);
1264
+ onResponse?.(assistantMessage);
1265
+ onUsage?.(result.usage ?? { inputTokens: 0, outputTokens: 0 });
1266
+ if (store && !conversationId) {
1267
+ const conv = await store.create({
1268
+ status: "active",
1269
+ provider: "agent",
1270
+ model: "agent",
1271
+ messages: []
1272
+ });
1273
+ if (!opts?.skipUserAppend) {
1274
+ await store.appendMessage(conv.id, {
1275
+ role: "user",
1276
+ content,
1277
+ status: "completed",
1278
+ attachments
1279
+ });
1280
+ }
1281
+ await store.appendMessage(conv.id, {
1282
+ role: "assistant",
1283
+ content: result.text,
1284
+ status: "completed",
1285
+ toolCalls: assistantMessage.toolCalls,
1286
+ usage: result.usage
1287
+ });
1288
+ const updated = await store.get(conv.id);
1289
+ if (updated)
1290
+ setConversation(updated);
1291
+ setConversationId(conv.id);
1292
+ }
1293
+ } catch (err) {
1294
+ setError(err instanceof Error ? err : new Error(String(err)));
1295
+ onError?.(err instanceof Error ? err : new Error(String(err)));
1296
+ } finally {
1297
+ setIsLoading(false);
1298
+ }
1299
+ return;
1300
+ }
512
1301
  if (!chatServiceRef.current) {
513
1302
  throw new Error("Chat service not initialized");
514
1303
  }
@@ -516,25 +1305,28 @@ function useChat(options = {}) {
516
1305
  setError(null);
517
1306
  abortControllerRef.current = new AbortController;
518
1307
  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);
1308
+ if (!opts?.skipUserAppend) {
1309
+ const userMessage = {
1310
+ id: `msg_${Date.now()}`,
1311
+ conversationId: conversationId ?? "",
1312
+ role: "user",
1313
+ content,
1314
+ status: "completed",
1315
+ createdAt: new Date,
1316
+ updatedAt: new Date,
1317
+ attachments
1318
+ };
1319
+ setMessages((prev) => [...prev, userMessage]);
1320
+ onSend?.(userMessage);
1321
+ }
531
1322
  if (streaming) {
532
1323
  const result = await chatServiceRef.current.stream({
533
1324
  conversationId: conversationId ?? undefined,
534
1325
  content,
535
- attachments
1326
+ attachments,
1327
+ skipUserAppend: opts?.skipUserAppend
536
1328
  });
537
- if (!conversationId) {
1329
+ if (!conversationId && !opts?.skipUserAppend) {
538
1330
  setConversationId(result.conversationId);
539
1331
  }
540
1332
  const assistantMessage = {
@@ -615,7 +1407,8 @@ function useChat(options = {}) {
615
1407
  const result = await chatServiceRef.current.send({
616
1408
  conversationId: conversationId ?? undefined,
617
1409
  content,
618
- attachments
1410
+ attachments,
1411
+ skipUserAppend: opts?.skipUserAppend
619
1412
  });
620
1413
  setConversation(result.conversation);
621
1414
  setMessages(result.conversation.messages);
@@ -632,7 +1425,7 @@ function useChat(options = {}) {
632
1425
  setIsLoading(false);
633
1426
  abortControllerRef.current = null;
634
1427
  }
635
- }, [conversationId, streaming, onSend, onResponse, onError, messages]);
1428
+ }, [conversationId, streaming, onSend, onResponse, onError, onUsage, messages, agentMode, store]);
636
1429
  const clearConversation = React.useCallback(() => {
637
1430
  setMessages([]);
638
1431
  setConversation(null);
@@ -653,6 +1446,44 @@ function useChat(options = {}) {
653
1446
  abortControllerRef.current?.abort();
654
1447
  setIsLoading(false);
655
1448
  }, []);
1449
+ const createNewConversation = clearConversation;
1450
+ const editMessage = React.useCallback(async (messageId, newContent) => {
1451
+ if (!chatServiceRef.current || !conversationId)
1452
+ return;
1453
+ const msg = messages.find((m) => m.id === messageId);
1454
+ if (!msg || msg.role !== "user")
1455
+ return;
1456
+ await chatServiceRef.current.updateMessage(conversationId, messageId, { content: newContent });
1457
+ const truncated = await chatServiceRef.current.truncateAfter(conversationId, messageId);
1458
+ if (truncated) {
1459
+ setMessages(truncated.messages);
1460
+ }
1461
+ await sendMessage(newContent, undefined, { skipUserAppend: true });
1462
+ }, [conversationId, messages, sendMessage]);
1463
+ const forkConversation = React.useCallback(async (upToMessageId) => {
1464
+ if (!chatServiceRef.current)
1465
+ return null;
1466
+ const idToFork = conversationId ?? conversation?.id;
1467
+ if (!idToFork)
1468
+ return null;
1469
+ try {
1470
+ const forked = await chatServiceRef.current.forkConversation(idToFork, upToMessageId);
1471
+ setConversationId(forked.id);
1472
+ setConversation(forked);
1473
+ setMessages(forked.messages);
1474
+ return forked.id;
1475
+ } catch {
1476
+ return null;
1477
+ }
1478
+ }, [conversationId, conversation]);
1479
+ const updateConversationFn = React.useCallback(async (updates) => {
1480
+ if (!chatServiceRef.current || !conversationId)
1481
+ return null;
1482
+ const updated = await chatServiceRef.current.updateConversation(conversationId, updates);
1483
+ if (updated)
1484
+ setConversation(updated);
1485
+ return updated;
1486
+ }, [conversationId]);
656
1487
  const addToolApprovalResponse = React.useCallback((_toolCallId, _result) => {
657
1488
  throw new Error(`addToolApprovalResponse: Tool approval requires server route with toUIMessageStreamResponse. ` + `Use createChatRoute and @ai-sdk/react useChat for tools with requireApproval.`);
658
1489
  }, []);
@@ -667,6 +1498,10 @@ function useChat(options = {}) {
667
1498
  setConversationId,
668
1499
  regenerate,
669
1500
  stop,
1501
+ createNewConversation,
1502
+ editMessage,
1503
+ forkConversation,
1504
+ updateConversation: updateConversationFn,
670
1505
  ...hasApprovalTools && { addToolApprovalResponse }
671
1506
  };
672
1507
  }
@@ -710,11 +1545,94 @@ function useProviders() {
710
1545
  refresh: loadProviders
711
1546
  };
712
1547
  }
1548
+ // src/presentation/hooks/useMessageSelection.ts
1549
+ import * as React3 from "react";
1550
+ "use client";
1551
+ function useMessageSelection(messageIds) {
1552
+ const [selectedIds, setSelectedIds] = React3.useState(() => new Set);
1553
+ const idSet = React3.useMemo(() => new Set(messageIds), [messageIds.join(",")]);
1554
+ React3.useEffect(() => {
1555
+ setSelectedIds((prev) => {
1556
+ const next = new Set;
1557
+ for (const id of prev) {
1558
+ if (idSet.has(id))
1559
+ next.add(id);
1560
+ }
1561
+ return next.size === prev.size ? prev : next;
1562
+ });
1563
+ }, [idSet]);
1564
+ const toggle = React3.useCallback((id) => {
1565
+ setSelectedIds((prev) => {
1566
+ const next = new Set(prev);
1567
+ if (next.has(id))
1568
+ next.delete(id);
1569
+ else
1570
+ next.add(id);
1571
+ return next;
1572
+ });
1573
+ }, []);
1574
+ const selectAll = React3.useCallback(() => {
1575
+ setSelectedIds(new Set(messageIds));
1576
+ }, [messageIds.join(",")]);
1577
+ const clearSelection = React3.useCallback(() => {
1578
+ setSelectedIds(new Set);
1579
+ }, []);
1580
+ const isSelected = React3.useCallback((id) => selectedIds.has(id), [selectedIds]);
1581
+ const selectedCount = selectedIds.size;
1582
+ return {
1583
+ selectedIds,
1584
+ toggle,
1585
+ selectAll,
1586
+ clearSelection,
1587
+ isSelected,
1588
+ selectedCount
1589
+ };
1590
+ }
1591
+ // src/presentation/hooks/useConversations.ts
1592
+ import * as React4 from "react";
1593
+ "use client";
1594
+ function useConversations(options) {
1595
+ const { store, projectId, tags, limit = 50 } = options;
1596
+ const [conversations, setConversations] = React4.useState([]);
1597
+ const [isLoading, setIsLoading] = React4.useState(true);
1598
+ const refresh = React4.useCallback(async () => {
1599
+ setIsLoading(true);
1600
+ try {
1601
+ const list = await store.list({
1602
+ status: "active",
1603
+ projectId,
1604
+ tags,
1605
+ limit
1606
+ });
1607
+ setConversations(list);
1608
+ } finally {
1609
+ setIsLoading(false);
1610
+ }
1611
+ }, [store, projectId, tags, limit]);
1612
+ React4.useEffect(() => {
1613
+ refresh();
1614
+ }, [refresh]);
1615
+ const deleteConversation = React4.useCallback(async (id) => {
1616
+ const ok = await store.delete(id);
1617
+ if (ok) {
1618
+ setConversations((prev) => prev.filter((c) => c.id !== id));
1619
+ }
1620
+ return ok;
1621
+ }, [store]);
1622
+ return {
1623
+ conversations,
1624
+ isLoading,
1625
+ refresh,
1626
+ deleteConversation
1627
+ };
1628
+ }
713
1629
 
714
1630
  // src/presentation/hooks/index.ts
715
1631
  import { useCompletion } from "@ai-sdk/react";
716
1632
  export {
717
1633
  useProviders,
1634
+ useMessageSelection,
1635
+ useConversations,
718
1636
  useCompletion,
719
1637
  useChat
720
1638
  };