Skip to content

Commit b83b66b

Browse files
committed
fix subagents step not updating live in tool preview. Refresh
subagent tool view and include input params for certain tools in preview
1 parent 3476646 commit b83b66b

6 files changed

Lines changed: 188 additions & 72 deletions

File tree

src/ai/daemon-ai.ts

Lines changed: 1 addition & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import type {
2525
import { debug, toolDebug } from "../utils/debug-logger";
2626
import { getOpenRouterReportedCost } from "../utils/openrouter-reported-cost";
2727
import { getWorkspacePath } from "../utils/workspace-manager";
28+
import { extractFinalAssistantText } from "./message-utils";
2829
import { TRANSCRIPTION_MODEL, buildOpenRouterChatSettings, getResponseModel } from "./model-config";
2930
import { sanitizeMessagesForInput } from "./sanitize-messages";
3031
import { type InteractionMode, buildDaemonSystemPrompt } from "./system-prompt";
@@ -54,40 +55,6 @@ function normalizeStreamError(error: unknown): Error {
5455
return new Error(String(error));
5556
}
5657

57-
/**
58-
* Extract the final text content from the last assistant message.
59-
* In multi-step agent loops, we only want to speak the final response, not intermediate text.
60-
*/
61-
function extractFinalAssistantText(messages: ModelMessage[]): string {
62-
// Find the last assistant message
63-
for (let i = messages.length - 1; i >= 0; i--) {
64-
const msg = messages[i];
65-
if (msg?.role === "assistant") {
66-
const content = msg.content;
67-
if (Array.isArray(content)) {
68-
// Find the last text part. In some models/providers, intermediate
69-
// "thoughts" might be included as separate text blocks before the final answer.
70-
// We prioritize the last text block in the message for the final response.
71-
for (let j = content.length - 1; j >= 0; j--) {
72-
const part = content[j];
73-
if (
74-
part &&
75-
typeof part === "object" &&
76-
"type" in part &&
77-
part.type === "text" &&
78-
"text" in part &&
79-
typeof part.text === "string"
80-
) {
81-
return part.text;
82-
}
83-
}
84-
// If this assistant message had no text parts, continue searching previous messages
85-
}
86-
}
87-
}
88-
return "";
89-
}
90-
9158
/**
9259
* The DAEMON agent instance.
9360
* Handles the agent loop internally, allowing for multi-step tool usage.

src/ai/message-utils.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import type { ModelMessage } from "ai";
2+
3+
export function extractFinalAssistantText(messages: ModelMessage[]): string {
4+
for (let i = messages.length - 1; i >= 0; i--) {
5+
const msg = messages[i];
6+
if (msg?.role === "assistant") {
7+
const content = msg.content;
8+
if (Array.isArray(content)) {
9+
for (let j = content.length - 1; j >= 0; j--) {
10+
const part = content[j];
11+
if (
12+
part &&
13+
typeof part === "object" &&
14+
"type" in part &&
15+
part.type === "text" &&
16+
"text" in part &&
17+
typeof part.text === "string"
18+
) {
19+
return part.text;
20+
}
21+
}
22+
}
23+
}
24+
}
25+
return "";
26+
}

src/ai/tools/subagents.ts

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,13 @@
55

66
import { createOpenRouter } from "@openrouter/ai-sdk-provider";
77
import { tool } from "ai";
8-
import { ToolLoopAgent, stepCountIs } from "ai";
8+
import { type ModelMessage, ToolLoopAgent, stepCountIs } from "ai";
99
import type { ToolSet } from "ai";
1010
import { z } from "zod";
1111
import { getDaemonManager } from "../../state/daemon-state";
1212
import type { SubagentProgressEmitter } from "../../types";
1313
import { getOpenRouterReportedCost } from "../../utils/openrouter-reported-cost";
14+
import { extractFinalAssistantText } from "../message-utils";
1415
import { buildOpenRouterChatSettings, getSubagentModel } from "../model-config";
1516
import { buildToolSet } from "./tool-registry";
1617

@@ -60,7 +61,7 @@ RULES:
6061
- The final summary needs to be self contained and needs to provide enough information to the main agent so it is clear what you have done and what the results are.
6162
6263
Today's date: ${new Date().toISOString().split("T")[0]}
63-
`;
64+
`;
6465
}
6566

6667
// Global emitter that will be set by the daemon-ai module
@@ -109,7 +110,6 @@ Provide a concise summary for display and a very specific task description (espe
109110
stopWhen: stepCountIs(MAX_SUBAGENT_STEPS),
110111
});
111112

112-
let responseText = "";
113113
let costTotal = 0;
114114
let hasCost = false;
115115

@@ -119,9 +119,7 @@ Provide a concise summary for display and a very specific task description (espe
119119
});
120120

121121
for await (const part of stream.fullStream) {
122-
if (part.type === "text-delta") {
123-
responseText += part.text;
124-
} else if (part.type === "finish-step") {
122+
if (part.type === "finish-step") {
125123
const reportedCost = getOpenRouterReportedCost(part.providerMetadata);
126124
if (reportedCost !== undefined) {
127125
costTotal += reportedCost;
@@ -145,6 +143,9 @@ Provide a concise summary for display and a very specific task description (espe
145143
}
146144
}
147145

146+
const responseMessages = await stream.response.then((response) => response.messages);
147+
const finalResponse = extractFinalAssistantText(responseMessages);
148+
148149
const streamUsage = await stream.usage;
149150
if (streamUsage) {
150151
progressEmitter?.onSubagentUsage({
@@ -163,7 +164,7 @@ Provide a concise summary for display and a very specific task description (espe
163164
return {
164165
success: true,
165166
summary,
166-
response: responseText || "Task completed but no text response generated.",
167+
response: finalResponse || "Task completed but no text response generated.",
167168
};
168169
} catch (error) {
169170
const errorMessage = error instanceof Error ? error.message : String(error);

src/components/ToolCallView.tsx

Lines changed: 9 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,18 @@
11
import { useMemo } from "react";
2-
import { COLORS } from "../ui/constants";
2+
import { useToolApprovalForCall } from "../hooks/use-tool-approval";
33
import type { ToolCall } from "../types";
4+
import { COLORS } from "../ui/constants";
5+
import { ApprovalPicker } from "./ApprovalPicker";
46
import {
5-
getToolLayout,
7+
ErrorPreviewView,
8+
ResultPreviewView,
9+
ToolBodyView,
10+
ToolHeaderView,
611
defaultToolLayout,
712
getDefaultAbbreviation,
8-
ToolHeaderView,
9-
ToolBodyView,
10-
ResultPreviewView,
11-
ErrorPreviewView,
1213
getStatusBorderColor,
14+
getToolLayout,
1315
} from "./tool-layouts";
14-
import { ApprovalPicker } from "./ApprovalPicker";
15-
import { useToolApprovalForCall } from "../hooks/use-tool-approval";
1616

1717
interface ToolCallViewProps {
1818
call: ToolCall;
@@ -68,10 +68,7 @@ export function ToolCallView({ call, result, showOutput = true }: ToolCallViewPr
6868
const toolName = layout.abbreviation ?? getDefaultAbbreviation(call.name);
6969
const borderColor = getStatusBorderColor(call.status);
7070

71-
const customBody = useMemo(() => {
72-
if (!layout.renderBody) return null;
73-
return layout.renderBody({ call, result, showOutput });
74-
}, [layout, call, result, showOutput]);
71+
const customBody = layout.renderBody ? layout.renderBody({ call, result, showOutput }) : null;
7572

7673
return (
7774
<box

src/components/tool-layouts/components.tsx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { TextAttributes } from "@opentui/core";
2-
import { COLORS } from "../../ui/constants";
3-
import type { ToolHeader, ToolBody, ToolBodyLine } from "./types";
42
import type { ToolCallStatus } from "../../types";
3+
import { COLORS } from "../../ui/constants";
4+
import type { ToolBody, ToolBodyLine, ToolHeader } from "./types";
55

66
interface ToolHeaderViewProps {
77
toolName: string;
@@ -11,11 +11,12 @@ interface ToolHeaderViewProps {
1111
}
1212

1313
export function ToolHeaderView({ toolName, header, isRunning, toolColor }: ToolHeaderViewProps) {
14+
const displayName = toolName.toUpperCase();
1415
return (
1516
<box flexDirection="row" alignItems="center" justifyContent="space-between" width="100%">
1617
<text>
1718
<span fg={toolColor}>{"↯ "}</span>
18-
<span fg={toolColor}>{toolName}</span>
19+
<span fg={toolColor}>{displayName}</span>
1920
{header?.primary && <span fg={COLORS.TOOL_INPUT_TEXT}>{` ${header.primary}`}</span>}
2021
{header?.secondary && (
2122
<span

0 commit comments

Comments
 (0)