diff --git a/ui/src/ui/app-chat.ts b/ui/src/ui/app-chat.ts index 77149f9ad..0b35fb445 100644 --- a/ui/src/ui/app-chat.ts +++ b/ui/src/ui/app-chat.ts @@ -21,6 +21,7 @@ type ChatHost = { basePath: string; hello: GatewayHelloOk | null; chatAvatarUrl: string | null; + refreshSessionsAfterChat: boolean; }; export function isChatBusy(host: ChatHost) { @@ -41,6 +42,14 @@ export function isChatStopCommand(text: string) { ); } +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 = ""; @@ -71,6 +80,7 @@ async function sendChatMessageNow( attachments?: ChatAttachment[]; previousAttachments?: ChatAttachment[]; restoreAttachments?: boolean; + refreshSessions?: boolean; }, ) { resetToolStream(host as unknown as Parameters[0]); @@ -94,6 +104,9 @@ async function sendChatMessageNow( if (ok && !host.chatRunId) { void flushChatQueue(host); } + if (ok && opts?.refreshSessions) { + host.refreshSessionsAfterChat = true; + } return ok; } @@ -132,6 +145,7 @@ export async function handleSendChat( return; } + const refreshSessions = isChatResetCommand(message); if (messageOverride == null) { host.chatMessage = ""; // Clear attachments when sending @@ -149,13 +163,14 @@ export async function handleSendChat( 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), + loadSessions(host as unknown as MoltbotApp, { activeMinutes: 0 }), refreshChatAvatar(host), ]); scheduleChatScroll(host as unknown as Parameters[0], true); diff --git a/ui/src/ui/app-gateway.ts b/ui/src/ui/app-gateway.ts index b2355709c..ba1df61e1 100644 --- a/ui/src/ui/app-gateway.ts +++ b/ui/src/ui/app-gateway.ts @@ -26,6 +26,7 @@ import { import type { MoltbotApp } from "./app"; import type { ExecApprovalRequest } from "./controllers/exec-approval"; import { loadAssistantIdentity } from "./controllers/assistant-identity"; +import { loadSessions } from "./controllers/sessions"; type GatewayHost = { settings: UiSettings; @@ -50,6 +51,7 @@ type GatewayHost = { assistantAgentId: string | null; sessionKey: string; chatRunId: string | null; + refreshSessionsAfterChat: boolean; execApprovalQueue: ExecApprovalRequest[]; execApprovalError: string | null; }; @@ -194,6 +196,12 @@ function handleGatewayEventUnsafe(host: GatewayHost, evt: GatewayEventFrame) { void flushChatQueueForEvent( host as unknown as Parameters[0], ); + if (host.refreshSessionsAfterChat) { + host.refreshSessionsAfterChat = false; + if (state === "final") { + void loadSessions(host as unknown as MoltbotApp, { activeMinutes: 0 }); + } + } } if (state === "final") void loadChatHistory(host as unknown as MoltbotApp); return; diff --git a/ui/src/ui/app-lifecycle.ts b/ui/src/ui/app-lifecycle.ts index 71af9d202..cf5214250 100644 --- a/ui/src/ui/app-lifecycle.ts +++ b/ui/src/ui/app-lifecycle.ts @@ -35,6 +35,9 @@ type LifecycleHost = { export function handleConnected(host: LifecycleHost) { host.basePath = inferBasePath(); + applySettingsFromUrl( + host as unknown as Parameters[0], + ); syncTabWithLocation( host as unknown as Parameters[0], true, @@ -46,9 +49,6 @@ export function handleConnected(host: LifecycleHost) { host as unknown as Parameters[0], ); window.addEventListener("popstate", host.popStateHandler); - applySettingsFromUrl( - host as unknown as Parameters[0], - ); connectGateway(host as unknown as Parameters[0]); startNodesPolling(host as unknown as Parameters[0]); if (host.tab === "logs") { diff --git a/ui/src/ui/app-render.helpers.ts b/ui/src/ui/app-render.helpers.ts index 22f8d90db..c2190e1c9 100644 --- a/ui/src/ui/app-render.helpers.ts +++ b/ui/src/ui/app-render.helpers.ts @@ -5,6 +5,7 @@ import type { AppViewState } from "./app-view-state"; import { iconForTab, pathForTab, titleForTab, type Tab } from "./navigation"; import { icons } from "./icons"; import { loadChatHistory } from "./controllers/chat"; +import { refreshChat } from "./app-chat"; import { syncUrlWithSessionKey } from "./app-settings"; import type { SessionsListResult } from "./types"; import type { ThemeMode } from "./theme"; @@ -39,7 +40,12 @@ export function renderTab(state: AppViewState, tab: Tab) { } export function renderChatControls(state: AppViewState) { - const sessionOptions = resolveSessionOptions(state.sessionKey, state.sessionsResult); + const mainSessionKey = resolveMainSessionKey(state.hello, state.sessionsResult); + const sessionOptions = resolveSessionOptions( + state.sessionKey, + state.sessionsResult, + mainSessionKey, + ); const disableThinkingToggle = state.onboarding; const disableFocusToggle = state.onboarding; const showThinking = state.onboarding ? false : state.settings.chatShowThinking; @@ -87,9 +93,9 @@ export function renderChatControls(state: AppViewState) { ?disabled=${state.chatLoading || !state.connected} @click=${() => { state.resetToolStream(); - void loadChatHistory(state); + void refreshChat(state as unknown as Parameters[0]); }} - title="Refresh chat history" + title="Refresh chat data" > ${refreshIcon} @@ -132,15 +138,47 @@ export function renderChatControls(state: AppViewState) { `; } -function resolveSessionOptions(sessionKey: string, sessions: SessionsListResult | null) { +type SessionDefaultsSnapshot = { + mainSessionKey?: string; + mainKey?: string; +}; + +function resolveMainSessionKey( + hello: AppViewState["hello"], + sessions: SessionsListResult | null, +): string | null { + const snapshot = hello?.snapshot as { sessionDefaults?: SessionDefaultsSnapshot } | undefined; + const mainSessionKey = snapshot?.sessionDefaults?.mainSessionKey?.trim(); + if (mainSessionKey) return mainSessionKey; + const mainKey = snapshot?.sessionDefaults?.mainKey?.trim(); + if (mainKey) return mainKey; + if (sessions?.sessions?.some((row) => row.key === "main")) return "main"; + return null; +} + +function resolveSessionOptions( + sessionKey: string, + sessions: SessionsListResult | null, + mainSessionKey?: string | null, +) { const seen = new Set(); const options: Array<{ key: string; displayName?: string }> = []; + const resolvedMain = + mainSessionKey && sessions?.sessions?.find((s) => s.key === mainSessionKey); const resolvedCurrent = sessions?.sessions?.find((s) => s.key === sessionKey); - // Add current session key first - seen.add(sessionKey); - options.push({ key: sessionKey, displayName: resolvedCurrent?.displayName }); + // Add main session key first + if (mainSessionKey) { + seen.add(mainSessionKey); + options.push({ key: mainSessionKey, displayName: resolvedMain?.displayName }); + } + + // Add current session key next + if (!seen.has(sessionKey)) { + seen.add(sessionKey); + options.push({ key: sessionKey, displayName: resolvedCurrent?.displayName }); + } // Add sessions from the result if (sessions?.sessions) { diff --git a/ui/src/ui/app.ts b/ui/src/ui/app.ts index 26f4a5836..50ffcdf76 100644 --- a/ui/src/ui/app.ts +++ b/ui/src/ui/app.ts @@ -258,6 +258,7 @@ export class MoltbotApp extends LitElement { private logsScrollFrame: number | null = null; private toolStreamById = new Map(); private toolStreamOrder: string[] = []; + refreshSessionsAfterChat = false; basePath = ""; private popStateHandler = () => onPopStateInternal( diff --git a/ui/src/ui/controllers/sessions.ts b/ui/src/ui/controllers/sessions.ts index 5c5077037..7e87f1911 100644 --- a/ui/src/ui/controllers/sessions.ts +++ b/ui/src/ui/controllers/sessions.ts @@ -14,18 +14,29 @@ export type SessionsState = { sessionsIncludeUnknown: boolean; }; -export async function loadSessions(state: SessionsState) { +export async function loadSessions( + state: SessionsState, + overrides?: { + activeMinutes?: number; + limit?: number; + includeGlobal?: boolean; + includeUnknown?: boolean; + }, +) { if (!state.client || !state.connected) return; if (state.sessionsLoading) return; state.sessionsLoading = true; state.sessionsError = null; try { + const includeGlobal = overrides?.includeGlobal ?? state.sessionsIncludeGlobal; + const includeUnknown = overrides?.includeUnknown ?? state.sessionsIncludeUnknown; + const activeMinutes = + overrides?.activeMinutes ?? toNumber(state.sessionsFilterActive, 0); + const limit = overrides?.limit ?? toNumber(state.sessionsFilterLimit, 0); const params: Record = { - includeGlobal: state.sessionsIncludeGlobal, - includeUnknown: state.sessionsIncludeUnknown, + includeGlobal, + includeUnknown, }; - const activeMinutes = toNumber(state.sessionsFilterActive, 0); - const limit = toNumber(state.sessionsFilterLimit, 0); if (activeMinutes > 0) params.activeMinutes = activeMinutes; if (limit > 0) params.limit = limit; const res = (await state.client.request("sessions.list", params)) as