@alpaca-editor/core 1.0.4134 → 1.0.4140

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 (126) hide show
  1. package/dist/config/config.js +7 -0
  2. package/dist/config/config.js.map +1 -1
  3. package/dist/editor/FieldListField.js +3 -4
  4. package/dist/editor/FieldListField.js.map +1 -1
  5. package/dist/editor/Terminal.js +1 -1
  6. package/dist/editor/Terminal.js.map +1 -1
  7. package/dist/editor/Titlebar.js +0 -1
  8. package/dist/editor/Titlebar.js.map +1 -1
  9. package/dist/editor/ai/AgentCostDisplay.d.ts +3 -1
  10. package/dist/editor/ai/AgentCostDisplay.js +26 -2
  11. package/dist/editor/ai/AgentCostDisplay.js.map +1 -1
  12. package/dist/editor/ai/AgentStatusBadge.d.ts +26 -0
  13. package/dist/editor/ai/AgentStatusBadge.js +110 -0
  14. package/dist/editor/ai/AgentStatusBadge.js.map +1 -0
  15. package/dist/editor/ai/AgentTerminal.js +289 -198
  16. package/dist/editor/ai/AgentTerminal.js.map +1 -1
  17. package/dist/editor/ai/Agents.d.ts +2 -2
  18. package/dist/editor/ai/Agents.js +115 -19
  19. package/dist/editor/ai/Agents.js.map +1 -1
  20. package/dist/editor/ai/AiResponseMessage.js +259 -45
  21. package/dist/editor/ai/AiResponseMessage.js.map +1 -1
  22. package/dist/editor/ai/ContextInfoBar.js +124 -113
  23. package/dist/editor/ai/ContextInfoBar.js.map +1 -1
  24. package/dist/editor/ai/ToolCallDisplay.d.ts +1 -0
  25. package/dist/editor/ai/ToolCallDisplay.js +70 -58
  26. package/dist/editor/ai/ToolCallDisplay.js.map +1 -1
  27. package/dist/editor/ai/useAgentStatus.d.ts +13 -0
  28. package/dist/editor/ai/useAgentStatus.js +101 -0
  29. package/dist/editor/ai/useAgentStatus.js.map +1 -0
  30. package/dist/editor/client/EditorShell.js +23 -8
  31. package/dist/editor/client/EditorShell.js.map +1 -1
  32. package/dist/editor/client/itemsRepository.js.map +1 -1
  33. package/dist/editor/commands/localizeItem/LocalizeItemDialog.js +5 -5
  34. package/dist/editor/commands/localizeItem/LocalizeItemDialog.js.map +1 -1
  35. package/dist/editor/control-center/About.js +1 -1
  36. package/dist/editor/control-center/About.js.map +1 -1
  37. package/dist/editor/control-center/AllAgentsPanel.d.ts +5 -0
  38. package/dist/editor/control-center/AllAgentsPanel.js +126 -0
  39. package/dist/editor/control-center/AllAgentsPanel.js.map +1 -0
  40. package/dist/editor/control-center/WebSocketMessages.js +1 -0
  41. package/dist/editor/control-center/WebSocketMessages.js.map +1 -1
  42. package/dist/editor/control-center/setup-steps/AiSetupStep/tools/GenerateToolsSection.js +42 -7
  43. package/dist/editor/control-center/setup-steps/AiSetupStep/tools/GenerateToolsSection.js.map +1 -1
  44. package/dist/editor/media-selector/AiImageSearch.d.ts +1 -1
  45. package/dist/editor/media-selector/AiImageSearch.js +162 -103
  46. package/dist/editor/media-selector/AiImageSearch.js.map +1 -1
  47. package/dist/editor/media-selector/TreeSelector.js +20 -4
  48. package/dist/editor/media-selector/TreeSelector.js.map +1 -1
  49. package/dist/editor/menubar/toolbar-sections/UtilityControls.js +5 -2
  50. package/dist/editor/menubar/toolbar-sections/UtilityControls.js.map +1 -1
  51. package/dist/editor/page-editor-chrome/PlaceholderDropZone.d.ts +1 -1
  52. package/dist/editor/page-editor-chrome/PlaceholderDropZone.js +7 -5
  53. package/dist/editor/page-editor-chrome/PlaceholderDropZone.js.map +1 -1
  54. package/dist/editor/page-viewer/DeviceToolbar.js +2 -2
  55. package/dist/editor/page-viewer/DeviceToolbar.js.map +1 -1
  56. package/dist/editor/page-viewer/PageViewerFrame.js +18 -11
  57. package/dist/editor/page-viewer/PageViewerFrame.js.map +1 -1
  58. package/dist/editor/services/agentService.d.ts +53 -48
  59. package/dist/editor/services/agentService.js +137 -79
  60. package/dist/editor/services/agentService.js.map +1 -1
  61. package/dist/editor/services/aiService.d.ts +1 -1
  62. package/dist/editor/services/editService.js +1 -0
  63. package/dist/editor/services/editService.js.map +1 -1
  64. package/dist/editor/sidebar/GraphQL.js +20 -7
  65. package/dist/editor/sidebar/GraphQL.js.map +1 -1
  66. package/dist/editor/sidebar/SEOInfo.js +1 -2
  67. package/dist/editor/sidebar/SEOInfo.js.map +1 -1
  68. package/dist/editor/sidebar/Translations.js +10 -7
  69. package/dist/editor/sidebar/Translations.js.map +1 -1
  70. package/dist/editor/ui/ItemNameDialogNew.js +1 -1
  71. package/dist/editor/ui/ItemSearch.js +10 -4
  72. package/dist/editor/ui/ItemSearch.js.map +1 -1
  73. package/dist/page-wizard/steps/CollectStep.js +2 -2
  74. package/dist/page-wizard/steps/CollectStep.js.map +1 -1
  75. package/dist/page-wizard/steps/FieldEditor.js +2 -2
  76. package/dist/page-wizard/steps/FieldEditor.js.map +1 -1
  77. package/dist/revision.d.ts +2 -2
  78. package/dist/revision.js +2 -2
  79. package/dist/splash-screen/NewPage.js +2 -2
  80. package/dist/splash-screen/NewPage.js.map +1 -1
  81. package/dist/splash-screen/RecentPages.js +1 -1
  82. package/dist/splash-screen/RecentPages.js.map +1 -1
  83. package/dist/styles.css +167 -22
  84. package/dist/tour/Tour.js +15 -11
  85. package/dist/tour/Tour.js.map +1 -1
  86. package/package.json +1 -1
  87. package/src/config/config.tsx +7 -0
  88. package/src/editor/FieldListField.tsx +13 -13
  89. package/src/editor/Terminal.tsx +1 -1
  90. package/src/editor/Titlebar.tsx +0 -1
  91. package/src/editor/ai/AgentCostDisplay.tsx +57 -1
  92. package/src/editor/ai/AgentStatusBadge.tsx +144 -0
  93. package/src/editor/ai/AgentTerminal.tsx +345 -219
  94. package/src/editor/ai/Agents.tsx +179 -30
  95. package/src/editor/ai/AiResponseMessage.tsx +411 -114
  96. package/src/editor/ai/ContextInfoBar.tsx +134 -131
  97. package/src/editor/ai/ToolCallDisplay.tsx +217 -176
  98. package/src/editor/ai/useAgentStatus.ts +123 -0
  99. package/src/editor/client/EditorShell.tsx +34 -8
  100. package/src/editor/client/itemsRepository.ts +1 -2
  101. package/src/editor/commands/localizeItem/LocalizeItemDialog.tsx +5 -5
  102. package/src/editor/control-center/About.tsx +0 -14
  103. package/src/editor/control-center/AllAgentsPanel.tsx +300 -0
  104. package/src/editor/control-center/WebSocketMessages.tsx +1 -0
  105. package/src/editor/control-center/setup-steps/AiSetupStep/tools/GenerateToolsSection.tsx +49 -8
  106. package/src/editor/media-selector/AiImageSearch.tsx +162 -172
  107. package/src/editor/media-selector/TreeSelector.tsx +137 -116
  108. package/src/editor/menubar/toolbar-sections/UtilityControls.tsx +9 -1
  109. package/src/editor/page-editor-chrome/PlaceholderDropZone.tsx +7 -4
  110. package/src/editor/page-viewer/DeviceToolbar.tsx +15 -11
  111. package/src/editor/page-viewer/PageViewerFrame.tsx +20 -14
  112. package/src/editor/services/agentService.ts +217 -129
  113. package/src/editor/services/aiService.ts +2 -2
  114. package/src/editor/services/editService.ts +1 -0
  115. package/src/editor/sidebar/GraphQL.tsx +143 -117
  116. package/src/editor/sidebar/SEOInfo.tsx +1 -2
  117. package/src/editor/sidebar/Translations.tsx +14 -12
  118. package/src/editor/ui/ItemNameDialogNew.tsx +1 -1
  119. package/src/editor/ui/ItemSearch.tsx +11 -4
  120. package/src/editor/ui/SimpleTabs.tsx +1 -1
  121. package/src/page-wizard/steps/CollectStep.tsx +2 -2
  122. package/src/page-wizard/steps/FieldEditor.tsx +13 -15
  123. package/src/revision.ts +2 -2
  124. package/src/splash-screen/NewPage.tsx +2 -2
  125. package/src/splash-screen/RecentPages.tsx +1 -1
  126. package/src/tour/Tour.tsx +61 -48
