mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-05 00:10:25 +00:00
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:
committed by
GitHub
parent
a42e3cb78a
commit
38e6da1fe0
@@ -1,10 +1,12 @@
|
||||
import { randomUUID } from "node:crypto";
|
||||
import { loadConfig, resolveGatewayPort } from "../config/config.js";
|
||||
import { GatewayClient } from "../gateway/client.js";
|
||||
import { GATEWAY_CLIENT_CAPS } from "../gateway/protocol/client-info.js";
|
||||
import {
|
||||
type HelloOk,
|
||||
PROTOCOL_VERSION,
|
||||
type SessionsListParams,
|
||||
type SessionsPatchResult,
|
||||
type SessionsPatchParams,
|
||||
} from "../gateway/protocol/index.js";
|
||||
import { GATEWAY_CLIENT_MODES, GATEWAY_CLIENT_NAMES } from "../utils/message-channel.js";
|
||||
@@ -22,6 +24,7 @@ export type ChatSendOptions = {
|
||||
thinking?: string;
|
||||
deliver?: boolean;
|
||||
timeoutMs?: number;
|
||||
runId?: string;
|
||||
};
|
||||
|
||||
export type GatewayEvent = {
|
||||
@@ -116,6 +119,7 @@ export class GatewayChatClient {
|
||||
clientVersion: VERSION,
|
||||
platform: process.platform,
|
||||
mode: GATEWAY_CLIENT_MODES.UI,
|
||||
caps: [GATEWAY_CLIENT_CAPS.TOOL_EVENTS],
|
||||
instanceId: randomUUID(),
|
||||
minProtocol: PROTOCOL_VERSION,
|
||||
maxProtocol: PROTOCOL_VERSION,
|
||||
@@ -153,7 +157,7 @@ export class GatewayChatClient {
|
||||
}
|
||||
|
||||
async sendChat(opts: ChatSendOptions): Promise<{ runId: string }> {
|
||||
const runId = randomUUID();
|
||||
const runId = opts.runId ?? randomUUID();
|
||||
await this.client.request("chat.send", {
|
||||
sessionKey: opts.sessionKey,
|
||||
message: opts.message,
|
||||
@@ -195,8 +199,8 @@ export class GatewayChatClient {
|
||||
return await this.client.request<GatewayAgentsList>("agents.list", {});
|
||||
}
|
||||
|
||||
async patchSession(opts: SessionsPatchParams) {
|
||||
return await this.client.request("sessions.patch", opts);
|
||||
async patchSession(opts: SessionsPatchParams): Promise<SessionsPatchResult> {
|
||||
return await this.client.request<SessionsPatchResult>("sessions.patch", opts);
|
||||
}
|
||||
|
||||
async resetSession(key: string) {
|
||||
|
||||
@@ -29,6 +29,8 @@ describe("tui command handlers", () => {
|
||||
abortActive: vi.fn(),
|
||||
setActivityStatus,
|
||||
formatSessionKey: vi.fn(),
|
||||
applySessionInfoFromPatch: vi.fn(),
|
||||
noteLocalRunId: vi.fn(),
|
||||
});
|
||||
|
||||
await handleCommand("/context");
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import type { Component, TUI } from "@mariozechner/pi-tui";
|
||||
import { randomUUID } from "node:crypto";
|
||||
import type { SessionsPatchResult } from "../gateway/protocol/index.js";
|
||||
import type { ChatLog } from "./components/chat-log.js";
|
||||
import type { GatewayChatClient } from "./gateway-chat.js";
|
||||
import type {
|
||||
@@ -38,6 +40,9 @@ type CommandHandlerContext = {
|
||||
abortActive: () => Promise<void>;
|
||||
setActivityStatus: (text: string) => void;
|
||||
formatSessionKey: (key: string) => string;
|
||||
applySessionInfoFromPatch: (result: SessionsPatchResult) => void;
|
||||
noteLocalRunId: (runId: string) => void;
|
||||
forgetLocalRunId?: (runId: string) => void;
|
||||
};
|
||||
|
||||
export function createCommandHandlers(context: CommandHandlerContext) {
|
||||
@@ -57,6 +62,9 @@ export function createCommandHandlers(context: CommandHandlerContext) {
|
||||
abortActive,
|
||||
setActivityStatus,
|
||||
formatSessionKey,
|
||||
applySessionInfoFromPatch,
|
||||
noteLocalRunId,
|
||||
forgetLocalRunId,
|
||||
} = context;
|
||||
|
||||
const setAgent = async (id: string) => {
|
||||
@@ -81,11 +89,12 @@ export function createCommandHandlers(context: CommandHandlerContext) {
|
||||
selector.onSelect = (item) => {
|
||||
void (async () => {
|
||||
try {
|
||||
await client.patchSession({
|
||||
const result = await client.patchSession({
|
||||
key: state.currentSessionKey,
|
||||
model: item.value,
|
||||
});
|
||||
chatLog.addSystem(`model set to ${item.value}`);
|
||||
applySessionInfoFromPatch(result);
|
||||
await refreshSessionInfo();
|
||||
} catch (err) {
|
||||
chatLog.addSystem(`model set failed: ${String(err)}`);
|
||||
@@ -284,11 +293,12 @@ export function createCommandHandlers(context: CommandHandlerContext) {
|
||||
await openModelSelector();
|
||||
} else {
|
||||
try {
|
||||
await client.patchSession({
|
||||
const result = await client.patchSession({
|
||||
key: state.currentSessionKey,
|
||||
model: args,
|
||||
});
|
||||
chatLog.addSystem(`model set to ${args}`);
|
||||
applySessionInfoFromPatch(result);
|
||||
await refreshSessionInfo();
|
||||
} catch (err) {
|
||||
chatLog.addSystem(`model set failed: ${String(err)}`);
|
||||
@@ -309,11 +319,12 @@ export function createCommandHandlers(context: CommandHandlerContext) {
|
||||
break;
|
||||
}
|
||||
try {
|
||||
await client.patchSession({
|
||||
const result = await client.patchSession({
|
||||
key: state.currentSessionKey,
|
||||
thinkingLevel: args,
|
||||
});
|
||||
chatLog.addSystem(`thinking set to ${args}`);
|
||||
applySessionInfoFromPatch(result);
|
||||
await refreshSessionInfo();
|
||||
} catch (err) {
|
||||
chatLog.addSystem(`think failed: ${String(err)}`);
|
||||
@@ -325,12 +336,13 @@ export function createCommandHandlers(context: CommandHandlerContext) {
|
||||
break;
|
||||
}
|
||||
try {
|
||||
await client.patchSession({
|
||||
const result = await client.patchSession({
|
||||
key: state.currentSessionKey,
|
||||
verboseLevel: args,
|
||||
});
|
||||
chatLog.addSystem(`verbose set to ${args}`);
|
||||
await refreshSessionInfo();
|
||||
applySessionInfoFromPatch(result);
|
||||
await loadHistory();
|
||||
} catch (err) {
|
||||
chatLog.addSystem(`verbose failed: ${String(err)}`);
|
||||
}
|
||||
@@ -341,11 +353,12 @@ export function createCommandHandlers(context: CommandHandlerContext) {
|
||||
break;
|
||||
}
|
||||
try {
|
||||
await client.patchSession({
|
||||
const result = await client.patchSession({
|
||||
key: state.currentSessionKey,
|
||||
reasoningLevel: args,
|
||||
});
|
||||
chatLog.addSystem(`reasoning set to ${args}`);
|
||||
applySessionInfoFromPatch(result);
|
||||
await refreshSessionInfo();
|
||||
} catch (err) {
|
||||
chatLog.addSystem(`reasoning failed: ${String(err)}`);
|
||||
@@ -362,11 +375,12 @@ export function createCommandHandlers(context: CommandHandlerContext) {
|
||||
const next =
|
||||
normalized ?? (current === "off" ? "tokens" : current === "tokens" ? "full" : "off");
|
||||
try {
|
||||
await client.patchSession({
|
||||
const result = await client.patchSession({
|
||||
key: state.currentSessionKey,
|
||||
responseUsage: next === "off" ? null : next,
|
||||
});
|
||||
chatLog.addSystem(`usage footer: ${next}`);
|
||||
applySessionInfoFromPatch(result);
|
||||
await refreshSessionInfo();
|
||||
} catch (err) {
|
||||
chatLog.addSystem(`usage failed: ${String(err)}`);
|
||||
@@ -383,11 +397,12 @@ export function createCommandHandlers(context: CommandHandlerContext) {
|
||||
break;
|
||||
}
|
||||
try {
|
||||
await client.patchSession({
|
||||
const result = await client.patchSession({
|
||||
key: state.currentSessionKey,
|
||||
elevatedLevel: args,
|
||||
});
|
||||
chatLog.addSystem(`elevated set to ${args}`);
|
||||
applySessionInfoFromPatch(result);
|
||||
await refreshSessionInfo();
|
||||
} catch (err) {
|
||||
chatLog.addSystem(`elevated failed: ${String(err)}`);
|
||||
@@ -399,11 +414,12 @@ export function createCommandHandlers(context: CommandHandlerContext) {
|
||||
break;
|
||||
}
|
||||
try {
|
||||
await client.patchSession({
|
||||
const result = await client.patchSession({
|
||||
key: state.currentSessionKey,
|
||||
groupActivation: args === "always" ? "always" : "mention",
|
||||
});
|
||||
chatLog.addSystem(`activation set to ${args}`);
|
||||
applySessionInfoFromPatch(result);
|
||||
await refreshSessionInfo();
|
||||
} catch (err) {
|
||||
chatLog.addSystem(`activation failed: ${String(err)}`);
|
||||
@@ -448,17 +464,24 @@ export function createCommandHandlers(context: CommandHandlerContext) {
|
||||
try {
|
||||
chatLog.addUser(text);
|
||||
tui.requestRender();
|
||||
const runId = randomUUID();
|
||||
noteLocalRunId(runId);
|
||||
state.activeChatRunId = runId;
|
||||
setActivityStatus("sending");
|
||||
const { runId } = await client.sendChat({
|
||||
await client.sendChat({
|
||||
sessionKey: state.currentSessionKey,
|
||||
message: text,
|
||||
thinking: opts.thinking,
|
||||
deliver: deliverDefault,
|
||||
timeoutMs: opts.timeoutMs,
|
||||
runId,
|
||||
});
|
||||
state.activeChatRunId = runId;
|
||||
setActivityStatus("waiting");
|
||||
} catch (err) {
|
||||
if (state.activeChatRunId) {
|
||||
forgetLocalRunId?.(state.activeChatRunId);
|
||||
}
|
||||
state.activeChatRunId = null;
|
||||
chatLog.addSystem(`send failed: ${String(err)}`);
|
||||
setActivityStatus("error");
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -10,10 +10,24 @@ type EventHandlerContext = {
|
||||
state: TuiStateAccess;
|
||||
setActivityStatus: (text: string) => void;
|
||||
refreshSessionInfo?: () => Promise<void>;
|
||||
loadHistory?: () => Promise<void>;
|
||||
isLocalRunId?: (runId: string) => boolean;
|
||||
forgetLocalRunId?: (runId: string) => void;
|
||||
clearLocalRunIds?: () => void;
|
||||
};
|
||||
|
||||
export function createEventHandlers(context: EventHandlerContext) {
|
||||
const { chatLog, tui, state, setActivityStatus, refreshSessionInfo } = context;
|
||||
const {
|
||||
chatLog,
|
||||
tui,
|
||||
state,
|
||||
setActivityStatus,
|
||||
refreshSessionInfo,
|
||||
loadHistory,
|
||||
isLocalRunId,
|
||||
forgetLocalRunId,
|
||||
clearLocalRunIds,
|
||||
} = context;
|
||||
const finalizedRuns = new Map<string, number>();
|
||||
const sessionRuns = new Map<string, number>();
|
||||
let streamAssembler = new TuiStreamAssembler();
|
||||
@@ -50,6 +64,7 @@ export function createEventHandlers(context: EventHandlerContext) {
|
||||
finalizedRuns.clear();
|
||||
sessionRuns.clear();
|
||||
streamAssembler = new TuiStreamAssembler();
|
||||
clearLocalRunIds?.();
|
||||
};
|
||||
|
||||
const noteSessionRun = (runId: string) => {
|
||||
@@ -95,6 +110,11 @@ export function createEventHandlers(context: EventHandlerContext) {
|
||||
}
|
||||
if (evt.state === "final") {
|
||||
if (isCommandMessage(evt.message)) {
|
||||
if (isLocalRunId?.(evt.runId)) {
|
||||
forgetLocalRunId?.(evt.runId);
|
||||
} else {
|
||||
void loadHistory?.();
|
||||
}
|
||||
const text = extractTextFromMessage(evt.message);
|
||||
if (text) {
|
||||
chatLog.addSystem(text);
|
||||
@@ -107,6 +127,11 @@ export function createEventHandlers(context: EventHandlerContext) {
|
||||
tui.requestRender();
|
||||
return;
|
||||
}
|
||||
if (isLocalRunId?.(evt.runId)) {
|
||||
forgetLocalRunId?.(evt.runId);
|
||||
} else {
|
||||
void loadHistory?.();
|
||||
}
|
||||
const stopReason =
|
||||
evt.message && typeof evt.message === "object" && !Array.isArray(evt.message)
|
||||
? typeof (evt.message as Record<string, unknown>).stopReason === "string"
|
||||
@@ -129,6 +154,11 @@ export function createEventHandlers(context: EventHandlerContext) {
|
||||
state.activeChatRunId = null;
|
||||
setActivityStatus("aborted");
|
||||
void refreshSessionInfo?.();
|
||||
if (isLocalRunId?.(evt.runId)) {
|
||||
forgetLocalRunId?.(evt.runId);
|
||||
} else {
|
||||
void loadHistory?.();
|
||||
}
|
||||
}
|
||||
if (evt.state === "error") {
|
||||
chatLog.addSystem(`run error: ${evt.errorMessage ?? "unknown"}`);
|
||||
@@ -137,6 +167,11 @@ export function createEventHandlers(context: EventHandlerContext) {
|
||||
state.activeChatRunId = null;
|
||||
setActivityStatus("error");
|
||||
void refreshSessionInfo?.();
|
||||
if (isLocalRunId?.(evt.runId)) {
|
||||
forgetLocalRunId?.(evt.runId);
|
||||
} else {
|
||||
void loadHistory?.();
|
||||
}
|
||||
}
|
||||
tui.requestRender();
|
||||
};
|
||||
@@ -148,12 +183,20 @@ export function createEventHandlers(context: EventHandlerContext) {
|
||||
const evt = payload as AgentEvent;
|
||||
syncSessionKey();
|
||||
// Agent events (tool streaming, lifecycle) are emitted per-run. Filter against the
|
||||
// active chat run id, not the session id.
|
||||
// active chat run id, not the session id. Tool results can arrive after the chat
|
||||
// final event, so accept finalized runs for tool updates.
|
||||
const isActiveRun = evt.runId === state.activeChatRunId;
|
||||
if (!isActiveRun && !sessionRuns.has(evt.runId)) {
|
||||
const isKnownRun = isActiveRun || sessionRuns.has(evt.runId) || finalizedRuns.has(evt.runId);
|
||||
if (!isKnownRun) {
|
||||
return;
|
||||
}
|
||||
if (evt.stream === "tool") {
|
||||
const verbose = state.sessionInfo.verboseLevel ?? "off";
|
||||
const allowToolEvents = verbose !== "off";
|
||||
const allowToolOutput = verbose === "full";
|
||||
if (!allowToolEvents) {
|
||||
return;
|
||||
}
|
||||
const data = evt.data ?? {};
|
||||
const phase = asString(data.phase, "");
|
||||
const toolCallId = asString(data.toolCallId, "");
|
||||
@@ -164,13 +207,20 @@ export function createEventHandlers(context: EventHandlerContext) {
|
||||
if (phase === "start") {
|
||||
chatLog.startTool(toolCallId, toolName, data.args);
|
||||
} else if (phase === "update") {
|
||||
if (!allowToolOutput) {
|
||||
return;
|
||||
}
|
||||
chatLog.updateToolResult(toolCallId, data.partialResult, {
|
||||
partial: true,
|
||||
});
|
||||
} else if (phase === "result") {
|
||||
chatLog.updateToolResult(toolCallId, data.result, {
|
||||
isError: Boolean(data.isError),
|
||||
});
|
||||
if (allowToolOutput) {
|
||||
chatLog.updateToolResult(toolCallId, data.result, {
|
||||
isError: Boolean(data.isError),
|
||||
});
|
||||
} else {
|
||||
chatLog.updateToolResult(toolCallId, { content: [] }, { isError: Boolean(data.isError) });
|
||||
}
|
||||
}
|
||||
tui.requestRender();
|
||||
return;
|
||||
|
||||
113
src/tui/tui-session-actions.test.ts
Normal file
113
src/tui/tui-session-actions.test.ts
Normal file
@@ -0,0 +1,113 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import type { TuiStateAccess } from "./tui-types.js";
|
||||
import { createSessionActions } from "./tui-session-actions.js";
|
||||
|
||||
describe("tui session actions", () => {
|
||||
it("queues session refreshes and applies the latest result", async () => {
|
||||
let resolveFirst: ((value: unknown) => void) | undefined;
|
||||
let resolveSecond: ((value: unknown) => void) | undefined;
|
||||
|
||||
const listSessions = vi
|
||||
.fn()
|
||||
.mockImplementationOnce(
|
||||
() =>
|
||||
new Promise((resolve) => {
|
||||
resolveFirst = resolve;
|
||||
}),
|
||||
)
|
||||
.mockImplementationOnce(
|
||||
() =>
|
||||
new Promise((resolve) => {
|
||||
resolveSecond = resolve;
|
||||
}),
|
||||
);
|
||||
|
||||
const state: TuiStateAccess = {
|
||||
agentDefaultId: "main",
|
||||
sessionMainKey: "agent:main:main",
|
||||
sessionScope: "global",
|
||||
agents: [],
|
||||
currentAgentId: "main",
|
||||
currentSessionKey: "agent:main:main",
|
||||
currentSessionId: null,
|
||||
activeChatRunId: null,
|
||||
historyLoaded: false,
|
||||
sessionInfo: {},
|
||||
initialSessionApplied: true,
|
||||
isConnected: true,
|
||||
autoMessageSent: false,
|
||||
toolsExpanded: false,
|
||||
showThinking: false,
|
||||
connectionStatus: "connected",
|
||||
activityStatus: "idle",
|
||||
statusTimeout: null,
|
||||
lastCtrlCAt: 0,
|
||||
};
|
||||
|
||||
const updateFooter = vi.fn();
|
||||
const updateAutocompleteProvider = vi.fn();
|
||||
const requestRender = vi.fn();
|
||||
|
||||
const { refreshSessionInfo } = createSessionActions({
|
||||
client: { listSessions } as { listSessions: typeof listSessions },
|
||||
chatLog: { addSystem: vi.fn() } as unknown as import("./components/chat-log.js").ChatLog,
|
||||
tui: { requestRender } as unknown as import("@mariozechner/pi-tui").TUI,
|
||||
opts: {},
|
||||
state,
|
||||
agentNames: new Map(),
|
||||
initialSessionInput: "",
|
||||
initialSessionAgentId: null,
|
||||
resolveSessionKey: vi.fn(),
|
||||
updateHeader: vi.fn(),
|
||||
updateFooter,
|
||||
updateAutocompleteProvider,
|
||||
setActivityStatus: vi.fn(),
|
||||
});
|
||||
|
||||
const first = refreshSessionInfo();
|
||||
const second = refreshSessionInfo();
|
||||
|
||||
await Promise.resolve();
|
||||
expect(listSessions).toHaveBeenCalledTimes(1);
|
||||
|
||||
resolveFirst?.({
|
||||
ts: Date.now(),
|
||||
path: "/tmp/sessions.json",
|
||||
count: 1,
|
||||
defaults: {},
|
||||
sessions: [
|
||||
{
|
||||
key: "agent:main:main",
|
||||
model: "old",
|
||||
modelProvider: "anthropic",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
await first;
|
||||
await Promise.resolve();
|
||||
|
||||
expect(listSessions).toHaveBeenCalledTimes(2);
|
||||
|
||||
resolveSecond?.({
|
||||
ts: Date.now(),
|
||||
path: "/tmp/sessions.json",
|
||||
count: 1,
|
||||
defaults: {},
|
||||
sessions: [
|
||||
{
|
||||
key: "agent:main:main",
|
||||
model: "Minimax-M2.1",
|
||||
modelProvider: "minimax",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
await second;
|
||||
|
||||
expect(state.sessionInfo.model).toBe("Minimax-M2.1");
|
||||
expect(updateAutocompleteProvider).toHaveBeenCalledTimes(2);
|
||||
expect(updateFooter).toHaveBeenCalledTimes(2);
|
||||
expect(requestRender).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
});
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { TUI } from "@mariozechner/pi-tui";
|
||||
import type { SessionsPatchResult } from "../gateway/protocol/index.js";
|
||||
import type { ChatLog } from "./components/chat-log.js";
|
||||
import type { GatewayAgentsList, GatewayChatClient } from "./gateway-chat.js";
|
||||
import type { TuiOptions, TuiStateAccess } from "./tui-types.js";
|
||||
@@ -23,6 +24,30 @@ type SessionActionContext = {
|
||||
updateFooter: () => void;
|
||||
updateAutocompleteProvider: () => void;
|
||||
setActivityStatus: (text: string) => void;
|
||||
clearLocalRunIds?: () => void;
|
||||
};
|
||||
|
||||
type SessionInfoDefaults = {
|
||||
model?: string | null;
|
||||
modelProvider?: string | null;
|
||||
contextTokens?: number | null;
|
||||
};
|
||||
|
||||
type SessionInfoEntry = {
|
||||
thinkingLevel?: string;
|
||||
verboseLevel?: string;
|
||||
reasoningLevel?: string;
|
||||
model?: string;
|
||||
modelProvider?: string;
|
||||
modelOverride?: string;
|
||||
providerOverride?: string;
|
||||
contextTokens?: number | null;
|
||||
inputTokens?: number | null;
|
||||
outputTokens?: number | null;
|
||||
totalTokens?: number | null;
|
||||
responseUsage?: "on" | "off" | "tokens" | "full";
|
||||
updatedAt?: number | null;
|
||||
displayName?: string;
|
||||
};
|
||||
|
||||
export function createSessionActions(context: SessionActionContext) {
|
||||
@@ -40,8 +65,10 @@ export function createSessionActions(context: SessionActionContext) {
|
||||
updateFooter,
|
||||
updateAutocompleteProvider,
|
||||
setActivityStatus,
|
||||
clearLocalRunIds,
|
||||
} = context;
|
||||
let refreshSessionInfoPromise: Promise<void> | null = null;
|
||||
let refreshSessionInfoPromise: Promise<void> = Promise.resolve();
|
||||
let lastSessionDefaults: SessionInfoDefaults | null = null;
|
||||
|
||||
const applyAgentsResult = (result: GatewayAgentsList) => {
|
||||
state.agentDefaultId = normalizeAgentId(result.defaultId);
|
||||
@@ -99,58 +126,173 @@ export function createSessionActions(context: SessionActionContext) {
|
||||
}
|
||||
};
|
||||
|
||||
const refreshSessionInfo = async () => {
|
||||
if (refreshSessionInfoPromise) {
|
||||
return refreshSessionInfoPromise;
|
||||
const resolveModelSelection = (entry?: SessionInfoEntry) => {
|
||||
if (entry?.modelProvider || entry?.model) {
|
||||
return {
|
||||
modelProvider: entry.modelProvider ?? state.sessionInfo.modelProvider,
|
||||
model: entry.model ?? state.sessionInfo.model,
|
||||
};
|
||||
}
|
||||
refreshSessionInfoPromise = (async () => {
|
||||
try {
|
||||
const listAgentId =
|
||||
state.currentSessionKey === "global" || state.currentSessionKey === "unknown"
|
||||
? undefined
|
||||
: state.currentAgentId;
|
||||
const result = await client.listSessions({
|
||||
includeGlobal: false,
|
||||
includeUnknown: false,
|
||||
agentId: listAgentId,
|
||||
});
|
||||
const entry = result.sessions.find((row) => {
|
||||
// Exact match
|
||||
if (row.key === state.currentSessionKey) {
|
||||
return true;
|
||||
}
|
||||
// Also match canonical keys like "agent:default:main" against "main"
|
||||
const parsed = parseAgentSessionKey(row.key);
|
||||
return parsed?.rest === state.currentSessionKey;
|
||||
});
|
||||
state.sessionInfo = {
|
||||
thinkingLevel: entry?.thinkingLevel,
|
||||
verboseLevel: entry?.verboseLevel,
|
||||
reasoningLevel: entry?.reasoningLevel,
|
||||
model: entry?.model ?? result.defaults?.model ?? undefined,
|
||||
modelProvider: entry?.modelProvider ?? result.defaults?.modelProvider ?? undefined,
|
||||
contextTokens: entry?.contextTokens ?? result.defaults?.contextTokens,
|
||||
inputTokens: entry?.inputTokens ?? null,
|
||||
outputTokens: entry?.outputTokens ?? null,
|
||||
totalTokens: entry?.totalTokens ?? null,
|
||||
responseUsage: entry?.responseUsage,
|
||||
updatedAt: entry?.updatedAt ?? null,
|
||||
displayName: entry?.displayName,
|
||||
};
|
||||
} catch (err) {
|
||||
chatLog.addSystem(`sessions list failed: ${String(err)}`);
|
||||
}
|
||||
updateAutocompleteProvider();
|
||||
updateFooter();
|
||||
tui.requestRender();
|
||||
})();
|
||||
const overrideModel = entry?.modelOverride?.trim();
|
||||
if (overrideModel) {
|
||||
const overrideProvider = entry?.providerOverride?.trim() || state.sessionInfo.modelProvider;
|
||||
return { modelProvider: overrideProvider, model: overrideModel };
|
||||
}
|
||||
return {
|
||||
modelProvider: state.sessionInfo.modelProvider,
|
||||
model: state.sessionInfo.model,
|
||||
};
|
||||
};
|
||||
|
||||
const applySessionInfo = (params: {
|
||||
entry?: SessionInfoEntry | null;
|
||||
defaults?: SessionInfoDefaults | null;
|
||||
force?: boolean;
|
||||
}) => {
|
||||
const entry = params.entry ?? undefined;
|
||||
const defaults = params.defaults ?? lastSessionDefaults ?? undefined;
|
||||
const previousDefaults = lastSessionDefaults;
|
||||
const defaultsChanged = params.defaults
|
||||
? previousDefaults?.model !== params.defaults.model ||
|
||||
previousDefaults?.modelProvider !== params.defaults.modelProvider ||
|
||||
previousDefaults?.contextTokens !== params.defaults.contextTokens
|
||||
: false;
|
||||
if (params.defaults) {
|
||||
lastSessionDefaults = params.defaults;
|
||||
}
|
||||
|
||||
const entryUpdatedAt = entry?.updatedAt ?? null;
|
||||
const currentUpdatedAt = state.sessionInfo.updatedAt ?? null;
|
||||
const modelChanged =
|
||||
(entry?.modelProvider !== undefined &&
|
||||
entry.modelProvider !== state.sessionInfo.modelProvider) ||
|
||||
(entry?.model !== undefined && entry.model !== state.sessionInfo.model);
|
||||
if (
|
||||
!params.force &&
|
||||
entryUpdatedAt !== null &&
|
||||
currentUpdatedAt !== null &&
|
||||
entryUpdatedAt < currentUpdatedAt &&
|
||||
!defaultsChanged &&
|
||||
!modelChanged
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const next = { ...state.sessionInfo };
|
||||
if (entry?.thinkingLevel !== undefined) {
|
||||
next.thinkingLevel = entry.thinkingLevel;
|
||||
}
|
||||
if (entry?.verboseLevel !== undefined) {
|
||||
next.verboseLevel = entry.verboseLevel;
|
||||
}
|
||||
if (entry?.reasoningLevel !== undefined) {
|
||||
next.reasoningLevel = entry.reasoningLevel;
|
||||
}
|
||||
if (entry?.responseUsage !== undefined) {
|
||||
next.responseUsage = entry.responseUsage;
|
||||
}
|
||||
if (entry?.inputTokens !== undefined) {
|
||||
next.inputTokens = entry.inputTokens;
|
||||
}
|
||||
if (entry?.outputTokens !== undefined) {
|
||||
next.outputTokens = entry.outputTokens;
|
||||
}
|
||||
if (entry?.totalTokens !== undefined) {
|
||||
next.totalTokens = entry.totalTokens;
|
||||
}
|
||||
if (entry?.contextTokens !== undefined || defaults?.contextTokens !== undefined) {
|
||||
next.contextTokens =
|
||||
entry?.contextTokens ?? defaults?.contextTokens ?? state.sessionInfo.contextTokens;
|
||||
}
|
||||
if (entry?.displayName !== undefined) {
|
||||
next.displayName = entry.displayName;
|
||||
}
|
||||
if (entry?.updatedAt !== undefined) {
|
||||
next.updatedAt = entry.updatedAt;
|
||||
}
|
||||
|
||||
const selection = resolveModelSelection(entry);
|
||||
if (selection.modelProvider !== undefined) {
|
||||
next.modelProvider = selection.modelProvider;
|
||||
}
|
||||
if (selection.model !== undefined) {
|
||||
next.model = selection.model;
|
||||
}
|
||||
|
||||
state.sessionInfo = next;
|
||||
updateAutocompleteProvider();
|
||||
updateFooter();
|
||||
tui.requestRender();
|
||||
};
|
||||
|
||||
const runRefreshSessionInfo = async () => {
|
||||
try {
|
||||
await refreshSessionInfoPromise;
|
||||
} finally {
|
||||
refreshSessionInfoPromise = null;
|
||||
const resolveListAgentId = () => {
|
||||
if (state.currentSessionKey === "global" || state.currentSessionKey === "unknown") {
|
||||
return undefined;
|
||||
}
|
||||
const parsed = parseAgentSessionKey(state.currentSessionKey);
|
||||
return parsed?.agentId ? normalizeAgentId(parsed.agentId) : state.currentAgentId;
|
||||
};
|
||||
const listAgentId = resolveListAgentId();
|
||||
const result = await client.listSessions({
|
||||
includeGlobal: false,
|
||||
includeUnknown: false,
|
||||
agentId: listAgentId,
|
||||
});
|
||||
const normalizeMatchKey = (key: string) => parseAgentSessionKey(key)?.rest ?? key;
|
||||
const currentMatchKey = normalizeMatchKey(state.currentSessionKey);
|
||||
const entry = result.sessions.find((row) => {
|
||||
// Exact match
|
||||
if (row.key === state.currentSessionKey) {
|
||||
return true;
|
||||
}
|
||||
// Also match canonical keys like "agent:default:main" against "main"
|
||||
return normalizeMatchKey(row.key) === currentMatchKey;
|
||||
});
|
||||
if (entry?.key && entry.key !== state.currentSessionKey) {
|
||||
updateAgentFromSessionKey(entry.key);
|
||||
state.currentSessionKey = entry.key;
|
||||
updateHeader();
|
||||
}
|
||||
applySessionInfo({
|
||||
entry,
|
||||
defaults: result.defaults,
|
||||
});
|
||||
} catch (err) {
|
||||
chatLog.addSystem(`sessions list failed: ${String(err)}`);
|
||||
}
|
||||
};
|
||||
|
||||
const refreshSessionInfo = async () => {
|
||||
refreshSessionInfoPromise = refreshSessionInfoPromise.then(
|
||||
runRefreshSessionInfo,
|
||||
runRefreshSessionInfo,
|
||||
);
|
||||
await refreshSessionInfoPromise;
|
||||
};
|
||||
|
||||
const applySessionInfoFromPatch = (result?: SessionsPatchResult | null) => {
|
||||
if (!result?.entry) {
|
||||
return;
|
||||
}
|
||||
if (result.key && result.key !== state.currentSessionKey) {
|
||||
updateAgentFromSessionKey(result.key);
|
||||
state.currentSessionKey = result.key;
|
||||
updateHeader();
|
||||
}
|
||||
const resolved = result.resolved;
|
||||
const entry =
|
||||
resolved && (resolved.modelProvider || resolved.model)
|
||||
? {
|
||||
...result.entry,
|
||||
modelProvider: resolved.modelProvider ?? result.entry.modelProvider,
|
||||
model: resolved.model ?? result.entry.model,
|
||||
}
|
||||
: result.entry;
|
||||
applySessionInfo({ entry, force: true });
|
||||
};
|
||||
|
||||
const loadHistory = async () => {
|
||||
try {
|
||||
const history = await client.loadHistory({
|
||||
@@ -161,9 +303,12 @@ export function createSessionActions(context: SessionActionContext) {
|
||||
messages?: unknown[];
|
||||
sessionId?: string;
|
||||
thinkingLevel?: string;
|
||||
verboseLevel?: string;
|
||||
};
|
||||
state.currentSessionId = typeof record.sessionId === "string" ? record.sessionId : null;
|
||||
state.sessionInfo.thinkingLevel = record.thinkingLevel ?? state.sessionInfo.thinkingLevel;
|
||||
state.sessionInfo.verboseLevel = record.verboseLevel ?? state.sessionInfo.verboseLevel;
|
||||
const showTools = (state.sessionInfo.verboseLevel ?? "off") !== "off";
|
||||
chatLog.clearAll();
|
||||
chatLog.addSystem(`session ${state.currentSessionKey}`);
|
||||
for (const entry of record.messages ?? []) {
|
||||
@@ -195,6 +340,9 @@ export function createSessionActions(context: SessionActionContext) {
|
||||
continue;
|
||||
}
|
||||
if (message.role === "toolResult") {
|
||||
if (!showTools) {
|
||||
continue;
|
||||
}
|
||||
const toolCallId = asString(message.toolCallId, "");
|
||||
const toolName = asString(message.toolName, "tool");
|
||||
const component = chatLog.startTool(toolCallId, toolName, {});
|
||||
@@ -227,6 +375,7 @@ export function createSessionActions(context: SessionActionContext) {
|
||||
state.activeChatRunId = null;
|
||||
state.currentSessionId = null;
|
||||
state.historyLoaded = false;
|
||||
clearLocalRunIds?.();
|
||||
updateHeader();
|
||||
updateFooter();
|
||||
await loadHistory();
|
||||
@@ -255,6 +404,7 @@ export function createSessionActions(context: SessionActionContext) {
|
||||
applyAgentsResult,
|
||||
refreshAgents,
|
||||
refreshSessionInfo,
|
||||
applySessionInfoFromPatch,
|
||||
loadHistory,
|
||||
setSession,
|
||||
abortActive,
|
||||
|
||||
@@ -95,6 +95,7 @@ export async function runTui(opts: TuiOptions) {
|
||||
let wasDisconnected = false;
|
||||
let toolsExpanded = false;
|
||||
let showThinking = false;
|
||||
const localRunIds = new Set<string>();
|
||||
|
||||
const deliverDefault = opts.deliver ?? false;
|
||||
const autoMessage = opts.message?.trim();
|
||||
@@ -225,6 +226,29 @@ export async function runTui(opts: TuiOptions) {
|
||||
},
|
||||
};
|
||||
|
||||
const noteLocalRunId = (runId: string) => {
|
||||
if (!runId) {
|
||||
return;
|
||||
}
|
||||
localRunIds.add(runId);
|
||||
if (localRunIds.size > 200) {
|
||||
const [first] = localRunIds;
|
||||
if (first) {
|
||||
localRunIds.delete(first);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const forgetLocalRunId = (runId: string) => {
|
||||
localRunIds.delete(runId);
|
||||
};
|
||||
|
||||
const isLocalRunId = (runId: string) => localRunIds.has(runId);
|
||||
|
||||
const clearLocalRunIds = () => {
|
||||
localRunIds.clear();
|
||||
};
|
||||
|
||||
const client = new GatewayChatClient({
|
||||
url: opts.url,
|
||||
token: opts.token,
|
||||
@@ -522,9 +546,16 @@ export async function runTui(opts: TuiOptions) {
|
||||
updateFooter,
|
||||
updateAutocompleteProvider,
|
||||
setActivityStatus,
|
||||
clearLocalRunIds,
|
||||
});
|
||||
const { refreshAgents, refreshSessionInfo, loadHistory, setSession, abortActive } =
|
||||
sessionActions;
|
||||
const {
|
||||
refreshAgents,
|
||||
refreshSessionInfo,
|
||||
applySessionInfoFromPatch,
|
||||
loadHistory,
|
||||
setSession,
|
||||
abortActive,
|
||||
} = sessionActions;
|
||||
|
||||
const { handleChatEvent, handleAgentEvent } = createEventHandlers({
|
||||
chatLog,
|
||||
@@ -532,6 +563,10 @@ export async function runTui(opts: TuiOptions) {
|
||||
state,
|
||||
setActivityStatus,
|
||||
refreshSessionInfo,
|
||||
loadHistory,
|
||||
isLocalRunId,
|
||||
forgetLocalRunId,
|
||||
clearLocalRunIds,
|
||||
});
|
||||
|
||||
const { handleCommand, sendMessage, openModelSelector, openAgentSelector, openSessionSelector } =
|
||||
@@ -545,12 +580,15 @@ export async function runTui(opts: TuiOptions) {
|
||||
openOverlay,
|
||||
closeOverlay,
|
||||
refreshSessionInfo,
|
||||
applySessionInfoFromPatch,
|
||||
loadHistory,
|
||||
setSession,
|
||||
refreshAgents,
|
||||
abortActive,
|
||||
setActivityStatus,
|
||||
formatSessionKey,
|
||||
noteLocalRunId,
|
||||
forgetLocalRunId,
|
||||
});
|
||||
|
||||
const { runLocalShellLine } = createLocalShellRunner({
|
||||
|
||||
Reference in New Issue
Block a user