@alpaca-editor/core 1.0.4133 → 1.0.4134

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.
@@ -88,10 +88,116 @@ interface TodoItem {
88
88
  text: string;
89
89
  done?: boolean;
90
90
  note?: string;
91
- messageId: string;
91
+ messageId?: string;
92
92
  sourceTitle?: string;
93
93
  }
94
94
 
95
+ // Helper to extract todos from potentially incomplete JSON during streaming
96
+ const extractPartialTodos = (jsonText: string): any[] => {
97
+ // First try to parse complete JSON
98
+ try {
99
+ const parsed = JSON.parse(jsonText);
100
+ return Array.isArray(parsed) ? parsed : parsed?.items || [];
101
+ } catch (e) {
102
+ // If JSON is incomplete, try to extract whatever todo items we can find
103
+ const items: any[] = [];
104
+
105
+ // Look for individual todo objects in the partial JSON
106
+ // Match patterns like: { "text": "...", "done": false, "note": "..." }
107
+ // Handle various field orderings (text can be anywhere in the object)
108
+ const textPattern = /"text"\s*:\s*"([^"]+)"/g;
109
+ const textMatches: Array<{ text: string; startIdx: number }> = [];
110
+ let textMatch;
111
+ while ((textMatch = textPattern.exec(jsonText)) !== null) {
112
+ if (textMatch[1]) {
113
+ textMatches.push({
114
+ text: textMatch[1],
115
+ startIdx: textMatch.index,
116
+ });
117
+ }
118
+ }
119
+
120
+ // For each text field found, try to find the enclosing object
121
+ for (const { text, startIdx } of textMatches) {
122
+ // Find the opening brace before this text field
123
+ let openBrace = -1;
124
+ for (let i = startIdx - 1; i >= 0; i--) {
125
+ if (jsonText[i] === "{") {
126
+ openBrace = i;
127
+ break;
128
+ }
129
+ if (jsonText[i] === "}") break; // Hit another object's end
130
+ }
131
+
132
+ if (openBrace === -1) continue;
133
+
134
+ // Find the closing brace after this text field
135
+ let closeBrace = -1;
136
+ let depth = 0;
137
+ for (let i = openBrace; i < jsonText.length; i++) {
138
+ if (jsonText[i] === "{") depth++;
139
+ if (jsonText[i] === "}") {
140
+ depth--;
141
+ if (depth === 0) {
142
+ closeBrace = i;
143
+ break;
144
+ }
145
+ }
146
+ }
147
+
148
+ // Extract the object and try to parse it
149
+ const objStr =
150
+ closeBrace !== -1
151
+ ? jsonText.substring(openBrace, closeBrace + 1)
152
+ : jsonText.substring(openBrace) + "}"; // Try to close incomplete object
153
+
154
+ try {
155
+ const obj = JSON.parse(objStr);
156
+ if (obj.text) {
157
+ items.push({
158
+ text: obj.text,
159
+ done: obj.done === true,
160
+ note: obj.note || undefined,
161
+ });
162
+ }
163
+ } catch (e) {
164
+ // Skip malformed objects
165
+ }
166
+ }
167
+
168
+ // Also try to extract from partial objects at the end
169
+ // Look for the last opening brace and try to parse up to where we have valid content
170
+ const lines = jsonText.split("\n");
171
+ for (let i = lines.length - 1; i >= 0; i--) {
172
+ const partialJson = lines.slice(0, i + 1).join("\n");
173
+ // Try to close any open braces/brackets
174
+ let testJson = partialJson;
175
+ const openBraces = (testJson.match(/\{/g) || []).length;
176
+ const closeBraces = (testJson.match(/\}/g) || []).length;
177
+ const openBrackets = (testJson.match(/\[/g) || []).length;
178
+ const closeBrackets = (testJson.match(/\]/g) || []).length;
179
+
180
+ // Add missing closing characters
181
+ testJson += "]".repeat(openBrackets - closeBrackets);
182
+ testJson += "}".repeat(openBraces - closeBraces);
183
+
184
+ try {
185
+ const parsed = JSON.parse(testJson);
186
+ const partialItems = Array.isArray(parsed)
187
+ ? parsed
188
+ : parsed?.items || [];
189
+ if (partialItems.length > items.length) {
190
+ return partialItems;
191
+ }
192
+ } catch (e) {
193
+ continue;
194
+ }
195
+ }
196
+
197
+ return items;
198
+ }
199
+ };
200
+
95
201
  const extractTodosFromMessages = (messages: AgentChatMessage[]): TodoItem[] => {
96
202
  const todos: TodoItem[] = [];
97
203
  const fencedTodoToken = "```todo_list";
@@ -126,12 +232,20 @@ const extractTodosFromMessages = (messages: AgentChatMessage[]): TodoItem[] => {
126
232
 
127
233
  try {
128
234
  let jsonText = "";
235
+ let isComplete = true;
236
+
129
237
  if (isFenced) {
130
238
  const afterToken = todoStart + fencedTodoToken.length;
131
239
  const closePos = content.indexOf("```", afterToken);
132
- if (closePos === -1) break;
133
- jsonText = content.slice(afterToken, closePos).trim();
134
- cursor = closePos + 3;
240
+ if (closePos === -1) {
241
+ // Incomplete fenced block - extract what we have so far
242
+ jsonText = content.slice(afterToken).trim();
243
+ isComplete = false;
244
+ cursor = content.length; // Process till end
245
+ } else {
246
+ jsonText = content.slice(afterToken, closePos).trim();
247
+ cursor = closePos + 3;
248
+ }
135
249
  } else {
136
250
  const afterToken = todoStart + plainTodoToken.length;
137
251
  const braceStart = content.indexOf("{", afterToken);
@@ -149,24 +263,56 @@ const extractTodosFromMessages = (messages: AgentChatMessage[]): TodoItem[] => {
149
263
  }
150
264
  }
151
265
  }
152
- if (braceEnd === -1) break;
153
- jsonText = content.slice(braceStart, braceEnd + 1).trim();
154
- cursor = braceEnd + 1;
266
+
267
+ if (braceEnd === -1) {
268
+ // Incomplete JSON - extract what we have
269
+ jsonText = content.slice(braceStart).trim();
270
+ isComplete = false;
271
+ cursor = content.length;
272
+ } else {
273
+ jsonText = content.slice(braceStart, braceEnd + 1).trim();
274
+ cursor = braceEnd + 1;
275
+ }
155
276
  }
156
277
 
157
- const parsed = JSON.parse(jsonText);
158
- const todoItems = Array.isArray(parsed) ? parsed : parsed?.items || [];
159
- const title = Array.isArray(parsed) ? undefined : parsed?.title;
278
+ // Use the partial extraction helper for incomplete JSON
279
+ const todoItems = isComplete
280
+ ? (() => {
281
+ try {
282
+ const parsed = JSON.parse(jsonText);
283
+ return Array.isArray(parsed) ? parsed : parsed?.items || [];
284
+ } catch (e) {
285
+ return [];
286
+ }
287
+ })()
288
+ : extractPartialTodos(jsonText);
289
+
290
+ const title = (() => {
291
+ try {
292
+ const parsed = JSON.parse(jsonText);
293
+ return Array.isArray(parsed) ? undefined : parsed?.title;
294
+ } catch (e) {
295
+ return undefined;
296
+ }
297
+ })();
160
298
 
161
299
  todoItems.forEach((item: any) => {
162
300
  if (!item) return;
163
301
  const text =
164
- item.text || item.label || String(item.task || item.title || "");
302
+ item.text ||
303
+ item.content ||
304
+ item.label ||
305
+ String(item.task || item.title || "");
165
306
  if (!text) return;
166
307
  todos.push({
167
308
  id: item.id,
168
309
  text,
169
- done: !!(item.done ?? item.completed ?? item.checked),
310
+ done: !!(
311
+ item.done ??
312
+ item.completed ??
313
+ item.checked ??
314
+ item.status === "completed"
315
+ ),
170
316
  note: item.note || item.description,
171
317
  messageId: message.id,
172
318
  sourceTitle: title,
@@ -183,22 +329,101 @@ const extractTodosFromMessages = (messages: AgentChatMessage[]): TodoItem[] => {
183
329
  };
184
330
 
185
331
  // TodoListPanel component
186
- const TodoListPanel = ({ messages }: { messages: AgentChatMessage[] }) => {
332
+ const TodoListPanel = ({
333
+ messages,
334
+ agentMetadata,
335
+ }: {
336
+ messages: AgentChatMessage[];
337
+ agentMetadata: AgentMetadata | null;
338
+ }) => {
187
339
  const [isExpanded, setIsExpanded] = useState(true);
188
- const todos = useMemo(() => extractTodosFromMessages(messages), [messages]);
189
340
 
190
- // Check if there's an active streaming message with todo content
341
+ const todos = useMemo(() => {
342
+ // First try to get todos from agent metadata (real-time updates)
343
+ const metadataTodos = (() => {
344
+ try {
345
+ const context = (agentMetadata as any)?.additionalData?.context;
346
+ const todoList = context?.todoList;
347
+ if (todoList?.items && Array.isArray(todoList.items)) {
348
+ return todoList.items
349
+ .map((item: any, idx: number) => ({
350
+ id: item.id || `metadata-${idx}`,
351
+ text:
352
+ item.text ||
353
+ item.label ||
354
+ String(item.task || item.title || ""),
355
+ done: !!(item.done ?? item.completed ?? item.checked),
356
+ note: item.note || item.description,
357
+ messageId: undefined,
358
+ sourceTitle: todoList.title,
359
+ }))
360
+ .filter((item: any) => item.text);
361
+ }
362
+ } catch (e) {
363
+ // Fallback to extracting from messages
364
+ }
365
+ return null;
366
+ })();
367
+
368
+ // If we have metadata todos, use them; otherwise extract from messages
369
+ if (metadataTodos && metadataTodos.length > 0) {
370
+ return metadataTodos;
371
+ }
372
+
373
+ return extractTodosFromMessages(messages);
374
+ }, [messages, agentMetadata]);
375
+
376
+ // Check if there's an active streaming message with incomplete todo content
191
377
  const isUpdating = useMemo(() => {
192
378
  return messages.some((msg) => {
193
379
  if (msg.role !== "assistant" || msg.isCompleted) return false;
194
380
  const content = msg.content || "";
195
- return content.includes("```todo_list") || content.includes("todo_list");
381
+
382
+ // Check for incomplete fenced todo blocks
383
+ const fencedStart = content.indexOf("```todo_list");
384
+ if (fencedStart !== -1) {
385
+ const afterStart = fencedStart + "```todo_list".length;
386
+ const closePos = content.indexOf("```", afterStart);
387
+ if (closePos === -1) {
388
+ // Incomplete fenced block
389
+ return true;
390
+ }
391
+ }
392
+
393
+ // Check for incomplete plain todo blocks
394
+ const plainStart = content.indexOf("todo_list");
395
+ if (plainStart !== -1 && plainStart !== fencedStart) {
396
+ const before = plainStart > 0 ? content[plainStart - 1] : "\n";
397
+ if (before === "\n" || before === "\r" || plainStart === 0) {
398
+ const braceStart = content.indexOf("{", plainStart);
399
+ if (braceStart !== -1) {
400
+ let depth = 0;
401
+ let braceEnd = -1;
402
+ for (let i = braceStart; i < content.length; i++) {
403
+ if (content[i] === "{") depth++;
404
+ if (content[i] === "}") {
405
+ depth--;
406
+ if (depth === 0) {
407
+ braceEnd = i;
408
+ break;
409
+ }
410
+ }
411
+ }
412
+ if (braceEnd === -1) {
413
+ // Incomplete plain block
414
+ return true;
415
+ }
416
+ }
417
+ }
418
+ }
419
+
420
+ return false;
196
421
  });
197
422
  }, [messages]);
198
423
 
199
424
  if (todos.length === 0 && !isUpdating) return null;
200
425
 
201
- const completedCount = todos.filter((t) => t.done).length;
426
+ const completedCount = todos.filter((t: TodoItem) => t.done).length;
202
427
  const totalCount = todos.length;
203
428
 
204
429
  return (
@@ -229,66 +454,69 @@ const TodoListPanel = ({ messages }: { messages: AgentChatMessage[] }) => {
229
454
  </button>
230
455
  {isExpanded && (
231
456
  <div className="max-h-64 overflow-y-auto px-4 pb-3">
232
- {isUpdating && todos.length === 0 ? (
233
- <div className="flex items-center justify-center gap-2 py-4 text-xs text-gray-500">
234
- <Loader2 className="h-4 w-4 animate-spin" strokeWidth={1} />
235
- <span>Loading todo list...</span>
236
- </div>
237
- ) : (
238
- <>
239
- <div className="space-y-1.5">
240
- {todos.map((todo, idx) => (
241
- <div
242
- key={todo.id || `${todo.messageId}-${idx}`}
243
- className="flex items-start gap-2 rounded bg-white p-2 text-xs"
244
- >
245
- <div className="flex-shrink-0 pt-0.5">
246
- {todo.done ? (
247
- <div className="flex h-4 w-4 items-center justify-center rounded border-2 border-green-500 bg-green-500">
248
- <svg
249
- className="h-3 w-3 text-white"
250
- fill="none"
251
- strokeWidth={2}
252
- stroke="currentColor"
253
- viewBox="0 0 24 24"
254
- >
255
- <path
256
- strokeLinecap="round"
257
- strokeLinejoin="round"
258
- d="M5 13l4 4L19 7"
259
- />
260
- </svg>
261
- </div>
262
- ) : (
263
- <div className="h-4 w-4 rounded border-2 border-gray-300" />
264
- )}
265
- </div>
266
- <div className="min-w-0 flex-1">
267
- <div
268
- className={`${
269
- todo.done
270
- ? "text-gray-500 line-through"
271
- : "text-gray-900"
272
- }`}
273
- >
274
- {todo.text}
457
+ {todos.length > 0 && (
458
+ <div className="space-y-1.5">
459
+ {todos.map((todo: TodoItem, idx: number) => (
460
+ <div
461
+ key={todo.id || `${todo.messageId}-${idx}`}
462
+ className="flex items-start gap-2 rounded bg-white p-2 text-xs"
463
+ >
464
+ <div className="flex-shrink-0 pt-0.5">
465
+ {todo.done ? (
466
+ <div className="flex h-4 w-4 items-center justify-center rounded border-2 border-green-500 bg-green-500">
467
+ <svg
468
+ className="h-3 w-3 text-white"
469
+ fill="none"
470
+ strokeWidth={2}
471
+ stroke="currentColor"
472
+ viewBox="0 0 24 24"
473
+ >
474
+ <path
475
+ strokeLinecap="round"
476
+ strokeLinejoin="round"
477
+ d="M5 13l4 4L19 7"
478
+ />
479
+ </svg>
275
480
  </div>
276
- {todo.note && (
277
- <div className="mt-0.5 text-xs text-gray-500">
278
- {todo.note}
279
- </div>
280
- )}
481
+ ) : (
482
+ <div className="h-4 w-4 rounded border-2 border-gray-300" />
483
+ )}
484
+ </div>
485
+ <div className="min-w-0 flex-1">
486
+ <div
487
+ className={`${
488
+ todo.done
489
+ ? "text-gray-500 line-through"
490
+ : "text-gray-900"
491
+ }`}
492
+ >
493
+ {todo.text}
281
494
  </div>
495
+ {todo.note && (
496
+ <div className="mt-0.5 text-xs text-gray-500">
497
+ {todo.note}
498
+ </div>
499
+ )}
282
500
  </div>
283
- ))}
284
- </div>
285
- {isUpdating && todos.length > 0 && (
286
- <div className="mt-2 flex items-center gap-2 rounded bg-blue-50 px-3 py-2 text-xs text-blue-700">
287
- <Loader2 className="h-3 w-3 animate-spin" strokeWidth={1} />
288
- <span>Updating todo list...</span>
289
501
  </div>
290
- )}
291
- </>
502
+ ))}
503
+ </div>
504
+ )}
505
+ {isUpdating && (
506
+ <div
507
+ className={`flex items-center gap-2 rounded px-3 py-2 text-xs ${
508
+ todos.length > 0
509
+ ? "mt-2 bg-blue-50 text-blue-700"
510
+ : "justify-center bg-white text-gray-500"
511
+ }`}
512
+ >
513
+ <Loader2 className="h-3 w-3 animate-spin" strokeWidth={1} />
514
+ <span>
515
+ {todos.length > 0
516
+ ? "Updating todo list..."
517
+ : "Loading todo list..."}
518
+ </span>
519
+ </div>
292
520
  )}
293
521
  </div>
294
522
  )}
@@ -423,7 +651,7 @@ export function AgentTerminal({
423
651
  const [messages, setMessages] = useState<AgentChatMessage[]>([]);
424
652
  const [prompt, setPrompt] = useState("");
425
653
  const [inputPlaceholder, setInputPlaceholder] = useState<string>(
426
- "Type your message... (Enter to send, Ctrl+Enter for new line)",
654
+ "Type your message... (Enter to send, Shift+Enter or Ctrl+Enter for new line)",
427
655
  );
428
656
  const [isLoading, setIsLoading] = useState(false);
429
657
  const [isConnecting, setIsConnecting] = useState(false);
@@ -552,6 +780,18 @@ export function AgentTerminal({
552
780
  initialCostLimit: number;
553
781
  } | null>(null);
554
782
 
783
+ // Live running totals from backend status updates (tokenUsage)
784
+ const [liveTotals, setLiveTotals] = useState<{
785
+ input: number;
786
+ output: number;
787
+ cached: number;
788
+ inputCost: number;
789
+ outputCost: number;
790
+ cachedCost: number;
791
+ totalCost: number;
792
+ currency?: string;
793
+ } | null>(null);
794
+
555
795
  // Flag to track when we should create a new message
556
796
  const shouldCreateNewMessage = useRef(false);
557
797
 
@@ -1102,6 +1342,26 @@ export function AgentTerminal({
1102
1342
  case "statusUpdate":
1103
1343
  try {
1104
1344
  const kind = (message as any)?.data?.kind;
1345
+ // Live token usage totals update from backend
1346
+ if (kind === "tokenUsage") {
1347
+ const totals = (message as any)?.data?.totals;
1348
+ if (totals) {
1349
+ setLiveTotals({
1350
+ input: Number(totals.totalInputTokens) || 0,
1351
+ output: Number(totals.totalOutputTokens) || 0,
1352
+ cached: Number(totals.totalCachedInputTokens) || 0,
1353
+ inputCost: Number(totals.totalInputTokenCost) || 0,
1354
+ outputCost: Number(totals.totalOutputTokenCost) || 0,
1355
+ cachedCost:
1356
+ Number(totals.totalCachedInputTokenCost) || 0,
1357
+ totalCost: Number(totals.totalCost) || 0,
1358
+ currency: totals.currency,
1359
+ });
1360
+ // Force a re-render to update cost display immediately
1361
+ setMessages((prev) => [...prev]);
1362
+ }
1363
+ break;
1364
+ }
1105
1365
  if (kind === "toolApprovalsRequired") {
1106
1366
  const data = (message as any).data || {};
1107
1367
  const msgId: string | undefined = data.messageId;
@@ -1313,6 +1573,25 @@ export function AgentTerminal({
1313
1573
  (updatedMessage.inputTokens || 0) +
1314
1574
  (updatedMessage.outputTokens || 0);
1315
1575
 
1576
+ // Update cost data if provided in the completed event
1577
+ if (data.inputTokenCost !== undefined) {
1578
+ updatedMessage.inputTokenCost = data.inputTokenCost;
1579
+ }
1580
+ if (data.outputTokenCost !== undefined) {
1581
+ updatedMessage.outputTokenCost =
1582
+ data.outputTokenCost;
1583
+ }
1584
+ if (
1585
+ data.cachedInputTokenCost !== undefined ||
1586
+ data.cachedTokenCost !== undefined
1587
+ ) {
1588
+ updatedMessage.cachedInputTokenCost =
1589
+ data.cachedInputTokenCost ?? data.cachedTokenCost;
1590
+ }
1591
+ if (data.totalCost !== undefined) {
1592
+ updatedMessage.totalCost = data.totalCost;
1593
+ }
1594
+
1316
1595
  // Handle content that might only be sent in the completed event
1317
1596
  if (data.deltaContent && data.deltaContent.trim()) {
1318
1597
  if (!data.isIncremental) {
@@ -2069,7 +2348,14 @@ export function AgentTerminal({
2069
2348
  };
2070
2349
 
2071
2350
  const handleKeyPress = (e: React.KeyboardEvent) => {
2072
- if (e.key === "Enter" && !e.ctrlKey && !e.metaKey) {
2351
+ // Submit only on plain Enter (no Ctrl/Meta/Shift/Alt)
2352
+ if (
2353
+ e.key === "Enter" &&
2354
+ !e.ctrlKey &&
2355
+ !e.metaKey &&
2356
+ !e.shiftKey &&
2357
+ !e.altKey
2358
+ ) {
2073
2359
  e.preventDefault();
2074
2360
  handleSubmit();
2075
2361
  }
@@ -2944,7 +3230,7 @@ export function AgentTerminal({
2944
3230
  {renderContextInfoBar()}
2945
3231
 
2946
3232
  {/* Todo List Panel */}
2947
- <TodoListPanel messages={messages} />
3233
+ <TodoListPanel messages={messages} agentMetadata={agentMetadata} />
2948
3234
 
2949
3235
  {/* Input */}
2950
3236
  <div className="border-t border-gray-200 p-4">
@@ -3172,7 +3458,21 @@ export function AgentTerminal({
3172
3458
  </div>
3173
3459
  </div>
3174
3460
  <div className="mt-1 flex items-center gap-2 text-[10px] text-gray-500">
3175
- <AgentCostDisplay totalTokens={totalTokens} />
3461
+ <AgentCostDisplay
3462
+ totalTokens={
3463
+ liveTotals
3464
+ ? {
3465
+ input: liveTotals.input,
3466
+ output: liveTotals.output,
3467
+ cached: liveTotals.cached,
3468
+ inputCost: liveTotals.inputCost,
3469
+ outputCost: liveTotals.outputCost,
3470
+ cachedCost: liveTotals.cachedCost,
3471
+ totalCost: liveTotals.totalCost,
3472
+ }
3473
+ : totalTokens
3474
+ }
3475
+ />
3176
3476
  {(() => {
3177
3477
  try {
3178
3478
  const s = (window as any).__agentContextWindowStatus;
@@ -14,12 +14,15 @@ export type AiProfile = {
14
14
  id: string;
15
15
  name: string;
16
16
  instructions: string;
17
+ managedTodoInstructions?: string;
17
18
  defaultModelId: string | null; // Guid as string, nullable
18
19
  models: AiModel[]; // Array of model objects with id and name
19
20
  prompts: { prompt: string; title: string }[];
20
21
  errorMessage?: string;
21
22
  // Whether a new agent should be seeded with current item/selection/field
22
23
  includeEditorContextOnCreate?: boolean;
24
+ // When true, the agent should run in managed TODO list mode.
25
+ managedTodoMode?: boolean;
23
26
  // Tools that are allowed when the user switches to "Ask" mode
24
27
  askModeTools?: string[];
25
28
  // Optional cost limit in USD, sourced from profile
package/src/revision.ts CHANGED
@@ -1,2 +1,2 @@
1
- export const version = "1.0.4133";
2
- export const buildDate = "2025-10-01 12:31:38";
1
+ export const version = "1.0.4134";
2
+ export const buildDate = "2025-10-02 00:59:02";