mirror of
https://github.com/vercel/eve.git
synced 2026-09-20 05:35:39 +08:00
fix(tui): keep background subagent activity visible (#3475)
Signed-off-by: owenkephart <owen.kephart@vercel.com>
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"eve": patch
|
||||
---
|
||||
|
||||
Keep the dev TUI’s activity indicator animating while a background subagent is still running after its parent turn completes.
|
||||
@@ -27,6 +27,20 @@ function respond(request: MockModelRequest): MockModelResponse | string {
|
||||
|
||||
// Framework announcements are model context, not scenario turns.
|
||||
const message = [...request.userMessages].reverse().find(isScenarioMessage) ?? "";
|
||||
if (message === "TUI-HANG-IN-PROCESS") {
|
||||
return startHangLabTool(request, "hold-in-process", "hold_in_process", { durationMs: 45_000 });
|
||||
}
|
||||
if (message === "TUI-HANG-WORKFLOW") {
|
||||
return startHangLabTool(request, "hold-workflow", "hold_workflow", { duration: "45s" });
|
||||
}
|
||||
if (message === "TUI-HANG-COMMAND") {
|
||||
return startHangLabTool(request, "hold-command", "hold_command", { durationSeconds: 45 });
|
||||
}
|
||||
if (message === "TUI-HANG-SUBAGENT") {
|
||||
return startHangLabTool(request, "hold-subagent", "hang-worker", {
|
||||
message: "Hold for 45 seconds.",
|
||||
});
|
||||
}
|
||||
if (message.includes("TASK-FANOUT-INTERACTIVE-CHECK")) return "TASK-FANOUT-INTERACTIVE-OK";
|
||||
if (message.includes("TASK-CANCEL-NOW")) return cancelWorkerTask(request);
|
||||
if (message.includes("CHILD-TASK-EXCLUSIVITY-RACE")) return raceBusyWorker(request);
|
||||
@@ -159,6 +173,14 @@ function respond(request: MockModelRequest): MockModelResponse | string {
|
||||
if (message === "TASK-HITL-ROUTING") {
|
||||
return startApprovalWorker(request, "task-hitl-worker", "TASK-HITL-STARTED");
|
||||
}
|
||||
if (message === "TUI-IDLE-WORK-REPRO") {
|
||||
return startApprovalWorker(
|
||||
request,
|
||||
`tui-idle-work-repro-${scenarioUserMessageCount(request)}`,
|
||||
"TUI-IDLE-WORK-STARTED",
|
||||
{ delayed: true },
|
||||
);
|
||||
}
|
||||
if (message === "TASK-C7-AUTHORIZATION") {
|
||||
return startApprovalWorker(request, "task-c7-authorization-worker", "TASK-C7-STARTED");
|
||||
}
|
||||
@@ -184,6 +206,19 @@ function respond(request: MockModelRequest): MockModelResponse | string {
|
||||
return `Mock reply: ${message}`;
|
||||
}
|
||||
|
||||
function startHangLabTool(
|
||||
request: MockModelRequest,
|
||||
callIdPrefix: string,
|
||||
name: string,
|
||||
input: unknown,
|
||||
): MockModelResponse | string {
|
||||
const callId = `${callIdPrefix}-${scenarioUserMessageCount(request)}`;
|
||||
if (resultById(request, callId) === undefined) {
|
||||
return { toolCalls: [{ id: callId, input, name }] };
|
||||
}
|
||||
return `TUI-HANG-LAB-COMPLETE:${callIdPrefix}`;
|
||||
}
|
||||
|
||||
function startAuthSnapshotTask(
|
||||
request: MockModelRequest,
|
||||
message: string,
|
||||
@@ -365,6 +400,7 @@ function startApprovalWorker(
|
||||
request: MockModelRequest,
|
||||
callId: string,
|
||||
completedText: string,
|
||||
options?: { delayed?: boolean },
|
||||
): MockModelResponse | string {
|
||||
if (resultById(request, callId) === undefined) {
|
||||
return {
|
||||
@@ -375,9 +411,11 @@ function startApprovalWorker(
|
||||
message:
|
||||
callId === "task-c7-authorization-worker"
|
||||
? "Run the C7 authorization mode, then return C7-AUTHORIZATION-COMPLETE."
|
||||
: callId === "task-hitl-worker"
|
||||
? "Run three approval gates in order, then return CHILD-GATES-COMPLETE."
|
||||
: "Run both approval gates in order, then return CHILD-GATES-COMPLETE.",
|
||||
: options?.delayed === true
|
||||
? "Wait twenty seconds before the approval gates, then run three approval gates in order and return CHILD-GATES-COMPLETE."
|
||||
: callId === "task-hitl-worker"
|
||||
? "Run three approval gates in order, then return CHILD-GATES-COMPLETE."
|
||||
: "Run both approval gates in order, then return CHILD-GATES-COMPLETE.",
|
||||
},
|
||||
name: "approval-worker",
|
||||
},
|
||||
|
||||
@@ -19,10 +19,21 @@ function respond(request: MockModelRequest): MockModelResponse | string {
|
||||
const continuation = request.userMessages.some((message) =>
|
||||
message.includes("continuation approval gates"),
|
||||
);
|
||||
const delayed = request.userMessages.some((message) =>
|
||||
message.toLowerCase().includes("wait twenty seconds before the approval gates"),
|
||||
);
|
||||
const threeGates =
|
||||
continuation ||
|
||||
request.userMessages.some((message) => message.includes("three approval gates"));
|
||||
const id = (gate: string) => (continuation ? `continuation-${gate}` : `approval-${gate}`);
|
||||
if (
|
||||
delayed &&
|
||||
request.toolResults.find((result) => result.id === "approval-wait") === undefined
|
||||
) {
|
||||
return {
|
||||
toolCalls: [{ id: "approval-wait", input: { delay: "20s" }, name: "wait_before_approval" }],
|
||||
};
|
||||
}
|
||||
const first = request.toolResults.find((result) => result.id === id("first"));
|
||||
if (first === undefined) {
|
||||
return {
|
||||
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
import { defineWorkflowTool, type WorkflowToolDefinition } from "eve/tools";
|
||||
import { sleep } from "workflow";
|
||||
import { z } from "zod";
|
||||
|
||||
type Input = { delay: "20s" };
|
||||
|
||||
async function execute({ delay }: Input): Promise<{ waited: "20s" }> {
|
||||
"use workflow";
|
||||
await sleep(delay);
|
||||
return { waited: delay };
|
||||
}
|
||||
|
||||
const tool: WorkflowToolDefinition<Input, { waited: "20s" }> = defineWorkflowTool({
|
||||
description: "Wait before the deterministic approval sequence.",
|
||||
inputSchema: z.object({ delay: z.literal("20s") }),
|
||||
execute,
|
||||
});
|
||||
|
||||
export default tool;
|
||||
@@ -0,0 +1,23 @@
|
||||
import { defineAgent } from "eve";
|
||||
import { mockModel, type MockModelRequest, type MockModelResponse } from "eve/evals";
|
||||
|
||||
function respond(request: MockModelRequest): MockModelResponse | string {
|
||||
if (request.toolResults.some((result) => result.id === "hang-worker-hold")) {
|
||||
return "HANG-WORKER-COMPLETE";
|
||||
}
|
||||
return {
|
||||
toolCalls: [
|
||||
{
|
||||
id: "hang-worker-hold",
|
||||
input: { durationMs: 45_000 },
|
||||
name: "hold",
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
export default defineAgent({
|
||||
description: "Test-only subagent that holds a foreground delegation open.",
|
||||
model: mockModel(respond),
|
||||
modelContextWindowTokens: 1_000_000,
|
||||
});
|
||||
@@ -0,0 +1,11 @@
|
||||
import { defineTool } from "eve/tools";
|
||||
import { z } from "zod";
|
||||
|
||||
export default defineTool({
|
||||
description: "Test-only subagent hold.",
|
||||
inputSchema: z.object({ durationMs: z.literal(45_000) }),
|
||||
async execute({ durationMs }) {
|
||||
await new Promise((resolve) => setTimeout(resolve, durationMs));
|
||||
return { held: `${durationMs}ms` };
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,18 @@
|
||||
import { execFile } from "node:child_process";
|
||||
import { promisify } from "node:util";
|
||||
|
||||
import { defineTool } from "eve/tools";
|
||||
import { z } from "zod";
|
||||
|
||||
const runFile = promisify(execFile);
|
||||
|
||||
export default defineTool({
|
||||
description: "Test-only foreground child-process hold.",
|
||||
inputSchema: z.object({ durationSeconds: z.literal(45) }),
|
||||
async execute({ durationSeconds }) {
|
||||
console.error(`[tui-hang-lab] command started; sleeping ${durationSeconds}s`);
|
||||
await runFile("sleep", [String(durationSeconds)]);
|
||||
console.error("[tui-hang-lab] command completed");
|
||||
return { held: `${durationSeconds}s` };
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,11 @@
|
||||
import { defineTool } from "eve/tools";
|
||||
import { z } from "zod";
|
||||
|
||||
export default defineTool({
|
||||
description: "Test-only in-process foreground hold.",
|
||||
inputSchema: z.object({ durationMs: z.literal(45_000) }),
|
||||
async execute({ durationMs }) {
|
||||
await new Promise((resolve) => setTimeout(resolve, durationMs));
|
||||
return { held: `${durationMs}ms` };
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,19 @@
|
||||
import { defineWorkflowTool, type WorkflowToolDefinition } from "eve/tools";
|
||||
import { sleep } from "workflow";
|
||||
import { z } from "zod";
|
||||
|
||||
type Input = { duration: "45s" };
|
||||
|
||||
async function execute({ duration }: Input): Promise<{ held: "45s" }> {
|
||||
"use workflow";
|
||||
await sleep(duration);
|
||||
return { held: duration };
|
||||
}
|
||||
|
||||
const tool: WorkflowToolDefinition<Input, { held: "45s" }> = defineWorkflowTool({
|
||||
description: "Test-only durable foreground workflow hold.",
|
||||
inputSchema: z.object({ duration: z.literal("45s") }),
|
||||
execute,
|
||||
});
|
||||
|
||||
export default tool;
|
||||
@@ -0,0 +1,51 @@
|
||||
#!/usr/bin/env bash
|
||||
# Reproduce a TUI that follows background work after its foreground turn ended.
|
||||
#
|
||||
# Run this from the repository root:
|
||||
# e2e/fixtures/fixture-tasks/scripts/reproduce-tui-idle-work.sh
|
||||
#
|
||||
# In the TUI, send TUI-IDLE-WORK-REPRO. The parent reports
|
||||
# TUI-IDLE-WORK-STARTED and returns to its prompt; its background task then
|
||||
# durably waits 20 seconds before asking for first_gate, second_gate, and
|
||||
# third_gate approvals.
|
||||
set -euo pipefail
|
||||
|
||||
repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../../.." && pwd)"
|
||||
port="${EVE_TUI_REPRO_PORT:-3217}"
|
||||
log_file="${TMPDIR:-/tmp}/eve-tui-idle-work-${port}.log"
|
||||
|
||||
cleanup() {
|
||||
if [[ -n "${server_pid:-}" ]] && kill -0 "$server_pid" 2>/dev/null; then
|
||||
kill "$server_pid" 2>/dev/null || true
|
||||
wait "$server_pid" 2>/dev/null || true
|
||||
fi
|
||||
}
|
||||
trap cleanup EXIT INT TERM
|
||||
|
||||
cd "$repo_root"
|
||||
EVE_E2E_MODEL=mock pnpm --filter fixture-tasks exec eve dev --no-ui --port "$port" >"$log_file" 2>&1 &
|
||||
server_pid=$!
|
||||
|
||||
until curl --silent --fail --max-time 1 "http://127.0.0.1:${port}/eve/v1/health" >/dev/null; do
|
||||
if ! kill -0 "$server_pid" 2>/dev/null; then
|
||||
cat "$log_file" >&2
|
||||
exit 1
|
||||
fi
|
||||
sleep 0.2
|
||||
done
|
||||
|
||||
printf 'Fixture server: http://127.0.0.1:%s\nServer log: %s\n\n' "$port" "$log_file"
|
||||
cat <<'EOF'
|
||||
Background idle-follow repro:
|
||||
TUI-IDLE-WORK-REPRO
|
||||
Returns to the prompt, then waits 20 seconds before requesting approval.
|
||||
|
||||
Foreground hang lab (each holds for 45 seconds):
|
||||
TUI-HANG-IN-PROCESS ordinary Node.js promise timer
|
||||
TUI-HANG-WORKFLOW durable workflow sleep
|
||||
TUI-HANG-COMMAND child process: sleep 45 (logs start/completion)
|
||||
TUI-HANG-SUBAGENT foreground subagent whose own tool waits
|
||||
EOF
|
||||
printf '\n'
|
||||
|
||||
pnpm --filter fixture-tasks exec eve dev --url "http://127.0.0.1:${port}"
|
||||
@@ -1331,6 +1331,117 @@ describe("TerminalRenderer (inline scrollback)", () => {
|
||||
renderer.shutdown();
|
||||
});
|
||||
|
||||
it("keeps the activity ticker running while a background subagent remains live", async () => {
|
||||
vi.useFakeTimers();
|
||||
try {
|
||||
const { screen, renderer } = makeRenderer();
|
||||
renderer.renderAgentHeader({ name: "Weather Agent", serverUrl: "http://localhost:3000" });
|
||||
renderer.beginSubagent({ callId: "background", name: "researcher" });
|
||||
renderer.backgroundSubagent({ callId: "background" });
|
||||
renderer.upsertSubagentTool({
|
||||
callId: "background",
|
||||
subagentName: "researcher",
|
||||
childCallId: "child-hold",
|
||||
toolName: "hold",
|
||||
input: { durationMs: 45_000 },
|
||||
status: "executing",
|
||||
});
|
||||
|
||||
await renderer.renderStream(streamOf([{ type: "finish" }]), {
|
||||
continueSession: true,
|
||||
submittedPrompt: "start background work",
|
||||
});
|
||||
const outputBeforeTick = screen.rawOutput().length;
|
||||
await vi.advanceTimersByTimeAsync(90);
|
||||
expect(screen.rawOutput().length).toBeGreaterThan(outputBeforeTick);
|
||||
|
||||
renderer.completeSubagent({ authoritative: true, callId: "background" });
|
||||
const outputAfterCompletion = screen.rawOutput().length;
|
||||
await vi.advanceTimersByTimeAsync(180);
|
||||
expect(screen.rawOutput()).toHaveLength(outputAfterCompletion);
|
||||
renderer.shutdown();
|
||||
} finally {
|
||||
vi.useRealTimers();
|
||||
}
|
||||
});
|
||||
|
||||
it("keeps the ticker running when one of two background subagents completes", async () => {
|
||||
vi.useFakeTimers();
|
||||
try {
|
||||
const { screen, renderer } = makeRenderer();
|
||||
renderer.renderAgentHeader({ name: "Weather Agent", serverUrl: "http://localhost:3000" });
|
||||
const startBackground = (callId: string) => {
|
||||
renderer.beginSubagent({ callId, name: "hang-worker" });
|
||||
renderer.backgroundSubagent({ callId });
|
||||
renderer.upsertSubagentTool({
|
||||
callId,
|
||||
subagentName: "hang-worker",
|
||||
childCallId: `${callId}-hold`,
|
||||
toolName: "hold",
|
||||
input: { durationMs: 45_000 },
|
||||
status: "executing",
|
||||
});
|
||||
};
|
||||
|
||||
startBackground("first");
|
||||
await renderer.renderStream(streamOf([{ type: "finish" }]), {
|
||||
continueSession: true,
|
||||
submittedPrompt: "start first background worker",
|
||||
});
|
||||
await renderer.renderStream(
|
||||
streamOf([
|
||||
{ type: "assistant-complete", id: "a", text: "Mock reply: a" },
|
||||
{ type: "finish" },
|
||||
]),
|
||||
{ continueSession: true, submittedPrompt: "a" },
|
||||
);
|
||||
startBackground("second");
|
||||
await renderer.renderStream(streamOf([{ type: "finish" }]), {
|
||||
continueSession: true,
|
||||
submittedPrompt: "start second background worker",
|
||||
});
|
||||
|
||||
renderer.completeSubagent({ authoritative: true, callId: "first" });
|
||||
const outputBeforeTick = screen.rawOutput().length;
|
||||
await vi.advanceTimersByTimeAsync(90);
|
||||
expect(screen.rawOutput().length).toBeGreaterThan(outputBeforeTick);
|
||||
|
||||
renderer.completeSubagent({ authoritative: true, callId: "second" });
|
||||
const outputAfterCompletion = screen.rawOutput().length;
|
||||
await vi.advanceTimersByTimeAsync(180);
|
||||
expect(screen.rawOutput()).toHaveLength(outputAfterCompletion);
|
||||
renderer.shutdown();
|
||||
} finally {
|
||||
vi.useRealTimers();
|
||||
}
|
||||
});
|
||||
|
||||
it("stops background activity ticking when a session boundary abandons its child", async () => {
|
||||
vi.useFakeTimers();
|
||||
try {
|
||||
const { screen, renderer } = makeRenderer();
|
||||
renderer.renderAgentHeader({ name: "Weather Agent", serverUrl: "http://localhost:3000" });
|
||||
renderer.beginSubagent({ callId: "background", name: "researcher" });
|
||||
renderer.backgroundSubagent({ callId: "background" });
|
||||
renderer.upsertSubagentTool({
|
||||
callId: "background",
|
||||
subagentName: "researcher",
|
||||
childCallId: "child-hold",
|
||||
toolName: "hold",
|
||||
input: { durationMs: 45_000 },
|
||||
status: "executing",
|
||||
});
|
||||
|
||||
renderer.renderSessionBoundary();
|
||||
const outputAfterBoundary = screen.rawOutput().length;
|
||||
await vi.advanceTimersByTimeAsync(180);
|
||||
expect(screen.rawOutput()).toHaveLength(outputAfterBoundary);
|
||||
renderer.shutdown();
|
||||
} finally {
|
||||
vi.useRealTimers();
|
||||
}
|
||||
});
|
||||
|
||||
it("commits completed foreground turns ahead of a live background subagent", async () => {
|
||||
const { screen, renderer } = makeRenderer(48, 8);
|
||||
renderer.renderAgentHeader({ name: "Weather Agent", serverUrl: "http://localhost:3000" });
|
||||
@@ -4505,6 +4616,30 @@ describe("TerminalRenderer setup flow session", () => {
|
||||
renderer.shutdown();
|
||||
});
|
||||
|
||||
it("does not repaint while inherited stdio owns the terminal", async () => {
|
||||
vi.useFakeTimers();
|
||||
try {
|
||||
const { screen, renderer } = makeRenderer();
|
||||
renderer.setupFlow.begin("Add integration", "pulse");
|
||||
renderer.beginSubagent({ callId: "background", name: "researcher" });
|
||||
renderer.backgroundSubagent({ callId: "background" });
|
||||
let release!: () => void;
|
||||
const inherited = renderer.setupFlow.withInheritedStdio(
|
||||
() => new Promise<void>((resolve) => (release = resolve)),
|
||||
);
|
||||
const outputDuringHandoff = screen.rawOutput().length;
|
||||
await vi.advanceTimersByTimeAsync(180);
|
||||
expect(screen.rawOutput()).toHaveLength(outputDuringHandoff);
|
||||
|
||||
release();
|
||||
await inherited;
|
||||
renderer.setupFlow.end();
|
||||
renderer.shutdown();
|
||||
} finally {
|
||||
vi.useRealTimers();
|
||||
}
|
||||
});
|
||||
|
||||
it("discards inherited subprocess output when restoring the transcript", async () => {
|
||||
const { screen, input, renderer } = makeRenderer();
|
||||
|
||||
|
||||
@@ -765,7 +765,7 @@ export class TerminalRenderer implements AgentTUIRenderer {
|
||||
|
||||
async readPrompt(options?: AgentTUISessionOptions): Promise<string> {
|
||||
this.#start(options);
|
||||
this.#stopTicker();
|
||||
this.#syncBackgroundActivityTicker();
|
||||
this.#commitTurnStats();
|
||||
this.#inputActive = true;
|
||||
this.#promptPlaceholderActive = true;
|
||||
@@ -1050,11 +1050,11 @@ export class TerminalRenderer implements AgentTUIRenderer {
|
||||
this.#requestTurnCancel = undefined;
|
||||
this.#sendSteering = undefined;
|
||||
this.#detachInput();
|
||||
this.#stopTicker();
|
||||
this.#streamDraftActive = false;
|
||||
if (this.#turnIndicator.kind === "waiting") {
|
||||
this.#turnIndicator = { kind: "idle" };
|
||||
}
|
||||
this.#syncBackgroundActivityTicker();
|
||||
this.#status = completedTurnStatus({
|
||||
interrupted: this.#interrupted,
|
||||
cancelled: this.#turnCancelled,
|
||||
@@ -1068,6 +1068,7 @@ export class TerminalRenderer implements AgentTUIRenderer {
|
||||
// prefix wedged and freeze scrollback for the rest of the session.
|
||||
if (this.#interrupted || turnState.cancelled) this.#settleCurrentTurnToolBlocks(turnState);
|
||||
this.#finalizeAllBlocks();
|
||||
this.#syncBackgroundActivityTicker();
|
||||
this.#diagnostics?.reportStats();
|
||||
this.#paint();
|
||||
|
||||
@@ -1117,6 +1118,7 @@ export class TerminalRenderer implements AgentTUIRenderer {
|
||||
this.#sweepPreparingToolBlocks(turnState);
|
||||
if (turnState.cancelled) this.#settleCurrentTurnToolBlocks(turnState);
|
||||
this.#finalizeAllBlocks();
|
||||
this.#syncBackgroundActivityTicker();
|
||||
this.#diagnostics?.reportStats();
|
||||
this.#paint();
|
||||
}
|
||||
@@ -1624,6 +1626,7 @@ export class TerminalRenderer implements AgentTUIRenderer {
|
||||
const wasBackground = this.#backgroundSubagentCallIds.has(update.callId);
|
||||
this.#backgroundSubagentCallIds.add(update.callId);
|
||||
if (!wasBackground) this.#moveSubagentCohortToBackgroundTail(update.callId);
|
||||
this.#syncBackgroundActivityTicker();
|
||||
this.#paint();
|
||||
}
|
||||
|
||||
@@ -1648,6 +1651,7 @@ export class TerminalRenderer implements AgentTUIRenderer {
|
||||
if (block.subagentCallId === update.callId) block.live = true;
|
||||
}
|
||||
}
|
||||
this.#syncBackgroundActivityTicker();
|
||||
this.#paint();
|
||||
}
|
||||
|
||||
@@ -1785,6 +1789,7 @@ export class TerminalRenderer implements AgentTUIRenderer {
|
||||
this.#nextSubmittedPromptOrigin = undefined;
|
||||
this.#fileContents.clear();
|
||||
this.#turnClock.reset();
|
||||
this.#syncBackgroundActivityTicker();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -2900,7 +2905,7 @@ export class TerminalRenderer implements AgentTUIRenderer {
|
||||
this.#flowInterrupt = undefined;
|
||||
this.#disarmFlowIdleTrap();
|
||||
this.#detachInput();
|
||||
this.#stopTicker();
|
||||
this.#clearTicker();
|
||||
this.#live.clear();
|
||||
this.#removeLogCapture();
|
||||
this.#altScreen.enter({ cursor: "visible", mouse: false });
|
||||
@@ -2921,6 +2926,8 @@ export class TerminalRenderer implements AgentTUIRenderer {
|
||||
if (this.#setupFlow !== undefined) {
|
||||
this.#startTicker();
|
||||
this.#armFlowIdleTrap();
|
||||
} else {
|
||||
this.#syncBackgroundActivityTicker();
|
||||
}
|
||||
this.#live.reset();
|
||||
this.#paint();
|
||||
@@ -3087,7 +3094,7 @@ export class TerminalRenderer implements AgentTUIRenderer {
|
||||
rejectReader?.(interruptedError());
|
||||
this.#detachInput();
|
||||
this.#stopCaretBlink();
|
||||
this.#stopTicker();
|
||||
this.#clearTicker();
|
||||
if (this.#logLevelHintTimer !== undefined) {
|
||||
clearTimeout(this.#logLevelHintTimer);
|
||||
this.#logLevelHintTimer = undefined;
|
||||
@@ -3359,7 +3366,7 @@ export class TerminalRenderer implements AgentTUIRenderer {
|
||||
}
|
||||
|
||||
#startTicker() {
|
||||
this.#stopTicker();
|
||||
this.#clearTicker();
|
||||
this.#tickTimer = setInterval(() => {
|
||||
this.#spinnerIndex += 1;
|
||||
this.#paint();
|
||||
@@ -3375,12 +3382,36 @@ export class TerminalRenderer implements AgentTUIRenderer {
|
||||
}
|
||||
|
||||
#stopTicker() {
|
||||
if (this.#hasLiveBackgroundActivity()) return;
|
||||
this.#clearTicker();
|
||||
}
|
||||
|
||||
#clearTicker() {
|
||||
if (this.#tickTimer) {
|
||||
clearInterval(this.#tickTimer);
|
||||
this.#tickTimer = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
#hasLiveBackgroundActivity(): boolean {
|
||||
return this.#blocks.some(
|
||||
(block) =>
|
||||
block.live &&
|
||||
block.subagentCallId !== undefined &&
|
||||
(this.#backgroundSubagentCallIds.has(block.subagentCallId) ||
|
||||
this.#provisionalSubagentCallIds.has(block.subagentCallId)),
|
||||
);
|
||||
}
|
||||
|
||||
/** Keeps mutable subagent sections visibly active after their parent turn settles. */
|
||||
#syncBackgroundActivityTicker(): void {
|
||||
if (this.#hasLiveBackgroundActivity()) {
|
||||
this.#startTicker();
|
||||
} else if (!this.#streamDraftActive && this.#turnIndicator.kind === "idle") {
|
||||
this.#clearTicker();
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Block management
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user