diff --git a/autogpt_platform/frontend/src/app/(platform)/copilot/components/ChatMessagesContainer/ChatMessagesContainer.tsx b/autogpt_platform/frontend/src/app/(platform)/copilot/components/ChatMessagesContainer/ChatMessagesContainer.tsx
index 620c108388..f2c0d9e442 100644
--- a/autogpt_platform/frontend/src/app/(platform)/copilot/components/ChatMessagesContainer/ChatMessagesContainer.tsx
+++ b/autogpt_platform/frontend/src/app/(platform)/copilot/components/ChatMessagesContainer/ChatMessagesContainer.tsx
@@ -51,6 +51,7 @@ function renderSegments(
messageID: string,
onRetry?: () => void,
isLastMessage?: boolean,
+ isMessageStreaming?: boolean,
): React.ReactNode[] {
return segments.map((seg, segIdx) => {
if (seg.kind === "collapsed-group") {
@@ -64,6 +65,7 @@ function renderSegments(
partIndex={seg.index}
onRetry={onRetry}
isLastMessage={isLastMessage}
+ isMessageStreaming={isMessageStreaming}
/>
);
});
@@ -375,6 +377,7 @@ export function ChatMessagesContainer({
message.id,
isLastAssistant ? onRetry : undefined,
isLastAssistant,
+ isCurrentlyStreaming,
)
: message.parts.map((part, i) => (
))}
{isLastInTurn && !isCurrentlyStreaming && (
diff --git a/autogpt_platform/frontend/src/app/(platform)/copilot/components/ChatMessagesContainer/components/MessagePartRenderer.tsx b/autogpt_platform/frontend/src/app/(platform)/copilot/components/ChatMessagesContainer/components/MessagePartRenderer.tsx
index b86f73d86b..a4dbfb04be 100644
--- a/autogpt_platform/frontend/src/app/(platform)/copilot/components/ChatMessagesContainer/components/MessagePartRenderer.tsx
+++ b/autogpt_platform/frontend/src/app/(platform)/copilot/components/ChatMessagesContainer/components/MessagePartRenderer.tsx
@@ -95,6 +95,7 @@ interface Props {
partIndex: number;
onRetry?: () => void;
isLastMessage?: boolean;
+ isMessageStreaming?: boolean;
}
export function MessagePartRenderer({
@@ -103,6 +104,7 @@ export function MessagePartRenderer({
partIndex,
onRetry,
isLastMessage,
+ isMessageStreaming,
}: Props) {
const key = `${messageID}-${partIndex}`;
@@ -176,6 +178,7 @@ export function MessagePartRenderer({
key={key}
part={part as ToolUIPart}
isLastMessage={isLastMessage}
+ isMessageStreaming={isMessageStreaming}
/>
);
case "tool-create_agent":
diff --git a/autogpt_platform/frontend/src/app/(platform)/copilot/tools/DecomposeGoal/DecomposeGoal.tsx b/autogpt_platform/frontend/src/app/(platform)/copilot/tools/DecomposeGoal/DecomposeGoal.tsx
index 53992d9e13..33b7e5939c 100644
--- a/autogpt_platform/frontend/src/app/(platform)/copilot/tools/DecomposeGoal/DecomposeGoal.tsx
+++ b/autogpt_platform/frontend/src/app/(platform)/copilot/tools/DecomposeGoal/DecomposeGoal.tsx
@@ -68,9 +68,17 @@ interface EditableStep {
interface Props {
part: ToolUIPart;
isLastMessage?: boolean;
+ // True while the parent assistant message is still streaming. We disable
+ // Approve/Modify in this window because the chat session is locked to
+ // the in-flight turn — sending a new user message would fail.
+ isMessageStreaming?: boolean;
}
-export function DecomposeGoalTool({ part, isLastMessage }: Props) {
+export function DecomposeGoalTool({
+ part,
+ isLastMessage,
+ isMessageStreaming,
+}: Props) {
const text = getAnimationText(part);
const { onSend } = useCopilotChatActions();
@@ -88,6 +96,12 @@ export function DecomposeGoalTool({ part, isLastMessage }: Props) {
isDecompositionOutput(output) &&
output.requires_approval;
+ // The Approve/Modify buttons are visible (so the user knows what's
+ // coming) but click-disabled while the assistant is still streaming
+ // its summary text after the tool call. The countdown ring keeps
+ // ticking so it stays in sync with the server-side timer.
+ const actionsEnabled = showActions && !isMessageStreaming;
+
// Authoritative countdown comes from the backend tool response so the
// server-side fallback timer and the client are guaranteed to agree.
const countdownSeconds =
@@ -181,14 +195,16 @@ export function DecomposeGoalTool({ part, isLastMessage }: Props) {
return () => clearInterval(interval);
}, [showActions, timerActive, part.toolCallId]);
- // Auto-approve when countdown reaches 0 (only if timer is still active and actions visible).
- // showActions prevents firing after isLastMessage changes (race condition guard).
+ // Auto-approve when countdown reaches 0 — but only after the assistant
+ // has finished streaming its summary text. Firing during streaming would
+ // hit the same locked-session failure as a manual click. If the timer
+ // hits 0 mid-stream, this effect re-runs when actionsEnabled flips true.
// approve() is stable via approvedRef — safe to omit from deps.
useEffect(() => {
- if (secondsLeft === 0 && timerActive && showActions) {
+ if (secondsLeft === 0 && timerActive && actionsEnabled) {
approve();
}
- }, [secondsLeft, timerActive, showActions]); // approve reads refs only — safe to omit
+ }, [secondsLeft, timerActive, actionsEnabled]); // approve reads refs only — safe to omit
const progress = secondsLeft / countdownSeconds;
const dashOffset = CIRCUMFERENCE * (1 - progress);
@@ -292,7 +308,11 @@ export function DecomposeGoalTool({ part, isLastMessage }: Props) {
{showActions && (
{isEditing ? (
-