@alpaca-editor/core 1.0.4085 → 1.0.4088

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 (146) hide show
  1. package/dist/components/ui/card.d.ts +1 -1
  2. package/dist/components/ui/paste-button.d.ts +14 -0
  3. package/dist/components/ui/paste-button.js +114 -0
  4. package/dist/components/ui/paste-button.js.map +1 -0
  5. package/dist/config/config.js +60 -3
  6. package/dist/config/config.js.map +1 -1
  7. package/dist/config/types.d.ts +25 -0
  8. package/dist/editor/ContentTree.js +43 -21
  9. package/dist/editor/ContentTree.js.map +1 -1
  10. package/dist/editor/FieldListField.js +62 -2
  11. package/dist/editor/FieldListField.js.map +1 -1
  12. package/dist/editor/ai/AgentTerminal.d.ts +3 -1
  13. package/dist/editor/ai/AgentTerminal.js +96 -74
  14. package/dist/editor/ai/AgentTerminal.js.map +1 -1
  15. package/dist/editor/ai/Agents.js +46 -2
  16. package/dist/editor/ai/Agents.js.map +1 -1
  17. package/dist/editor/ai/AiResponseMessage.js +171 -75
  18. package/dist/editor/ai/AiResponseMessage.js.map +1 -1
  19. package/dist/editor/ai/AiTerminal.js +27 -14
  20. package/dist/editor/ai/AiTerminal.js.map +1 -1
  21. package/dist/editor/client/EditorShell.js +110 -17
  22. package/dist/editor/client/EditorShell.js.map +1 -1
  23. package/dist/editor/client/editContext.d.ts +4 -0
  24. package/dist/editor/client/editContext.js.map +1 -1
  25. package/dist/editor/client/hooks/useSocketMessageHandler.d.ts +1 -0
  26. package/dist/editor/client/hooks/useSocketMessageHandler.js +54 -20
  27. package/dist/editor/client/hooks/useSocketMessageHandler.js.map +1 -1
  28. package/dist/editor/client/hooks/useWorkbox.d.ts +1 -1
  29. package/dist/editor/client/hooks/useWorkbox.js +4 -4
  30. package/dist/editor/client/hooks/useWorkbox.js.map +1 -1
  31. package/dist/editor/client/itemsRepository.d.ts +13 -1
  32. package/dist/editor/client/itemsRepository.js +34 -21
  33. package/dist/editor/client/itemsRepository.js.map +1 -1
  34. package/dist/editor/client/pageModelBuilder.js +1 -1
  35. package/dist/editor/client/pageModelBuilder.js.map +1 -1
  36. package/dist/editor/control-center/Setup.d.ts +1 -0
  37. package/dist/editor/control-center/Setup.js +18 -0
  38. package/dist/editor/control-center/Setup.js.map +1 -0
  39. package/dist/editor/control-center/setup-steps/AiSetupStep.d.ts +2 -0
  40. package/dist/editor/control-center/setup-steps/AiSetupStep.js +287 -0
  41. package/dist/editor/control-center/setup-steps/AiSetupStep.js.map +1 -0
  42. package/dist/editor/control-center/setup-steps/DbSetupStep.d.ts +2 -0
  43. package/dist/editor/control-center/setup-steps/DbSetupStep.js +46 -0
  44. package/dist/editor/control-center/setup-steps/DbSetupStep.js.map +1 -0
  45. package/dist/editor/control-center/setup-steps/IndexSetupStep.d.ts +2 -0
  46. package/dist/editor/control-center/setup-steps/IndexSetupStep.js +34 -0
  47. package/dist/editor/control-center/setup-steps/IndexSetupStep.js.map +1 -0
  48. package/dist/editor/control-center/setup-steps/SettingsSetupStep.d.ts +2 -0
  49. package/dist/editor/control-center/setup-steps/SettingsSetupStep.js +104 -0
  50. package/dist/editor/control-center/setup-steps/SettingsSetupStep.js.map +1 -0
  51. package/dist/editor/field-types/InternalLinkFieldEditor.js +3 -1
  52. package/dist/editor/field-types/InternalLinkFieldEditor.js.map +1 -1
  53. package/dist/editor/field-types/RichTextEditorComponent.js +1 -1
  54. package/dist/editor/field-types/RichTextEditorComponent.js.map +1 -1
  55. package/dist/editor/field-types/richtext/components/ReactSlate.js +2 -2
  56. package/dist/editor/field-types/richtext/components/ReactSlate.js.map +1 -1
  57. package/dist/editor/field-types/richtext/utils/profileServiceCache.d.ts +1 -1
  58. package/dist/editor/field-types/richtext/utils/profileServiceCache.js +16 -14
  59. package/dist/editor/field-types/richtext/utils/profileServiceCache.js.map +1 -1
  60. package/dist/editor/menubar/toolbar-sections/CompareControls.js +1 -1
  61. package/dist/editor/menubar/toolbar-sections/CompareControls.js.map +1 -1
  62. package/dist/editor/menubar/toolbar-sections/EditControls.js +1 -1
  63. package/dist/editor/menubar/toolbar-sections/EditControls.js.map +1 -1
  64. package/dist/editor/menubar/toolbar-sections/ViewportControls.js +1 -1
  65. package/dist/editor/menubar/toolbar-sections/ViewportControls.js.map +1 -1
  66. package/dist/editor/page-editor-chrome/InlineEditor.js +25 -6
  67. package/dist/editor/page-editor-chrome/InlineEditor.js.map +1 -1
  68. package/dist/editor/page-viewer/EditorForm.js +9 -2
  69. package/dist/editor/page-viewer/EditorForm.js.map +1 -1
  70. package/dist/editor/page-viewer/PageViewerFrame.js +5 -0
  71. package/dist/editor/page-viewer/PageViewerFrame.js.map +1 -1
  72. package/dist/editor/pageModel.d.ts +1 -0
  73. package/dist/editor/reviews/Comment.js +1 -1
  74. package/dist/editor/reviews/Comment.js.map +1 -1
  75. package/dist/editor/reviews/CommentDisplayPopover.js +3 -24
  76. package/dist/editor/reviews/CommentDisplayPopover.js.map +1 -1
  77. package/dist/editor/reviews/CommentPopover.js +3 -23
  78. package/dist/editor/reviews/CommentPopover.js.map +1 -1
  79. package/dist/editor/reviews/CommentView.js +2 -1
  80. package/dist/editor/reviews/CommentView.js.map +1 -1
  81. package/dist/editor/reviews/Comments.js +88 -37
  82. package/dist/editor/reviews/Comments.js.map +1 -1
  83. package/dist/editor/reviews/commentAi.js +3 -0
  84. package/dist/editor/reviews/commentAi.js.map +1 -1
  85. package/dist/editor/sidebar/Debug.js +1 -5
  86. package/dist/editor/sidebar/Debug.js.map +1 -1
  87. package/dist/editor/sidebar/ViewSelector.js +72 -6
  88. package/dist/editor/sidebar/ViewSelector.js.map +1 -1
  89. package/dist/editor/ui/Icons.d.ts +5 -0
  90. package/dist/editor/ui/Icons.js +14 -0
  91. package/dist/editor/ui/Icons.js.map +1 -1
  92. package/dist/editor/utils.d.ts +5 -0
  93. package/dist/editor/utils.js +29 -0
  94. package/dist/editor/utils.js.map +1 -1
  95. package/dist/revision.d.ts +2 -2
  96. package/dist/revision.js +2 -2
  97. package/dist/splash-screen/SplashScreen.js +2 -2
  98. package/dist/splash-screen/SplashScreen.js.map +1 -1
  99. package/dist/styles.css +3 -0
  100. package/dist/types.d.ts +2 -0
  101. package/package.json +1 -1
  102. package/src/components/ui/card.tsx +1 -1
  103. package/src/components/ui/paste-button.tsx +163 -0
  104. package/src/config/config.tsx +68 -2
  105. package/src/config/types.ts +26 -0
  106. package/src/editor/ContentTree.tsx +48 -23
  107. package/src/editor/FieldListField.tsx +75 -2
  108. package/src/editor/ai/AgentTerminal.tsx +118 -71
  109. package/src/editor/ai/Agents.tsx +52 -1
  110. package/src/editor/ai/AiResponseMessage.tsx +234 -78
  111. package/src/editor/ai/AiTerminal.tsx +30 -14
  112. package/src/editor/client/EditorShell.tsx +128 -25
  113. package/src/editor/client/editContext.ts +1 -0
  114. package/src/editor/client/hooks/useSocketMessageHandler.ts +70 -21
  115. package/src/editor/client/hooks/useWorkbox.ts +4 -4
  116. package/src/editor/client/itemsRepository.ts +56 -25
  117. package/src/editor/client/pageModelBuilder.ts +1 -1
  118. package/src/editor/control-center/Setup.tsx +26 -0
  119. package/src/editor/control-center/setup-steps/AiSetupStep.tsx +462 -0
  120. package/src/editor/control-center/setup-steps/DbSetupStep.tsx +84 -0
  121. package/src/editor/control-center/setup-steps/IndexSetupStep.tsx +56 -0
  122. package/src/editor/control-center/setup-steps/SettingsSetupStep.tsx +176 -0
  123. package/src/editor/field-types/InternalLinkFieldEditor.tsx +3 -1
  124. package/src/editor/field-types/RichTextEditorComponent.tsx +0 -1
  125. package/src/editor/field-types/richtext/components/ReactSlate.tsx +14 -6
  126. package/src/editor/field-types/richtext/utils/profileServiceCache.ts +42 -32
  127. package/src/editor/menubar/toolbar-sections/CompareControls.tsx +1 -0
  128. package/src/editor/menubar/toolbar-sections/EditControls.tsx +1 -0
  129. package/src/editor/menubar/toolbar-sections/ViewportControls.tsx +1 -0
  130. package/src/editor/page-editor-chrome/InlineEditor.tsx +29 -6
  131. package/src/editor/page-viewer/EditorForm.tsx +13 -2
  132. package/src/editor/page-viewer/PageViewerFrame.tsx +4 -0
  133. package/src/editor/pageModel.ts +1 -0
  134. package/src/editor/reviews/Comment.tsx +1 -1
  135. package/src/editor/reviews/CommentDisplayPopover.tsx +2 -22
  136. package/src/editor/reviews/CommentPopover.tsx +3 -24
  137. package/src/editor/reviews/CommentView.tsx +3 -2
  138. package/src/editor/reviews/Comments.tsx +162 -35
  139. package/src/editor/reviews/commentAi.ts +5 -0
  140. package/src/editor/sidebar/Debug.tsx +1 -5
  141. package/src/editor/sidebar/ViewSelector.tsx +144 -28
  142. package/src/editor/ui/Icons.tsx +55 -0
  143. package/src/editor/utils.ts +33 -0
  144. package/src/revision.ts +2 -2
  145. package/src/splash-screen/SplashScreen.tsx +5 -6
  146. package/src/types.ts +3 -0