@@ -30,6 +30,127 @@ type ContentSegment =
30
30
  items: { id?: string; text: string; done?: boolean; note?: string }[];
31
31
  };
32
32
 
33
+ // Helper to extract partial quick actions from potentially incomplete JSON during streaming
34
+ const extractPartialQuickActions = (jsonText: string): QuickAction[] => {
35
+ const actions: QuickAction[] = [];
36
+
37
+ // First try to parse complete JSON
38
+ try {
39
+ const parsed = JSON.parse(jsonText);
40
+ const rawActions = Array.isArray(parsed)
41
+ ? parsed
42
+ : parsed?.actions || parsed?.buttons || parsed?.choices || [];
43
+ if (Array.isArray(rawActions)) {
44
+ rawActions.forEach((a: any) => {
45
+ if (!a) return;
46
+ const label = a.label || a.text || String(a.value || a.prompt || "");
47
+ if (label) {
48
+ actions.push({
49
+ id: a.id,
50
+ label,
51
+ prompt: a.prompt,
52
+ value: a.value,
53
+ style: a.style,
54
+ behavior:
55
+ a.behavior &&
56
+ (a.behavior === "compose" || a.behavior === "submit")
57
+ ? a.behavior
58
+ : a.submit === false ||
59
+ a.compose === true ||
60
+ a.draft === true ||
61
+ a.mode === "compose"
62
+ ? "compose"
63
+ : undefined,
64
+ placeholder: a.placeholder,
65
+ });
66
+ }
67
+ });
68
+ }
69
+ return actions;
70
+ } catch (e) {
71
+ // JSON is incomplete, try to extract whatever complete action objects we can find
72
+ }
73
+
74
+ // Look for individual action objects in the partial JSON
75
+ // Match patterns like: { "label": "...", ... }
76
+ const labelPattern = /"(?:label|text)"\s*:\s*"([^"\\]*(\\.[^"\\]*)*)"/g;
77
+ const labelMatches: Array<{ label: string; startIdx: number }> = [];
78
+ let labelMatch;
79
+ while ((labelMatch = labelPattern.exec(jsonText)) !== null) {
80
+ if (labelMatch[1]) {
81
+ labelMatches.push({
82
+ label: labelMatch[1],
83
+ startIdx: labelMatch.index,
84
+ });
85
+ }
86
+ }
87
+
88
+ // For each label field found, try to find and parse the enclosing object
89
+ for (const { label, startIdx } of labelMatches) {
90
+ // Find the opening brace before this label field
91
+ let openBrace = -1;
92
+ for (let i = startIdx - 1; i >= 0; i--) {
93
+ if (jsonText[i] === "{") {
94
+ openBrace = i;
95
+ break;
96
+ }
97
+ if (jsonText[i] === "}") break; // Hit another object's end
98
+ }
99
+
100
+ if (openBrace === -1) continue;
101
+
102
+ // Find the closing brace after this label field
103
+ let closeBrace = -1;
104
+ let depth = 0;
105
+ for (let i = openBrace; i < jsonText.length; i++) {
106
+ if (jsonText[i] === "{") depth++;
107
+ if (jsonText[i] === "}") {
108
+ depth--;
109
+ if (depth === 0) {
110
+ closeBrace = i;
111
+ break;
112
+ }
113
+ }
114
+ }
115
+
116
+ // Extract the object and try to parse it
117
+ const objStr =
118
+ closeBrace !== -1
119
+ ? jsonText.substring(openBrace, closeBrace + 1)
120
+ : jsonText.substring(openBrace) + "}"; // Try to close incomplete object
121
+
122
+ try {
123
+ const obj = JSON.parse(objStr);
124
+ const actionLabel =
125
+ obj.label || obj.text || String(obj.value || obj.prompt || "");
126
+ if (actionLabel) {
127
+ actions.push({
128
+ id: obj.id,
129
+ label: actionLabel,
130
+ prompt: obj.prompt,
131
+ value: obj.value,
132
+ style: obj.style,
133
+ behavior:
134
+ obj.behavior &&
135
+ (obj.behavior === "compose" || obj.behavior === "submit")
136
+ ? obj.behavior
137
+ : obj.submit === false ||
138
+ obj.compose === true ||
139
+ obj.draft === true ||
140
+ obj.mode === "compose"
141
+ ? "compose"
142
+ : undefined,
143
+ placeholder: obj.placeholder,
144
+ });
145
+ }
146
+ } catch (e) {
147
+ // Skip malformed objects
148
+ }
149
+ }
150
+
151
+ return actions;
152
+ };
153
+
33
154
  function parseContentSegments(
34
155
  content?: string,
35
156
  hideIncomplete?: boolean,
@@ -151,16 +272,28 @@ function parseContentSegments(
151
272
  const jsonStart = afterOpenLineStart + openLineLength;
152
273
 
153
274
  const close = content.indexOf("```", jsonStart);
275
+ const jsonText = (
276
+ close === -1
277
+ ? content.slice(jsonStart)
278
+ : content.slice(jsonStart, close)
279
+ ).trim();
280
+
154
281
  if (close === -1) {
155
- if (hideIncomplete) {
282
+ // Incomplete fenced block during streaming
283
+ if (hideIncomplete && pick.type === "quick") {
284
+ // Try to parse partial quick actions and show what we have so far
285
+ const partialActions = extractPartialQuickActions(jsonText);
286
+ if (partialActions.length > 0) {
287
+ segments.push({ kind: "actions", actions: partialActions });
288
+ }
289
+ break;
290
+ } else if (hideIncomplete) {
156
291
  break;
157
292
  } else {
158
293
  segments.push({ kind: "text", text: content.slice(pick.start) });
159
294
  break;
160
295
  }
161
296
  }
162
-
163
- const jsonText = content.slice(jsonStart, close).trim();
164
297
  try {
165
298
  const parsed = JSON.parse(jsonText);
166
299
  if (pick.type === "quick") {
@@ -258,8 +391,22 @@ function parseContentSegments(
258
391
  }
259
392
 
260
393
  const braceEnd = findMatchingBraceEnd(braceStart);
394
+ const jsonText = (
395
+ braceEnd === -1
396
+ ? content.slice(braceStart)
397
+ : content.slice(braceStart, braceEnd + 1)
398
+ ).trim();
399
+
261
400
  if (braceEnd === -1) {
262
- if (hideIncomplete) {
401
+ // Incomplete plain JSON block during streaming
402
+ if (hideIncomplete && pick.type === "quick") {
403
+ // Try to parse partial quick actions and show what we have so far
404
+ const partialActions = extractPartialQuickActions(jsonText);
405
+ if (partialActions.length > 0) {
406
+ segments.push({ kind: "actions", actions: partialActions });
407
+ }
408
+ break;
409
+ } else if (hideIncomplete) {
263
410
  break;
264
411
  } else {
265
412
  // Incomplete JSON; include the rest as text
@@ -268,8 +415,6 @@ function parseContentSegments(
268
415
  break;
269
416
  }
270
417
  }
271
-
272
- const jsonText = content.slice(braceStart, braceEnd + 1).trim();
273
418
  try {
274
419
  const parsed = JSON.parse(jsonText);
275
420
  if (pick.type === "quick") {
@@ -342,6 +487,28 @@ function parseContentSegments(
342
487
  return segments;
343
488
  }
344
489
 
490
+ // Helper to extract command links from markdown
491
+ function extractCommandLinks(text: string): {
492
+ text: string;
493
+ links: Array<{ label: string; command: string; index: number }>;
494
+ } {
495
+ const links: Array<{ label: string; command: string; index: number }> = [];
496
+ const commandLinkPattern = /\[([^\]]+)\]\(command:([^)]+)\)/g;
497
+ let match;
498
+
499
+ while ((match = commandLinkPattern.exec(text)) !== null) {
500
+ if (match[1] && match[2]) {
501
+ links.push({
502
+ label: match[1],
503
+ command: match[2],
504
+ index: match.index,
505
+ });
506
+ }
507
+ }
508
+
509
+ return { text, links };
510
+ }
511
+
345
512
  function simpleFormatToHtml(text: string): string {
346
513
  return text
347
514
  .replace(/\*\*(.*?)\*\*/g, "<strong>$1</strong>")
@@ -452,16 +619,58 @@ export function AiResponseMessage({
452
619
  [messages],
453
620
  );
454
621
 
455
- const segmentsByMessageId = useMemo(() => {
456
- const result: { [messageId: string]: ContentSegment[] } = {};
622
+ // Create a flat structure of content items: Text, ToolCalls, Text, ToolCalls, etc.
623
+ type FlatItem =
624
+ | {
625
+ type: "segment";
626
+ segment: ContentSegment;
627
+ messageId: string;
628
+ message: Message;
629
+ }
630
+ | {
631
+ type: "toolcalls";
632
+ toolCalls: ToolCall[];
633
+ messageId: string;
634
+ message: Message;
635
+ };
636
+
637
+ const flatItems = useMemo(() => {
638
+ const items: FlatItem[] = [];
639
+
457
640
  nonToolAssistantMessages.forEach((message) => {
458
641
  const contentString = (message.content ||
459
642
  message.formattedContent ||
460
643
  "") as string;
461
- result[message.id] = parseContentSegments(contentString, isStreaming);
644
+ const segments = parseContentSegments(contentString, isStreaming);
645
+
646
+ // Add content segments
647
+ segments.forEach((segment) => {
648
+ items.push({
649
+ type: "segment",
650
+ segment,
651
+ messageId: message.id,
652
+ message,
653
+ });
654
+ });
655
+
656
+ // Add tool calls if present
657
+ const toolCalls =
658
+ message.tool_calls && message.tool_calls.length > 0
659
+ ? message.tool_calls
660
+ : preservedToolCalls[message.id] || [];
661
+
662
+ if (toolCalls.length > 0) {
663
+ items.push({
664
+ type: "toolcalls",
665
+ toolCalls,
666
+ messageId: message.id,
667
+ message,
668
+ });
669
+ }
462
670
  });
463
- return result;
464
- }, [nonToolAssistantMessages, isStreaming]);
671
+
672
+ return items;
673
+ }, [nonToolAssistantMessages, isStreaming, preservedToolCalls]);
465
674
 
466
675
  return (
467
676
  <div className="flex gap-3 p-4" data-testid="agent-message">
@@ -496,124 +705,212 @@ export function AiResponseMessage({
496
705
  </div>
497
706
  )}
498
707
 
499
- {nonToolAssistantMessages.map((message, filteredIndex) => {
500
- // Use preserved tool calls if current message has empty tool calls
501
- const toolCalls =
502
- message.tool_calls && message.tool_calls.length > 0
503
- ? message.tool_calls
504
- : preservedToolCalls[message.id] || [];
505
-
506
- // Retrieve precomputed segments for this message
507
- const segments = segmentsByMessageId[message.id] || [];
508
-
509
- return (
510
- <div
511
- key={filteredIndex}
512
- className={toolCalls.length > 0 ? "mb-2" : ""}
513
- >
514
- {segments.map((segment, idx) => {
515
- if (segment.kind === "text") {
516
- const html = simpleFormatToHtml(segment.text.trim());
517
- if (!html) return null;
518
- return (
519
- <div
520
- key={"txt-" + idx}
521
- className="prose prose-sm max-w-none text-xs text-gray-700 select-text"
522
- dangerouslySetInnerHTML={{ __html: html }}
523
- />
524
- );
525
- }
526
- if (segment.kind === "todo") {
527
- const todo = segment;
528
- return (
529
- <div key={"todo-" + idx} className="my-2">
530
- {todo.title && (
531
- <div className="mb-1 text-xs font-medium text-gray-800">
532
- {todo.title}
533
- </div>
534
- )}
535
- <div className="flex flex-col gap-1">
536
- {todo.items.map((item, iIdx) => (
537
- <label
538
- key={(item.id || item.text || "todo") + "-" + iIdx}
539
- className="flex items-start gap-2 text-xs text-gray-700"
540
- >
541
- <Checkbox
542
- checked={!!item.done}
543
- onCheckedChange={() => {}}
544
- aria-readonly
545
- className="mt-0.5"
546
- />
547
- <div>
548
- <div
549
- className={
550
- item.done ? "line-through opacity-70" : ""
551
- }
552
- >
553
- {item.text}
554
- </div>
555
- {item.note && (
556
- <div className="text-gray-1 text-xs">
557
- {item.note}
558
- </div>
559
- )}
560
- </div>
561
- </label>
562
- ))}
563
- </div>
564
- </div>
708
+ {flatItems.map((item, itemIndex) => {
709
+ const prevItem = itemIndex > 0 ? flatItems[itemIndex - 1] : null;
710
+ const shouldAddSpacing =
711
+ prevItem?.type === "toolcalls" && item.type === "segment";
712
+
713
+ if (item.type === "segment") {
714
+ const segment = item.segment;
715
+ const message = item.message;
716
+
717
+ if (segment.kind === "text") {
718
+ const trimmedText = segment.text.trim();
719
+ if (!trimmedText) return null;
720
+
721
+ // Check for command links in the text
722
+ const { text, links } = extractCommandLinks(trimmedText);
723
+
724
+ if (links.length > 0) {
725
+ // Split text around command links and render buttons
726
+ const parts: React.ReactElement[] = [];
727
+ let lastIndex = 0;
728
+
729
+ links.forEach((link, linkIdx) => {
730
+ // Add text before the link
731
+ if (link.index > lastIndex) {
732
+ const textBefore = text.substring(lastIndex, link.index);
733
+ const html = simpleFormatToHtml(textBefore);
734
+ if (html) {
735
+ parts.push(
736
+ <span
737
+ key={`text-${itemIndex}-${linkIdx}`}
738
+ dangerouslySetInnerHTML={{ __html: html }}
739
+ />,
740
+ );
741
+ }
742
+ }
743
+
744
+ // Add the button
745
+ parts.push(
746
+ <Button
747
+ key={`btn-${itemIndex}-${linkIdx}`}
748
+ size="sm"
749
+ variant="outline"
750
+ className="mx-1 inline-flex"
751
+ onClick={() =>
752
+ onQuickAction?.(
753
+ {
754
+ label: link.label,
755
+ value: link.command,
756
+ prompt: link.command,
757
+ },
758
+ message,
759
+ )
760
+ }
761
+ >
762
+ {link.label}
763
+ </Button>,
565
764
  );
765
+
766
+ // Find the end of this match in the original text
767
+ const matchEnd =
768
+ link.index +
769
+ `[${link.label}](command:${link.command})`.length;
770
+ lastIndex = matchEnd;
771
+ });
772
+
773
+ // Add any remaining text after the last link
774
+ if (lastIndex < text.length) {
775
+ const textAfter = text.substring(lastIndex);
776
+ const html = simpleFormatToHtml(textAfter);
777
+ if (html) {
778
+ parts.push(
779
+ <span
780
+ key={`text-${itemIndex}-end`}
781
+ dangerouslySetInnerHTML={{ __html: html }}
782
+ />,
783
+ );
784
+ }
566
785
  }
567
- const actions = segment.actions;
786
+
568
787
  return (
569
- <div key={"act-" + idx} className="my-2 flex flex-wrap gap-2">
570
- {actions.map((a, aIdx) => {
571
- const variant =
572
- a.style === "destructive"
573
- ? "destructive"
574
- : a.style === "outline"
575
- ? "outline"
576
- : a.style === "secondary"
577
- ? "secondary"
578
- : "default"; // primary
579
- return (
580
- <Button
581
- key={(a.id || a.label || "btn") + "-" + aIdx}
582
- size="sm"
583
- variant={variant as any}
584
- onClick={() =>
585
- onQuickAction?.(
586
- {
587
- label: a.label,
588
- prompt: a.prompt,
589
- value: a.value,
590
- behavior: a.behavior,
591
- placeholder: a.placeholder,
592
- },
593
- message,
594
- )
595
- }
596
- >
597
- {a.label}
598
- </Button>
599
- );
600
- })}
788
+ <div
789
+ key={`item-${itemIndex}`}
790
+ className={`prose prose-sm max-w-none text-xs text-gray-700 select-text ${shouldAddSpacing ? "mt-3" : ""}`}
791
+ >
792
+ {parts}
601
793
  </div>
602
794
  );
603
- })}
795
+ }
796
+
797
+ // No command links, render as normal
798
+ const html = simpleFormatToHtml(trimmedText);
799
+ if (!html) return null;
800
+ return (
801
+ <div
802
+ key={`item-${itemIndex}`}
803
+ className={`prose prose-sm max-w-none text-xs text-gray-700 select-text ${shouldAddSpacing ? "mt-3" : ""}`}
804
+ dangerouslySetInnerHTML={{ __html: html }}
805
+ />
806
+ );
807
+ }
808
+
809
+ if (segment.kind === "todo") {
810
+ const todo = segment;
811
+ return (
812
+ <div
813
+ key={`item-${itemIndex}`}
814
+ className={`my-2 ${shouldAddSpacing ? "mt-3" : ""}`}
815
+ >
816
+ {todo.title && (
817
+ <div className="mb-1 text-xs font-medium text-gray-800">
818
+ {todo.title}
819
+ </div>
820
+ )}
821
+ <div className="flex flex-col gap-1">
822
+ {todo.items.map((todoItem, iIdx) => (
823
+ <label
824
+ key={
825
+ (todoItem.id || todoItem.text || "todo") + "-" + iIdx
826
+ }
827
+ className="flex items-start gap-2 text-xs text-gray-700"
828
+ >
829
+ <Checkbox
830
+ checked={!!todoItem.done}
831
+ onCheckedChange={() => {}}
832
+ aria-readonly
833
+ className="mt-0.5"
834
+ />
835
+ <div>
836
+ <div
837
+ className={
838
+ todoItem.done ? "line-through opacity-70" : ""
839
+ }
840
+ >
841
+ {todoItem.text}
842
+ </div>
843
+ {todoItem.note && (
844
+ <div className="text-gray-1 text-xs">
845
+ {todoItem.note}
846
+ </div>
847
+ )}
848
+ </div>
849
+ </label>
850
+ ))}
851
+ </div>
852
+ </div>
853
+ );
854
+ }
855
+
856
+ // segment.kind === "actions"
857
+ const actions = segment.actions;
858
+ return (
859
+ <div
860
+ key={`item-${itemIndex}`}
861
+ className={`my-2 flex flex-wrap gap-2 ${shouldAddSpacing ? "mt-3" : ""}`}
862
+ >
863
+ {actions.map((a, aIdx) => {
864
+ const variant =
865
+ a.style === "destructive"
866
+ ? "destructive"
867
+ : a.style === "outline"
868
+ ? "outline"
869
+ : a.style === "secondary"
870
+ ? "secondary"
871
+ : "default"; // primary
872
+ return (
873
+ <Button
874
+ key={(a.id || a.label || "btn") + "-" + aIdx}
875
+ size="sm"
876
+ variant={variant as any}
877
+ onClick={() =>
878
+ onQuickAction?.(
879
+ {
880
+ label: a.label,
881
+ prompt: a.prompt,
882
+ value: a.value,
883
+ behavior: a.behavior,
884
+ placeholder: a.placeholder,
885
+ },
886
+ message,
887
+ )
888
+ }
889
+ >
890
+ {a.label}
891
+ </Button>
892
+ );
893
+ })}
894
+ </div>
895
+ );
896
+ }
897
+
898
+ // item.type === "toolcalls"
899
+ return (
900
+ <div key={`item-${itemIndex}`} className="mt-3">
604
901
  <ToolCallDisplay
605
- toolCalls={toolCalls}
902
+ toolCalls={item.toolCalls}
606
903
  finished={finished}
607
904
  openPopovers={openPopovers}
608
905
  setOpenPopovers={setOpenPopovers}
609
- messageId={message.id}
906
+ messageId={item.messageId}
610
907
  />
611
908
  </div>
612
909
  );
613
910
  })}
614
911
 
615
912
  {finished && editOperations.length > 0 && (
616
- <div className="my-2 flex items-center gap-2">
913
+ <div className="mt-3 flex items-center gap-2">
617
914
  <div className="tour-ai-response-message-changes text-xs">
618
915
  {editOperations.length} changes
619
916
  </div>