fix(coding-agent): keep pending tool renders after thinking toggle
closes #4167
This commit is contained in:
@@ -5,6 +5,7 @@
|
|||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
- Fixed `pi -p` treating prompts that start with YAML frontmatter as extension flags instead of user messages ([#4163](https://github.com/badlogic/pi-mono/issues/4163)).
|
- Fixed `pi -p` treating prompts that start with YAML frontmatter as extension flags instead of user messages ([#4163](https://github.com/badlogic/pi-mono/issues/4163)).
|
||||||
|
- Fixed pending tool results not updating in the live TUI after toggling thinking block visibility while the tool is running ([#4167](https://github.com/badlogic/pi-mono/issues/4167)).
|
||||||
|
|
||||||
## [0.73.0] - 2026-05-04
|
## [0.73.0] - 2026-05-04
|
||||||
|
|
||||||
|
|||||||
@@ -2633,6 +2633,7 @@ export class InteractiveMode {
|
|||||||
|
|
||||||
switch (event.type) {
|
switch (event.type) {
|
||||||
case "agent_start":
|
case "agent_start":
|
||||||
|
this.pendingTools.clear();
|
||||||
if (this.settingsManager.getShowTerminalProgress()) {
|
if (this.settingsManager.getShowTerminalProgress()) {
|
||||||
this.ui.terminal.setProgress(true);
|
this.ui.terminal.setProgress(true);
|
||||||
}
|
}
|
||||||
@@ -3100,6 +3101,7 @@ export class InteractiveMode {
|
|||||||
options: { updateFooter?: boolean; populateHistory?: boolean } = {},
|
options: { updateFooter?: boolean; populateHistory?: boolean } = {},
|
||||||
): void {
|
): void {
|
||||||
this.pendingTools.clear();
|
this.pendingTools.clear();
|
||||||
|
const renderedPendingTools = new Map<string, ToolExecutionComponent>();
|
||||||
|
|
||||||
if (options.updateFooter) {
|
if (options.updateFooter) {
|
||||||
this.footer.invalidate();
|
this.footer.invalidate();
|
||||||
@@ -3141,16 +3143,16 @@ export class InteractiveMode {
|
|||||||
}
|
}
|
||||||
component.updateResult({ content: [{ type: "text", text: errorMessage }], isError: true });
|
component.updateResult({ content: [{ type: "text", text: errorMessage }], isError: true });
|
||||||
} else {
|
} else {
|
||||||
this.pendingTools.set(content.id, component);
|
renderedPendingTools.set(content.id, component);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (message.role === "toolResult") {
|
} else if (message.role === "toolResult") {
|
||||||
// Match tool results to pending tool components
|
// Match tool results to pending tool components
|
||||||
const component = this.pendingTools.get(message.toolCallId);
|
const component = renderedPendingTools.get(message.toolCallId);
|
||||||
if (component) {
|
if (component) {
|
||||||
component.updateResult(message);
|
component.updateResult(message);
|
||||||
this.pendingTools.delete(message.toolCallId);
|
renderedPendingTools.delete(message.toolCallId);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// All other messages use standard rendering
|
// All other messages use standard rendering
|
||||||
@@ -3158,7 +3160,9 @@ export class InteractiveMode {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.pendingTools.clear();
|
for (const [toolCallId, component] of renderedPendingTools) {
|
||||||
|
this.pendingTools.set(toolCallId, component);
|
||||||
|
}
|
||||||
this.ui.requestRender();
|
this.ui.requestRender();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,164 @@
|
|||||||
|
import type { AgentMessage } from "@mariozechner/pi-agent-core";
|
||||||
|
import type { AssistantMessage, ToolResultMessage, Usage } from "@mariozechner/pi-ai";
|
||||||
|
import { Container, Text, type TUI } from "@mariozechner/pi-tui";
|
||||||
|
import stripAnsi from "strip-ansi";
|
||||||
|
import { beforeAll, describe, expect, test, vi } from "vitest";
|
||||||
|
import type { AgentSessionEvent } from "../../../src/core/agent-session.js";
|
||||||
|
import type { SessionContext } from "../../../src/core/session-manager.js";
|
||||||
|
import type { ToolExecutionComponent } from "../../../src/modes/interactive/components/tool-execution.js";
|
||||||
|
import { InteractiveMode } from "../../../src/modes/interactive/interactive-mode.js";
|
||||||
|
import { initTheme } from "../../../src/modes/interactive/theme/theme.js";
|
||||||
|
|
||||||
|
const TOOL_CALL_ID = "tool-4167";
|
||||||
|
const TOOL_NAME = "slow_tool";
|
||||||
|
|
||||||
|
const EMPTY_USAGE: Usage = {
|
||||||
|
input: 0,
|
||||||
|
output: 0,
|
||||||
|
cacheRead: 0,
|
||||||
|
cacheWrite: 0,
|
||||||
|
totalTokens: 0,
|
||||||
|
cost: {
|
||||||
|
input: 0,
|
||||||
|
output: 0,
|
||||||
|
cacheRead: 0,
|
||||||
|
cacheWrite: 0,
|
||||||
|
total: 0,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
type RenderSessionContextThis = {
|
||||||
|
pendingTools: Map<string, ToolExecutionComponent>;
|
||||||
|
chatContainer: Container;
|
||||||
|
footer: { invalidate(): void };
|
||||||
|
ui: TUI;
|
||||||
|
settingsManager: {
|
||||||
|
getShowImages(): boolean;
|
||||||
|
getImageWidthCells(): number;
|
||||||
|
};
|
||||||
|
sessionManager: { getCwd(): string };
|
||||||
|
session: { retryAttempt: number };
|
||||||
|
toolOutputExpanded: boolean;
|
||||||
|
isInitialized: boolean;
|
||||||
|
updateEditorBorderColor(): void;
|
||||||
|
getRegisteredToolDefinition(toolName: string): undefined;
|
||||||
|
addMessageToChat(message: AgentMessage, options?: { populateHistory?: boolean }): void;
|
||||||
|
};
|
||||||
|
|
||||||
|
type RenderSessionContext = (
|
||||||
|
this: RenderSessionContextThis,
|
||||||
|
sessionContext: SessionContext,
|
||||||
|
options?: { updateFooter?: boolean; populateHistory?: boolean },
|
||||||
|
) => void;
|
||||||
|
|
||||||
|
type HandleEvent = (this: RenderSessionContextThis, event: AgentSessionEvent) => Promise<void>;
|
||||||
|
|
||||||
|
function createFakeInteractiveModeThis(): RenderSessionContextThis {
|
||||||
|
const chatContainer = new Container();
|
||||||
|
return {
|
||||||
|
pendingTools: new Map<string, ToolExecutionComponent>(),
|
||||||
|
chatContainer,
|
||||||
|
footer: { invalidate: vi.fn() },
|
||||||
|
ui: { requestRender: vi.fn() } as unknown as TUI,
|
||||||
|
settingsManager: {
|
||||||
|
getShowImages: () => false,
|
||||||
|
getImageWidthCells: () => 60,
|
||||||
|
},
|
||||||
|
sessionManager: { getCwd: () => process.cwd() },
|
||||||
|
session: { retryAttempt: 0 },
|
||||||
|
toolOutputExpanded: false,
|
||||||
|
isInitialized: true,
|
||||||
|
updateEditorBorderColor: vi.fn(),
|
||||||
|
getRegisteredToolDefinition: (_toolName: string) => undefined,
|
||||||
|
addMessageToChat(message: AgentMessage) {
|
||||||
|
chatContainer.addChild(new Text(message.role, 0, 0));
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function createAssistantToolCallMessage(): AssistantMessage {
|
||||||
|
return {
|
||||||
|
role: "assistant",
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "toolCall",
|
||||||
|
id: TOOL_CALL_ID,
|
||||||
|
name: TOOL_NAME,
|
||||||
|
arguments: { delayMs: 10_000 },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
api: "test-api",
|
||||||
|
provider: "test-provider",
|
||||||
|
model: "test-model",
|
||||||
|
usage: EMPTY_USAGE,
|
||||||
|
stopReason: "toolUse",
|
||||||
|
timestamp: Date.now(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function createToolResultMessage(text: string): ToolResultMessage {
|
||||||
|
return {
|
||||||
|
role: "toolResult",
|
||||||
|
toolCallId: TOOL_CALL_ID,
|
||||||
|
toolName: TOOL_NAME,
|
||||||
|
content: [{ type: "text", text }],
|
||||||
|
isError: false,
|
||||||
|
timestamp: Date.now(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function createSessionContext(messages: AgentMessage[]): SessionContext {
|
||||||
|
return {
|
||||||
|
messages,
|
||||||
|
thinkingLevel: "off",
|
||||||
|
model: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderChat(container: Container): string {
|
||||||
|
return stripAnsi(container.render(120).join("\n"));
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("InteractiveMode.renderSessionContext", () => {
|
||||||
|
beforeAll(() => {
|
||||||
|
initTheme("dark");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("keeps unresolved rendered tool calls registered for live completion events", async () => {
|
||||||
|
const fakeThis = createFakeInteractiveModeThis();
|
||||||
|
const renderSessionContext = (
|
||||||
|
InteractiveMode.prototype as unknown as { renderSessionContext: RenderSessionContext }
|
||||||
|
).renderSessionContext;
|
||||||
|
const handleEvent = (InteractiveMode.prototype as unknown as { handleEvent: HandleEvent }).handleEvent;
|
||||||
|
|
||||||
|
renderSessionContext.call(fakeThis, createSessionContext([createAssistantToolCallMessage()]));
|
||||||
|
|
||||||
|
expect(fakeThis.pendingTools.has(TOOL_CALL_ID)).toBe(true);
|
||||||
|
|
||||||
|
await handleEvent.call(fakeThis, {
|
||||||
|
type: "tool_execution_end",
|
||||||
|
toolCallId: TOOL_CALL_ID,
|
||||||
|
toolName: TOOL_NAME,
|
||||||
|
result: { content: [{ type: "text", text: "FINAL_RESULT" }], details: undefined },
|
||||||
|
isError: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(fakeThis.pendingTools.has(TOOL_CALL_ID)).toBe(false);
|
||||||
|
expect(renderChat(fakeThis.chatContainer)).toContain("FINAL_RESULT");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("does not keep completed historical tool calls registered as pending", () => {
|
||||||
|
const fakeThis = createFakeInteractiveModeThis();
|
||||||
|
const renderSessionContext = (
|
||||||
|
InteractiveMode.prototype as unknown as { renderSessionContext: RenderSessionContext }
|
||||||
|
).renderSessionContext;
|
||||||
|
|
||||||
|
renderSessionContext.call(
|
||||||
|
fakeThis,
|
||||||
|
createSessionContext([createAssistantToolCallMessage(), createToolResultMessage("HISTORICAL_RESULT")]),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(fakeThis.pendingTools.size).toBe(0);
|
||||||
|
expect(renderChat(fakeThis.chatContainer)).toContain("HISTORICAL_RESULT");
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user