Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions src/renderer/components/thread/ChatPane/ChatPane.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1128,6 +1128,43 @@ describe("ChatPane", () => {
expect(screen.queryByRole("button", { name: "Subagent Result" })).not.toBeInTheDocument();
});

it("shows an intentionally cancelled Crossagent without an error indicator", async () => {
const thread = makeThread();
useAppStore.getState().applyRuntimeEvent(thread.id, {
type: "item.started",
threadId: thread.id,
itemId: "crossagent-cancelled",
itemType: "tool_call",
payload: {
name: "cancel probe",
status: "running",
isCrossagent: true,
crossagentStatus: "running",
},
});
useAppStore.getState().applyRuntimeEvent(thread.id, {
type: "item.completed",
threadId: thread.id,
itemId: "crossagent-cancelled",
payload: {
name: "cancel probe",
status: "error",
isCrossagent: true,
crossagentStatus: "cancelled",
},
});

renderChatPane(thread);
await waitFor(() => expect(hydrateThreadRuntimeItems).toHaveBeenCalledWith(thread.id));

const row = await screen.findByRole("button", {
name: "Open Crossagent: Crossagent: cancel probe",
});
expect(row).toHaveTextContent("cancelled");
expect(row).toHaveAccessibleDescription("cancelled");
expect(screen.queryByLabelText("error")).not.toBeInTheDocument();
});

