@parallelworks/ui 0.75.0 → 0.76.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.
@@ -44,6 +44,20 @@ function heldSendArgs(held) {
44
44
  preceding.length > 0 ? preceding : void 0
45
45
  ];
46
46
  }
47
+ /** The wire history for the messages shown, oldest first. */
48
+ function historyMessages(messages, selectedProvider) {
49
+ return messages.filter((m) => m.content || m.toolCalls?.length || m.parts?.length).flatMap((m) => {
50
+ const msg = {
51
+ role: m.role,
52
+ content: m.content || null
53
+ };
54
+ if (m.role === "assistant" && m.reasoning) msg.reasoning_content = m.reasoning;
55
+ if (m.role === "assistant" && m.toolCalls?.length) msg.tool_calls = m.toolCalls;
56
+ if (m.role === "assistant" && m.responsesOutput?.length && m.model === selectedProvider) msg.responses_output = m.responsesOutput;
57
+ if (m.role === "tool" && m.toolCallId) msg.tool_call_id = m.toolCallId;
58
+ return m.role === "assistant" ? toolTurnMessages(m.parts, msg) : [msg];
59
+ });
60
+ }
47
61
  var NO_TOOLS = [];
48
62
  var ChatContext = createContext(void 0);
49
63
  var ChatStreamContext = createContext(void 0);
@@ -291,6 +305,124 @@ function ChatProvider({ adapter, currentUser, navigation, activeConversationId =
291
305
  state.currentConversation,
292
306
  navigation
293
307
  ]);
