diff --git a/ui/src/ui/app-settings.ts b/ui/src/ui/app-settings.ts index 29574704d12..ce69dc92538 100644 --- a/ui/src/ui/app-settings.ts +++ b/ui/src/ui/app-settings.ts @@ -13,7 +13,7 @@ import { loadAgentSkills } from "./controllers/agent-skills.ts"; import { loadAgents } from "./controllers/agents.ts"; import { loadChannels } from "./controllers/channels.ts"; import { loadConfig, loadConfigSchema } from "./controllers/config.ts"; -import { loadCronJobs, loadCronStatus } from "./controllers/cron.ts"; +import { loadCronJobs, loadCronRuns, loadCronStatus } from "./controllers/cron.ts"; import { loadDebug } from "./controllers/debug.ts"; import { loadDevices } from "./controllers/devices.ts"; import { loadExecApprovals } from "./controllers/exec-approvals.ts"; @@ -34,14 +34,8 @@ import { import { saveSettings, type UiSettings } from "./storage.ts"; import { startThemeTransition, type ThemeTransitionContext } from "./theme-transition.ts"; import { resolveTheme, type ResolvedTheme, type ThemeMode, type ThemeName } from "./theme.ts"; -import { cleanupChatModuleState } from "./views/chat.ts"; - -/** - * Per-host theme listener cleanup functions. - * Prevents stale closures after component remount by keying cleanup by host instance. - */ -const systemThemeCleanupMap = new WeakMap void>(); import type { AgentsListResult, AttentionItem } from "./types.ts"; +import { resetChatViewState } from "./views/chat.ts"; type SettingsHost = { settings: UiSettings; @@ -62,6 +56,7 @@ type SettingsHost = { agentsSelectedId?: string | null; agentsPanel?: "overview" | "files" | "tools" | "skills" | "channels" | "cron"; pendingGatewayUrl?: string | null; + systemThemeCleanup?: (() => void) | null; }; export function applySettings(host: SettingsHost, next: UiSettings) { @@ -287,11 +282,8 @@ export function attachThemeListener(host: SettingsHost) { } export function detachThemeListener(host: SettingsHost) { - const cleanup = systemThemeCleanupMap.get(host); - if (cleanup) { - cleanup(); - systemThemeCleanupMap.delete(host); - } + host.systemThemeCleanup?.(); + host.systemThemeCleanup = null; } export function applyResolvedTheme(host: SettingsHost, resolved: ResolvedTheme) { @@ -307,16 +299,13 @@ export function applyResolvedTheme(host: SettingsHost, resolved: ResolvedTheme) function syncSystemThemeListener(host: SettingsHost) { // Clean up existing listener if mode is not "system" if (host.themeMode !== "system") { - const cleanup = systemThemeCleanupMap.get(host); - if (cleanup) { - cleanup(); - systemThemeCleanupMap.delete(host); - } + host.systemThemeCleanup?.(); + host.systemThemeCleanup = null; return; } // Skip if listener already attached for this host - if (systemThemeCleanupMap.has(host)) { + if (host.systemThemeCleanup) { return; } @@ -332,7 +321,7 @@ function syncSystemThemeListener(host: SettingsHost) { applyResolvedTheme(host, resolveTheme(host.theme, "system")); }; mql.addEventListener("change", onChange); - systemThemeCleanupMap.set(host, () => mql.removeEventListener("change", onChange)); + host.systemThemeCleanup = () => mql.removeEventListener("change", onChange); } export function syncTabWithLocation(host: SettingsHost, replace: boolean) { @@ -383,7 +372,7 @@ function applyTabSelection( // Cleanup chat module state when navigating away from chat if (prev === "chat" && next !== "chat") { - cleanupChatModuleState(); + resetChatViewState(); } if (next === "chat") { @@ -596,9 +585,12 @@ export async function loadChannelsTab(host: SettingsHost) { } export async function loadCron(host: SettingsHost) { + const app = host as unknown as OpenClawApp; + const activeCronJobId = app.cronRunsScope === "job" ? app.cronRunsJobId : null; await Promise.all([ - loadChannels(host as unknown as OpenClawApp, false), - loadCronStatus(host as unknown as OpenClawApp), - loadCronJobs(host as unknown as OpenClawApp), + loadChannels(app, false), + loadCronStatus(app), + loadCronJobs(app), + loadCronRuns(app, activeCronJobId), ]); } diff --git a/ui/src/ui/app.ts b/ui/src/ui/app.ts index 992e5f59126..3a126df6329 100644 --- a/ui/src/ui/app.ts +++ b/ui/src/ui/app.ts @@ -53,7 +53,7 @@ import { } from "./app-tool-stream.ts"; import type { AppViewState } from "./app-view-state.ts"; import { normalizeAssistantIdentity } from "./assistant-identity.ts"; -import { exportChatMarkdown } from "./chat-export.ts"; +import { exportChatMarkdown } from "./chat/export.ts"; import { loadAssistantIdentity as loadAssistantIdentityInternal } from "./controllers/assistant-identity.ts"; import type { DevicePairingList } from "./controllers/devices.ts"; import type { ExecApprovalRequest } from "./controllers/exec-approval.ts"; diff --git a/ui/src/ui/chat-export.ts b/ui/src/ui/chat-export.ts index 0611257ad98..ed5bbf931f8 100644 --- a/ui/src/ui/chat-export.ts +++ b/ui/src/ui/chat-export.ts @@ -1,25 +1 @@ -/** - * Export chat history as markdown file. - * Shared utility to prevent code duplication between chat.ts and app.ts. - */ -export function exportChatMarkdown(messages: unknown[], assistantName: string): void { - const history = Array.isArray(messages) ? messages : []; - if (history.length === 0) { - return; - } - const lines: string[] = [`# Chat with ${assistantName}`, ""]; - for (const msg of history) { - const m = msg as Record; - const role = m.role === "user" ? "You" : m.role === "assistant" ? assistantName : "Tool"; - const content = typeof m.content === "string" ? m.content : ""; - const ts = typeof m.timestamp === "number" ? new Date(m.timestamp).toISOString() : ""; - lines.push(`## ${role}${ts ? ` (${ts})` : ""}`, "", content, ""); - } - const blob = new Blob([lines.join("\n")], { type: "text/markdown" }); - const url = URL.createObjectURL(blob); - const a = document.createElement("a"); - a.href = url; - a.download = `chat-${assistantName}-${Date.now()}.md`; - a.click(); - URL.revokeObjectURL(url); -} +export { exportChatMarkdown } from "./chat/export.ts"; diff --git a/ui/src/ui/chat/export.ts b/ui/src/ui/chat/export.ts new file mode 100644 index 00000000000..31e15e592e2 --- /dev/null +++ b/ui/src/ui/chat/export.ts @@ -0,0 +1,24 @@ +/** + * Export chat history as markdown file. + */ +export function exportChatMarkdown(messages: unknown[], assistantName: string): void { + const history = Array.isArray(messages) ? messages : []; + if (history.length === 0) { + return; + } + const lines: string[] = [`# Chat with ${assistantName}`, ""]; + for (const msg of history) { + const m = msg as Record; + const role = m.role === "user" ? "You" : m.role === "assistant" ? assistantName : "Tool"; + const content = typeof m.content === "string" ? m.content : ""; + const ts = typeof m.timestamp === "number" ? new Date(m.timestamp).toISOString() : ""; + lines.push(`## ${role}${ts ? ` (${ts})` : ""}`, "", content, ""); + } + const blob = new Blob([lines.join("\n")], { type: "text/markdown" }); + const url = URL.createObjectURL(blob); + const link = document.createElement("a"); + link.href = url; + link.download = `chat-${assistantName}-${Date.now()}.md`; + link.click(); + URL.revokeObjectURL(url); +} diff --git a/ui/src/ui/views/chat.ts b/ui/src/ui/views/chat.ts index 134d96ff60d..322d634bb6d 100644 --- a/ui/src/ui/views/chat.ts +++ b/ui/src/ui/views/chat.ts @@ -1,8 +1,8 @@ import { html, nothing, type TemplateResult } from "lit"; import { ref } from "lit/directives/ref.js"; import { repeat } from "lit/directives/repeat.js"; -import { exportChatMarkdown } from "../chat-export.ts"; import { DeletedMessages } from "../chat/deleted-messages.ts"; +import { exportChatMarkdown } from "../chat/export.ts"; import { renderMessageGroup, renderReadingIndicatorGroup, @@ -149,11 +149,11 @@ let searchQuery = ""; let pinnedExpanded = false; /** - * Cleanup module-level state when navigating away from chat view. - * Prevents STT recording from continuing after tab switch (which would - * send transcripts to the wrong session) and resets ephemeral UI state. + * Reset module-level chat view state when navigating away from chat. + * Prevents STT recording from continuing after a tab switch and clears + * ephemeral search/slash UI that should not survive navigation. */ -export function cleanupChatModuleState() { +export function resetChatViewState() { if (sttRecording) { stopStt(); sttRecording = false; @@ -170,6 +170,8 @@ export function cleanupChatModuleState() { pinnedExpanded = false; } +export const cleanupChatModuleState = resetChatViewState; + function adjustTextareaHeight(el: HTMLTextAreaElement) { el.style.height = "auto"; el.style.height = `${Math.min(el.scrollHeight, 150)}px`;