mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-12 07:20:45 +00:00
fix(ui): address 4 review comments on dashboard-v2
- Reset chat module state on tab navigation (stops STT leak) - Extract exportChatMarkdown to shared helper (deduplicate) - Move theme listener cleanup to host instance (fix stale ref) - Load cron runs on initial Cron tab open
This commit is contained in:
@@ -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<SettingsHost, () => 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),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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<string, unknown>;
|
||||
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";
|
||||
|
||||
24
ui/src/ui/chat/export.ts
Normal file
24
ui/src/ui/chat/export.ts
Normal file
@@ -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<string, unknown>;
|
||||
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);
|
||||
}
|
||||
@@ -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`;
|
||||
|
||||
Reference in New Issue
Block a user