mirror of
https://github.com/moltbot/moltbot.git
synced 2026-04-20 21:23:23 +00:00
fix(tui): preserve optimistic user messages during active runs
This commit is contained in:
@@ -25,6 +25,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Web UI/markdown: stop bare auto-links from swallowing adjacent CJK text while preserving valid mixed-script path and query characters in rendered links. (#48410) Thanks @jnuyao.
|
||||
- Memory/FTS: add configurable trigram tokenization plus short-CJK substring fallback so memory search can find Chinese, Japanese, and Korean text without breaking mixed long-and-short queries. Thanks @carrotRakko.
|
||||
- Hooks/config: accept runtime channel plugin ids in `hooks.mappings[].channel` (for example `feishu`) instead of rejecting non-core channels during config validation. (#56226) Thanks @AiKrai001.
|
||||
- TUI/chat: keep optimistic outbound user messages visible during active runs by deferring local-run binding until the first gateway chat event reveals the real run id, preventing premature history reloads from wiping pending local sends. (#54722) Thanks @seanturner001.
|
||||
|
||||
## 2026.3.28
|
||||
|
||||
|
||||
@@ -34,6 +34,7 @@ function createHarness(params?: {
|
||||
const state = {
|
||||
currentSessionKey: "agent:main:main",
|
||||
activeChatRunId: params?.activeChatRunId ?? null,
|
||||
pendingOptimisticUserMessage: false,
|
||||
isConnected: params?.isConnected ?? true,
|
||||
sessionInfo: {},
|
||||
};
|
||||
@@ -126,6 +127,16 @@ describe("tui command handlers", () => {
|
||||
expect(requestRender).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("defers local run binding until gateway events provide a real run id", async () => {
|
||||
const { handleCommand, noteLocalRunId, state } = createHarness();
|
||||
|
||||
await handleCommand("/context");
|
||||
|
||||
expect(noteLocalRunId).not.toHaveBeenCalled();
|
||||
expect(state.activeChatRunId).toBeNull();
|
||||
expect(state.pendingOptimisticUserMessage).toBe(true);
|
||||
});
|
||||
|
||||
it("sends /btw without hijacking the active main run", async () => {
|
||||
const setActivityStatus = vi.fn();
|
||||
const { handleCommand, sendChat, addUser, noteLocalRunId, noteLocalBtwRunId, state } =
|
||||
@@ -173,7 +184,7 @@ describe("tui command handlers", () => {
|
||||
|
||||
it("reports send failures and marks activity status as error", async () => {
|
||||
const setActivityStatus = vi.fn();
|
||||
const { handleCommand, addSystem } = createHarness({
|
||||
const { handleCommand, addSystem, state } = createHarness({
|
||||
sendChat: vi.fn().mockRejectedValue(new Error("gateway down")),
|
||||
setActivityStatus,
|
||||
});
|
||||
@@ -182,6 +193,7 @@ describe("tui command handlers", () => {
|
||||
|
||||
expect(addSystem).toHaveBeenCalledWith("send failed: Error: gateway down");
|
||||
expect(setActivityStatus).toHaveBeenLastCalledWith("error");
|
||||
expect(state.pendingOptimisticUserMessage).toBe(false);
|
||||
});
|
||||
|
||||
it("sanitizes control sequences in /new and /reset failures", async () => {
|
||||
|
||||
@@ -72,7 +72,6 @@ export function createCommandHandlers(context: CommandHandlerContext) {
|
||||
setActivityStatus,
|
||||
formatSessionKey,
|
||||
applySessionInfoFromPatch,
|
||||
noteLocalRunId,
|
||||
noteLocalBtwRunId,
|
||||
forgetLocalRunId,
|
||||
forgetLocalBtwRunId,
|
||||
@@ -520,8 +519,7 @@ export function createCommandHandlers(context: CommandHandlerContext) {
|
||||
try {
|
||||
if (!isBtw) {
|
||||
chatLog.addUser(text);
|
||||
noteLocalRunId(runId);
|
||||
state.activeChatRunId = runId;
|
||||
state.pendingOptimisticUserMessage = true;
|
||||
setActivityStatus("sending");
|
||||
} else {
|
||||
noteLocalBtwRunId?.(runId);
|
||||
@@ -547,6 +545,7 @@ export function createCommandHandlers(context: CommandHandlerContext) {
|
||||
forgetLocalRunId?.(state.activeChatRunId);
|
||||
}
|
||||
if (!isBtw) {
|
||||
state.pendingOptimisticUserMessage = false;
|
||||
state.activeChatRunId = null;
|
||||
}
|
||||
chatLog.addSystem(`${isBtw ? "btw failed" : "send failed"}: ${String(err)}`);
|
||||
|
||||
@@ -58,6 +58,7 @@ describe("tui-event-handlers: handleAgentEvent", () => {
|
||||
currentSessionKey: "agent:main:main",
|
||||
currentSessionId: "session-1",
|
||||
activeChatRunId: "run-1",
|
||||
pendingOptimisticUserMessage: false,
|
||||
historyLoaded: true,
|
||||
sessionInfo: { verboseLevel: "on" },
|
||||
initialSessionApplied: true,
|
||||
@@ -126,6 +127,7 @@ describe("tui-event-handlers: handleAgentEvent", () => {
|
||||
state,
|
||||
setActivityStatus: context.setActivityStatus,
|
||||
loadHistory: context.loadHistory,
|
||||
noteLocalRunId: context.noteLocalRunId,
|
||||
isLocalRunId: context.isLocalRunId,
|
||||
forgetLocalRunId: context.forgetLocalRunId,
|
||||
isLocalBtwRunId: context.isLocalBtwRunId,
|
||||
@@ -466,6 +468,24 @@ describe("tui-event-handlers: handleAgentEvent", () => {
|
||||
expect(loadHistory).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("binds optimistic pending messages to the first gateway run id and skips history reload", () => {
|
||||
const { state, loadHistory, isLocalRunId, handleChatEvent } = createHandlersHarness({
|
||||
state: { activeChatRunId: null, pendingOptimisticUserMessage: true },
|
||||
});
|
||||
|
||||
handleChatEvent({
|
||||
runId: "run-gateway",
|
||||
sessionKey: state.currentSessionKey,
|
||||
state: "final",
|
||||
message: { content: [{ type: "text", text: "done" }] },
|
||||
});
|
||||
|
||||
expect(state.pendingOptimisticUserMessage).toBe(false);
|
||||
expect(state.activeChatRunId).toBeNull();
|
||||
expect(isLocalRunId("run-gateway")).toBe(false);
|
||||
expect(loadHistory).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
function createConcurrentRunHarness(localContent = "partial") {
|
||||
const { state, chatLog, setActivityStatus, loadHistory, handleChatEvent } =
|
||||
createHandlersHarness({
|
||||
|
||||
@@ -33,6 +33,7 @@ type EventHandlerContext = {
|
||||
setActivityStatus: (text: string) => void;
|
||||
refreshSessionInfo?: () => Promise<void>;
|
||||
loadHistory?: () => Promise<void>;
|
||||
noteLocalRunId?: (runId: string) => void;
|
||||
isLocalRunId?: (runId: string) => boolean;
|
||||
forgetLocalRunId?: (runId: string) => void;
|
||||
clearLocalRunIds?: () => void;
|
||||
@@ -50,6 +51,7 @@ export function createEventHandlers(context: EventHandlerContext) {
|
||||
setActivityStatus,
|
||||
refreshSessionInfo,
|
||||
loadHistory,
|
||||
noteLocalRunId,
|
||||
isLocalRunId,
|
||||
forgetLocalRunId,
|
||||
clearLocalRunIds,
|
||||
@@ -95,6 +97,7 @@ export function createEventHandlers(context: EventHandlerContext) {
|
||||
sessionRuns.clear();
|
||||
streamAssembler = new TuiStreamAssembler();
|
||||
pendingHistoryRefresh = false;
|
||||
state.pendingOptimisticUserMessage = false;
|
||||
clearLocalRunIds?.();
|
||||
clearLocalBtwRunIds?.();
|
||||
btw.clear();
|
||||
@@ -231,6 +234,10 @@ export function createEventHandlers(context: EventHandlerContext) {
|
||||
noteSessionRun(evt.runId);
|
||||
if (!state.activeChatRunId && !isLocalBtwRunId?.(evt.runId)) {
|
||||
state.activeChatRunId = evt.runId;
|
||||
if (state.pendingOptimisticUserMessage) {
|
||||
noteLocalRunId?.(evt.runId);
|
||||
state.pendingOptimisticUserMessage = false;
|
||||
}
|
||||
}
|
||||
if (evt.state === "delta") {
|
||||
const displayText = streamAssembler.ingestDelta(evt.runId, evt.message, state.showThinking);
|
||||
|
||||
@@ -108,6 +108,7 @@ export type TuiStateAccess = {
|
||||
currentSessionKey: string;
|
||||
currentSessionId: string | null;
|
||||
activeChatRunId: string | null;
|
||||
pendingOptimisticUserMessage?: boolean;
|
||||
historyLoaded: boolean;
|
||||
sessionInfo: SessionInfo;
|
||||
initialSessionApplied: boolean;
|
||||
|
||||
@@ -203,6 +203,7 @@ export async function runTui(opts: TuiOptions) {
|
||||
let initialSessionApplied = false;
|
||||
let currentSessionId: string | null = null;
|
||||
let activeChatRunId: string | null = null;
|
||||
let pendingOptimisticUserMessage = false;
|
||||
let historyLoaded = false;
|
||||
let isConnected = false;
|
||||
let wasDisconnected = false;
|
||||
@@ -274,6 +275,12 @@ export async function runTui(opts: TuiOptions) {
|
||||
set activeChatRunId(value) {
|
||||
activeChatRunId = value;
|
||||
},
|
||||
get pendingOptimisticUserMessage() {
|
||||
return pendingOptimisticUserMessage;
|
||||
},
|
||||
set pendingOptimisticUserMessage(value) {
|
||||
pendingOptimisticUserMessage = value;
|
||||
},
|
||||
get historyLoaded() {
|
||||
return historyLoaded;
|
||||
},
|
||||
@@ -712,6 +719,7 @@ export async function runTui(opts: TuiOptions) {
|
||||
setActivityStatus,
|
||||
refreshSessionInfo,
|
||||
loadHistory,
|
||||
noteLocalRunId,
|
||||
isLocalRunId,
|
||||
forgetLocalRunId,
|
||||
clearLocalRunIds,
|
||||
|
||||
Reference in New Issue
Block a user