mirror of
https://github.com/rait-09/obsidian-agent-client.git
synced 2026-07-22 06:43:37 +00:00
The previous implementation appended '...' AFTER slicing to maxLength, so the returned string could exceed the stated 'maximum length' by 3 characters. This affected callers such as forkSession (title 'Fork: <truncated>...' could reach 53 chars instead of the intended 50) and saveSessionLocally. Truncate to maxLength - ELLIPSIS.length and only then append the ellipsis, so the final length is bounded by maxLength. Handle the edge case maxLength <= ELLIPSIS.length by omitting the ellipsis. Drop the duplicate truncateTitle defined locally in SessionHistoryModal.tsx and import from utils/text; it shared the same bug and now disappears from a single fix site.
728 lines
19 KiB
TypeScript
728 lines
19 KiB
TypeScript
/**
|
|
* Session History Modal
|
|
*
|
|
* Contains the Obsidian Modal wrapper, the React content component,
|
|
* and the confirmation modal for session deletion.
|
|
*/
|
|
|
|
import { Modal, App, setIcon } from "obsidian";
|
|
import { EditTitleModal } from "./EditTitleModal";
|
|
import * as React from "react";
|
|
const { useState, useCallback } = React;
|
|
import { createRoot, Root } from "react-dom/client";
|
|
import type { SessionInfo } from "../types/session";
|
|
import { truncateTitle } from "../utils/text";
|
|
|
|
// ============================================================
|
|
// ConfirmDeleteModal (internal)
|
|
// ============================================================
|
|
|
|
/**
|
|
* Confirmation modal for session deletion.
|
|
*
|
|
* Displays session title and asks user to confirm deletion.
|
|
* Calls onConfirm callback only when user clicks Delete button.
|
|
*/
|
|
class ConfirmDeleteModal extends Modal {
|
|
private sessionTitle: string;
|
|
private onConfirm: () => void | Promise<void>;
|
|
|
|
constructor(
|
|
app: App,
|
|
sessionTitle: string,
|
|
onConfirm: () => void | Promise<void>,
|
|
) {
|
|
super(app);
|
|
this.sessionTitle = sessionTitle;
|
|
this.onConfirm = onConfirm;
|
|
}
|
|
|
|
onOpen() {
|
|
const { contentEl } = this;
|
|
contentEl.empty();
|
|
|
|
// Title
|
|
contentEl.createEl("h2", { text: "Delete session?" });
|
|
|
|
// Message
|
|
contentEl.createEl("p", {
|
|
text: `Are you sure you want to delete "${this.sessionTitle}"?`,
|
|
cls: "agent-client-confirm-delete-message",
|
|
});
|
|
|
|
contentEl.createEl("p", {
|
|
text: "This only removes the session from this plugin. The session data will remain on the agent side.",
|
|
cls: "agent-client-confirm-delete-warning",
|
|
});
|
|
|
|
// Buttons container
|
|
const buttonContainer = contentEl.createDiv({
|
|
cls: "agent-client-confirm-delete-buttons",
|
|
});
|
|
|
|
// Cancel button
|
|
const cancelButton = buttonContainer.createEl("button", {
|
|
text: "Cancel",
|
|
cls: "agent-client-confirm-delete-cancel",
|
|
});
|
|
cancelButton.addEventListener("click", () => {
|
|
this.close();
|
|
});
|
|
|
|
// Delete button
|
|
const deleteButton = buttonContainer.createEl("button", {
|
|
text: "Delete",
|
|
cls: "agent-client-confirm-delete-confirm mod-warning",
|
|
});
|
|
deleteButton.addEventListener("click", () => {
|
|
this.close();
|
|
void this.onConfirm();
|
|
});
|
|
}
|
|
|
|
onClose() {
|
|
const { contentEl } = this;
|
|
contentEl.empty();
|
|
}
|
|
}
|
|
|
|
// ============================================================
|
|
// SessionHistoryContent (internal)
|
|
// ============================================================
|
|
|
|
/**
|
|
* Props for SessionHistoryContent component.
|
|
*/
|
|
interface SessionHistoryContentProps {
|
|
/** Obsidian App instance (for creating modals) */
|
|
app: App;
|
|
/** List of sessions to display */
|
|
sessions: SessionInfo[];
|
|
/** Whether sessions are being fetched */
|
|
loading: boolean;
|
|
/** Error message if fetch fails */
|
|
error: string | null;
|
|
/** Whether there are more sessions to load */
|
|
hasMore: boolean;
|
|
/** Current working directory for filtering */
|
|
currentCwd: string;
|
|
|
|
// Capability flags (from useSessionHistory)
|
|
/** Whether session/list is supported (unstable) */
|
|
canList: boolean;
|
|
/** Whether session can be restored (load or resume supported) */
|
|
canRestore: boolean;
|
|
/** Whether session/fork is supported (unstable) */
|
|
canFork: boolean;
|
|
|
|
/** Whether using locally saved sessions (instead of agent session/list) */
|
|
isUsingLocalSessions: boolean;
|
|
|
|
/** Set of session IDs that have local data (for filtering) */
|
|
localSessionIds: Set<string>;
|
|
|
|
/** Whether the agent is ready (initialized) */
|
|
isAgentReady: boolean;
|
|
|
|
/** Whether debug mode is enabled (shows manual input form) */
|
|
debugMode: boolean;
|
|
|
|
/** Callback when a session is restored */
|
|
onRestoreSession: (sessionId: string, cwd: string) => Promise<void>;
|
|
/** Callback when a session is forked (create new branch) */
|
|
onForkSession: (sessionId: string, cwd: string) => Promise<void>;
|
|
/** Callback when a session is deleted */
|
|
onDeleteSession: (sessionId: string) => void | Promise<void>;
|
|
/** Callback when a session title is edited */
|
|
onEditTitle: (
|
|
sessionId: string,
|
|
newTitle: string,
|
|
sessionCwd: string,
|
|
) => void | Promise<void>;
|
|
/** Callback to load more sessions (pagination) */
|
|
onLoadMore: () => void;
|
|
/** Callback to fetch sessions with filter */
|
|
onFetchSessions: (cwd?: string) => void;
|
|
/** Callback to close the modal */
|
|
onClose: () => void;
|
|
}
|
|
|
|
/**
|
|
* Icon button component using Obsidian's setIcon.
|
|
*/
|
|
function IconButton({
|
|
iconName,
|
|
label,
|
|
className,
|
|
onClick,
|
|
}: {
|
|
iconName: string;
|
|
label: string;
|
|
className: string;
|
|
onClick: () => void;
|
|
}) {
|
|
const iconRef = React.useRef<HTMLDivElement>(null);
|
|
|
|
React.useEffect(() => {
|
|
if (iconRef.current) {
|
|
setIcon(iconRef.current, iconName);
|
|
}
|
|
}, [iconName]);
|
|
|
|
return (
|
|
<div
|
|
ref={iconRef}
|
|
className={className}
|
|
aria-label={label}
|
|
onClick={onClick}
|
|
/>
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Format timestamp as relative time.
|
|
* Examples: "2 hours ago", "yesterday", "3 days ago"
|
|
*/
|
|
function formatRelativeTime(date: Date): string {
|
|
const now = Date.now();
|
|
const timestamp = date.getTime();
|
|
const diffMs = now - timestamp;
|
|
const diffSeconds = Math.floor(diffMs / 1000);
|
|
const diffMinutes = Math.floor(diffSeconds / 60);
|
|
const diffHours = Math.floor(diffMinutes / 60);
|
|
const diffDays = Math.floor(diffHours / 24);
|
|
|
|
if (diffMinutes < 1) {
|
|
return "just now";
|
|
} else if (diffMinutes < 60) {
|
|
return `${diffMinutes} minute${diffMinutes === 1 ? "" : "s"} ago`;
|
|
} else if (diffHours < 24) {
|
|
return `${diffHours} hour${diffHours === 1 ? "" : "s"} ago`;
|
|
} else if (diffDays === 1) {
|
|
return "yesterday";
|
|
} else if (diffDays < 7) {
|
|
return `${diffDays} days ago`;
|
|
} else {
|
|
const month = date.toLocaleString("default", { month: "short" });
|
|
const day = date.getDate();
|
|
const year = date.getFullYear();
|
|
return `${month} ${day}, ${year}`;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Debug form for manual session input.
|
|
*/
|
|
function DebugForm({
|
|
currentCwd,
|
|
onRestoreSession,
|
|
onForkSession,
|
|
onClose,
|
|
}: {
|
|
currentCwd: string;
|
|
onRestoreSession: (sessionId: string, cwd: string) => Promise<void>;
|
|
onForkSession: (sessionId: string, cwd: string) => Promise<void>;
|
|
onClose: () => void;
|
|
}) {
|
|
const [sessionId, setSessionId] = useState("");
|
|
const [cwd, setCwd] = useState(currentCwd);
|
|
|
|
const handleRestore = useCallback(() => {
|
|
if (sessionId.trim()) {
|
|
onClose();
|
|
void onRestoreSession(sessionId.trim(), cwd.trim() || currentCwd);
|
|
}
|
|
}, [sessionId, cwd, currentCwd, onRestoreSession, onClose]);
|
|
|
|
const handleFork = useCallback(() => {
|
|
if (sessionId.trim()) {
|
|
onClose();
|
|
void onForkSession(sessionId.trim(), cwd.trim() || currentCwd);
|
|
}
|
|
}, [sessionId, cwd, currentCwd, onForkSession, onClose]);
|
|
|
|
return (
|
|
<div className="agent-client-session-history-debug">
|
|
<h3>Debug: Manual Session Input</h3>
|
|
|
|
<div className="agent-client-session-history-debug-group">
|
|
<label htmlFor="debug-session-id">Session ID:</label>
|
|
<input
|
|
id="debug-session-id"
|
|
type="text"
|
|
placeholder="Enter session ID..."
|
|
className="agent-client-session-history-debug-input"
|
|
value={sessionId}
|
|
onChange={(e) => setSessionId(e.target.value)}
|
|
/>
|
|
</div>
|
|
|
|
<div className="agent-client-session-history-debug-group">
|
|
<label htmlFor="debug-cwd">Working Directory (cwd):</label>
|
|
<input
|
|
id="debug-cwd"
|
|
type="text"
|
|
placeholder="Enter working directory..."
|
|
className="agent-client-session-history-debug-input"
|
|
value={cwd}
|
|
onChange={(e) => setCwd(e.target.value)}
|
|
/>
|
|
</div>
|
|
|
|
<div className="agent-client-session-history-debug-actions">
|
|
<button
|
|
className="agent-client-session-history-debug-button"
|
|
onClick={handleRestore}
|
|
>
|
|
Restore
|
|
</button>
|
|
<button
|
|
className="agent-client-session-history-debug-button"
|
|
onClick={handleFork}
|
|
>
|
|
Fork
|
|
</button>
|
|
</div>
|
|
|
|
<hr className="agent-client-session-history-debug-separator" />
|
|
</div>
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Session list item component.
|
|
*/
|
|
function SessionItem({
|
|
session,
|
|
canRestore,
|
|
canFork,
|
|
currentCwd,
|
|
onRestoreSession,
|
|
onForkSession,
|
|
onDeleteSession,
|
|
onEditTitle,
|
|
onClose,
|
|
}: {
|
|
session: SessionInfo;
|
|
canRestore: boolean;
|
|
canFork: boolean;
|
|
currentCwd: string;
|
|
onRestoreSession: (sessionId: string, cwd: string) => Promise<void>;
|
|
onForkSession: (sessionId: string, cwd: string) => Promise<void>;
|
|
onDeleteSession: (sessionId: string) => void | Promise<void>;
|
|
onEditTitle: (sessionId: string) => void;
|
|
onClose: () => void;
|
|
}) {
|
|
const handleRestore = useCallback(() => {
|
|
onClose();
|
|
void onRestoreSession(session.sessionId, session.cwd);
|
|
}, [session, onRestoreSession, onClose]);
|
|
|
|
const handleFork = useCallback(() => {
|
|
onClose();
|
|
void onForkSession(session.sessionId, session.cwd);
|
|
}, [session, onForkSession, onClose]);
|
|
|
|
const handleDelete = useCallback(() => {
|
|
void onDeleteSession(session.sessionId);
|
|
}, [session.sessionId, onDeleteSession]);
|
|
|
|
const handleEditTitle = useCallback(() => {
|
|
onEditTitle(session.sessionId);
|
|
}, [session.sessionId, onEditTitle]);
|
|
|
|
return (
|
|
<div className="agent-client-session-history-item">
|
|
<div className="agent-client-session-history-item-content">
|
|
<div className="agent-client-session-history-item-title">
|
|
<span>
|
|
{truncateTitle(session.title ?? "Untitled Session")}
|
|
</span>
|
|
</div>
|
|
<div className="agent-client-session-history-item-metadata">
|
|
{session.updatedAt && (
|
|
<span className="agent-client-session-history-item-timestamp">
|
|
{formatRelativeTime(new Date(session.updatedAt))}
|
|
</span>
|
|
)}
|
|
{session.cwd !== currentCwd && (
|
|
<span
|
|
className="agent-client-session-history-item-cwd"
|
|
title={session.cwd}
|
|
>
|
|
{session.cwd}
|
|
</span>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="agent-client-session-history-item-actions">
|
|
<IconButton
|
|
iconName="pencil"
|
|
label="Edit session title"
|
|
className="agent-client-session-history-action-icon agent-client-session-history-edit-icon"
|
|
onClick={handleEditTitle}
|
|
/>
|
|
{canRestore && (
|
|
<IconButton
|
|
iconName="play"
|
|
label="Restore session"
|
|
className="agent-client-session-history-action-icon agent-client-session-history-restore-icon"
|
|
onClick={handleRestore}
|
|
/>
|
|
)}
|
|
{canFork && (
|
|
<IconButton
|
|
iconName="git-branch"
|
|
label="Fork session (create new branch)"
|
|
className="agent-client-session-history-action-icon agent-client-session-history-fork-icon"
|
|
onClick={handleFork}
|
|
/>
|
|
)}
|
|
<IconButton
|
|
iconName="trash-2"
|
|
label="Delete session"
|
|
className="agent-client-session-history-action-icon agent-client-session-history-delete-icon"
|
|
onClick={handleDelete}
|
|
/>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Session history content component.
|
|
*
|
|
* Renders the content of the session history modal including:
|
|
* - Debug form (when debug mode enabled)
|
|
* - Local sessions banner
|
|
* - Filter toggle (for agent session/list)
|
|
* - Session list with load/resume/fork actions
|
|
* - Pagination
|
|
*/
|
|
function SessionHistoryContent({
|
|
app,
|
|
sessions,
|
|
loading,
|
|
error,
|
|
hasMore,
|
|
currentCwd,
|
|
canList,
|
|
canRestore,
|
|
canFork,
|
|
isUsingLocalSessions,
|
|
localSessionIds,
|
|
isAgentReady,
|
|
debugMode,
|
|
onRestoreSession,
|
|
onForkSession,
|
|
onDeleteSession,
|
|
onEditTitle,
|
|
onLoadMore,
|
|
onFetchSessions,
|
|
onClose,
|
|
}: SessionHistoryContentProps) {
|
|
const [filterByCurrentVault, setFilterByCurrentVault] = useState(true);
|
|
const [hideNonLocalSessions, setHideNonLocalSessions] = useState(false);
|
|
|
|
const handleFilterChange = useCallback(
|
|
(e: React.ChangeEvent<HTMLInputElement>) => {
|
|
const checked = e.target.checked;
|
|
setFilterByCurrentVault(checked);
|
|
const cwd = checked ? currentCwd : undefined;
|
|
onFetchSessions(cwd);
|
|
},
|
|
[currentCwd, onFetchSessions],
|
|
);
|
|
|
|
const handleRetry = useCallback(() => {
|
|
const cwd = filterByCurrentVault ? currentCwd : undefined;
|
|
onFetchSessions(cwd);
|
|
}, [filterByCurrentVault, currentCwd, onFetchSessions]);
|
|
|
|
// Wrap onDeleteSession to show confirmation modal
|
|
const handleDeleteWithConfirmation = useCallback(
|
|
(sessionId: string) => {
|
|
const targetSession = sessions.find(
|
|
(s) => s.sessionId === sessionId,
|
|
);
|
|
const sessionTitle = targetSession?.title ?? "Untitled Session";
|
|
|
|
const confirmModal = new ConfirmDeleteModal(
|
|
app,
|
|
sessionTitle,
|
|
() => {
|
|
void onDeleteSession(sessionId);
|
|
},
|
|
);
|
|
confirmModal.open();
|
|
},
|
|
[app, sessions, onDeleteSession],
|
|
);
|
|
|
|
// Open edit title modal for a session
|
|
const handleEditWithModal = useCallback(
|
|
(sessionId: string) => {
|
|
const targetSession = sessions.find(
|
|
(s) => s.sessionId === sessionId,
|
|
);
|
|
const currentTitle = targetSession?.title ?? "Untitled Session";
|
|
const sessionCwd = targetSession?.cwd ?? currentCwd;
|
|
|
|
const modal = new EditTitleModal(app, currentTitle, (newTitle) => {
|
|
void onEditTitle(sessionId, newTitle, sessionCwd);
|
|
});
|
|
modal.open();
|
|
},
|
|
[app, sessions, currentCwd, onEditTitle],
|
|
);
|
|
|
|
// Filter sessions based on hideNonLocalSessions setting
|
|
// Only applies to agent session/list (not local sessions which are already filtered)
|
|
const filteredSessions = React.useMemo(() => {
|
|
if (isUsingLocalSessions || !hideNonLocalSessions) {
|
|
return sessions;
|
|
}
|
|
return sessions.filter((s) => localSessionIds.has(s.sessionId));
|
|
}, [sessions, isUsingLocalSessions, hideNonLocalSessions, localSessionIds]);
|
|
|
|
// Show preparing message if agent is not ready
|
|
if (!isAgentReady) {
|
|
return (
|
|
<div className="agent-client-session-history-loading">
|
|
<p>Preparing agent...</p>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// Check if any session operation is available
|
|
const canPerformAnyOperation = canRestore || canFork;
|
|
|
|
// Show local sessions list (always show for delete functionality)
|
|
// - If agent supports list: use agent's session/list
|
|
// - If agent doesn't support list OR doesn't support restoration: use locally saved sessions
|
|
const canShowList =
|
|
canList || isUsingLocalSessions || !canPerformAnyOperation;
|
|
|
|
return (
|
|
<>
|
|
{/* Debug form */}
|
|
{debugMode && (
|
|
<DebugForm
|
|
currentCwd={currentCwd}
|
|
onRestoreSession={onRestoreSession}
|
|
onForkSession={onForkSession}
|
|
onClose={onClose}
|
|
/>
|
|
)}
|
|
|
|
{/* Warning banner for agents that don't support restoration */}
|
|
{!canPerformAnyOperation && (
|
|
<div className="agent-client-session-history-warning-banner">
|
|
<p>This agent does not support session restoration.</p>
|
|
</div>
|
|
)}
|
|
|
|
{/* Local sessions banner */}
|
|
{(isUsingLocalSessions || !canPerformAnyOperation) && (
|
|
<div className="agent-client-session-history-local-banner">
|
|
<span>These sessions are saved in the plugin.</span>
|
|
</div>
|
|
)}
|
|
|
|
{/* No list capability message */}
|
|
{!canShowList && !debugMode && (
|
|
<div className="agent-client-session-history-empty">
|
|
<p className="agent-client-session-history-empty-text">
|
|
Session list is not available for this agent.
|
|
</p>
|
|
<p className="agent-client-session-history-empty-text">
|
|
Enable Debug Mode in settings to manually enter session
|
|
IDs.
|
|
</p>
|
|
</div>
|
|
)}
|
|
|
|
{canShowList && (
|
|
<>
|
|
{/* Filter toggles - only for agent session/list */}
|
|
{canList && !isUsingLocalSessions && (
|
|
<div className="agent-client-session-history-filter">
|
|
<label className="agent-client-session-history-filter-label">
|
|
<input
|
|
type="checkbox"
|
|
checked={filterByCurrentVault}
|
|
onChange={handleFilterChange}
|
|
/>
|
|
<span>Show current vault only</span>
|
|
</label>
|
|
<label className="agent-client-session-history-filter-label">
|
|
<input
|
|
type="checkbox"
|
|
checked={hideNonLocalSessions}
|
|
onChange={(e) =>
|
|
setHideNonLocalSessions(
|
|
e.target.checked,
|
|
)
|
|
}
|
|
/>
|
|
<span>Hide sessions without local data</span>
|
|
</label>
|
|
</div>
|
|
)}
|
|
|
|
{/* Error state */}
|
|
{error && (
|
|
<div className="agent-client-session-history-error">
|
|
<p className="agent-client-session-history-error-text">
|
|
{error}
|
|
</p>
|
|
<button
|
|
className="agent-client-session-history-retry-button"
|
|
onClick={handleRetry}
|
|
>
|
|
Retry
|
|
</button>
|
|
</div>
|
|
)}
|
|
|
|
{/* Loading state */}
|
|
{!error && loading && filteredSessions.length === 0 && (
|
|
<div className="agent-client-session-history-loading">
|
|
<p>Loading sessions...</p>
|
|
</div>
|
|
)}
|
|
|
|
{/* Empty state */}
|
|
{!error && !loading && filteredSessions.length === 0 && (
|
|
<div className="agent-client-session-history-empty">
|
|
<p className="agent-client-session-history-empty-text">
|
|
No previous sessions
|
|
</p>
|
|
</div>
|
|
)}
|
|
|
|
{/* Session list */}
|
|
{!error && filteredSessions.length > 0 && (
|
|
<div className="agent-client-session-history-list">
|
|
{filteredSessions.map((session) => (
|
|
<SessionItem
|
|
key={session.sessionId}
|
|
session={session}
|
|
canRestore={canRestore}
|
|
canFork={canFork}
|
|
currentCwd={currentCwd}
|
|
onRestoreSession={onRestoreSession}
|
|
onForkSession={onForkSession}
|
|
onDeleteSession={
|
|
handleDeleteWithConfirmation
|
|
}
|
|
onEditTitle={handleEditWithModal}
|
|
onClose={onClose}
|
|
/>
|
|
))}
|
|
</div>
|
|
)}
|
|
|
|
{/* Load more button */}
|
|
{!error && hasMore && (
|
|
<div className="agent-client-session-history-load-more">
|
|
<button
|
|
className="agent-client-session-history-load-more-button"
|
|
disabled={loading}
|
|
onClick={onLoadMore}
|
|
>
|
|
{loading ? "Loading..." : "Load more"}
|
|
</button>
|
|
</div>
|
|
)}
|
|
</>
|
|
)}
|
|
</>
|
|
);
|
|
}
|
|
|
|
// ============================================================
|
|
// SessionHistoryModal (exported)
|
|
// ============================================================
|
|
|
|
/**
|
|
* Props for SessionHistoryModal (same as SessionHistoryContentProps minus onClose and app).
|
|
*/
|
|
export type SessionHistoryModalProps = Omit<
|
|
SessionHistoryContentProps,
|
|
"onClose" | "app"
|
|
>;
|
|
|
|
/**
|
|
* Modal for displaying and selecting from session history.
|
|
*
|
|
* This is a thin wrapper around the SessionHistoryContent React component.
|
|
* It extends Obsidian's Modal class for proper modal behavior (backdrop,
|
|
* escape key handling, etc.) while delegating all UI rendering to React.
|
|
*/
|
|
export class SessionHistoryModal extends Modal {
|
|
private root: Root | null = null;
|
|
private props: SessionHistoryModalProps;
|
|
|
|
constructor(app: App, props: SessionHistoryModalProps) {
|
|
super(app);
|
|
this.props = props;
|
|
}
|
|
|
|
/**
|
|
* Update modal props and re-render the React component.
|
|
* Call this when session data changes.
|
|
*/
|
|
updateProps(props: SessionHistoryModalProps) {
|
|
this.props = props;
|
|
this.renderContent();
|
|
}
|
|
|
|
/**
|
|
* Called when modal is opened.
|
|
* Creates React root and renders the content.
|
|
*/
|
|
onOpen() {
|
|
const { contentEl } = this;
|
|
contentEl.empty();
|
|
|
|
// Add modal title
|
|
contentEl.createEl("h2", { text: "Session history" });
|
|
|
|
// Create container for React content
|
|
const reactContainer = contentEl.createDiv();
|
|
|
|
// Create React root and render
|
|
this.root = createRoot(reactContainer);
|
|
this.renderContent();
|
|
}
|
|
|
|
/**
|
|
* Render or re-render the React content.
|
|
*/
|
|
private renderContent() {
|
|
if (this.root) {
|
|
this.root.render(
|
|
React.createElement(SessionHistoryContent, {
|
|
...this.props,
|
|
app: this.app,
|
|
onClose: () => this.close(),
|
|
}),
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Called when modal is closed.
|
|
* Unmounts React component and cleans up.
|
|
*/
|
|
onClose() {
|
|
if (this.root) {
|
|
this.root.unmount();
|
|
this.root = null;
|
|
}
|
|
const { contentEl } = this;
|
|
contentEl.empty();
|
|
}
|
|
}
|