* refactor(ui): enhance loadSessions function to accept overrides for session loading parameters - Updated loadSessions to include optional parameters for activeMinutes, limit, includeGlobal, and includeUnknown. - Modified refreshChat to use the new activeMinutes parameter when loading sessions. - Removed duplicate applySettingsFromUrl call in handleConnected function. * feat(ui): implement session refresh functionality after chat - Added `refreshSessionsAfterChat` property to `ChatHost` and `GatewayHost` types. - Introduced `isChatResetCommand` function to identify chat reset commands. - Updated `handleSendChat` to set `refreshSessions` based on chat reset commands. - Modified `handleGatewayEventUnsafe` to load sessions when chat is finalized and `refreshSessionsAfterChat` is true. - Enhanced `refreshChat` to load sessions with `activeMinutes` set to 0 for immediate refresh.
224 lines
7.1 KiB
TypeScript
224 lines
7.1 KiB
TypeScript
import { abortChatRun, loadChatHistory, sendChatMessage } from "./controllers/chat";
|
|
import { loadSessions } from "./controllers/sessions";
|
|
import { generateUUID } from "./uuid";
|
|
import { resetToolStream } from "./app-tool-stream";
|
|
import { scheduleChatScroll } from "./app-scroll";
|
|
import { setLastActiveSessionKey } from "./app-settings";
|
|
import { normalizeBasePath } from "./navigation";
|
|
import type { GatewayHelloOk } from "./gateway";
|
|
import { parseAgentSessionKey } from "../../../src/sessions/session-key-utils.js";
|
|
import type { MoltbotApp } from "./app";
|
|
import type { ChatAttachment, ChatQueueItem } from "./ui-types";
|
|
|
|
type ChatHost = {
|
|
connected: boolean;
|
|
chatMessage: string;
|
|
chatAttachments: ChatAttachment[];
|
|
chatQueue: ChatQueueItem[];
|
|
chatRunId: string | null;
|
|
chatSending: boolean;
|
|
sessionKey: string;
|
|
basePath: string;
|
|
hello: GatewayHelloOk | null;
|
|
chatAvatarUrl: string | null;
|
|
refreshSessionsAfterChat: boolean;
|
|
};
|
|
|
|
export function isChatBusy(host: ChatHost) {
|
|
return host.chatSending || Boolean(host.chatRunId);
|
|
}
|
|
|
|
export function isChatStopCommand(text: string) {
|
|
const trimmed = text.trim();
|
|
if (!trimmed) return false;
|
|
const normalized = trimmed.toLowerCase();
|
|
if (normalized === "/stop") return true;
|
|
return (
|
|
normalized === "stop" ||
|
|
normalized === "esc" ||
|
|
normalized === "abort" ||
|
|
normalized === "wait" ||
|
|
normalized === "exit"
|
|
);
|
|
}
|
|
|
|
function isChatResetCommand(text: string) {
|
|
const trimmed = text.trim();
|
|
if (!trimmed) return false;
|
|
const normalized = trimmed.toLowerCase();
|
|
if (normalized === "/new" || normalized === "/reset") return true;
|
|
return normalized.startsWith("/new ") || normalized.startsWith("/reset ");
|
|
}
|
|
|
|
export async function handleAbortChat(host: ChatHost) {
|
|
if (!host.connected) return;
|
|
host.chatMessage = "";
|
|
await abortChatRun(host as unknown as MoltbotApp);
|
|
}
|
|
|
|
function enqueueChatMessage(host: ChatHost, text: string, attachments?: ChatAttachment[]) {
|
|
const trimmed = text.trim();
|
|
const hasAttachments = Boolean(attachments && attachments.length > 0);
|
|
if (!trimmed && !hasAttachments) return;
|
|
host.chatQueue = [
|
|
...host.chatQueue,
|
|
{
|
|
id: generateUUID(),
|
|
text: trimmed,
|
|
createdAt: Date.now(),
|
|
attachments: hasAttachments ? attachments?.map((att) => ({ ...att })) : undefined,
|
|
},
|
|
];
|
|
}
|
|
|
|
async function sendChatMessageNow(
|
|
host: ChatHost,
|
|
message: string,
|
|
opts?: {
|
|
previousDraft?: string;
|
|
restoreDraft?: boolean;
|
|
attachments?: ChatAttachment[];
|
|
previousAttachments?: ChatAttachment[];
|
|
restoreAttachments?: boolean;
|
|
refreshSessions?: boolean;
|
|
},
|
|
) {
|
|
resetToolStream(host as unknown as Parameters<typeof resetToolStream>[0]);
|
|
const ok = await sendChatMessage(host as unknown as MoltbotApp, message, opts?.attachments);
|
|
if (!ok && opts?.previousDraft != null) {
|
|
host.chatMessage = opts.previousDraft;
|
|
}
|
|
if (!ok && opts?.previousAttachments) {
|
|
host.chatAttachments = opts.previousAttachments;
|
|
}
|
|
if (ok) {
|
|
setLastActiveSessionKey(host as unknown as Parameters<typeof setLastActiveSessionKey>[0], host.sessionKey);
|
|
}
|
|
if (ok && opts?.restoreDraft && opts.previousDraft?.trim()) {
|
|
host.chatMessage = opts.previousDraft;
|
|
}
|
|
if (ok && opts?.restoreAttachments && opts.previousAttachments?.length) {
|
|
host.chatAttachments = opts.previousAttachments;
|
|
}
|
|
scheduleChatScroll(host as unknown as Parameters<typeof scheduleChatScroll>[0]);
|
|
if (ok && !host.chatRunId) {
|
|
void flushChatQueue(host);
|
|
}
|
|
if (ok && opts?.refreshSessions) {
|
|
host.refreshSessionsAfterChat = true;
|
|
}
|
|
return ok;
|
|
}
|
|
|
|
async function flushChatQueue(host: ChatHost) {
|
|
if (!host.connected || isChatBusy(host)) return;
|
|
const [next, ...rest] = host.chatQueue;
|
|
if (!next) return;
|
|
host.chatQueue = rest;
|
|
const ok = await sendChatMessageNow(host, next.text, { attachments: next.attachments });
|
|
if (!ok) {
|
|
host.chatQueue = [next, ...host.chatQueue];
|
|
}
|
|
}
|
|
|
|
export function removeQueuedMessage(host: ChatHost, id: string) {
|
|
host.chatQueue = host.chatQueue.filter((item) => item.id !== id);
|
|
}
|
|
|
|
export async function handleSendChat(
|
|
host: ChatHost,
|
|
messageOverride?: string,
|
|
opts?: { restoreDraft?: boolean },
|
|
) {
|
|
if (!host.connected) return;
|
|
const previousDraft = host.chatMessage;
|
|
const message = (messageOverride ?? host.chatMessage).trim();
|
|
const attachments = host.chatAttachments ?? [];
|
|
const attachmentsToSend = messageOverride == null ? attachments : [];
|
|
const hasAttachments = attachmentsToSend.length > 0;
|
|
|
|
// Allow sending with just attachments (no message text required)
|
|
if (!message && !hasAttachments) return;
|
|
|
|
if (isChatStopCommand(message)) {
|
|
await handleAbortChat(host);
|
|
return;
|
|
}
|
|
|
|
const refreshSessions = isChatResetCommand(message);
|
|
if (messageOverride == null) {
|
|
host.chatMessage = "";
|
|
// Clear attachments when sending
|
|
host.chatAttachments = [];
|
|
}
|
|
|
|
if (isChatBusy(host)) {
|
|
enqueueChatMessage(host, message, attachmentsToSend);
|
|
return;
|
|
}
|
|
|
|
await sendChatMessageNow(host, message, {
|
|
previousDraft: messageOverride == null ? previousDraft : undefined,
|
|
restoreDraft: Boolean(messageOverride && opts?.restoreDraft),
|
|
attachments: hasAttachments ? attachmentsToSend : undefined,
|
|
previousAttachments: messageOverride == null ? attachments : undefined,
|
|
restoreAttachments: Boolean(messageOverride && opts?.restoreDraft),
|
|
refreshSessions,
|
|
});
|
|
}
|
|
|
|
export async function refreshChat(host: ChatHost) {
|
|
await Promise.all([
|
|
loadChatHistory(host as unknown as MoltbotApp),
|
|
loadSessions(host as unknown as MoltbotApp, { activeMinutes: 0 }),
|
|
refreshChatAvatar(host),
|
|
]);
|
|
scheduleChatScroll(host as unknown as Parameters<typeof scheduleChatScroll>[0], true);
|
|
}
|
|
|
|
export const flushChatQueueForEvent = flushChatQueue;
|
|
|
|
type SessionDefaultsSnapshot = {
|
|
defaultAgentId?: string;
|
|
};
|
|
|
|
function resolveAgentIdForSession(host: ChatHost): string | null {
|
|
const parsed = parseAgentSessionKey(host.sessionKey);
|
|
if (parsed?.agentId) return parsed.agentId;
|
|
const snapshot = host.hello?.snapshot as { sessionDefaults?: SessionDefaultsSnapshot } | undefined;
|
|
const fallback = snapshot?.sessionDefaults?.defaultAgentId?.trim();
|
|
return fallback || "main";
|
|
}
|
|
|
|
function buildAvatarMetaUrl(basePath: string, agentId: string): string {
|
|
const base = normalizeBasePath(basePath);
|
|
const encoded = encodeURIComponent(agentId);
|
|
return base ? `${base}/avatar/${encoded}?meta=1` : `/avatar/${encoded}?meta=1`;
|
|
}
|
|
|
|
export async function refreshChatAvatar(host: ChatHost) {
|
|
if (!host.connected) {
|
|
host.chatAvatarUrl = null;
|
|
return;
|
|
}
|
|
const agentId = resolveAgentIdForSession(host);
|
|
if (!agentId) {
|
|
host.chatAvatarUrl = null;
|
|
return;
|
|
}
|
|
host.chatAvatarUrl = null;
|
|
const url = buildAvatarMetaUrl(host.basePath, agentId);
|
|
try {
|
|
const res = await fetch(url, { method: "GET" });
|
|
if (!res.ok) {
|
|
host.chatAvatarUrl = null;
|
|
return;
|
|
}
|
|
const data = (await res.json()) as { avatarUrl?: unknown };
|
|
const avatarUrl = typeof data.avatarUrl === "string" ? data.avatarUrl.trim() : "";
|
|
host.chatAvatarUrl = avatarUrl || null;
|
|
} catch {
|
|
host.chatAvatarUrl = null;
|
|
}
|
|
}
|