TUI/Gateway: fix pi streaming + tool routing + model display + msg updating (#8432)

* TUI/Gateway: fix pi streaming + tool routing

* Tests: clarify verbose tool output expectation

* fix: avoid seq gaps for targeted tool events (#8432) (thanks @gumadeiras)
This commit is contained in:
Gustavo Madeira Santana
2026-02-04 17:12:16 -05:00
committed by GitHub
parent a42e3cb78a
commit 38e6da1fe0
32 changed files with 1227 additions and 208 deletions

View File

@@ -1,14 +1,14 @@
import type { TUI } from "@mariozechner/pi-tui";
import { describe, expect, it, vi } from "vitest";
import type { ChatLog } from "./components/chat-log.js";
import type { AgentEvent, ChatEvent, TuiStateAccess } from "./tui-types.js";
import { createEventHandlers } from "./tui-event-handlers.js";
type MockChatLog = {
startTool: ReturnType<typeof vi.fn>;
updateToolResult: ReturnType<typeof vi.fn>;
addSystem: ReturnType<typeof vi.fn>;
updateAssistant: ReturnType<typeof vi.fn>;
finalizeAssistant: ReturnType<typeof vi.fn>;
};
type MockChatLog = Pick<
ChatLog,
"startTool" | "updateToolResult" | "addSystem" | "updateAssistant" | "finalizeAssistant"
>;
type MockTui = Pick<TUI, "requestRender">;
describe("tui-event-handlers: handleAgentEvent", () => {
const makeState = (overrides?: Partial<TuiStateAccess>): TuiStateAccess => ({
@@ -21,7 +21,7 @@ describe("tui-event-handlers: handleAgentEvent", () => {
currentSessionId: "session-1",
activeChatRunId: "run-1",
historyLoaded: true,
sessionInfo: {},
sessionInfo: { verboseLevel: "on" },
initialSessionApplied: true,
isConnected: true,
autoMessageSent: false,
@@ -42,21 +42,40 @@ describe("tui-event-handlers: handleAgentEvent", () => {
updateAssistant: vi.fn(),
finalizeAssistant: vi.fn(),
};
const tui = { requestRender: vi.fn() };
const tui: MockTui = { requestRender: vi.fn() };
const setActivityStatus = vi.fn();
const loadHistory = vi.fn();
const localRunIds = new Set<string>();
const noteLocalRunId = (runId: string) => {
localRunIds.add(runId);
};
const forgetLocalRunId = (runId: string) => {
localRunIds.delete(runId);
};
const isLocalRunId = (runId: string) => localRunIds.has(runId);
const clearLocalRunIds = () => {
localRunIds.clear();
};
return { chatLog, tui, state, setActivityStatus };
return {
chatLog,
tui,
state,
setActivityStatus,
loadHistory,
noteLocalRunId,
forgetLocalRunId,
isLocalRunId,
clearLocalRunIds,
};
};
it("processes tool events when runId matches activeChatRunId (even if sessionId differs)", () => {
const state = makeState({ currentSessionId: "session-xyz", activeChatRunId: "run-123" });
const { chatLog, tui, setActivityStatus } = makeContext(state);
const { handleAgentEvent } = createEventHandlers({
// Casts are fine here: TUI runtime shape is larger than we need in unit tests.
// oxlint-disable-next-line typescript/no-explicit-any
chatLog: chatLog as any,
// oxlint-disable-next-line typescript/no-explicit-any
tui: tui as any,
chatLog,
tui,
state,
setActivityStatus,
});
@@ -82,10 +101,8 @@ describe("tui-event-handlers: handleAgentEvent", () => {
const state = makeState({ activeChatRunId: "run-1" });
const { chatLog, tui, setActivityStatus } = makeContext(state);
const { handleAgentEvent } = createEventHandlers({
// oxlint-disable-next-line typescript/no-explicit-any
chatLog: chatLog as any,
// oxlint-disable-next-line typescript/no-explicit-any
tui: tui as any,
chatLog,
tui,
state,
setActivityStatus,
});
@@ -107,10 +124,14 @@ describe("tui-event-handlers: handleAgentEvent", () => {
const state = makeState({ activeChatRunId: "run-9" });
const { tui, setActivityStatus } = makeContext(state);
const { handleAgentEvent } = createEventHandlers({
// oxlint-disable-next-line typescript/no-explicit-any
chatLog: { startTool: vi.fn(), updateToolResult: vi.fn() } as any,
// oxlint-disable-next-line typescript/no-explicit-any
tui: tui as any,
chatLog: {
startTool: vi.fn(),
updateToolResult: vi.fn(),
addSystem: vi.fn(),
updateAssistant: vi.fn(),
finalizeAssistant: vi.fn(),
},
tui,
state,
setActivityStatus,
});
@@ -131,10 +152,8 @@ describe("tui-event-handlers: handleAgentEvent", () => {
const state = makeState({ activeChatRunId: null });
const { chatLog, tui, setActivityStatus } = makeContext(state);
const { handleChatEvent, handleAgentEvent } = createEventHandlers({
// oxlint-disable-next-line typescript/no-explicit-any
chatLog: chatLog as any,
// oxlint-disable-next-line typescript/no-explicit-any
tui: tui as any,
chatLog,
tui,
state,
setActivityStatus,
});
@@ -165,10 +184,8 @@ describe("tui-event-handlers: handleAgentEvent", () => {
const state = makeState({ activeChatRunId: null });
const { chatLog, tui, setActivityStatus } = makeContext(state);
const { handleChatEvent, handleAgentEvent } = createEventHandlers({
// oxlint-disable-next-line typescript/no-explicit-any
chatLog: chatLog as any,
// oxlint-disable-next-line typescript/no-explicit-any
tui: tui as any,
chatLog,
tui,
state,
setActivityStatus,
});
@@ -194,14 +211,39 @@ describe("tui-event-handlers: handleAgentEvent", () => {
expect(tui.requestRender).not.toHaveBeenCalled();
});
it("accepts tool events after chat final for the same run", () => {
const state = makeState({ activeChatRunId: null });
const { chatLog, tui, setActivityStatus } = makeContext(state);
const { handleChatEvent, handleAgentEvent } = createEventHandlers({
chatLog,
tui,
state,
setActivityStatus,
});
handleChatEvent({
runId: "run-final",
sessionKey: state.currentSessionKey,
state: "final",
message: { content: [{ type: "text", text: "done" }] },
});
handleAgentEvent({
runId: "run-final",
stream: "tool",
data: { phase: "start", toolCallId: "tc-final", name: "session_status" },
});
expect(chatLog.startTool).toHaveBeenCalledWith("tc-final", "session_status", undefined);
expect(tui.requestRender).toHaveBeenCalled();
});
it("ignores lifecycle updates for non-active runs in the same session", () => {
const state = makeState({ activeChatRunId: "run-active" });
const { chatLog, tui, setActivityStatus } = makeContext(state);
const { handleChatEvent, handleAgentEvent } = createEventHandlers({
// oxlint-disable-next-line typescript/no-explicit-any
chatLog: chatLog as any,
// oxlint-disable-next-line typescript/no-explicit-any
tui: tui as any,
chatLog,
tui,
state,
setActivityStatus,
});
@@ -224,4 +266,95 @@ describe("tui-event-handlers: handleAgentEvent", () => {
expect(setActivityStatus).not.toHaveBeenCalled();
expect(tui.requestRender).not.toHaveBeenCalled();
});
it("suppresses tool events when verbose is off", () => {
const state = makeState({
activeChatRunId: "run-123",
sessionInfo: { verboseLevel: "off" },
});
const { chatLog, tui, setActivityStatus } = makeContext(state);
const { handleAgentEvent } = createEventHandlers({
chatLog,
tui,
state,
setActivityStatus,
});
handleAgentEvent({
runId: "run-123",
stream: "tool",
data: { phase: "start", toolCallId: "tc-off", name: "session_status" },
});
expect(chatLog.startTool).not.toHaveBeenCalled();
expect(tui.requestRender).not.toHaveBeenCalled();
});
it("omits tool output when verbose is on (non-full)", () => {
const state = makeState({
activeChatRunId: "run-123",
sessionInfo: { verboseLevel: "on" },
});
const { chatLog, tui, setActivityStatus } = makeContext(state);
const { handleAgentEvent } = createEventHandlers({
chatLog,
tui,
state,
setActivityStatus,
});
handleAgentEvent({
runId: "run-123",
stream: "tool",
data: {
phase: "update",
toolCallId: "tc-on",
name: "session_status",
partialResult: { content: [{ type: "text", text: "secret" }] },
},
});
handleAgentEvent({
runId: "run-123",
stream: "tool",
data: {
phase: "result",
toolCallId: "tc-on",
name: "session_status",
result: { content: [{ type: "text", text: "secret" }] },
isError: false,
},
});
expect(chatLog.updateToolResult).toHaveBeenCalledTimes(1);
expect(chatLog.updateToolResult).toHaveBeenCalledWith(
"tc-on",
{ content: [] },
{ isError: false },
);
});
it("refreshes history after a non-local chat final", () => {
const state = makeState({ activeChatRunId: null });
const { chatLog, tui, setActivityStatus, loadHistory, isLocalRunId, forgetLocalRunId } =
makeContext(state);
const { handleChatEvent } = createEventHandlers({
chatLog,
tui,
state,
setActivityStatus,
loadHistory,
isLocalRunId,
forgetLocalRunId,
});
handleChatEvent({
runId: "external-run",
sessionKey: state.currentSessionKey,
state: "final",
message: { content: [{ type: "text", text: "done" }] },
});
expect(loadHistory).toHaveBeenCalledTimes(1);
});
});