Skip to content

Commit 8db4981

Browse files
egavrindevagent
andcommitted
fix: flush grouped tool output promptly
- Track completed grouped tool calls in CLI and TUI renderers - Cover immediate flush behavior with a focused TUI regression Co-Authored-By: devagent <devagent@egavrin>
1 parent ed6a647 commit 8db4981

5 files changed

Lines changed: 81 additions & 11 deletions

File tree

packages/cli/src/main-event-handlers.ts

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
import {
22
SafetyMode,
33
loggedSubagentRunFromEvent,
4+
type DevAgentConfig,
5+
type EventBus,
6+
type EventMap,
7+
type VerbosityConfig,
48
} from "@devagent/runtime";
59

610
import {
@@ -37,12 +41,6 @@ import {
3741
presentToolAfterEvent,
3842
presentToolBeforeEvent,
3943
} from "./transcript-presenter.js";
40-
import type {
41-
DevAgentConfig,
42-
EventBus,
43-
EventMap,
44-
VerbosityConfig,
45-
} from "@devagent/runtime";
4644

4745
type Verbosity = "quiet" | "normal" | "verbose";
4846

@@ -106,9 +104,7 @@ export function setupEventHandlers(
106104
}
107105

108106
function createStatusLine(config: DevAgentConfig, verbosity: Verbosity): StatusLine | null {
109-
return verbosity === "quiet"
110-
? null
111-
: new StatusLine(config.model, getInteractiveSafetyMode(config));
107+
return verbosity === "quiet" ? null : new StatusLine(config.model, getInteractiveSafetyMode(config));
112108
}
113109

114110
function getInteractiveSafetyMode(config: DevAgentConfig): SafetyMode {
@@ -344,6 +340,7 @@ function startPendingToolGroup(
344340
ctx.os.pendingToolGroup = {
345341
name: event.name,
346342
count: 1,
343+
completed: 0,
347344
params: summary ? [summary] : [],
348345
totalDurationMs: 0,
349346
lastSuccess: true,
@@ -382,13 +379,17 @@ function appendToolResultToGroup(ctx: EventHandlerContext, event: EventMap["tool
382379
return false;
383380
}
384381
group.totalDurationMs += event.durationMs;
382+
group.completed++;
385383
if (!event.result.success) {
386384
group.lastSuccess = false;
387385
group.lastError = event.result.error ?? undefined;
388386
}
389387
if (group.count === 1) {
390388
renderToolResultParts(ctx, event);
391389
}
390+
if (group.count > 1 && group.completed >= group.count) {
391+
flushToolGroup(ctx);
392+
}
392393
return true;
393394
}
394395

packages/cli/src/output-state.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ export class OutputState {
6565
pendingToolGroup: {
6666
name: string;
6767
count: number;
68+
completed: number;
6869
params: string[];
6970
totalDurationMs: number;
7071
lastSuccess: boolean;

packages/cli/src/tui/App.test-utils.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { PassThrough, Writable } from "node:stream";
33

44
import type React from "react";
55

6-
export class TestInput extends PassThrough {
6+
class TestInput extends PassThrough {
77
readonly isTTY = true;
88

99
setRawMode(_value: boolean): void {}
@@ -17,7 +17,7 @@ export class TestInput extends PassThrough {
1717
}
1818
}
1919

20-
export class TestOutput extends Writable {
20+
class TestOutput extends Writable {
2121
readonly isTTY = true;
2222
readonly columns: number;
2323
readonly rows = 40;

packages/cli/src/tui/App.test.ts

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,57 @@ function ToolDiffHarness(): React.ReactElement {
180180
});
181181
}
182182

183+
function ToolGroupFlushHarness(): React.ReactElement {
184+
const bus = useMemo(() => new EventBus(), []);
185+
const { transcriptNodes, startTurn, completeTurn, nextId } = useAgentLog({
186+
bus,
187+
model: "test-model",
188+
});
189+
190+
useEffect(() => {
191+
startTurn(nextId("turn"), "Check LSP", Date.now());
192+
bus.emit("tool:before", {
193+
name: "lsp",
194+
params: { operation: "diagnostics", path: "src/tmp-lsp-validation.ts" },
195+
callId: "call-1",
196+
});
197+
bus.emit("tool:before", {
198+
name: "lsp",
199+
params: { operation: "symbols", path: "src/tmp-lsp-validation.ts" },
200+
callId: "call-2",
201+
});
202+
bus.emit("tool:after", {
203+
name: "lsp",
204+
callId: "call-1",
205+
durationMs: 1,
206+
result: {
207+
success: true,
208+
output: "No diagnostics.",
209+
error: null,
210+
artifacts: [],
211+
},
212+
});
213+
bus.emit("tool:after", {
214+
name: "lsp",
215+
callId: "call-2",
216+
durationMs: 9,
217+
result: {
218+
success: true,
219+
output: "1 symbol.",
220+
error: null,
221+
artifacts: [],
222+
},
223+
});
224+
completeTurn(nextId("summary"), makeTurnSummaryPart({ iterations: 1, toolCalls: 2, cost: 0, elapsedMs: 10 }));
225+
}, [bus, startTurn, completeTurn, nextId]);
226+
227+
return React.createElement(TranscriptView, {
228+
showWelcome: false,
229+
transcriptNodes,
230+
model: "test-model",
231+
});
232+
}
233+
183234
function StatusHarness(): React.ReactElement {
184235
const bus = useMemo(() => new EventBus(), []);
185236
const { transcriptNodes, startTurn, completeTurn, nextId } = useAgentLog({
@@ -701,6 +752,17 @@ describe("interactive completion notices", () => {
701752
expect(output).not.toContain("+++ b/src/new-file.ts");
702753
expect(output).toContain("... +2 more files");
703754
});
755+
756+
it("flushes grouped tool rows as soon as all calls complete", async () => {
757+
const view = renderForTest(React.createElement(ToolGroupFlushHarness));
758+
759+
await settle();
760+
761+
const plain = stripAnsi(view.stdout.readAll());
762+
expect(plain).toContain("Check LSP");
763+
expect(plain).toContain("✓ lsp ×2");
764+
expect(plain).toContain("tmp-lsp-validation.ts, tmp-lsp-validation.ts");
765+
});
704766
});
705767

706768
describe("framed TUI wrapping", () => {

packages/cli/src/tui/useAgentLog.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ interface UseAgentLogResult {
8282
interface PendingToolGroup {
8383
name: string;
8484
count: number;
85+
completed: number;
8586
summaries: string[];
8687
totalMs: number;
8788
lastSuccess: boolean;
@@ -214,6 +215,7 @@ function registerToolBeforeEvent(
214215
runtime.pendingGroupRef.current = {
215216
name: event.name,
216217
count: 1,
218+
completed: 0,
217219
summaries: summary ? [summary] : [],
218220
totalMs: 0,
219221
lastSuccess: true,
@@ -291,8 +293,12 @@ function applyPendingToolResult(
291293
return false;
292294
}
293295
pendingGroup.totalMs += event.durationMs;
296+
pendingGroup.completed++;
294297
if (!event.result.success) pendingGroup.lastSuccess = false;
295298
if (pendingGroup.count === 1) addToolAfterParts(event, runtime);
299+
if (pendingGroup.count > 1 && pendingGroup.completed >= pendingGroup.count) {
300+
runtime.flushGroup();
301+
}
296302
return true;
297303
}
298304

0 commit comments

Comments
 (0)