@stigmer/react 3.0.8-dev.20260613041848 → 3.0.8-dev.20260613071809
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.
- package/composer/ComposerToolbar.d.ts +9 -1
- package/composer/ComposerToolbar.d.ts.map +1 -1
- package/composer/ComposerToolbar.js +3 -3
- package/composer/ComposerToolbar.js.map +1 -1
- package/composer/SessionComposer.d.ts +12 -0
- package/composer/SessionComposer.d.ts.map +1 -1
- package/composer/SessionComposer.js +6 -3
- package/composer/SessionComposer.js.map +1 -1
- package/composer/icons.d.ts +2 -0
- package/composer/icons.d.ts.map +1 -1
- package/composer/icons.js +4 -0
- package/composer/icons.js.map +1 -1
- package/execution/ExecutionProgress.d.ts.map +1 -1
- package/execution/ExecutionProgress.js +5 -1
- package/execution/ExecutionProgress.js.map +1 -1
- package/execution/MessageEntry.d.ts +7 -0
- package/execution/MessageEntry.d.ts.map +1 -1
- package/execution/MessageEntry.js +7 -4
- package/execution/MessageEntry.js.map +1 -1
- package/execution/MessageThread.d.ts +45 -3
- package/execution/MessageThread.d.ts.map +1 -1
- package/execution/MessageThread.js +66 -11
- package/execution/MessageThread.js.map +1 -1
- package/execution/index.d.ts +2 -0
- package/execution/index.d.ts.map +1 -1
- package/execution/index.js +1 -0
- package/execution/index.js.map +1 -1
- package/execution/useAgentExecutionActions.d.ts +67 -0
- package/execution/useAgentExecutionActions.d.ts.map +1 -0
- package/execution/useAgentExecutionActions.js +105 -0
- package/execution/useAgentExecutionActions.js.map +1 -0
- package/execution/useExecutionStream.d.ts +27 -0
- package/execution/useExecutionStream.d.ts.map +1 -1
- package/execution/useExecutionStream.js +48 -5
- package/execution/useExecutionStream.js.map +1 -1
- package/index.d.ts +2 -2
- package/index.d.ts.map +1 -1
- package/index.js +1 -1
- package/index.js.map +1 -1
- package/internal/VirtualizedThread.d.ts +4 -1
- package/internal/VirtualizedThread.d.ts.map +1 -1
- package/internal/VirtualizedThread.js +5 -2
- package/internal/VirtualizedThread.js.map +1 -1
- package/internal/store/conversation-store.d.ts +22 -0
- package/internal/store/conversation-store.d.ts.map +1 -1
- package/internal/store/conversation-store.js +43 -2
- package/internal/store/conversation-store.js.map +1 -1
- package/internal/stream-controller.d.ts +46 -2
- package/internal/stream-controller.d.ts.map +1 -1
- package/internal/stream-controller.js +95 -4
- package/internal/stream-controller.js.map +1 -1
- package/internal/useFetch.d.ts +7 -0
- package/internal/useFetch.d.ts.map +1 -1
- package/internal/useFetch.js +21 -0
- package/internal/useFetch.js.map +1 -1
- package/package.json +4 -4
- package/session/SessionViewer.js +39 -2
- package/session/SessionViewer.js.map +1 -1
- package/session/useSessionConversation.d.ts +55 -3
- package/session/useSessionConversation.d.ts.map +1 -1
- package/session/useSessionConversation.js +95 -10
- package/session/useSessionConversation.js.map +1 -1
- package/session/useSessionExecutions.d.ts +17 -1
- package/session/useSessionExecutions.d.ts.map +1 -1
- package/session/useSessionExecutions.js +6 -2
- package/session/useSessionExecutions.js.map +1 -1
- package/src/composer/ComposerToolbar.tsx +32 -9
- package/src/composer/SessionComposer.tsx +22 -1
- package/src/composer/__tests__/SessionComposer-stop.test.tsx +98 -0
- package/src/composer/icons.tsx +15 -0
- package/src/execution/ExecutionProgress.tsx +12 -0
- package/src/execution/MessageEntry.tsx +57 -2
- package/src/execution/MessageThread.tsx +203 -5
- package/src/execution/__tests__/MessageThread.test.tsx +130 -0
- package/src/execution/__tests__/useAgentExecutionActions.test.tsx +299 -0
- package/src/execution/__tests__/useExecutionStream.test.tsx +95 -0
- package/src/execution/index.ts +6 -0
- package/src/execution/useAgentExecutionActions.ts +205 -0
- package/src/execution/useExecutionStream.ts +80 -4
- package/src/index.ts +3 -0
- package/src/internal/VirtualizedThread.tsx +10 -1
- package/src/internal/__tests__/stream-controller.test.ts +165 -10
- package/src/internal/__tests__/useFetch.test.tsx +59 -0
- package/src/internal/store/__tests__/conversation-store.test.ts +61 -0
- package/src/internal/store/conversation-store.ts +46 -3
- package/src/internal/stream-controller.ts +123 -3
- package/src/internal/useFetch.ts +26 -0
- package/src/session/SessionViewer.tsx +87 -1
- package/src/session/__tests__/useSessionConversation.test.tsx +145 -0
- package/src/session/useSessionConversation.ts +163 -14
- package/src/session/useSessionExecutions.ts +23 -1
- package/styles.css +1 -1
|
@@ -14,6 +14,13 @@ export interface MessageEntryProps {
|
|
|
14
14
|
readonly message: AgentMessage;
|
|
15
15
|
/** Additional CSS class names for the root container. */
|
|
16
16
|
readonly className?: string;
|
|
17
|
+
/**
|
|
18
|
+
* When provided on a `MESSAGE_HUMAN` entry, a hover-revealed "Edit" button
|
|
19
|
+
* appears on the bubble. Clicking it invokes this callback — the session
|
|
20
|
+
* chat uses it to stop the in-flight turn and pre-fill the composer with
|
|
21
|
+
* this message for editing. Ignored for non-human messages.
|
|
22
|
+
*/
|
|
23
|
+
readonly onEdit?: () => void;
|
|
17
24
|
}
|
|
18
25
|
|
|
19
26
|
/**
|
|
@@ -43,6 +50,7 @@ export interface MessageEntryProps {
|
|
|
43
50
|
export const MessageEntry = memo(function MessageEntry({
|
|
44
51
|
message,
|
|
45
52
|
className,
|
|
53
|
+
onEdit,
|
|
46
54
|
}: MessageEntryProps) {
|
|
47
55
|
useRenderTracer("MessageEntry", {
|
|
48
56
|
messageType: message.type,
|
|
@@ -52,7 +60,13 @@ export const MessageEntry = memo(function MessageEntry({
|
|
|
52
60
|
|
|
53
61
|
switch (message.type) {
|
|
54
62
|
case MessageType.MESSAGE_HUMAN:
|
|
55
|
-
return
|
|
63
|
+
return (
|
|
64
|
+
<HumanMessage
|
|
65
|
+
content={message.content}
|
|
66
|
+
className={className}
|
|
67
|
+
onEdit={onEdit}
|
|
68
|
+
/>
|
|
69
|
+
);
|
|
56
70
|
case MessageType.MESSAGE_AI:
|
|
57
71
|
return (
|
|
58
72
|
<AiMessage
|
|
@@ -79,21 +93,62 @@ export const MessageEntry = memo(function MessageEntry({
|
|
|
79
93
|
function HumanMessage({
|
|
80
94
|
content,
|
|
81
95
|
className,
|
|
96
|
+
onEdit,
|
|
82
97
|
}: {
|
|
83
98
|
content: string;
|
|
84
99
|
className?: string;
|
|
100
|
+
onEdit?: () => void;
|
|
85
101
|
}) {
|
|
86
102
|
return (
|
|
87
103
|
<div
|
|
88
104
|
role="article"
|
|
89
105
|
aria-label="User message"
|
|
90
|
-
className={cn(
|
|
106
|
+
className={cn(
|
|
107
|
+
"group relative ms-[20%] rounded-lg bg-muted-subtle px-4 py-3",
|
|
108
|
+
className,
|
|
109
|
+
)}
|
|
91
110
|
>
|
|
92
111
|
<p className="text-sm text-foreground whitespace-pre-wrap">{content}</p>
|
|
112
|
+
{onEdit && (
|
|
113
|
+
<button
|
|
114
|
+
type="button"
|
|
115
|
+
onClick={onEdit}
|
|
116
|
+
aria-label="Edit message"
|
|
117
|
+
title="Edit"
|
|
118
|
+
className={cn(
|
|
119
|
+
"absolute -top-2.5 -right-2.5 inline-flex h-7 w-7 items-center justify-center rounded-full",
|
|
120
|
+
"border border-border bg-card text-muted-foreground shadow-sm transition",
|
|
121
|
+
"hover:text-foreground hover:bg-accent-hover",
|
|
122
|
+
"opacity-0 group-hover:opacity-100 focus-visible:opacity-100",
|
|
123
|
+
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
|
|
124
|
+
)}
|
|
125
|
+
>
|
|
126
|
+
<EditIcon />
|
|
127
|
+
</button>
|
|
128
|
+
)}
|
|
93
129
|
</div>
|
|
94
130
|
);
|
|
95
131
|
}
|
|
96
132
|
|
|
133
|
+
function EditIcon() {
|
|
134
|
+
return (
|
|
135
|
+
<svg
|
|
136
|
+
width="13"
|
|
137
|
+
height="13"
|
|
138
|
+
viewBox="0 0 24 24"
|
|
139
|
+
fill="none"
|
|
140
|
+
stroke="currentColor"
|
|
141
|
+
strokeWidth="2"
|
|
142
|
+
strokeLinecap="round"
|
|
143
|
+
strokeLinejoin="round"
|
|
144
|
+
aria-hidden="true"
|
|
145
|
+
>
|
|
146
|
+
<path d="M12 20h9" />
|
|
147
|
+
<path d="M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4Z" />
|
|
148
|
+
</svg>
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
|
|
97
152
|
function AiMessage({
|
|
98
153
|
content,
|
|
99
154
|
isStreaming,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import { lazy, memo, Suspense, useCallback, useMemo } from "react";
|
|
3
|
+
import { lazy, memo, Suspense, useCallback, useMemo, useState } from "react";
|
|
4
4
|
import { create } from "@bufbuild/protobuf";
|
|
5
5
|
import type { AgentExecution } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/api_pb";
|
|
6
6
|
import type { AgentMessage, ToolCall } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/message_pb";
|
|
@@ -60,6 +60,38 @@ export interface MessageThreadProps {
|
|
|
60
60
|
* first snapshot.
|
|
61
61
|
*/
|
|
62
62
|
readonly pendingUserMessage?: string | null;
|
|
63
|
+
/**
|
|
64
|
+
* Marks the pending user message as failed-to-send. The optimistic
|
|
65
|
+
* bubble renders an inline "Couldn't send — Retry" affordance instead
|
|
66
|
+
* of the sending indicator, so a failed follow-up never silently
|
|
67
|
+
* vanishes. Pair with {@link onRetrySend}.
|
|
68
|
+
*
|
|
69
|
+
* @default false
|
|
70
|
+
*/
|
|
71
|
+
readonly pendingMessageFailed?: boolean;
|
|
72
|
+
/**
|
|
73
|
+
* Retry handler for a failed pending message. Wired to the inline
|
|
74
|
+
* "Retry" control when {@link pendingMessageFailed} is `true`.
|
|
75
|
+
*/
|
|
76
|
+
readonly onRetrySend?: () => void;
|
|
77
|
+
/**
|
|
78
|
+
* When provided, the in-flight human turn (the active execution's prompt)
|
|
79
|
+
* shows a hover "Edit" affordance. Clicking it invokes this callback with
|
|
80
|
+
* the message text — the session chat stops the running execution and
|
|
81
|
+
* pre-fills the composer for an edit-and-resubmit.
|
|
82
|
+
*
|
|
83
|
+
* Provide this only while the active execution is stoppable; the SDK marks
|
|
84
|
+
* exactly the active-stream human turn editable. When omitted, no edit
|
|
85
|
+
* control is shown (backward compatible).
|
|
86
|
+
*/
|
|
87
|
+
readonly onEditMessage?: (text: string) => void;
|
|
88
|
+
/**
|
|
89
|
+
* Retry handler for a terminal-failed execution that exposed a server
|
|
90
|
+
* error reason. Receives the originating message; the consumer typically
|
|
91
|
+
* resends it as a new execution. When omitted, no Retry control is shown
|
|
92
|
+
* beside the surfaced failure reason.
|
|
93
|
+
*/
|
|
94
|
+
readonly onRetryExecution?: (message: string) => void;
|
|
63
95
|
/** Additional CSS class names for the root container. */
|
|
64
96
|
readonly className?: string;
|
|
65
97
|
/**
|
|
@@ -178,10 +210,11 @@ export interface MessageThreadProps {
|
|
|
178
210
|
* part of the public API.
|
|
179
211
|
*/
|
|
180
212
|
export type ThreadItem =
|
|
181
|
-
| { readonly kind: "message"; readonly message: AgentMessage; readonly key: string; readonly isPending?: boolean }
|
|
213
|
+
| { readonly kind: "message"; readonly message: AgentMessage; readonly key: string; readonly isPending?: boolean; readonly isFailed?: boolean; readonly isEditable?: boolean }
|
|
182
214
|
| { readonly kind: "tool-group"; readonly toolCalls: readonly ToolCall[]; readonly subAgentExecutions: readonly SubAgentExecution[]; readonly key: string }
|
|
183
215
|
| { readonly kind: "sub-agent"; readonly subAgentExecution: SubAgentExecution; readonly key: string }
|
|
184
216
|
| { readonly kind: "phase-badge"; readonly phase: ExecutionPhase; readonly key: string }
|
|
217
|
+
| { readonly kind: "execution-error"; readonly error: string; readonly retryMessage?: string; readonly key: string }
|
|
185
218
|
| { readonly kind: "approval-request"; readonly pendingApproval: PendingApproval; readonly key: string }
|
|
186
219
|
| { readonly kind: "setup-progress"; readonly workspaceEntries: readonly WorkspaceEntry[]; readonly serverPhase?: string; readonly isAwaitingResponse?: boolean; readonly key: string }
|
|
187
220
|
| { readonly kind: "context-compacted"; readonly event: SummarizationEventView; readonly key: string }
|
|
@@ -216,6 +249,8 @@ export function buildThreadItems(
|
|
|
216
249
|
includeApprovals: boolean,
|
|
217
250
|
workspaceEntries: readonly WorkspaceEntry[] | undefined,
|
|
218
251
|
summarizationEvents?: readonly SummarizationEventView[],
|
|
252
|
+
pendingMessageFailed = false,
|
|
253
|
+
editableActiveTurn = false,
|
|
219
254
|
): ThreadItem[] {
|
|
220
255
|
const items: ThreadItem[] = [];
|
|
221
256
|
const allExecutions = activeStreamExecution
|
|
@@ -274,6 +309,9 @@ export function buildThreadItems(
|
|
|
274
309
|
kind: "message",
|
|
275
310
|
message: syntheticHumanMsg,
|
|
276
311
|
key: bridgePending ? "pending-user-turn" : `${execId}-spec`,
|
|
312
|
+
// The active execution's prompt is the one a user can edit-and-resubmit
|
|
313
|
+
// (stop + rephrase). Only mark it when the consumer enabled editing.
|
|
314
|
+
isEditable: isActiveStreamExec && editableActiveTurn,
|
|
277
315
|
});
|
|
278
316
|
}
|
|
279
317
|
|
|
@@ -393,6 +431,22 @@ export function buildThreadItems(
|
|
|
393
431
|
phase: lastPhase,
|
|
394
432
|
key: `phase-${lastPhase}`,
|
|
395
433
|
});
|
|
434
|
+
|
|
435
|
+
// The server populates `status.error` only on EXECUTION_FAILED. Surface it
|
|
436
|
+
// beside the badge so a failure that produced no messages still explains
|
|
437
|
+
// itself (the CLI shows this reason; the chat previously showed nothing).
|
|
438
|
+
// Kept as its own item so the badge component stays presentational.
|
|
439
|
+
const reason = lastExec?.status?.error;
|
|
440
|
+
if (reason) {
|
|
441
|
+
const specMessage = lastExec?.spec?.message;
|
|
442
|
+
items.push({
|
|
443
|
+
kind: "execution-error",
|
|
444
|
+
error: reason,
|
|
445
|
+
retryMessage:
|
|
446
|
+
specMessage && specMessage !== "execute" ? specMessage : undefined,
|
|
447
|
+
key: `execution-error-${lastExec?.metadata?.id ?? lastPhase}`,
|
|
448
|
+
});
|
|
449
|
+
}
|
|
396
450
|
}
|
|
397
451
|
|
|
398
452
|
if (
|
|
@@ -430,7 +484,10 @@ export function buildThreadItems(
|
|
|
430
484
|
kind: "message",
|
|
431
485
|
message: syntheticPending,
|
|
432
486
|
key: "pending-user-turn",
|
|
433
|
-
|
|
487
|
+
// A failed turn shows the inline error instead of the dimmed
|
|
488
|
+
// sending state — the two are mutually exclusive.
|
|
489
|
+
isPending: !pendingMessageFailed,
|
|
490
|
+
isFailed: pendingMessageFailed,
|
|
434
491
|
});
|
|
435
492
|
}
|
|
436
493
|
}
|
|
@@ -465,6 +522,10 @@ export function MessageThread({
|
|
|
465
522
|
executions,
|
|
466
523
|
activeStreamExecution,
|
|
467
524
|
pendingUserMessage,
|
|
525
|
+
pendingMessageFailed = false,
|
|
526
|
+
onRetrySend,
|
|
527
|
+
onRetryExecution,
|
|
528
|
+
onEditMessage,
|
|
468
529
|
className,
|
|
469
530
|
formatToolCallSummary,
|
|
470
531
|
onApprovalSubmit,
|
|
@@ -482,9 +543,10 @@ export function MessageThread({
|
|
|
482
543
|
useRenderTracer("MessageThread", { executions, activeStreamExecution });
|
|
483
544
|
|
|
484
545
|
const includeApprovals = onApprovalSubmit != null;
|
|
546
|
+
const editableActiveTurn = onEditMessage != null;
|
|
485
547
|
const items = useMemo(
|
|
486
|
-
() => buildThreadItems(executions, activeStreamExecution, pendingUserMessage, includeApprovals, workspaceEntries, summarizationEvents),
|
|
487
|
-
[executions, activeStreamExecution, pendingUserMessage, includeApprovals, workspaceEntries, summarizationEvents],
|
|
548
|
+
() => buildThreadItems(executions, activeStreamExecution, pendingUserMessage, includeApprovals, workspaceEntries, summarizationEvents, pendingMessageFailed, editableActiveTurn),
|
|
549
|
+
[executions, activeStreamExecution, pendingUserMessage, includeApprovals, workspaceEntries, summarizationEvents, pendingMessageFailed, editableActiveTurn],
|
|
488
550
|
);
|
|
489
551
|
|
|
490
552
|
useKeyStability(items);
|
|
@@ -517,6 +579,9 @@ export function MessageThread({
|
|
|
517
579
|
org={org}
|
|
518
580
|
planActionsDisabled={planActionsDisabled}
|
|
519
581
|
centerContent={centerContent}
|
|
582
|
+
onRetrySend={onRetrySend}
|
|
583
|
+
onRetryExecution={onRetryExecution}
|
|
584
|
+
onEditMessage={onEditMessage}
|
|
520
585
|
/>
|
|
521
586
|
</Suspense>
|
|
522
587
|
</div>
|
|
@@ -536,6 +601,9 @@ export function MessageThread({
|
|
|
536
601
|
onBuildFromPlan={onBuildFromPlan}
|
|
537
602
|
org={org}
|
|
538
603
|
planActionsDisabled={planActionsDisabled}
|
|
604
|
+
onRetrySend={onRetrySend}
|
|
605
|
+
onRetryExecution={onRetryExecution}
|
|
606
|
+
onEditMessage={onEditMessage}
|
|
539
607
|
/>
|
|
540
608
|
);
|
|
541
609
|
}
|
|
@@ -560,6 +628,9 @@ interface NonVirtualizedThreadProps {
|
|
|
560
628
|
readonly onBuildFromPlan?: () => void;
|
|
561
629
|
readonly org?: string;
|
|
562
630
|
readonly planActionsDisabled?: boolean;
|
|
631
|
+
readonly onRetrySend?: () => void;
|
|
632
|
+
readonly onRetryExecution?: (message: string) => void;
|
|
633
|
+
readonly onEditMessage?: (text: string) => void;
|
|
563
634
|
}
|
|
564
635
|
|
|
565
636
|
function NonVirtualizedThread({
|
|
@@ -574,6 +645,9 @@ function NonVirtualizedThread({
|
|
|
574
645
|
onBuildFromPlan,
|
|
575
646
|
org,
|
|
576
647
|
planActionsDisabled,
|
|
648
|
+
onRetrySend,
|
|
649
|
+
onRetryExecution,
|
|
650
|
+
onEditMessage,
|
|
577
651
|
}: NonVirtualizedThreadProps) {
|
|
578
652
|
const { scrollRef, sentinelRef, contentRef, isFollowing, jumpToLatest } =
|
|
579
653
|
useAutoScroll();
|
|
@@ -609,6 +683,9 @@ function NonVirtualizedThread({
|
|
|
609
683
|
onBuildFromPlan={onBuildFromPlan}
|
|
610
684
|
org={org}
|
|
611
685
|
planActionsDisabled={planActionsDisabled}
|
|
686
|
+
onRetrySend={onRetrySend}
|
|
687
|
+
onRetryExecution={onRetryExecution}
|
|
688
|
+
onEditMessage={onEditMessage}
|
|
612
689
|
/>
|
|
613
690
|
</ThreadItemWrapper>
|
|
614
691
|
))}
|
|
@@ -645,6 +722,9 @@ export interface ThreadItemRendererProps {
|
|
|
645
722
|
readonly onBuildFromPlan?: () => void;
|
|
646
723
|
readonly org?: string;
|
|
647
724
|
readonly planActionsDisabled?: boolean;
|
|
725
|
+
readonly onRetrySend?: () => void;
|
|
726
|
+
readonly onRetryExecution?: (message: string) => void;
|
|
727
|
+
readonly onEditMessage?: (text: string) => void;
|
|
648
728
|
}
|
|
649
729
|
|
|
650
730
|
/**
|
|
@@ -666,13 +746,26 @@ export function ThreadItemRenderer({
|
|
|
666
746
|
onBuildFromPlan,
|
|
667
747
|
org,
|
|
668
748
|
planActionsDisabled,
|
|
749
|
+
onRetrySend,
|
|
750
|
+
onRetryExecution,
|
|
751
|
+
onEditMessage,
|
|
669
752
|
}: ThreadItemRendererProps) {
|
|
670
753
|
switch (item.kind) {
|
|
671
754
|
case "message":
|
|
755
|
+
if (item.isFailed) {
|
|
756
|
+
return (
|
|
757
|
+
<FailedUserMessage message={item.message} onRetry={onRetrySend} />
|
|
758
|
+
);
|
|
759
|
+
}
|
|
672
760
|
return (
|
|
673
761
|
<MessageEntry
|
|
674
762
|
message={item.message}
|
|
675
763
|
className={item.isPending ? "opacity-70" : undefined}
|
|
764
|
+
onEdit={
|
|
765
|
+
item.isEditable && onEditMessage
|
|
766
|
+
? () => onEditMessage(item.message.content)
|
|
767
|
+
: undefined
|
|
768
|
+
}
|
|
676
769
|
/>
|
|
677
770
|
);
|
|
678
771
|
case "tool-group":
|
|
@@ -697,6 +790,14 @@ export function ThreadItemRenderer({
|
|
|
697
790
|
<ExecutionPhaseBadge phase={item.phase} />
|
|
698
791
|
</div>
|
|
699
792
|
);
|
|
793
|
+
case "execution-error":
|
|
794
|
+
return (
|
|
795
|
+
<ExecutionErrorNotice
|
|
796
|
+
error={item.error}
|
|
797
|
+
retryMessage={item.retryMessage}
|
|
798
|
+
onRetry={onRetryExecution}
|
|
799
|
+
/>
|
|
800
|
+
);
|
|
700
801
|
case "approval-request":
|
|
701
802
|
return (
|
|
702
803
|
<ApprovalCardRow
|
|
@@ -736,6 +837,103 @@ export function ThreadItemRenderer({
|
|
|
736
837
|
}
|
|
737
838
|
}
|
|
738
839
|
|
|
840
|
+
// ---------------------------------------------------------------------------
|
|
841
|
+
// FailedUserMessage — optimistic turn whose send failed
|
|
842
|
+
// ---------------------------------------------------------------------------
|
|
843
|
+
|
|
844
|
+
/**
|
|
845
|
+
* Renders a user message whose send failed: the message itself stays visible
|
|
846
|
+
* (so the typed text is never lost) with an inline, actionable error beneath
|
|
847
|
+
* it. The error copy is intentionally short — the full reason is surfaced by
|
|
848
|
+
* the consumer's send-error banner; this is the in-thread "Retry" affordance.
|
|
849
|
+
*/
|
|
850
|
+
function FailedUserMessage({
|
|
851
|
+
message,
|
|
852
|
+
onRetry,
|
|
853
|
+
}: {
|
|
854
|
+
message: AgentMessage;
|
|
855
|
+
onRetry?: () => void;
|
|
856
|
+
}) {
|
|
857
|
+
return (
|
|
858
|
+
<div className="flex flex-col gap-1">
|
|
859
|
+
<MessageEntry message={message} />
|
|
860
|
+
<div
|
|
861
|
+
role="alert"
|
|
862
|
+
className="mx-4 flex items-center gap-2 text-xs text-destructive"
|
|
863
|
+
>
|
|
864
|
+
<span className="min-w-0 flex-1 truncate">Couldn’t send.</span>
|
|
865
|
+
{onRetry && (
|
|
866
|
+
<button
|
|
867
|
+
type="button"
|
|
868
|
+
onClick={onRetry}
|
|
869
|
+
className="shrink-0 rounded font-medium underline-offset-2 hover:underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
|
870
|
+
>
|
|
871
|
+
Retry
|
|
872
|
+
</button>
|
|
873
|
+
)}
|
|
874
|
+
</div>
|
|
875
|
+
</div>
|
|
876
|
+
);
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
// ---------------------------------------------------------------------------
|
|
880
|
+
// ExecutionErrorNotice — server failure reason for a terminal-failed execution
|
|
881
|
+
// ---------------------------------------------------------------------------
|
|
882
|
+
|
|
883
|
+
/**
|
|
884
|
+
* Renders the server-reported failure reason (`AgentExecutionStatus.error`)
|
|
885
|
+
* for an execution that died — typically before producing any messages. The
|
|
886
|
+
* reason can be a long Temporal error, so it is clamped by default with a
|
|
887
|
+
* Show more / Show less toggle. An optional Retry resends the originating
|
|
888
|
+
* message as a fresh execution.
|
|
889
|
+
*/
|
|
890
|
+
function ExecutionErrorNotice({
|
|
891
|
+
error,
|
|
892
|
+
retryMessage,
|
|
893
|
+
onRetry,
|
|
894
|
+
}: {
|
|
895
|
+
error: string;
|
|
896
|
+
retryMessage?: string;
|
|
897
|
+
onRetry?: (message: string) => void;
|
|
898
|
+
}) {
|
|
899
|
+
const [expanded, setExpanded] = useState(false);
|
|
900
|
+
const canRetry = !!onRetry && !!retryMessage;
|
|
901
|
+
|
|
902
|
+
return (
|
|
903
|
+
<div
|
|
904
|
+
role="alert"
|
|
905
|
+
className="mx-4 flex flex-col gap-1.5 rounded-md bg-destructive-subtle px-3 py-2"
|
|
906
|
+
>
|
|
907
|
+
<p
|
|
908
|
+
className={cn(
|
|
909
|
+
"text-xs whitespace-pre-wrap break-words text-destructive",
|
|
910
|
+
!expanded && "line-clamp-3",
|
|
911
|
+
)}
|
|
912
|
+
>
|
|
913
|
+
{error}
|
|
914
|
+
</p>
|
|
915
|
+
<div className="flex items-center gap-3 text-xs">
|
|
916
|
+
<button
|
|
917
|
+
type="button"
|
|
918
|
+
onClick={() => setExpanded((v) => !v)}
|
|
919
|
+
className="font-medium text-muted-foreground underline-offset-2 hover:underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
|
920
|
+
>
|
|
921
|
+
{expanded ? "Show less" : "Show more"}
|
|
922
|
+
</button>
|
|
923
|
+
{canRetry && (
|
|
924
|
+
<button
|
|
925
|
+
type="button"
|
|
926
|
+
onClick={() => onRetry!(retryMessage!)}
|
|
927
|
+
className="font-medium text-foreground underline-offset-2 hover:underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
|
928
|
+
>
|
|
929
|
+
Retry
|
|
930
|
+
</button>
|
|
931
|
+
)}
|
|
932
|
+
</div>
|
|
933
|
+
</div>
|
|
934
|
+
);
|
|
935
|
+
}
|
|
936
|
+
|
|
739
937
|
// ---------------------------------------------------------------------------
|
|
740
938
|
// ApprovalCardRow — stabilizes the onSubmit callback for React.memo
|
|
741
939
|
// ---------------------------------------------------------------------------
|
|
@@ -73,6 +73,7 @@ function makeExecution(opts: {
|
|
|
73
73
|
phase?: ExecutionPhase;
|
|
74
74
|
interactionMode?: InteractionMode;
|
|
75
75
|
aiContent?: string;
|
|
76
|
+
error?: string;
|
|
76
77
|
}): AgentExecution {
|
|
77
78
|
const exec = create(AgentExecutionSchema);
|
|
78
79
|
|
|
@@ -91,6 +92,9 @@ function makeExecution(opts: {
|
|
|
91
92
|
|
|
92
93
|
const status = create(AgentExecutionStatusSchema);
|
|
93
94
|
status.phase = opts.phase ?? ExecutionPhase.EXECUTION_COMPLETED;
|
|
95
|
+
if (opts.error !== undefined) {
|
|
96
|
+
status.error = opts.error;
|
|
97
|
+
}
|
|
94
98
|
|
|
95
99
|
const humanMsg = create(AgentMessageSchema);
|
|
96
100
|
humanMsg.type = MessageType.MESSAGE_HUMAN;
|
|
@@ -246,6 +250,132 @@ describe("MessageThread", () => {
|
|
|
246
250
|
expect(screen.getByText(/failed/i)).toBeTruthy();
|
|
247
251
|
});
|
|
248
252
|
|
|
253
|
+
it("surfaces the server failure reason for a FAILED execution", () => {
|
|
254
|
+
const exec = makeExecution({
|
|
255
|
+
id: "exec-fail-reason",
|
|
256
|
+
phase: ExecutionPhase.EXECUTION_FAILED,
|
|
257
|
+
error: "Activity task timed out (RETRY_STATE_MAXIMUM_ATTEMPTS_REACHED)",
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
render(<MessageThread executions={[exec]} />);
|
|
261
|
+
|
|
262
|
+
expect(
|
|
263
|
+
screen.getByText(/Activity task timed out/i),
|
|
264
|
+
).toBeTruthy();
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
it("does NOT surface a failure reason for a COMPLETED execution", () => {
|
|
268
|
+
const exec = makeExecution({
|
|
269
|
+
id: "exec-ok",
|
|
270
|
+
phase: ExecutionPhase.EXECUTION_COMPLETED,
|
|
271
|
+
error: "stale error that should be ignored",
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
render(<MessageThread executions={[exec]} />);
|
|
275
|
+
|
|
276
|
+
expect(screen.queryByText(/stale error/i)).toBeNull();
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
it("offers a Retry that resends the originating message on failure", () => {
|
|
280
|
+
const exec = makeExecution({
|
|
281
|
+
id: "exec-retry",
|
|
282
|
+
specMessage: "do the thing",
|
|
283
|
+
phase: ExecutionPhase.EXECUTION_FAILED,
|
|
284
|
+
error: "boom",
|
|
285
|
+
});
|
|
286
|
+
const onRetryExecution = vi.fn();
|
|
287
|
+
|
|
288
|
+
render(
|
|
289
|
+
<MessageThread executions={[exec]} onRetryExecution={onRetryExecution} />,
|
|
290
|
+
);
|
|
291
|
+
|
|
292
|
+
fireEvent.click(screen.getByRole("button", { name: /retry/i }));
|
|
293
|
+
expect(onRetryExecution).toHaveBeenCalledWith("do the thing");
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
it("renders a failed pending message with an inline Retry", () => {
|
|
297
|
+
const onRetrySend = vi.fn();
|
|
298
|
+
|
|
299
|
+
render(
|
|
300
|
+
<MessageThread
|
|
301
|
+
executions={[]}
|
|
302
|
+
pendingUserMessage="unsent message"
|
|
303
|
+
pendingMessageFailed
|
|
304
|
+
onRetrySend={onRetrySend}
|
|
305
|
+
/>,
|
|
306
|
+
);
|
|
307
|
+
|
|
308
|
+
expect(screen.getByText("unsent message")).toBeTruthy();
|
|
309
|
+
fireEvent.click(screen.getByRole("button", { name: /retry/i }));
|
|
310
|
+
expect(onRetrySend).toHaveBeenCalledOnce();
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
it("shows an Edit affordance on the in-flight human turn and routes onEditMessage", () => {
|
|
314
|
+
const active = makeExecution({
|
|
315
|
+
id: "exec-active",
|
|
316
|
+
specMessage: "fix the bug",
|
|
317
|
+
phase: ExecutionPhase.EXECUTION_IN_PROGRESS,
|
|
318
|
+
aiContent: "working on it",
|
|
319
|
+
});
|
|
320
|
+
const onEditMessage = vi.fn();
|
|
321
|
+
|
|
322
|
+
render(
|
|
323
|
+
<MessageThread
|
|
324
|
+
executions={[]}
|
|
325
|
+
activeStreamExecution={active}
|
|
326
|
+
onEditMessage={onEditMessage}
|
|
327
|
+
/>,
|
|
328
|
+
);
|
|
329
|
+
|
|
330
|
+
const editBtn = screen.getByRole("button", { name: "Edit message" });
|
|
331
|
+
fireEvent.click(editBtn);
|
|
332
|
+
expect(onEditMessage).toHaveBeenCalledWith("fix the bug");
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
it("shows no Edit affordance when onEditMessage is omitted", () => {
|
|
336
|
+
const active = makeExecution({
|
|
337
|
+
id: "exec-active",
|
|
338
|
+
specMessage: "fix the bug",
|
|
339
|
+
phase: ExecutionPhase.EXECUTION_IN_PROGRESS,
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
render(
|
|
343
|
+
<MessageThread executions={[]} activeStreamExecution={active} />,
|
|
344
|
+
);
|
|
345
|
+
|
|
346
|
+
expect(
|
|
347
|
+
screen.queryByRole("button", { name: "Edit message" }),
|
|
348
|
+
).toBeNull();
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
it("marks only the active-stream human turn editable, not completed turns", () => {
|
|
352
|
+
const completed = makeExecution({
|
|
353
|
+
id: "exec-done",
|
|
354
|
+
specMessage: "old turn",
|
|
355
|
+
phase: ExecutionPhase.EXECUTION_COMPLETED,
|
|
356
|
+
});
|
|
357
|
+
const active = makeExecution({
|
|
358
|
+
id: "exec-active",
|
|
359
|
+
specMessage: "new turn",
|
|
360
|
+
phase: ExecutionPhase.EXECUTION_IN_PROGRESS,
|
|
361
|
+
});
|
|
362
|
+
const onEditMessage = vi.fn();
|
|
363
|
+
|
|
364
|
+
render(
|
|
365
|
+
<MessageThread
|
|
366
|
+
executions={[completed]}
|
|
367
|
+
activeStreamExecution={active}
|
|
368
|
+
onEditMessage={onEditMessage}
|
|
369
|
+
/>,
|
|
370
|
+
);
|
|
371
|
+
|
|
372
|
+
const editBtns = screen.getAllByRole("button", { name: "Edit message" });
|
|
373
|
+
expect(editBtns).toHaveLength(1);
|
|
374
|
+
|
|
375
|
+
fireEvent.click(editBtns[0]);
|
|
376
|
+
expect(onEditMessage).toHaveBeenCalledWith("new turn");
|
|
377
|
+
});
|
|
378
|
+
|
|
249
379
|
it("renders plan-completion card when last execution is completed Plan mode", () => {
|
|
250
380
|
const exec = makeExecution({
|
|
251
381
|
id: "exec-plan",
|