mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-24 19:35:28 -06:00
fix(ui): re-render composer when input history recall mutates draft (#112634)
* fix(ui): re-render composer when input history recall mutates draft * test(chat-state): add unit tests for composer history invalidation
This commit is contained in:
@@ -102,6 +102,9 @@ export class ChatStateController<TState extends ChatPageHost> implements Reactiv
|
||||
const result = navigateInputHistory(input);
|
||||
if (result.handled) {
|
||||
this.composerPersistence.schedule();
|
||||
// A history recall mutates chatMessage directly; without invalidating,
|
||||
// the composer textarea stays empty until an unrelated event re-renders.
|
||||
state.renderLifecycle.invalidate();
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
@@ -485,6 +485,126 @@ describe("ChatStateController render lifecycle", () => {
|
||||
expect(cancelAnimationFrame).toHaveBeenCalledWith(2);
|
||||
expect(painted).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("invalidates the render lifecycle when input history recall mutates the draft", () => {
|
||||
const requestUpdate = vi.fn();
|
||||
const host = {
|
||||
addController: () => undefined,
|
||||
removeController: () => undefined,
|
||||
requestUpdate,
|
||||
updateComplete: Promise.resolve(true),
|
||||
} satisfies ReactiveControllerHost;
|
||||
const controller = new ChatStateController<ChatPageHost>(host);
|
||||
controller.hostConnected();
|
||||
const renderLifecycle = controller.createRenderLifecycle();
|
||||
|
||||
const navigateHistory = vi.fn().mockReturnValue({
|
||||
handled: true,
|
||||
preventDefault: true,
|
||||
restoreCaret: "up" as const,
|
||||
decision: "handled:history-up" as const,
|
||||
historyNavigationActiveBefore: false,
|
||||
historyNavigationActiveAfter: true,
|
||||
selectionStart: 0,
|
||||
selectionEnd: 0,
|
||||
valueLength: 10,
|
||||
});
|
||||
|
||||
const state = {
|
||||
settings: undefined,
|
||||
assistantAgentId: null,
|
||||
agentsList: null,
|
||||
hello: null,
|
||||
sessionKey: "agent:main:current",
|
||||
chatLoading: false,
|
||||
chatMessages: [],
|
||||
chatQueue: [],
|
||||
renderLifecycle,
|
||||
handleSendChat: vi.fn().mockResolvedValue(undefined),
|
||||
handleChatDraftChange: vi.fn(),
|
||||
handleChatInputHistoryKey: navigateHistory,
|
||||
} as unknown as ChatPageHost;
|
||||
|
||||
controller.attach(state);
|
||||
|
||||
const input = {
|
||||
key: "ArrowUp" as const,
|
||||
selectionStart: 0,
|
||||
selectionEnd: 0,
|
||||
valueLength: 0,
|
||||
altKey: false,
|
||||
ctrlKey: false,
|
||||
metaKey: false,
|
||||
shiftKey: false,
|
||||
isComposing: false,
|
||||
keyCode: 0,
|
||||
};
|
||||
const result = state.handleChatInputHistoryKey!(input);
|
||||
|
||||
expect(result.handled).toBe(true);
|
||||
expect(navigateHistory).toHaveBeenCalledWith(input);
|
||||
expect(requestUpdate).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("does not invalidate the render lifecycle when input history key is not handled", () => {
|
||||
const requestUpdate = vi.fn();
|
||||
const host = {
|
||||
addController: () => undefined,
|
||||
removeController: () => undefined,
|
||||
requestUpdate,
|
||||
updateComplete: Promise.resolve(true),
|
||||
} satisfies ReactiveControllerHost;
|
||||
const controller = new ChatStateController<ChatPageHost>(host);
|
||||
controller.hostConnected();
|
||||
const renderLifecycle = controller.createRenderLifecycle();
|
||||
|
||||
const navigateHistory = vi.fn().mockReturnValue({
|
||||
handled: false,
|
||||
preventDefault: false,
|
||||
restoreCaret: null,
|
||||
decision: "blocked:modifier-or-composition" as const,
|
||||
historyNavigationActiveBefore: false,
|
||||
historyNavigationActiveAfter: false,
|
||||
selectionStart: 0,
|
||||
selectionEnd: 0,
|
||||
valueLength: 10,
|
||||
});
|
||||
|
||||
const state = {
|
||||
settings: undefined,
|
||||
assistantAgentId: null,
|
||||
agentsList: null,
|
||||
hello: null,
|
||||
sessionKey: "agent:main:current",
|
||||
chatLoading: false,
|
||||
chatMessages: [],
|
||||
chatQueue: [],
|
||||
renderLifecycle,
|
||||
handleSendChat: vi.fn().mockResolvedValue(undefined),
|
||||
handleChatDraftChange: vi.fn(),
|
||||
handleChatInputHistoryKey: navigateHistory,
|
||||
} as unknown as ChatPageHost;
|
||||
|
||||
controller.attach(state);
|
||||
|
||||
const input = {
|
||||
key: "ArrowUp" as const,
|
||||
selectionStart: 5,
|
||||
selectionEnd: 5,
|
||||
valueLength: 10,
|
||||
altKey: false,
|
||||
ctrlKey: false,
|
||||
metaKey: false,
|
||||
shiftKey: false,
|
||||
isComposing: false,
|
||||
keyCode: 0,
|
||||
};
|
||||
const result = state.handleChatInputHistoryKey!(input);
|
||||
|
||||
expect(result.handled).toBe(false);
|
||||
expect(navigateHistory).toHaveBeenCalledWith(input);
|
||||
expect(requestUpdate).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("session pull request refresh", () => {
|
||||
|
||||
Reference in New Issue
Block a user