it("separates the collapsed Agent label from its step count", async () => {
const thread = makeThread();
useAppStore.getState().applyRuntimeEvent(thread.id, {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -447,6 +447,55 @@ describe("SubAgentContent", () => {
);
});

it("shows an explicit terminal status for a cancelled Crossagent", async () => {
const threadId = "thread-1";
const runningParent = makeSubAgentItem("parent-1");
const parentItem: RuntimeChatItem = {
...runningParent,
state: "completed",
payload: {
...(runningParent.payload as ToolCallPayload),
status: "error",
isCrossagent: true,
crossagentStatus: "cancelled",
},
};

useAppStore.setState({
runtimeItemIdsByThread: { [threadId]: [parentItem.id] },
runtimeItemsByIdByThread: { [threadId]: { [parentItem.id]: parentItem } },
runtimeStructuralVersionByThread: { [threadId]: 1 },
});

render(<SubAgentContent threadId={threadId} parentItemId={parentItem.id} />);

expect(await screen.findByText("Cancelled")).toBeInTheDocument();
});

it("derives the terminal status for persisted Crossagents without the new status field", async () => {
const threadId = "thread-1";
const runningParent = makeSubAgentItem("parent-1");
const parentItem: RuntimeChatItem = {
...runningParent,
state: "completed",
payload: {
...(runningParent.payload as ToolCallPayload),
status: "success",
isCrossagent: true,
},
};

useAppStore.setState({
runtimeItemIdsByThread: { [threadId]: [parentItem.id] },
runtimeItemsByIdByThread: { [threadId]: { [parentItem.id]: parentItem } },
runtimeStructuralVersionByThread: { [threadId]: 1 },
});

render(<SubAgentContent threadId={threadId} parentItemId={parentItem.id} />);

expect(await screen.findByText("Completed")).toBeInTheDocument();
});

it("hands an open target to its host and consumes the transient store signal", async () => {
const threadId = "thread-1";
const parentItem = makeSubAgentItem("parent-1");
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { useEffect, useId, useRef, type ReactNode } from "react";
import { Surface } from "@heroui/react";
import { Trans, useLingui } from "@lingui/react/macro";
import { Bot, X } from "lucide-react";
import type { ProjectLocation, ToolCallPayload } from "@/shared/contracts";
Expand All @@ -21,6 +22,7 @@ import { ChatScrollControls, type ChatScrollControlsHandle } from "../../ChatScr
import { ChatTurnElapsedFooter, type TurnTiming } from "../../ChatTurnElapsed";
import { MessageList } from "../MessageList";
import { buildSubAgentProgressParts } from "./subAgentProgressMeta";
import { chatMessageSurfaceClass } from "./chatMessageSurface";
import { deriveToolDisplay, isCrossagentTool, isWorkflowTool } from "./toolDisplay";
import { WorkflowOverlayBody } from "./WorkflowOverlayBody";
import { parseWorkflowInfo, type WorkflowInfo } from "./workflowDisplay";
Expand Down Expand Up @@ -138,6 +140,12 @@ export function SubAgentContent({
}
: null;
const turn = resolveSubAgentTurnTiming(item, payload, isRunning);
const crossagentStatus =
isCrossagent && !isRunning
? payload?.crossagentStatus === "running"
? null
: (payload?.crossagentStatus ?? (payload?.status === "success" ? "completed" : "failed"))
: null;

const renderWorkflow = !!(workflow && workflow.manifestPath);
return (
Expand All @@ -164,6 +172,7 @@ export function SubAgentContent({
entries={childEntries}
stickToBottom={isRunning}
turn={turn}
crossagentStatus={crossagentStatus}
workflow={workflow}
workflowProgress={workflowProgress}
/>
Expand Down Expand Up @@ -330,6 +339,7 @@ function ChildList({
entries,
stickToBottom,
turn,
crossagentStatus,
workflow,
workflowProgress,
}: {
Expand All @@ -338,6 +348,7 @@ function ChildList({
entries: readonly ChatTimelineEntry[];
stickToBottom: boolean;
turn: TurnTiming | null;
crossagentStatus: "completed" | "failed" | "cancelled" | null;
workflow: WorkflowInfo | null;
workflowProgress: WorkflowOverlayProgress | null;
}) {
Expand Down Expand Up @@ -374,7 +385,14 @@ function ChildList({
<WorkflowOverlayHeader workflow={workflow} progress={workflowProgress} />
) : null
}
footer={turn ? <ChatTurnElapsedFooter turn={turn} /> : null}
footer={
crossagentStatus || turn ? (
<>
{crossagentStatus ? <CrossagentStatusFooter status={crossagentStatus} /> : null}
{turn ? <ChatTurnElapsedFooter turn={turn} /> : null}
</>
) : null
}
emptyContent={
workflow ? (
<WorkflowEmptyState progress={workflowProgress} />
Expand Down Expand Up @@ -407,6 +425,24 @@ function ChildList({
);
}

function CrossagentStatusFooter({ status }: { status: "completed" | "failed" | "cancelled" }) {
const { t } = useLingui();
const label =
status === "completed" ? t`Completed` : status === "cancelled" ? t`Cancelled` : t`Failed`;
return (
<div className="mx-auto w-full max-w-[920px]">
<Surface variant="transparent" className={chatMessageSurfaceClass}>
<span
className="text-[length:var(--lc-chat-font-size-meta)] text-foreground-muted"
aria-live="polite"
>
{label}
</span>
</Surface>
</div>
);
}

function resolveSubAgentTurnTiming(
item: RuntimeChatItem,
payload: ToolCallPayload | undefined,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { memo, useState, type ReactNode } from "react";
import { memo, useId, useState, type ReactNode } from "react";
import { Tooltip } from "@heroui/react";
import { msg } from "@lingui/core/macro";
import { Trans, useLingui } from "@lingui/react/macro";
Expand Down Expand Up @@ -81,9 +81,11 @@ export const SubAgentToolCall = memo(function SubAgentToolCall({
(workflowRun.run === null || isWorkflowRunLive(workflowRun.run));
const isRunning = !isCompleted || workflowIsLive;
const titleRef = useShimmer<HTMLElement>(isRunning);
const statusDescriptionId = useId();
if (!payload?.name) return null;
const display = deriveToolDisplay(payload);
const isCrossagent = isCrossagentTool(payload);
const describesCancelledStatus = isCrossagent && payload.crossagentStatus === "cancelled";
const displayTitle = normalizeCallTitleSeparator(display.title);
const displayPrefix = display.parts
? normalizeCallTitleSeparator(display.parts.prefix)
Expand Down Expand Up @@ -118,6 +120,7 @@ export const SubAgentToolCall = memo(function SubAgentToolCall({
aria-label={
isCrossagent ? t`Open Crossagent: ${display.title}` : t`Open subagent: ${display.title}`
}
{...(describesCancelledStatus ? { "aria-describedby": statusDescriptionId } : {})}
>
<span className="size-3 shrink-0 text-[color:var(--muted)]">
<Icon className="size-3" />
Expand Down Expand Up @@ -155,7 +158,10 @@ export const SubAgentToolCall = memo(function SubAgentToolCall({
</code>
)}
{status.rightLabel ? (
<span className={`shrink-0 tabular-nums font-medium ${status.rightLabelClassName}`}>
<span
className={`shrink-0 tabular-nums font-medium ${status.rightLabelClassName}`}
{...(describesCancelledStatus ? { id: statusDescriptionId } : {})}
>
{status.rightLabel}
</span>
) : null}
Expand Down Expand Up @@ -268,6 +274,12 @@ function resolveStatus(
rightLabelClassName: "!text-[color:var(--muted)]",
};
}
if (payload?.crossagentStatus === "cancelled") {
return {
rightLabel: <Trans>cancelled</Trans>,
rightLabelClassName: "!text-[color:var(--muted)]",
};
}
if (payload?.status === "error") {
const icon = <CircleAlert className="size-3 text-danger" aria-label={t(msg`error`)} />;
return {
Expand Down
4 changes: 4 additions & 0 deletions src/renderer/locales/de/messages.po
Original file line number Diff line number Diff line change
Expand Up @@ -1957,10 +1957,12 @@ msgstr "Entfernen des Projekts abbrechen"
msgid "Cancel workflow"
msgstr "Workflow abbrechen"

#: src/renderer/components/thread/ChatPane/parts/items/SubAgentToolCall.tsx
#: src/renderer/components/thread/ChatPane/parts/items/WorkflowOverlayBody.tsx
msgid "cancelled"
msgstr "abgebrochen"

#: src/renderer/components/thread/ChatPane/parts/items/SubAgentOverlay.tsx
#: src/renderer/components/thread/ThreadGoalDock.tsx
#: src/renderer/utils/prStatus.ts
#: src/renderer/views/GitHubActionsView/GitHubActionsRunList.tsx
Expand Down Expand Up @@ -2754,6 +2756,7 @@ msgstr "Füllen Sie die Eingabeaufforderungen in diesem Terminal aus. Wird gesch
msgid "completed"
msgstr "abgeschlossen"

#: src/renderer/components/thread/ChatPane/parts/items/SubAgentOverlay.tsx
#: src/renderer/utils/prStatus.ts
msgid "Completed"
msgstr "Abgeschlossen"
Expand Down Expand Up @@ -4568,6 +4571,7 @@ msgstr "Factory Droid"
msgid "failed"
msgstr "fehlgeschlagen"

#: src/renderer/components/thread/ChatPane/parts/items/SubAgentOverlay.tsx
#: src/renderer/components/thread/ThreadGoalDock.tsx
#: src/renderer/utils/prStatus.ts
#: src/renderer/views/GitHubActionsView/GitHubActionsRunList.tsx
Expand Down
4 changes: 4 additions & 0 deletions src/renderer/locales/en/messages.po
Original file line number Diff line number Diff line change
Expand Up @@ -1962,10 +1962,12 @@ msgstr "Cancel removing project"
msgid "Cancel workflow"
msgstr "Cancel workflow"

#: src/renderer/components/thread/ChatPane/parts/items/SubAgentToolCall.tsx
#: src/renderer/components/thread/ChatPane/parts/items/WorkflowOverlayBody.tsx
msgid "cancelled"
msgstr "cancelled"

#: src/renderer/components/thread/ChatPane/parts/items/SubAgentOverlay.tsx
#: src/renderer/components/thread/ThreadGoalDock.tsx
#: src/renderer/utils/prStatus.ts
#: src/renderer/views/GitHubActionsView/GitHubActionsRunList.tsx
Expand Down Expand Up @@ -2759,6 +2761,7 @@ msgstr "Complete the prompts in this terminal. Closes when finished."
msgid "completed"
msgstr "completed"

#: src/renderer/components/thread/ChatPane/parts/items/SubAgentOverlay.tsx
#: src/renderer/utils/prStatus.ts
msgid "Completed"
msgstr "Completed"
Expand Down Expand Up @@ -4573,6 +4576,7 @@ msgstr "Factory Droid"
msgid "failed"
msgstr "failed"

#: src/renderer/components/thread/ChatPane/parts/items/SubAgentOverlay.tsx
#: src/renderer/components/thread/ThreadGoalDock.tsx
#: src/renderer/utils/prStatus.ts
#: src/renderer/views/GitHubActionsView/GitHubActionsRunList.tsx
Expand Down
4 changes: 4 additions & 0 deletions src/renderer/locales/es/messages.po
Original file line number Diff line number Diff line change
Expand Up @@ -1957,10 +1957,12 @@ msgstr "Cancelar eliminación del proyecto"
msgid "Cancel workflow"
msgstr "Cancelar flujo de trabajo"

#: src/renderer/components/thread/ChatPane/parts/items/SubAgentToolCall.tsx
#: src/renderer/components/thread/ChatPane/parts/items/WorkflowOverlayBody.tsx
msgid "cancelled"
msgstr "cancelado"

#: src/renderer/components/thread/ChatPane/parts/items/SubAgentOverlay.tsx
#: src/renderer/components/thread/ThreadGoalDock.tsx
#: src/renderer/utils/prStatus.ts
#: src/renderer/views/GitHubActionsView/GitHubActionsRunList.tsx
Expand Down Expand Up @@ -2754,6 +2756,7 @@ msgstr "Completa las instrucciones en este terminal. Se cerrará al finalizar."
msgid "completed"
msgstr "completado"

#: src/renderer/components/thread/ChatPane/parts/items/SubAgentOverlay.tsx
#: src/renderer/utils/prStatus.ts
msgid "Completed"
msgstr "Completado"
Expand Down Expand Up @@ -4568,6 +4571,7 @@ msgstr "Factory Droid"
msgid "failed"
msgstr "fallido"

#: src/renderer/components/thread/ChatPane/parts/items/SubAgentOverlay.tsx
#: src/renderer/components/thread/ThreadGoalDock.tsx
#: src/renderer/utils/prStatus.ts
#: src/renderer/views/GitHubActionsView/GitHubActionsRunList.tsx
Expand Down
4 changes: 4 additions & 0 deletions src/renderer/locales/fr/messages.po
Original file line number Diff line number Diff line change
Expand Up @@ -1957,10 +1957,12 @@ msgstr "Annuler la suppression du projet"
msgid "Cancel workflow"
msgstr "Annuler le workflow"

#: src/renderer/components/thread/ChatPane/parts/items/SubAgentToolCall.tsx
#: src/renderer/components/thread/ChatPane/parts/items/WorkflowOverlayBody.tsx
msgid "cancelled"
msgstr "annulé"

#: src/renderer/components/thread/ChatPane/parts/items/SubAgentOverlay.tsx
#: src/renderer/components/thread/ThreadGoalDock.tsx
#: src/renderer/utils/prStatus.ts
#: src/renderer/views/GitHubActionsView/GitHubActionsRunList.tsx
Expand Down Expand Up @@ -2754,6 +2756,7 @@ msgstr "Complétez les invites dans ce terminal. Se ferme une fois terminé."
msgid "completed"
msgstr "terminé"

#: src/renderer/components/thread/ChatPane/parts/items/SubAgentOverlay.tsx
#: src/renderer/utils/prStatus.ts
msgid "Completed"
msgstr "Terminé"
Expand Down Expand Up @@ -4568,6 +4571,7 @@ msgstr "Factory Droid"
msgid "failed"
msgstr "échoué"

#: src/renderer/components/thread/ChatPane/parts/items/SubAgentOverlay.tsx
#: src/renderer/components/thread/ThreadGoalDock.tsx
#: src/renderer/utils/prStatus.ts
#: src/renderer/views/GitHubActionsView/GitHubActionsRunList.tsx
Expand Down
4 changes: 4 additions & 0 deletions src/renderer/locales/ja/messages.po
Original file line number Diff line number Diff line change
Expand Up @@ -1956,10 +1956,12 @@ msgstr "プロジェクトの削除をキャンセル"
msgid "Cancel workflow"
msgstr "ワークフローをキャンセル"

#: src/renderer/components/thread/ChatPane/parts/items/SubAgentToolCall.tsx
#: src/renderer/components/thread/ChatPane/parts/items/WorkflowOverlayBody.tsx
msgid "cancelled"
msgstr "キャンセルされました"

#: src/renderer/components/thread/ChatPane/parts/items/SubAgentOverlay.tsx
#: src/renderer/components/thread/ThreadGoalDock.tsx
#: src/renderer/utils/prStatus.ts
#: src/renderer/views/GitHubActionsView/GitHubActionsRunList.tsx
Expand Down Expand Up @@ -2753,6 +2755,7 @@ msgstr "このターミナルでプロンプトを完了します。終了した
msgid "completed"
msgstr "完了しました"

#: src/renderer/components/thread/ChatPane/parts/items/SubAgentOverlay.tsx
#: src/renderer/utils/prStatus.ts
msgid "Completed"
msgstr "完了"
Expand Down Expand Up @@ -4567,6 +4570,7 @@ msgstr "Factory Droid"
msgid "failed"
msgstr "失敗しました"

#: src/renderer/components/thread/ChatPane/parts/items/SubAgentOverlay.tsx
#: src/renderer/components/thread/ThreadGoalDock.tsx
#: src/renderer/utils/prStatus.ts
#: src/renderer/views/GitHubActionsView/GitHubActionsRunList.tsx
Expand Down
Loading