fix(ui): improve chat session dropdown and refresh behavior (#3682)

* 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.
This commit is contained in:
Tyler Yust 2026-01-28 23:24:46 -08:00 committed by GitHub
parent 718bc3f9c8
commit 6372242da7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 89 additions and 16 deletions

View File

@ -21,6 +21,7 @@ type ChatHost = {
basePath: string; basePath: string;
hello: GatewayHelloOk | null; hello: GatewayHelloOk | null;
chatAvatarUrl: string | null; chatAvatarUrl: string | null;
refreshSessionsAfterChat: boolean;
}; };
export function isChatBusy(host: ChatHost) { 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) { export async function handleAbortChat(host: ChatHost) {
if (!host.connected) return; if (!host.connected) return;
host.chatMessage = ""; host.chatMessage = "";
@ -71,6 +80,7 @@ async function sendChatMessageNow(
attachments?: ChatAttachment[]; attachments?: ChatAttachment[];
previousAttachments?: ChatAttachment[]; previousAttachments?: ChatAttachment[];
restoreAttachments?: boolean; restoreAttachments?: boolean;
refreshSessions?: boolean;
}, },
) { ) {
resetToolStream(host as unknown as Parameters<typeof resetToolStream>[0]); resetToolStream(host as unknown as Parameters<typeof resetToolStream>[0]);
@ -94,6 +104,9 @@ async function sendChatMessageNow(
if (ok && !host.chatRunId) { if (ok && !host.chatRunId) {
void flushChatQueue(host); void flushChatQueue(host);
} }
if (ok && opts?.refreshSessions) {
host.refreshSessionsAfterChat = true;
}
return ok; return ok;
} }
@ -132,6 +145,7 @@ export async function handleSendChat(
return; return;
} }
const refreshSessions = isChatResetCommand(message);
if (messageOverride == null) { if (messageOverride == null) {
host.chatMessage = ""; host.chatMessage = "";
// Clear attachments when sending // Clear attachments when sending
@ -149,13 +163,14 @@ export async function handleSendChat(
attachments: hasAttachments ? attachmentsToSend : undefined, attachments: hasAttachments ? attachmentsToSend : undefined,
previousAttachments: messageOverride == null ? attachments : undefined, previousAttachments: messageOverride == null ? attachments : undefined,
restoreAttachments: Boolean(messageOverride && opts?.restoreDraft), restoreAttachments: Boolean(messageOverride && opts?.restoreDraft),
refreshSessions,
}); });
} }
export async function refreshChat(host: ChatHost) { export async function refreshChat(host: ChatHost) {
await Promise.all([ await Promise.all([
loadChatHistory(host as unknown as MoltbotApp), loadChatHistory(host as unknown as MoltbotApp),
loadSessions(host as unknown as MoltbotApp), loadSessions(host as unknown as MoltbotApp, { activeMinutes: 0 }),
refreshChatAvatar(host), refreshChatAvatar(host),
]); ]);
scheduleChatScroll(host as unknown as Parameters<typeof scheduleChatScroll>[0], true); scheduleChatScroll(host as unknown as Parameters<typeof scheduleChatScroll>[0], true);

View File

@ -26,6 +26,7 @@ import {
import type { MoltbotApp } from "./app"; import type { MoltbotApp } from "./app";
import type { ExecApprovalRequest } from "./controllers/exec-approval"; import type { ExecApprovalRequest } from "./controllers/exec-approval";
import { loadAssistantIdentity } from "./controllers/assistant-identity"; import { loadAssistantIdentity } from "./controllers/assistant-identity";
import { loadSessions } from "./controllers/sessions";
type GatewayHost = { type GatewayHost = {
settings: UiSettings; settings: UiSettings;
@ -50,6 +51,7 @@ type GatewayHost = {
assistantAgentId: string | null; assistantAgentId: string | null;
sessionKey: string; sessionKey: string;
chatRunId: string | null; chatRunId: string | null;
refreshSessionsAfterChat: boolean;
execApprovalQueue: ExecApprovalRequest[]; execApprovalQueue: ExecApprovalRequest[];
execApprovalError: string | null; execApprovalError: string | null;
}; };
@ -194,6 +196,12 @@ function handleGatewayEventUnsafe(host: GatewayHost, evt: GatewayEventFrame) {
void flushChatQueueForEvent( void flushChatQueueForEvent(
host as unknown as Parameters<typeof flushChatQueueForEvent>[0], host as unknown as Parameters<typeof flushChatQueueForEvent>[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); if (state === "final") void loadChatHistory(host as unknown as MoltbotApp);
return; return;

View File

@ -35,6 +35,9 @@ type LifecycleHost = {
export function handleConnected(host: LifecycleHost) { export function handleConnected(host: LifecycleHost) {
host.basePath = inferBasePath(); host.basePath = inferBasePath();
applySettingsFromUrl(
host as unknown as Parameters<typeof applySettingsFromUrl>[0],
);
syncTabWithLocation( syncTabWithLocation(
host as unknown as Parameters<typeof syncTabWithLocation>[0], host as unknown as Parameters<typeof syncTabWithLocation>[0],
true, true,
@ -46,9 +49,6 @@ export function handleConnected(host: LifecycleHost) {
host as unknown as Parameters<typeof attachThemeListener>[0], host as unknown as Parameters<typeof attachThemeListener>[0],
); );
window.addEventListener("popstate", host.popStateHandler); window.addEventListener("popstate", host.popStateHandler);
applySettingsFromUrl(
host as unknown as Parameters<typeof applySettingsFromUrl>[0],
);
connectGateway(host as unknown as Parameters<typeof connectGateway>[0]); connectGateway(host as unknown as Parameters<typeof connectGateway>[0]);
startNodesPolling(host as unknown as Parameters<typeof startNodesPolling>[0]); startNodesPolling(host as unknown as Parameters<typeof startNodesPolling>[0]);
if (host.tab === "logs") { if (host.tab === "logs") {

View File

@ -5,6 +5,7 @@ import type { AppViewState } from "./app-view-state";
import { iconForTab, pathForTab, titleForTab, type Tab } from "./navigation"; import { iconForTab, pathForTab, titleForTab, type Tab } from "./navigation";
import { icons } from "./icons"; import { icons } from "./icons";
import { loadChatHistory } from "./controllers/chat"; import { loadChatHistory } from "./controllers/chat";
import { refreshChat } from "./app-chat";
import { syncUrlWithSessionKey } from "./app-settings"; import { syncUrlWithSessionKey } from "./app-settings";
import type { SessionsListResult } from "./types"; import type { SessionsListResult } from "./types";
import type { ThemeMode } from "./theme"; import type { ThemeMode } from "./theme";
@ -39,7 +40,12 @@ export function renderTab(state: AppViewState, tab: Tab) {
} }
export function renderChatControls(state: AppViewState) { 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 disableThinkingToggle = state.onboarding;
const disableFocusToggle = state.onboarding; const disableFocusToggle = state.onboarding;
const showThinking = state.onboarding ? false : state.settings.chatShowThinking; const showThinking = state.onboarding ? false : state.settings.chatShowThinking;
@ -87,9 +93,9 @@ export function renderChatControls(state: AppViewState) {
?disabled=${state.chatLoading || !state.connected} ?disabled=${state.chatLoading || !state.connected}
@click=${() => { @click=${() => {
state.resetToolStream(); state.resetToolStream();
void loadChatHistory(state); void refreshChat(state as unknown as Parameters<typeof refreshChat>[0]);
}} }}
title="Refresh chat history" title="Refresh chat data"
> >
${refreshIcon} ${refreshIcon}
</button> </button>
@ -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<string>(); const seen = new Set<string>();
const options: Array<{ key: string; displayName?: string }> = []; 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); const resolvedCurrent = sessions?.sessions?.find((s) => s.key === sessionKey);
// Add current session key first // Add main session key first
seen.add(sessionKey); if (mainSessionKey) {
options.push({ key: sessionKey, displayName: resolvedCurrent?.displayName }); 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 // Add sessions from the result
if (sessions?.sessions) { if (sessions?.sessions) {

View File

@ -258,6 +258,7 @@ export class MoltbotApp extends LitElement {
private logsScrollFrame: number | null = null; private logsScrollFrame: number | null = null;
private toolStreamById = new Map<string, ToolStreamEntry>(); private toolStreamById = new Map<string, ToolStreamEntry>();
private toolStreamOrder: string[] = []; private toolStreamOrder: string[] = [];
refreshSessionsAfterChat = false;
basePath = ""; basePath = "";
private popStateHandler = () => private popStateHandler = () =>
onPopStateInternal( onPopStateInternal(

View File

@ -14,18 +14,29 @@ export type SessionsState = {
sessionsIncludeUnknown: boolean; 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.client || !state.connected) return;
if (state.sessionsLoading) return; if (state.sessionsLoading) return;
state.sessionsLoading = true; state.sessionsLoading = true;
state.sessionsError = null; state.sessionsError = null;
try { 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<string, unknown> = { const params: Record<string, unknown> = {
includeGlobal: state.sessionsIncludeGlobal, includeGlobal,
includeUnknown: state.sessionsIncludeUnknown, includeUnknown,
}; };
const activeMinutes = toNumber(state.sessionsFilterActive, 0);
const limit = toNumber(state.sessionsFilterLimit, 0);
if (activeMinutes > 0) params.activeMinutes = activeMinutes; if (activeMinutes > 0) params.activeMinutes = activeMinutes;
if (limit > 0) params.limit = limit; if (limit > 0) params.limit = limit;
const res = (await state.client.request("sessions.list", params)) as const res = (await state.client.request("sessions.list", params)) as