308
+ const streamReply = useCallback(async (targetConversationId, request, parentId, onFailure, routePart) => {
309
+ backgroundStreamsRef.current.set(targetConversationId, {
310
+ content: "",
311
+ reasoning: "",
312
+ parts: [],
313
+ messageId: null,
314
+ isActive: true
315
+ });
316
+ dispatch({
317
+ type: "SET_STREAMING",
318
+ isStreaming: true,
319
+ conversationId: targetConversationId
320
+ });
321
+ dispatch({
322
+ type: "CLEAR_STREAMING_CONTENT",
323
+ conversationId: targetConversationId
324
+ });
325
+ const controller = new AbortController();
326
+ abortControllerRef.current = controller;
327
+ try {
328
+ let thinkingStart = null;
329
+ let thinkingEnd = null;
330
+ const markStreamStart = () => {
331
+ if (thinkingStart === null) thinkingStart = Date.now();
332
+ };
333
+ let liveParts = [];
334
+ const { content: assistantContent, messageId: assistantMessageId, toolCalls, model: resolvedModel, reasoning: assistantReasoning, responsesOutput } = await adapter.streamCompletion(request, {
335
+ onContent: (chunk) => {
336
+ markStreamStart();
337
+ onStreamingChunk(chunk);
338
+ },
339
+ onReasoning: (reasoning) => {
340
+ markStreamStart();
341
+ thinkingEnd = Date.now();
342
+ dispatch({
343
+ type: "APPEND_STREAMING_REASONING",
344
+ reasoning,
345
+ conversationId: targetConversationId
346
+ });
347
+ const bgStream = backgroundStreamsRef.current.get(targetConversationId);
348
+ if (bgStream) bgStream.reasoning += reasoning;
349
+ },
350
+ onMessageId: (messageId) => {
351
+ markStreamStart();
352
+ const bgStream = backgroundStreamsRef.current.get(targetConversationId);
353
+ if (bgStream) bgStream.messageId = messageId;
354
+ },
355
+ onPart: (delta) => {
356
+ markStreamStart();
357
+ if (routePart?.(delta)) return;
358
+ liveParts = applyPartDelta(liveParts, delta);
359
+ dispatch({
360
+ type: "APPLY_STREAMING_PART",
361
+ delta,
362
+ conversationId: targetConversationId
363
+ });
364
+ const bgStream = backgroundStreamsRef.current.get(targetConversationId);
365
+ if (bgStream) bgStream.parts = liveParts;
366
+ }
367
+ }, controller.signal);
368
+ const thinkingDuration = assistantReasoning && thinkingStart !== null ? (thinkingEnd ?? Date.now()) - thinkingStart : null;
369
+ const assistantMessage = {
370
+ id: assistantMessageId || crypto.randomUUID(),
371
+ parentId,
372
+ role: "assistant",
373
+ content: assistantContent || (toolCalls.length > 0 ? null : ""),
374
+ toolCalls: toolCalls.length > 0 ? toolCalls : void 0,
375
+ model: request.useResponses ? request.model : resolvedModel || request.model,
376
+ providerId: state.selectedProvider,
377
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
378
+ reasoning: assistantReasoning || void 0,
379
+ reasoningDuration: thinkingDuration ?? void 0,
380
+ responsesOutput: responsesOutput.length ? responsesOutput : void 0,
381
+ ...liveParts.length ? { parts: finalizeParts(liveParts) } : {}
382
+ };
383
+ dispatch({
384
+ type: "ADD_MESSAGE",
385
+ message: assistantMessage,
386
+ conversationId: targetConversationId
387
+ });
388
+ return "sent";
389
+ } catch (error) {
390
+ if (error instanceof DOMException && error.name === "AbortError") return "aborted";
391
+ console.error("Failed to send message:", error);
392
+ onFailure(errorText(error));
393
+ return "failed";
394
+ } finally {
395
+ abortControllerRef.current = null;
396
+ const { unrevealed } = stopAnimationLoop();
397
+ if (unrevealed) dispatch({
398
+ type: "APPEND_STREAMING_CONTENT",
399
+ content: unrevealed,
400
+ conversationId: targetConversationId
401
+ });
402
+ streamingConversationIdRef.current = null;
403
+ backgroundStreamsRef.current.delete(targetConversationId);
404
+ setTimeout(() => {
405
+ streamingToConversationRef.current = null;
406
+ }, 500);
407
+ dispatch({
408
+ type: "SET_STREAMING",
409
+ isStreaming: false,
410
+ conversationId: targetConversationId
411
+ });
412
+ dispatch({
413
+ type: "CLEAR_STREAMING_CONTENT",
414
+ conversationId: targetConversationId
415
+ });
416
+ }
417
+ }, [
418
+ adapter,
419
+ state.selectedProvider,
420
+ onStreamingChunk,
421
+ stopAnimationLoop,
422
+ streamingConversationIdRef,
423
+ backgroundStreamsRef,
424
+ abortControllerRef
425
+ ]);
294
426
  const sendMessage = useCallback(async (content, attachmentIds, parentId, attachmentMeta, precedingMessages) => {
295
427
  if (isSendingRef.current) {
296
428
  const convId = currentConversationIdRef.current;
@@ -405,13 +537,6 @@ function ChatProvider({ adapter, currentUser, navigation, activeConversationId =
405
537
  message: userMessage,
406
538
  conversationId: targetConversationId
407
539
  });
408
- backgroundStreamsRef.current.set(targetConversationId, {
409
- content: "",
410
- reasoning: "",
411
- parts: [],
412
- messageId: null,
413
- isActive: true
414
- });
415
540
  const setMessageError = (errorMsg) => {
416
541
  dispatch({
417
542
  type: "UPDATE_MESSAGE",
@@ -421,17 +546,7 @@ function ChatProvider({ adapter, currentUser, navigation, activeConversationId =
421
546
  });
422
547
  };
423
548
  const apiMessages = [
424
- ...(isNewConversation ? [] : state.currentConversation?.messages || []).filter((m) => m.content || m.toolCalls?.length || m.parts?.length).flatMap((m) => {
425
- const msg = {
426
- role: m.role,
427
- content: m.content || null
428
- };
429
- if (m.role === "assistant" && m.reasoning) msg.reasoning_content = m.reasoning;
430
- if (m.role === "assistant" && m.toolCalls?.length) msg.tool_calls = m.toolCalls;
431
- if (m.role === "assistant" && m.responsesOutput?.length && m.model === state.selectedProvider) msg.responses_output = m.responsesOutput;
432
- if (m.role === "tool" && m.toolCallId) msg.tool_call_id = m.toolCallId;
433
- return m.role === "assistant" ? toolTurnMessages(m.parts, msg) : [msg];
434
- }),
549
+ ...historyMessages(isNewConversation ? [] : state.currentConversation?.messages || [], state.selectedProvider),
435
550
  ...precedingApiMessages,
436
551
  {
437
552
  role: "user",
@@ -439,28 +554,10 @@ function ChatProvider({ adapter, currentUser, navigation, activeConversationId =
439
554
  attachment_ids: attachmentIds?.length ? attachmentIds : void 0
440
555
  }
441
556
  ];
442
- dispatch({
443
- type: "SET_STREAMING",
444
- isStreaming: true,
445
- conversationId: targetConversationId
446
- });
447
- dispatch({
448
- type: "CLEAR_STREAMING_CONTENT",
449
- conversationId: targetConversationId
450
- });
451
- const controller = new AbortController();
452
- abortControllerRef.current = controller;
453
- let sendSucceeded = false;
454
- let wasAborted = false;
557
+ const useResponses = state.models.find((m) => m.id === modelString)?.supports_responses === true;
558
+ let outcome = "failed";
455
559
  try {
456
- const useResponses = state.models.find((m) => m.id === modelString)?.supports_responses === true;
457
- let thinkingStart = null;
458
- let thinkingEnd = null;
459
- const markStreamStart = () => {
460
- if (thinkingStart === null) thinkingStart = Date.now();
461
- };
462
- let liveParts = [];
463
- const { content: assistantContent, messageId: assistantMessageId, toolCalls, model: resolvedModel, reasoning: assistantReasoning, responsesOutput } = await adapter.streamCompletion({
560
+ outcome = await streamReply(targetConversationId, {
464
561
  model: modelString,
465
562
  messages: apiMessages,
466
563
  conversationId,
@@ -469,94 +566,11 @@ function ChatProvider({ adapter, currentUser, navigation, activeConversationId =
469
566
  allocation: state.selectedAllocation,
470
567
  useResponses,
471
568
  ...tools ? { tools: enabledToolIds } : {}
472
- }, {
473
- onContent: (chunk) => {
474
- markStreamStart();
475
- onStreamingChunk(chunk);
476
- },
477
- onReasoning: (reasoning) => {
478
- markStreamStart();
479
- thinkingEnd = Date.now();
480
- dispatch({
481
- type: "APPEND_STREAMING_REASONING",
482
- reasoning,
483
- conversationId: targetConversationId
484
- });
485
- const bgStream = backgroundStreamsRef.current.get(targetConversationId);
486
- if (bgStream) bgStream.reasoning += reasoning;
487
- },
488
- onMessageId: (messageId) => {
489
- markStreamStart();
490
- const bgStream = backgroundStreamsRef.current.get(targetConversationId);
491
- if (bgStream) bgStream.messageId = messageId;
492
- },
493
- onPart: (delta) => {
494
- markStreamStart();
495
- liveParts = applyPartDelta(liveParts, delta);
496
- dispatch({
497
- type: "APPLY_STREAMING_PART",
498
- delta,
499
- conversationId: targetConversationId
500
- });
501
- const bgStream = backgroundStreamsRef.current.get(targetConversationId);
502
- if (bgStream) bgStream.parts = liveParts;
503
- }
504
- }, controller.signal);
505
- const thinkingDuration = assistantReasoning && thinkingStart !== null ? (thinkingEnd ?? Date.now()) - thinkingStart : null;
506
- const assistantMessage = {
507
- id: assistantMessageId || crypto.randomUUID(),
508
- parentId: userMessageId,
509
- role: "assistant",
510
- content: assistantContent || (toolCalls.length > 0 ? null : ""),
511
- toolCalls: toolCalls.length > 0 ? toolCalls : void 0,
512
- model: useResponses ? modelString : resolvedModel || modelString,
513
- providerId: state.selectedProvider,
514
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
515
- reasoning: assistantReasoning || void 0,
516
- reasoningDuration: thinkingDuration ?? void 0,
517
- responsesOutput: responsesOutput.length ? responsesOutput : void 0,
518
- ...liveParts.length ? { parts: finalizeParts(liveParts) } : {}
519
- };
520
- dispatch({
521
- type: "ADD_MESSAGE",
522
- message: assistantMessage,
523
- conversationId: targetConversationId
524
- });
525
- if (isNewConversation) loadConversations();
526
- sendSucceeded = true;
527
- return true;
528
- } catch (error) {
529
- if (error instanceof DOMException && error.name === "AbortError") {
530
- wasAborted = true;
531
- return false;
532
- }
533
- console.error("Failed to send message:", error);
534
- setMessageError(errorText(error));
535
- return false;
569
+ }, userMessageId, setMessageError);
570
+ if (outcome === "sent" && isNewConversation) loadConversations();
571
+ return outcome === "sent";
536
572
  } finally {
537
- abortControllerRef.current = null;
538
- const { unrevealed } = stopAnimationLoop();
539
- if (unrevealed) dispatch({
540
- type: "APPEND_STREAMING_CONTENT",
541
- content: unrevealed,
542
- conversationId: targetConversationId
543
- });
544
- streamingConversationIdRef.current = null;
545
- backgroundStreamsRef.current.delete(targetConversationId);
546
- setTimeout(() => {
547
- streamingToConversationRef.current = null;
548
- }, 500);
549
- dispatch({
550
- type: "SET_STREAMING",
551
- isStreaming: false,
552
- conversationId: targetConversationId
553
- });
554
- dispatch({
555
- type: "CLEAR_STREAMING_CONTENT",
556
- conversationId: targetConversationId
557
- });
558
- if (wasAborted) isSendingRef.current = false;
559
- else if (sendSucceeded) {
573
+ if (outcome === "sent") {
560
574
  const next = heldSendArgs(pendingQueueRef.current);
561
575
  pendingQueueRef.current = [];
562
576
  dispatch({ type: "CLEAR_QUEUED_MESSAGES" });
@@ -581,16 +595,84 @@ function ChatProvider({ adapter, currentUser, navigation, activeConversationId =
581
595
  createConversation,
582
596
  loadConversations,
583
597
  navigation,
584
- onStreamingChunk,
585
598
  startStreaming,
586
- stopAnimationLoop,
587
- streamingConversationIdRef,
588
- backgroundStreamsRef,
589
- abortControllerRef
599
+ streamReply
590
600
  ]);
591
601
  useEffect(() => {
592
602
  sendMessageRef.current = sendMessage;
593
603
  }, [sendMessage]);
604
+ const answerToolApproval = useCallback(async (approvalId, answer) => {
605
+ const conversation = state.currentConversation;
606
+ const modelString = state.selectedProvider;
607
+ if (answer.kind !== "confirm" || !conversation || !modelString || isSendingRef.current) return;
608
+ const pausedAt = conversation.messages.findIndex((m) => m.role === "assistant" && m.parts?.some((p) => p.kind === "approval" && p.id === approvalId && !p.resolved));
609
+ const paused = conversation.messages[pausedAt];
610
+ if (!paused) return;
611
+ isSendingRef.current = true;
612
+ const targetConversationId = conversation.id;
613
+ const t = resolvedConfig.strings.agentTranscript;
614
+ let pausedParts = paused.parts ?? [];
615
+ const updatePaused = (updates) => dispatch({
616
+ type: "UPDATE_MESSAGE",
617
+ messageId: paused.id,
618
+ updates,
619
+ conversationId: targetConversationId
620
+ });
621
+ const applyToPaused = (delta) => {
622
+ pausedParts = applyPartDelta(pausedParts, delta);
623
+ updatePaused({ parts: pausedParts });
624
+ };
625
+ applyToPaused({
626
+ type: "approval_resolved",
627
+ id: approvalId,
628
+ answer: answer.allowed ? t.approvalAllowed : t.approvalDenied
629
+ });
630
+ applyToPaused({
631
+ type: "tool_decision",
632
+ id: approvalId,
633
+ decision: {
634
+ status: answer.allowed ? "approved" : "denied",
635
+ by: "user",
636
+ name: currentUser.name || currentUser.username,
637
+ at: DateTime.now().toUTC().toISO()
638
+ }
639
+ });
640
+ streamingToConversationRef.current = targetConversationId;
641
+ startStreaming(targetConversationId);
642
+ try {
643
+ await streamReply(targetConversationId, {
644
+ model: modelString,
645
+ messages: historyMessages(conversation.messages.slice(0, pausedAt + 1), state.selectedProvider),
646
+ conversationId: targetConversationId,
647
+ parentMessageId: paused.id,
648
+ userMessageId: crypto.randomUUID(),
649
+ allocation: state.selectedAllocation,
650
+ ...tools ? { tools: enabledToolIds } : {},
651
+ resume: {
652
+ messageId: paused.id,
653
+ toolCallId: approvalId,
654
+ allowed: answer.allowed,
655
+ ...answer.denyMessage ? { feedback: answer.denyMessage } : {}
656
+ }
657
+ }, paused.id, (message) => updatePaused({ error: message }), (delta) => {
658
+ if (delta.type !== "tool_end" || delta.id !== approvalId) return false;
659
+ applyToPaused(delta);
660
+ return true;
661
+ });
662
+ } finally {
663
+ isSendingRef.current = false;
664
+ }
665
+ }, [
666
+ resolvedConfig,
667
+ state.currentConversation,
668
+ state.selectedProvider,
669
+ state.selectedAllocation,
670
+ tools,
671
+ enabledToolIds,
672
+ startStreaming,
673
+ streamReply,
674
+ currentUser
675
+ ]);
594
676
  const updateConversationTitle = useCallback(async (id, title) => {
595
677
  try {
596
678
  await adapter.conversations.rename(id, title);
@@ -807,6 +889,7 @@ function ChatProvider({ adapter, currentUser, navigation, activeConversationId =
807
889
  deleteConversation,
808
890
  sendMessage,
809
891
  retryMessage,
892
+ answerToolApproval,
810
893
  setSelectedProvider,
811
894
  setSelectedAllocation,
812
895
  setToolEnabled,
@@ -854,6 +937,7 @@ function ChatProvider({ adapter, currentUser, navigation, activeConversationId =
854
937
  deleteConversation,
855
938
  sendMessage,
856
939
  retryMessage,
940
+ answerToolApproval,
857
941
  setSelectedProvider,
858
942
  setSelectedAllocation,
859
943
  setToolEnabled,
@@ -34,6 +34,10 @@ function applyPartDelta(parts, delta) {
34
34
  ...p,
35
35
  args: p.args + delta.args
36
36
  } : p);
37
+ case "tool_wait": return parts.map((p) => p.kind === "tool_call" && p.id === delta.id ? {
38
+ ...p,
39
+ status: "waiting"
40
+ } : p);
37
41
  case "tool_end": return parts.map((p) => p.kind === "tool_call" && p.id === delta.id ? {
38
42
  ...p,
39
43
  status: delta.isError ? "error" : "ok",
@@ -99,7 +103,7 @@ function isStreamedText(part) {
99
103
  * calls comes back as the answer alone. */
100
104
  function toolTurnMessages(parts, answer) {
101
105
  const calls = (parts ?? []).filter((p) => p.kind === "tool_call" && p.status !== "running" && p.result !== void 0);
102
- if (calls.length === 0) return [answer];
106
+ if (calls.length === 0) return answer.content || answer.tool_calls?.length ? [answer] : [];
103
107
  return [
104
108
  {
105
109
  role: "assistant",
@@ -15,6 +15,8 @@ export interface ChatStrings {
15
15
  addGroups: string;
16
16
  info: ReactNode;
17
17
  shareButton: string;
18
+ /** Beside the share button in a shared conversation that can use tools. */
19
+ toolResultsShared: string;
18
20
  };
19
21
  blockedGroup: {
20
22
  title: (group: string) => string;
@@ -67,6 +69,8 @@ export interface ChatStrings {
67
69
  turnAllOff: string;
68
70
  /** Closes the tools modal; each switch takes effect as it is flipped. */
69
71
  done: string;
72
+ /** Marks a tool whose calls wait for the reader's approval. */
73
+ asksFirst: string;
70
74
  };
71
75
  allocationPicker: {
72
76
  choose: string;
@@ -121,6 +125,9 @@ export interface ChatStrings {
121
125
  activity: string;
122
126
  /** A finished turn's label when it ran tools without reasoning. */
123
127
  ranToolCalls: (count: number) => string;
128
+ /** The same, in a shared conversation, for a turn someone else started:
129
+ * its tools ran with that person's access. */
130
+ ranToolCallsAs: (count: number, name: string) => string;
124
131
  };
125
132
  thread: {
126
133
  viewOnly: string;
@@ -243,6 +250,7 @@ export interface ChatStrings {
243
250
  deniedByReviewer: string;
244
251
  approvalExpired: string;
245
252
  approvalTimedOut: string;
253
+ reviewTimedOut: string;
246
254
  reviewFailed: string;
247
255
  writeOwnAnswer: string;
248
256
  subagent: string;
@@ -252,6 +260,13 @@ export interface ChatStrings {
252
260
  approvalOptions: string;
253
261
  /** Names a question the agent waits on that has no heading of its own. */
254
262
  approvalLabel: string;
263
+ /** A call the reader approved or declined. */
264
+ approvalAllowed: string;
265
+ approvalDenied: string;
266
+ /** A call nobody answered in time. */
267
+ approvalRequestExpired: string;
268
+ /** A call only the person who asked for it may answer. */
269
+ approvalWaitingOn: (name: string) => string;
255
270
  /** Names the field for the reason a request is denied. */
256
271
  feedbackLabel: string;
257
272
  /** Names the field for an answer of the reader's own. */
@@ -20,7 +20,8 @@ var defaultChatStrings = {
20
20
  /* @__PURE__ */ jsx("strong", { children: "Collaborate" }),
21
21
  " permission allows adding messages."
22
22
  ] }),
23
- shareButton: "Share conversation"
23
+ shareButton: "Share conversation",
24
+ toolResultsShared: "Tool results here are visible to the people it's shared with"
24
25
  },
25
26
  blockedGroup: {
26
27
  title: (group) => `Billing group \u201c${group}\u201d is over its allocation`,
@@ -59,12 +60,13 @@ var defaultChatStrings = {
59
60
  tools: "Tools",
60
61
  someToolsOff: "Some tools are off",
61
62
  toolsOn: (enabled, total) => `${enabled.toLocaleString()}/${total.toLocaleString()}`,
62
- toolsDescription: "What the assistant may look up for you.",
63
+ toolsDescription: "What the assistant may look up or do for you.",
63
64
  searchTools: "Search tools",
64
65
  noToolsMatch: "No tools match",
65
66
  turnAllOn: "Turn all on",
66
67
  turnAllOff: "Turn all off",
67
- done: "Done"
68
+ done: "Done",
69
+ asksFirst: "Asks first"
68
70
  },
69
71
  allocationPicker: {
70
72
  choose: "Allocation",
@@ -150,7 +152,8 @@ var defaultChatStrings = {
150
152
  thoughtFor: (duration) => `Thought for ${duration}`,
151
153
  thoughtProcess: "Thought process",
152
154
  activity: "Activity",
153
- ranToolCalls: (count) => `Ran ${countOf(count, "tool call", "tool calls")}`
155
+ ranToolCalls: (count) => `Ran ${countOf(count, "tool call", "tool calls")}`,
156
+ ranToolCallsAs: (count, name) => `Ran ${countOf(count, "tool call", "tool calls")} as ${name}`
154
157
  },
155
158
  thread: {
156
159
  viewOnly: "You have view only access",
@@ -260,8 +263,9 @@ var defaultChatStrings = {
260
263
  allowedByReviewer: "Allowed by the auto-reviewer",
261
264
  deniedByReviewer: "Denied by the auto-reviewer",
262
265
  approvalExpired: "Expired without an answer",
263
- approvalTimedOut: "No answer in time",
264
- reviewFailed: "The auto-reviewer could not decide",
266
+ approvalTimedOut: "Denied: no answer in time",
267
+ reviewTimedOut: "Denied: the auto-reviewer timed out",
268
+ reviewFailed: "Denied: the auto-reviewer failed",
265
269
  writeOwnAnswer: "Write my own answer…",
266
270
  subagent: "Task",
267
271
  tasks: "Tasks",
@@ -269,6 +273,10 @@ var defaultChatStrings = {
269
273
  approvalPending: "Waiting for an answer in the terminal",
270
274
  approvalOptions: "Offered choices",
271
275
  approvalLabel: "Waiting for your answer",
276
+ approvalAllowed: "Allowed",
277
+ approvalDenied: "Declined",
278
+ approvalRequestExpired: "This request expired. Ask again to run it.",
279
+ approvalWaitingOn: (name) => `Waiting for ${name} to answer`,
272
280
  feedbackLabel: "Feedback for the agent",
273
281
  ownAnswerLabel: "Your answer",
274
282
  todoPending: "To do",
@@ -45,7 +45,7 @@ export interface TokenUsage {
45
45
  completion_tokens?: number;
46
46
  total_tokens?: number;
47
47
  }
48
- export type ToolCallStatus = 'running' | 'ok' | 'error';
48
+ export type ToolCallStatus = 'running' | 'waiting' | 'ok' | 'error';
49
49
  export interface TextPart {
50
50
  kind: 'text';
51
51
  text: string;
@@ -65,6 +65,8 @@ export interface ToolDecision {
65
65
  /** ISO time of the answer. */
66
66
  at?: string;
67
67
  note?: string;
68
+ /** Why it was settled this way, such as the auto-reviewer's rationale. */
69
+ reason?: string;
68
70
  }
69
71
  export interface ToolCallPart {
70
72
  kind: 'tool_call';
@@ -120,6 +122,8 @@ export interface ApprovalPart {
120
122
  options?: ApprovalOption[];
121
123
  plan?: string;
122
124
  canAllowDirectory?: boolean;
125
+ expiresAt?: string;
126
+ waitingOn?: string;
123
127
  resolved?: boolean;
124
128
  answer?: string;
125
129
  }
@@ -65,7 +65,7 @@ function useRelativeTime(value, format = "long") {
65
65
  const unit = unitOf(age);
66
66
  const key = `${format}:${at}:${unit}:${Math.trunc(age / unit)}`;
67
67
  if (last.current?.key !== key) {
68
- const label = format === "compact" ? compactLabel(age, t) : DateTime.fromMillis(at).toRelative({ base: DateTime.fromMillis(current) }) ?? "";
68
+ const label = format === "compact" ? compactLabel(age, t) : Math.abs(age) < SECOND ? t.now : DateTime.fromMillis(at).toRelative({ base: DateTime.fromMillis(current) }) ?? "";
69
69
  last.current = {
70
70
  key,
71
71
  label