@@ -2,11 +2,12 @@ import React, { useState, useEffect, useMemo } from "react";
2
2
 
3
3
  import { useEditContext } from "../client/editContext";
4
4
  import { EditOperation } from "../../types";
5
- import { Message } from "./AiTerminal";
5
+ import { Message, ToolCall } from "./AiTerminal";
6
6
  import { ToolCallDisplay } from "./ToolCallDisplay";
7
7
 
8
8
  import { X, Bot, Loader2 } from "lucide-react";
9
9
  import { Button } from "../../components/ui/button";
10
+ import { Checkbox } from "../../components/ui/checkbox";
10
11
 
11
12
  type QuickAction = {
12
13
  id?: string;
@@ -18,7 +19,12 @@ type QuickAction = {
18
19
 
19
20
  type ContentSegment =
20
21
  | { kind: "text"; text: string }
21
- | { kind: "actions"; actions: QuickAction[] };
22
+ | { kind: "actions"; actions: QuickAction[] }
23
+ | {
24
+ kind: "todo";
25
+ title?: string;
26
+ items: { id?: string; text: string; done?: boolean; note?: string }[];
27
+ };
22
28
 
23
29
  function parseContentSegments(
24
30
  content?: string,
@@ -27,22 +33,24 @@ function parseContentSegments(
27
33
  if (!content) return [];
28
34
 
29
35
  const segments: ContentSegment[] = [];
30
- const fencedOpenToken = "```quick_action_buttons";
31
- const plainToken = "quick_action_buttons";
36
+ const fencedQuickToken = "```quick_action_buttons";
37
+ const plainQuickToken = "quick_action_buttons";
38
+ const fencedTodoToken = "```todo_list";
39
+ const plainTodoToken = "todo_list";
32
40
 
33
41
  // Helper: find next valid plain token occurrence (at line start or after newline)
34
- const findNextPlainToken = (fromIndex: number): number => {
35
- let idx = content.indexOf(plainToken, fromIndex);
42
+ const findNextPlainHeader = (token: string, fromIndex: number): number => {
43
+ let idx = content.indexOf(token, fromIndex);
36
44
  while (idx !== -1) {
37
45
  const before = idx > 0 ? content[idx - 1] : "\n";
38
- const after = content[idx + plainToken.length] || "\n";
46
+ const after = content[idx + token.length] || "\n";
39
47
  const isBoundaryBefore = before === "\n" || before === "\r";
40
48
  const isBoundaryAfter =
41
49
  after === "\n" || after === "\r" || after === ":" || /\s/.test(after);
42
50
  // Ensure it's not the fenced token which would have ``` right before
43
51
  const isFencedPrefix = content.slice(Math.max(0, idx - 3), idx) === "```";
44
52
  if (isBoundaryBefore && isBoundaryAfter && !isFencedPrefix) return idx;
45
- idx = content.indexOf(plainToken, idx + 1);
53
+ idx = content.indexOf(token, idx + 1);
46
54
  }
47
55
  return -1;
48
56
  };
@@ -84,32 +92,54 @@ function parseContentSegments(
84
92
 
85
93
  let cursor = 0;
86
94
  while (cursor < content.length) {
87
- const nextFenced = content.indexOf(fencedOpenToken, cursor);
88
- const nextPlain = findNextPlainToken(cursor);
89
-
90
- // Determine which token occurs first
91
- let useKind: "fenced" | "plain" | null = null;
92
- let start = -1;
93
- if (nextFenced !== -1 && (nextPlain === -1 || nextFenced <= nextPlain)) {
94
- useKind = "fenced";
95
- start = nextFenced;
96
- } else if (nextPlain !== -1) {
97
- useKind = "plain";
98
- start = nextPlain;
95
+ const nextFencedQuick = content.indexOf(fencedQuickToken, cursor);
96
+ const nextPlainQuick = findNextPlainHeader(plainQuickToken, cursor);
97
+ const nextFencedTodo = content.indexOf(fencedTodoToken, cursor);
98
+ const nextPlainTodo = findNextPlainHeader(plainTodoToken, cursor);
99
+
100
+ // Determine which token occurs first among all supported tokens
101
+ type TokenPick = {
102
+ start: number;
103
+ kind: "fenced" | "plain";
104
+ type: "quick" | "todo";
105
+ };
106
+ const candidates: TokenPick[] = [];
107
+ if (nextFencedQuick !== -1)
108
+ candidates.push({
109
+ start: nextFencedQuick,
110
+ kind: "fenced",
111
+ type: "quick",
112
+ });
113
+ if (nextPlainQuick !== -1)
114
+ candidates.push({ start: nextPlainQuick, kind: "plain", type: "quick" });
115
+ if (nextFencedTodo !== -1)
116
+ candidates.push({ start: nextFencedTodo, kind: "fenced", type: "todo" });
117
+ if (nextPlainTodo !== -1)
118
+ candidates.push({ start: nextPlainTodo, kind: "plain", type: "todo" });
119
+
120
+ let pick: TokenPick | null = null;
121
+ if (candidates.length > 0) {
122
+ pick = candidates.reduce((min, cur) =>
123
+ cur.start < min.start ? cur : min,
124
+ );
99
125
  }
100
126
 
101
- if (useKind === null || start === -1) {
127
+ if (pick === null) {
102
128
  const tail = content.slice(cursor);
103
129
  if (tail) segments.push({ kind: "text", text: tail });
104
130
  break;
105
131
  }
106
132
 
107
133
  // Push any text before the token
108
- const textBefore = content.slice(cursor, start);
134
+ const textBefore = content.slice(cursor, pick.start);
109
135
  if (textBefore) segments.push({ kind: "text", text: textBefore });
110
136
 
111
- if (useKind === "fenced") {
112
- const afterOpenLineStart = start + fencedOpenToken.length;
137
+ if (pick.kind === "fenced") {
138
+ const openTokenLength =
139
+ pick.type === "quick"
140
+ ? fencedQuickToken.length
141
+ : fencedTodoToken.length;
142
+ const afterOpenLineStart = pick.start + openTokenLength;
113
143
  const openLineMatch = content
114
144
  .slice(afterOpenLineStart)
115
145
  .match(/^[^\n\r]*\r?\n?/);
@@ -121,7 +151,7 @@ function parseContentSegments(
121
151
  if (hideIncomplete) {
122
152
  break;
123
153
  } else {
124
- segments.push({ kind: "text", text: content.slice(start) });
154
+ segments.push({ kind: "text", text: content.slice(pick.start) });
125
155
  break;
126
156
  }
127
157
  }
@@ -129,31 +159,60 @@ function parseContentSegments(
129
159
  const jsonText = content.slice(jsonStart, close).trim();
130
160
  try {
131
161
  const parsed = JSON.parse(jsonText);
132
- const rawActions = Array.isArray(parsed)
133
- ? parsed
134
- : parsed?.actions || parsed?.buttons || parsed?.choices || [];
135
- const actions: QuickAction[] = [];
136
- if (Array.isArray(rawActions)) {
137
- rawActions.forEach((a) => {
138
- if (!a) return;
139
- const action: QuickAction = {
140
- id: a.id,
141
- label: a.label || a.text || String(a.value || a.prompt || ""),
142
- prompt: a.prompt,
143
- value: a.value,
144
- style: a.style,
145
- };
146
- if (action.label) actions.push(action);
147
- });
148
- }
149
- if (actions.length > 0) {
150
- segments.push({ kind: "actions", actions });
162
+ if (pick.type === "quick") {
163
+ const rawActions = Array.isArray(parsed)
164
+ ? parsed
165
+ : parsed?.actions || parsed?.buttons || parsed?.choices || [];
166
+ const actions: QuickAction[] = [];
167
+ if (Array.isArray(rawActions)) {
168
+ rawActions.forEach((a) => {
169
+ if (!a) return;
170
+ const action: QuickAction = {
171
+ id: a.id,
172
+ label: a.label || a.text || String(a.value || a.prompt || ""),
173
+ prompt: a.prompt,
174
+ value: a.value,
175
+ style: a.style,
176
+ };
177
+ if (action.label) actions.push(action);
178
+ });
179
+ }
180
+ if (actions.length > 0) {
181
+ segments.push({ kind: "actions", actions });
182
+ }
183
+ } else {
184
+ // todo_list
185
+ const todoItems = Array.isArray(parsed)
186
+ ? parsed
187
+ : parsed?.items || [];
188
+ const title = Array.isArray(parsed) ? undefined : parsed?.title;
189
+ const cleaned = (Array.isArray(todoItems) ? todoItems : [])
190
+ .map((i) => {
191
+ if (!i) return null;
192
+ const text = i.text || i.label || String(i.task || i.title || "");
193
+ if (!text) return null;
194
+ return {
195
+ id: i.id,
196
+ text,
197
+ done: !!(i.done ?? i.completed ?? i.checked),
198
+ note: i.note || i.description,
199
+ } as { id?: string; text: string; done?: boolean; note?: string };
200
+ })
201
+ .filter(Boolean) as {
202
+ id?: string;
203
+ text: string;
204
+ done?: boolean;
205
+ note?: string;
206
+ }[];
207
+ if (cleaned.length > 0) {
208
+ segments.push({ kind: "todo", title, items: cleaned });
209
+ }
151
210
  }
152
211
  } catch {
153
212
  if (!hideIncomplete) {
154
213
  segments.push({
155
214
  kind: "text",
156
- text: content.slice(start, close + 3),
215
+ text: content.slice(pick.start, close + 3),
157
216
  });
158
217
  }
159
218
  }
@@ -162,7 +221,9 @@ function parseContentSegments(
162
221
  }
163
222
 
164
223
  // Plain header without backticks
165
- const afterHeaderStart = start + plainToken.length;
224
+ const headerTokenLength =
225
+ pick.type === "quick" ? plainQuickToken.length : plainTodoToken.length;
226
+ const afterHeaderStart = pick.start + headerTokenLength;
166
227
  const headerRestMatch = content
167
228
  .slice(afterHeaderStart)
168
229
  .match(/^[^\n\r]*\r?\n?/);
@@ -187,7 +248,7 @@ function parseContentSegments(
187
248
  break;
188
249
  } else {
189
250
  // Incomplete JSON; include the rest as text
190
- const remaining = content.slice(start);
251
+ const remaining = content.slice(pick.start);
191
252
  if (remaining) segments.push({ kind: "text", text: remaining });
192
253
  break;
193
254
  }
@@ -196,29 +257,55 @@ function parseContentSegments(
196
257
  const jsonText = content.slice(braceStart, braceEnd + 1).trim();
197
258
  try {
198
259
  const parsed = JSON.parse(jsonText);
199
- const rawActions = Array.isArray(parsed)
200
- ? parsed
201
- : parsed?.actions || parsed?.buttons || parsed?.choices || [];
202
- const actions: QuickAction[] = [];
203
- if (Array.isArray(rawActions)) {
204
- rawActions.forEach((a) => {
205
- if (!a) return;
206
- const action: QuickAction = {
207
- id: a.id,
208
- label: a.label || a.text || String(a.value || a.prompt || ""),
209
- prompt: a.prompt,
210
- value: a.value,
211
- style: a.style,
212
- };
213
- if (action.label) actions.push(action);
214
- });
215
- }
216
- if (actions.length > 0) {
217
- segments.push({ kind: "actions", actions });
260
+ if (pick.type === "quick") {
261
+ const rawActions = Array.isArray(parsed)
262
+ ? parsed
263
+ : parsed?.actions || parsed?.buttons || parsed?.choices || [];
264
+ const actions: QuickAction[] = [];
265
+ if (Array.isArray(rawActions)) {
266
+ rawActions.forEach((a) => {
267
+ if (!a) return;
268
+ const action: QuickAction = {
269
+ id: a.id,
270
+ label: a.label || a.text || String(a.value || a.prompt || ""),
271
+ prompt: a.prompt,
272
+ value: a.value,
273
+ style: a.style,
274
+ };
275
+ if (action.label) actions.push(action);
276
+ });
277
+ }
278
+ if (actions.length > 0) {
279
+ segments.push({ kind: "actions", actions });
280
+ }
281
+ } else {
282
+ const todoItems = Array.isArray(parsed) ? parsed : parsed?.items || [];
283
+ const title = Array.isArray(parsed) ? undefined : parsed?.title;
284
+ const cleaned = (Array.isArray(todoItems) ? todoItems : [])
285
+ .map((i) => {
286
+ if (!i) return null;
287
+ const text = i.text || i.label || String(i.task || i.title || "");
288
+ if (!text) return null;
289
+ return {
290
+ id: i.id,
291
+ text,
292
+ done: !!(i.done ?? i.completed ?? i.checked),
293
+ note: i.note || i.description,
294
+ } as { id?: string; text: string; done?: boolean; note?: string };
295
+ })
296
+ .filter(Boolean) as {
297
+ id?: string;
298
+ text: string;
299
+ done?: boolean;
300
+ note?: string;
301
+ }[];
302
+ if (cleaned.length > 0) {
303
+ segments.push({ kind: "todo", title, items: cleaned });
304
+ }
218
305
  }
219
306
  } catch {
220
307
  // On parse failure, keep original as text (excluding already-pushed prefix)
221
- const fallbackText = content.slice(start, braceEnd + 1);
308
+ const fallbackText = content.slice(pick.start, braceEnd + 1);
222
309
  if (!hideIncomplete && fallbackText) {
223
310
  segments.push({ kind: "text", text: fallbackText });
224
311
  }
@@ -260,7 +347,7 @@ export function AiResponseMessage({
260
347
 
261
348
  // Store tool calls to preserve them during streaming
262
349
  const [preservedToolCalls, setPreservedToolCalls] = useState<{
263
- [messageId: string]: any[];
350
+ [messageId: string]: ToolCall[];
264
351
  }>({});
265
352
 
266
353
  // Track popover open state for each tool call
@@ -268,18 +355,46 @@ export function AiResponseMessage({
268
355
  [key: string]: boolean;
269
356
  }>({});
270
357
 
271
- // Update preserved tool calls when messages change
272
- useEffect(() => {
273
- const newPreservedToolCalls = { ...preservedToolCalls };
358
+ // Helper to avoid unnecessary updates when tool calls are unchanged
359
+ const areToolCallsEqual = (a?: ToolCall[], b?: ToolCall[]): boolean => {
360
+ if (a === b) return true;
361
+ if (!a || !b) return false;
362
+ if (a.length !== b.length) return false;
363
+ for (let i = 0; i < a.length; i++) {
364
+ const ai = a[i];
365
+ const bi = b[i];
366
+ if (
367
+ ai?.id !== bi?.id ||
368
+ ai?.displayName !== bi?.displayName ||
369
+ ai?.function?.name !== bi?.function?.name ||
370
+ ai?.function?.arguments !== bi?.function?.arguments
371
+ ) {
372
+ return false;
373
+ }
374
+ }
375
+ return true;
376
+ };
274
377
 
275
- messages.forEach((message) => {
276
- if (message.tool_calls && message.tool_calls.length > 0) {
277
- // Store non-empty tool calls
278
- newPreservedToolCalls[message.id] = message.tool_calls;
378
+ // Update preserved tool calls when messages change, but only if values actually change
379
+ useEffect(() => {
380
+ setPreservedToolCalls((prev) => {
381
+ let changed = false;
382
+ const next = { ...prev };
383
+
384
+ for (const message of messages) {
385
+ const hasToolCalls = !!(
386
+ message.tool_calls && message.tool_calls.length > 0
387
+ );
388
+ if (!hasToolCalls) continue;
389
+
390
+ if (!areToolCallsEqual(next[message.id], message.tool_calls)) {
391
+ next[message.id] = message.tool_calls!;
392
+ changed = true;
393
+ }
279
394
  }
280
- });
281
395
 
282
- setPreservedToolCalls(newPreservedToolCalls);
396
+ return changed ? next : prev;
397
+ });
283
398
  }, [messages]);
284
399
 
285
400
  const reversedEditOperations = [...editOperations].reverse();
@@ -317,7 +432,7 @@ export function AiResponseMessage({
317
432
  }, [nonToolAssistantMessages, isStreaming]);
318
433
 
319
434
  return (
320
- <div className="flex gap-3 p-4">
435
+ <div className="flex gap-3 p-4" data-testid="agent-message">
321
436
  <div className="flex-shrink-0">
322
437
  <Bot className="h-6 w-6 text-green-600" strokeWidth={1} />
323
438
  </div>
@@ -376,6 +491,47 @@ export function AiResponseMessage({
376
491
  />
377
492
  );
378
493
  }
494
+ if (segment.kind === "todo") {
495
+ const todo = segment;
496
+ return (
497
+ <div key={"todo-" + idx} className="my-2">
498
+ {todo.title && (
499
+ <div className="mb-1 text-sm font-medium text-gray-800">
500
+ {todo.title}
501
+ </div>
502
+ )}
503
+ <div className="flex flex-col gap-1">
504
+ {todo.items.map((item, iIdx) => (
505
+ <label
506
+ key={(item.id || item.text || "todo") + "-" + iIdx}
507
+ className="flex items-start gap-2 text-sm text-gray-700"
508
+ >
509
+ <Checkbox
510
+ checked={!!item.done}
511
+ onCheckedChange={() => {}}
512
+ aria-readonly
513
+ className="mt-0.5"
514
+ />
515
+ <div>
516
+ <div
517
+ className={
518
+ item.done ? "line-through opacity-70" : ""
519
+ }
520
+ >
521
+ {item.text}
522
+ </div>
523
+ {item.note && (
524
+ <div className="text-xs text-gray-500">
525
+ {item.note}
526
+ </div>
527
+ )}
528
+ </div>
529
+ </label>
530
+ ))}
531
+ </div>
532
+ </div>
533
+ );
534
+ }
379
535
  const actions = segment.actions;
380
536
  return (
381
537
  <div key={"act-" + idx} className="my-2 flex flex-wrap gap-2">
@@ -342,20 +342,36 @@ export function AiTerminal({
342
342
 
343
343
  // Build complete message history for API call
344
344
  const conversationHistory = [...messagesRef.current, userMessage];
345
- const messages = [
346
- ...(options?.hiddenSystemPrompt
347
- ? [
348
- {
349
- role: "system",
350
- name: "system",
351
- content: options.hiddenSystemPrompt,
352
- id: crypto.randomUUID(), // Use UUID instead of hardcoded 0
353
- toolCalls: [],
354
- },
355
- ]
356
- : []),
357
- ...conversationHistory,
358
- ];
345
+ const shouldAddTodoFormat = /\b(to-?do|check\s*list|task\s*list)\b/i.test(
346
+ text,
347
+ );
348
+ const todoFormatInstructions =
349
+ "If you produce a to-do list, output it as a 'todo_list' JSON block. Prefer one of these two forms only, with no extra commentary before/after it: \n" +
350
+ '```todo_list\n{ "title": "<optional title>", "items": [ { "text": "<task>", "done": false, "note": "<optional>" } ] }\n```\n' +
351
+ "or a plain header: todo_list: { ... }. Include only fields: title (optional), items (array of {text, done?, note?}).";
352
+ const systemMessages = options?.hiddenSystemPrompt
353
+ ? [
354
+ {
355
+ role: "system",
356
+ name: "system",
357
+ content: options.hiddenSystemPrompt,
358
+ id: crypto.randomUUID(),
359
+ toolCalls: [],
360
+ },
361
+ ]
362
+ : [];
363
+ const todoSystem = shouldAddTodoFormat
364
+ ? [
365
+ {
366
+ role: "system",
367
+ name: "system",
368
+ content: todoFormatInstructions,
369
+ id: crypto.randomUUID(),
370
+ toolCalls: [],
371
+ },
372
+ ]
373
+ : [];
374
+ const messages = [...systemMessages, ...todoSystem, ...conversationHistory];
359
375
 
360
376
  const response = await executePrompt(
361
377
  messages,