a1143aa525
* fix(acp): handle claude-acp async_launched subagents end-to-end Claude Agent SDK's Task tool with run_in_background=true returns session/prompt with stop_reason=end_turn while subagents stay alive in the background, reporting `_meta.claudeCode.toolResponse.status = "async_launched"` instead of a terminal top-level status. kandev previously treated stop_reason as authoritative, cancelled the subagent card on prompt return, and fired an "agent finished without producing any output" notice — even though the dispatch did produce work and the subagent's child events would arrive seconds later. Backend: - `SubagentTaskPayload` gains `IsAsync` / `OutputFile` / `CanReadOutputFile` so the async-launched envelope is preserved. - `convertToolCallResultUpdate` recognizes async_launched as terminal for the Task tool itself (dispatch is the completion). - `cancelActiveToolCalls` preserves subagent_task tool calls through the prompt-return sweep so a late terminal tool_call_update from the SDK still lands correctly (covers the in-turn-complete case). Frontend: - `subagentMetaChips` emits a "background" chip when `is_async` or `status == "async_launched"` so the card is visually distinct. - `computeEmptyTurnNotice` suppresses the notice when any subagent task message exists for the turn (the dispatch IS output). Reproduced via `acpdbg prompt` against `claude-acp` 0.31.1; captured frames in acp-debug/claude-acp-prompt-20260602-110119.jsonl drive the backend test fixtures verbatim. * fix(acp): harden async_launched override against future SDK frames Address PR review feedback (greptile #1244, claude review): - Move the async_launched override inside the mutex lock so it can inspect the stored payload kind before flipping the status. - Drop the `status == ""` precondition. The earlier in_progress synthesis (Title/RawInput/Content/Locations) was implicitly trumping the override; a future SDK frame that combines async_launched with any of those fields would have left the card stuck in_progress forever. The override is now unconditional for async_launched subagent_task frames. - Gate the override on `payload.Kind() == ToolKindSubagentTask`. A hypothetical future claude-acp tool that reuses the `async_launched` literal in its own `_meta.claudeCode.toolResponse.status` would have been incorrectly marked complete by the prior unscoped check. Pinned by two new tests: - TestConvertToolCallResultUpdate_AsyncLaunched_OverridesInProgress - TestConvertToolCallResultUpdate_AsyncLaunched_NonSubagentNotTerminated --------- Co-authored-by: Kandev Agent <agent@kandev.dev>
65 lines
2.2 KiB
TypeScript
65 lines
2.2 KiB
TypeScript
import type { SubagentTaskPayload } from "@/components/task/chat/types";
|
|
|
|
export type SubagentMetaChip = {
|
|
label: string;
|
|
value: string;
|
|
};
|
|
|
|
const MAX_ID_LENGTH = 12;
|
|
|
|
function truncateId(value: string): string {
|
|
if (value.length <= MAX_ID_LENGTH) return value;
|
|
return `${value.slice(0, MAX_ID_LENGTH)}…`;
|
|
}
|
|
|
|
function formatDuration(ms: number): string {
|
|
if (ms < 1000) return `${ms}ms`;
|
|
return `${(ms / 1000).toFixed(1)}s`;
|
|
}
|
|
|
|
function formatTokens(tokens: number): string {
|
|
return `${tokens.toLocaleString("en-US")} tokens`;
|
|
}
|
|
|
|
function formatTools(count: number): string {
|
|
return `${count} tool${count === 1 ? "" : "s"}`;
|
|
}
|
|
|
|
/**
|
|
* Turns a SubagentTaskPayload into an ordered list of display chips. Different
|
|
* agents populate different subsets (Claude: tokens/duration/tool_use_count;
|
|
* OpenCode: model/child_session_id; Cursor: duration_ms only),
|
|
* so each field is only included when meaningfully present. Duration and tokens
|
|
* are skipped when zero; tool_use_count is shown even at zero since "0 tools"
|
|
* is meaningful for a completed subagent.
|
|
*/
|
|
export function subagentMetaChips(payload: SubagentTaskPayload | undefined): SubagentMetaChip[] {
|
|
if (!payload) return [];
|
|
|
|
const chips: SubagentMetaChip[] = [];
|
|
|
|
// Claude Code's Task with run_in_background=true: the dispatch is terminal,
|
|
// but the subagent runs out-of-band and writes its result to output_file.
|
|
// Surface a "background" chip so the UI doesn't look like a normal completion.
|
|
if (payload.is_async || payload.status === "async_launched") {
|
|
chips.push({ label: "background", value: "background" });
|
|
}
|
|
if (typeof payload.duration_ms === "number" && payload.duration_ms > 0) {
|
|
chips.push({ label: "duration", value: formatDuration(payload.duration_ms) });
|
|
}
|
|
if (typeof payload.total_tokens === "number" && payload.total_tokens > 0) {
|
|
chips.push({ label: "tokens", value: formatTokens(payload.total_tokens) });
|
|
}
|
|
if (typeof payload.tool_use_count === "number") {
|
|
chips.push({ label: "tools", value: formatTools(payload.tool_use_count) });
|
|
}
|
|
if (payload.model) {
|
|
chips.push({ label: "model", value: payload.model });
|
|
}
|
|
if (payload.child_session_id) {
|
|
chips.push({ label: "session", value: truncateId(payload.child_session_id) });
|
|
}
|
|
|
|
return chips;
|
|
}
|