fix(ui): break app chat settings cycle
This commit is contained in:
parent
d518260bb8
commit
cd8fbf16b1
@ -2,6 +2,40 @@
|
|||||||
|
|
||||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||||
import { handleSendChat, refreshChatAvatar, type ChatHost } from "./app-chat.ts";
|
import { handleSendChat, refreshChatAvatar, type ChatHost } from "./app-chat.ts";
|
||||||
|
import { sendChatMessage } from "./controllers/chat.ts";
|
||||||
|
import { saveSettings, type UiSettings } from "./storage.ts";
|
||||||
|
|
||||||
|
vi.mock("./controllers/chat.ts", () => ({
|
||||||
|
abortChatRun: vi.fn(),
|
||||||
|
loadChatHistory: vi.fn(),
|
||||||
|
sendChatMessage: vi.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("./storage.ts", async () => {
|
||||||
|
const actual = await vi.importActual<typeof import("./storage.ts")>("./storage.ts");
|
||||||
|
return {
|
||||||
|
...actual,
|
||||||
|
saveSettings: vi.fn(),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
vi.mock("./app-scroll.ts", async () => {
|
||||||
|
const actual = await vi.importActual<typeof import("./app-scroll.ts")>("./app-scroll.ts");
|
||||||
|
return {
|
||||||
|
...actual,
|
||||||
|
resetChatScroll: vi.fn(),
|
||||||
|
scheduleChatScroll: vi.fn(),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
vi.mock("./app-tool-stream.ts", async () => {
|
||||||
|
const actual =
|
||||||
|
await vi.importActual<typeof import("./app-tool-stream.ts")>("./app-tool-stream.ts");
|
||||||
|
return {
|
||||||
|
...actual,
|
||||||
|
resetToolStream: vi.fn(),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
function makeHost(overrides?: Partial<ChatHost>): ChatHost {
|
function makeHost(overrides?: Partial<ChatHost>): ChatHost {
|
||||||
return {
|
return {
|
||||||
@ -71,6 +105,7 @@ describe("refreshChatAvatar", () => {
|
|||||||
describe("handleSendChat", () => {
|
describe("handleSendChat", () => {
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
vi.unstubAllGlobals();
|
vi.unstubAllGlobals();
|
||||||
|
vi.clearAllMocks();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("keeps slash-command model changes in sync with the chat header cache", async () => {
|
it("keeps slash-command model changes in sync with the chat header cache", async () => {
|
||||||
@ -128,4 +163,48 @@ describe("handleSendChat", () => {
|
|||||||
value: "openai/gpt-5-mini",
|
value: "openai/gpt-5-mini",
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("persists the last active session key without depending on app-settings", async () => {
|
||||||
|
vi.mocked(sendChatMessage).mockResolvedValueOnce("run-1");
|
||||||
|
|
||||||
|
const settings: UiSettings = {
|
||||||
|
gatewayUrl: "ws://localhost:18789",
|
||||||
|
token: "",
|
||||||
|
sessionKey: "main",
|
||||||
|
lastActiveSessionKey: "main",
|
||||||
|
theme: "claw",
|
||||||
|
themeMode: "system",
|
||||||
|
chatFocusMode: false,
|
||||||
|
chatShowThinking: true,
|
||||||
|
chatShowToolCalls: true,
|
||||||
|
splitRatio: 0.6,
|
||||||
|
navCollapsed: false,
|
||||||
|
navWidth: 220,
|
||||||
|
navGroupsCollapsed: {},
|
||||||
|
borderRadius: 50,
|
||||||
|
};
|
||||||
|
const host = Object.assign(
|
||||||
|
makeHost({
|
||||||
|
chatMessage: "hello",
|
||||||
|
sessionKey: "agent:ops:main",
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
applySessionKey: "main",
|
||||||
|
settings,
|
||||||
|
},
|
||||||
|
) as ChatHost & { applySessionKey: string; settings: UiSettings };
|
||||||
|
|
||||||
|
await handleSendChat(host);
|
||||||
|
|
||||||
|
expect(vi.mocked(sendChatMessage)).toHaveBeenCalledWith(
|
||||||
|
host as unknown as Parameters<typeof sendChatMessage>[0],
|
||||||
|
"hello",
|
||||||
|
undefined,
|
||||||
|
);
|
||||||
|
expect(host.settings.lastActiveSessionKey).toBe("agent:ops:main");
|
||||||
|
expect(host.applySessionKey).toBe("agent:ops:main");
|
||||||
|
expect(vi.mocked(saveSettings)).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({ lastActiveSessionKey: "agent:ops:main" }),
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,6 +1,5 @@
|
|||||||
import { parseAgentSessionKey } from "../../../src/sessions/session-key-utils.js";
|
import { parseAgentSessionKey } from "../../../src/sessions/session-key-utils.js";
|
||||||
import { scheduleChatScroll, resetChatScroll } from "./app-scroll.ts";
|
import { scheduleChatScroll, resetChatScroll } from "./app-scroll.ts";
|
||||||
import { setLastActiveSessionKey } from "./app-settings.ts";
|
|
||||||
import { resetToolStream } from "./app-tool-stream.ts";
|
import { resetToolStream } from "./app-tool-stream.ts";
|
||||||
import type { OpenClawApp } from "./app.ts";
|
import type { OpenClawApp } from "./app.ts";
|
||||||
import { executeSlashCommand } from "./chat/slash-command-executor.ts";
|
import { executeSlashCommand } from "./chat/slash-command-executor.ts";
|
||||||
@ -10,6 +9,7 @@ import { loadModels } from "./controllers/models.ts";
|
|||||||
import { loadSessions } from "./controllers/sessions.ts";
|
import { loadSessions } from "./controllers/sessions.ts";
|
||||||
import type { GatewayBrowserClient, GatewayHelloOk } from "./gateway.ts";
|
import type { GatewayBrowserClient, GatewayHelloOk } from "./gateway.ts";
|
||||||
import { normalizeBasePath } from "./navigation.ts";
|
import { normalizeBasePath } from "./navigation.ts";
|
||||||
|
import { saveSettings, type UiSettings } from "./storage.ts";
|
||||||
import type { ChatModelOverride, ModelCatalogEntry } from "./types.ts";
|
import type { ChatModelOverride, ModelCatalogEntry } from "./types.ts";
|
||||||
import type { ChatAttachment, ChatQueueItem } from "./ui-types.ts";
|
import type { ChatAttachment, ChatQueueItem } from "./ui-types.ts";
|
||||||
import { generateUUID } from "./uuid.ts";
|
import { generateUUID } from "./uuid.ts";
|
||||||
@ -38,6 +38,11 @@ export type ChatHost = {
|
|||||||
onSlashAction?: (action: string) => void;
|
onSlashAction?: (action: string) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type ChatSessionSettingsHost = {
|
||||||
|
settings: UiSettings;
|
||||||
|
applySessionKey: string;
|
||||||
|
};
|
||||||
|
|
||||||
export const CHAT_SESSIONS_ACTIVE_MINUTES = 120;
|
export const CHAT_SESSIONS_ACTIVE_MINUTES = 120;
|
||||||
|
|
||||||
export function isChatBusy(host: ChatHost) {
|
export function isChatBusy(host: ChatHost) {
|
||||||
@ -82,6 +87,33 @@ export async function handleAbortChat(host: ChatHost) {
|
|||||||
await abortChatRun(host as unknown as OpenClawApp);
|
await abortChatRun(host as unknown as OpenClawApp);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isChatSessionSettingsHost(host: ChatHost): host is ChatHost & ChatSessionSettingsHost {
|
||||||
|
return (
|
||||||
|
"settings" in host &&
|
||||||
|
typeof host.settings === "object" &&
|
||||||
|
host.settings !== null &&
|
||||||
|
"applySessionKey" in host &&
|
||||||
|
typeof host.applySessionKey === "string"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function persistLastActiveSessionKey(host: ChatHost, next: string) {
|
||||||
|
if (!isChatSessionSettingsHost(host)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const trimmed = next.trim();
|
||||||
|
if (!trimmed || host.settings.lastActiveSessionKey === trimmed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const settings = {
|
||||||
|
...host.settings,
|
||||||
|
lastActiveSessionKey: trimmed,
|
||||||
|
};
|
||||||
|
host.settings = settings;
|
||||||
|
host.applySessionKey = settings.lastActiveSessionKey;
|
||||||
|
saveSettings(settings);
|
||||||
|
}
|
||||||
|
|
||||||
function enqueueChatMessage(
|
function enqueueChatMessage(
|
||||||
host: ChatHost,
|
host: ChatHost,
|
||||||
text: string,
|
text: string,
|
||||||
@ -132,10 +164,7 @@ async function sendChatMessageNow(
|
|||||||
host.chatAttachments = opts.previousAttachments;
|
host.chatAttachments = opts.previousAttachments;
|
||||||
}
|
}
|
||||||
if (ok) {
|
if (ok) {
|
||||||
setLastActiveSessionKey(
|
persistLastActiveSessionKey(host, host.sessionKey);
|
||||||
host as unknown as Parameters<typeof setLastActiveSessionKey>[0],
|
|
||||||
host.sessionKey,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
if (ok && opts?.restoreDraft && opts.previousDraft?.trim()) {
|
if (ok && opts?.restoreDraft && opts.previousDraft?.trim()) {
|
||||||
host.chatMessage = opts.previousDraft;
|
host.chatMessage = opts.previousDraft;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user