packages/desktop/src/renderer/pages/conversation/Preview/README.en.md
The Preview module is the file preview and editing system in AionUi, supporting viewing and editing of multiple file formats. The module adopts a multi-tab architecture, allowing multiple files to be opened simultaneously, with each file displayed in its own tab. The Preview module integrates advanced features such as real-time streaming updates, version history, split-screen preview, and keyboard shortcuts, providing users with powerful file handling capabilities.
Supported Viewers:
.md, .markdown) - Complete Markdown rendering.js, .ts, .tsx, .py, .java, etc.) - Syntax highlighting.png, .jpg, .jpeg, .gif, .svg, etc.) - Image viewer.diff, .patch) - Diff comparison.pdf) - PDF document viewer.doc, .docx, .odt) - Word document viewer.xls, .xlsx, .ods, .csv) - Spreadsheet viewer.ppt, .pptx, .odp) - Presentation viewer.html, .htm) - HTML renderingSupported Editors:
Cmd/Ctrl + S to save, Cmd/Ctrl + W to close tabpreview/
├── context/ # React Context
│ ├── PreviewContext.tsx # Core context: Tab management, content updates, saving
│ └── PreviewToolbarExtrasContext.tsx # Toolbar extension context
├── components/
│ ├── PreviewPanel/ # Main panel component
│ │ ├── PreviewPanel.tsx # Main component (manages view states, split-screen, edit mode)
│ │ ├── PreviewTabs.tsx # Tab bar (tab switching, context menu)
│ │ ├── PreviewToolbar.tsx # Toolbar (view switching, edit, save, history)
│ │ ├── PreviewContextMenu.tsx # Context menu
│ │ ├── PreviewConfirmModals.tsx # Confirmation dialogs
│ │ └── PreviewHistoryDropdown.tsx # History version dropdown
│ ├── viewers/ # Viewer components
│ │ ├── MarkdownViewer.tsx # Markdown rendering
│ │ ├── CodeViewer.tsx # Code highlighting
│ │ ├── ImageViewer.tsx # Image viewer
│ │ ├── DiffViewer.tsx # Diff comparison
│ │ ├── PDFViewer.tsx # PDF viewer
│ │ ├── OfficeDocViewer.tsx # Office document viewer (Word, PPT)
│ │ ├── ExcelViewer.tsx # Excel viewer
│ │ ├── HTMLViewer.tsx # HTML rendering
│ │ └── URLViewer.tsx # URL web page viewer
│ ├── editors/ # Editor components
│ │ ├── MarkdownEditor.tsx # Markdown editor
│ │ ├── TextEditor.tsx # Code editor (Monaco)
│ │ └── HTMLEditor.tsx # HTML editor
│ └── renderers/ # Special renderers
│ ├── HTMLRenderer.tsx # HTML iframe renderer
│ └── SelectionToolbar.tsx # HTML selection toolbar
├── hooks/ # Custom hooks
│ ├── usePreviewHistory.ts # Version history management
│ ├── usePreviewKeyboardShortcuts.ts # Keyboard shortcut handling
│ ├── useScrollSync.ts # Scroll synchronization
│ ├── useTabOverflow.ts # Tab overflow handling
│ └── useThemeDetection.ts # Theme detection
├── utils/ # Utility functions
│ └── fileUtils.ts # File operation utilities
├── types/ # TypeScript types
│ └── index.ts # Type definitions
└── constants.ts # Configuration constants
The core state management of the Preview module, responsible for tab management, content updates, and saving.
State:
interface PreviewContextValue {
// Panel state
isOpen: boolean; // Whether preview panel is open
tabs: PreviewTab[]; // All open tabs
activeTabId: string | null; // Currently active tab ID
activeTab: PreviewTab | null; // Currently active tab
// Operations
openPreview: (content: string, type: PreviewContentType, metadata?: PreviewMetadata) => void;
closePreview: () => void;
closeTab: (tabId: string) => void;
switchTab: (tabId: string) => void;
updateContent: (content: string) => void;
saveContent: (tabId?: string) => Promise<boolean>;
// Tab finding and management
findPreviewTab: (type: PreviewContentType, content?: string, metadata?: PreviewMetadata) => PreviewTab | null;
closePreviewByIdentity: (type: PreviewContentType, content?: string, metadata?: PreviewMetadata) => void;
// Send box integration
addToSendBox: (text: string) => void;
setSendBoxHandler: (handler: ((text: string) => void) | null) => void;
}
Tab Data Structure:
interface PreviewTab {
id: string; // Unique identifier
content: string; // File content
contentType: PreviewContentType; // Content type
metadata?: PreviewMetadata; // Metadata (file path, title, etc.)
title: string; // Tab title
isDirty?: boolean; // Whether there are unsaved changes
originalContent?: string; // Original content (for comparison)
}
Smart Tab Reuse Mechanism:
When opening a file, the system searches for existing tabs with the same identity in the following priority order:
metadata.filePathmetadata.fileNamemetadata.titlecontentIf a matching tab is found:
If no matching tab is found:
Used by viewer components to inject custom buttons into the toolbar.
interface PreviewToolbarExtras {
leftButtons?: React.ReactNode; // Extra buttons on left side of toolbar
rightButtons?: React.ReactNode; // Extra buttons on right side of toolbar
}
When an agent writes to workspace files, the Preview module automatically receives streaming updates without manual refresh.
// Subscribe to file content updates
ipcBridge.fileStream.contentUpdate.on(({ filePath, content, operation }) => {
if (operation === 'delete') {
// File deleted, close corresponding tab
closeTabByFilePath(filePath);
} else {
// File written, update content (with debounce)
updateTabContent(filePath, content);
}
});
To avoid frequent agent writes interrupting the preview, streaming updates use 500ms debounce:
This avoids frequent interruptions to typing animations, providing a smoother experience.
To avoid conflicts between user saves and streaming updates:
// Mark when saving file
savingFilesRef.current.add(filePath);
// Check during streaming update
if (savingFilesRef.current.has(filePath) || tab.isDirty) {
return; // Skip update
}
import { PreviewProvider, usePreviewContext } from './preview';
function App() {
return (
<PreviewProvider>
<YourComponent />
</PreviewProvider>
);
}
function YourComponent() {
const { openPreview } = usePreviewContext();
const handleOpenFile = async (filePath: string) => {
const content = await readFile(filePath);
openPreview(content, 'markdown', {
fileName: 'example.md',
filePath: '/path/to/example.md',
workspace: '/workspace/root',
});
};
return <button onClick={handleOpenFile}>Open File</button>;
}
// Markdown file
openPreview(markdownContent, 'markdown', {
fileName: 'README.md',
filePath: '/workspace/README.md',
workspace: '/workspace',
});
// Code file
openPreview(codeContent, 'code', {
fileName: 'app.tsx',
filePath: '/workspace/src/app.tsx',
workspace: '/workspace',
language: 'typescript',
});
// Image file
openPreview(base64Content, 'image', {
fileName: 'screenshot.png',
filePath: '/workspace/screenshot.png',
workspace: '/workspace',
});
// Diff file
openPreview(diffContent, 'diff', {
fileName: 'changes.diff',
});
// Find tab
const tab = findPreviewTab('markdown', undefined, {
filePath: '/workspace/README.md',
});
// Close specific tab
if (tab) {
closeTab(tab.id);
}
// Close tab by identity
closePreviewByIdentity('markdown', undefined, {
filePath: '/workspace/README.md',
});
function SendBox() {
const { setSendBoxHandler } = usePreviewContext();
const [text, setText] = useState('');
useEffect(() => {
// Register handler
setSendBoxHandler((content) => {
setText((prev) => prev + content);
});
return () => {
setSendBoxHandler(null);
};
}, [setSendBoxHandler]);
return <textarea value={text} onChange={(e) => setText(e.target.value)} />;
}
Manage file version history (Git-based).
const {
historyVersions, // List of history versions
historyLoading, // Loading state
snapshotSaving, // Snapshot saving state
historyError, // Error message
historyTarget, // Currently viewing history version
refreshHistory, // Refresh history
handleSaveSnapshot, // Save snapshot
handleSnapshotSelect, // Select history version
} = usePreviewHistory({ activeTab, updateContent });
Register global keyboard shortcuts.
Supported shortcuts:
Cmd/Ctrl + S - Save current tabCmd/Ctrl + W - Close current tab (not implemented, reserved)usePreviewKeyboardShortcuts({
isDirty: activeTab?.isDirty,
onSave: () => saveContent(),
});
Synchronize scroll position between editor and preview.
const { handleEditorScroll, handlePreviewScroll } = useScrollSync({
enabled: isSplitScreenEnabled,
editorContainerRef,
previewContainerRef,
});
Handle tab bar overflow, automatically show fade effects.
const { tabsContainerRef, tabFadeState } = useTabOverflow([tabs, activeTabId]);
Detect current theme (light/dark).
const currentTheme = useThemeDetection(); // 'light' | 'dark'
Click the "Edit" button in the toolbar or double-click the content area to enter edit mode.
Editable types:
.md, .markdown).html, .htm)Markdown Editor:
Code Editor (Monaco):
HTML Editor:
Cmd/Ctrl + SClick the split-screen button in the toolbar to enable split-screen mode.
In split-screen mode:
Drag the divider in the middle to adjust the left-right ratio:
Version history is Git-based and allows viewing all historical versions of a file.
Prerequisites:
workspace and filePath metadataClick the "Save snapshot" button to create a new Git commit, saving the current state.
Avoid opening the same file multiple times, reducing memory usage.
500ms debounce avoids frequent updates, improving performance and user experience.
Use IntersectionObserver to monitor tab visibility, automatically show fade effects.
Scroll synchronization uses requestAnimationFrame to optimize performance.
PreviewPanel.tsxrenderContent() functionPreviewContentType type definitionUse PreviewToolbarExtrasContext in viewer components:
const { setExtras } = usePreviewToolbarExtrasContext();
useEffect(() => {
setExtras({
rightButtons: <CustomButton />,
});
return () => setExtras(null);
}, []);
Streaming updates use 500ms debounce to avoid frequent agent writes interrupting the preview. This is a tradeoff between performance and user experience.
Streaming updates are automatic and cannot be disabled. If you don't want to receive updates, enter edit mode (streaming updates are ignored during editing).
The following file types do not support editing:
These file types only provide viewing functionality.
Defined in constants.ts:
// Default split ratio
export const DEFAULT_SPLIT_RATIO = 50;
// Minimum split width
export const MIN_SPLIT_WIDTH = 30;
// Maximum split width
export const MAX_SPLIT_WIDTH = 70;
// File types with built-in open button
export const FILE_TYPES_WITH_BUILTIN_OPEN = ['pdf', 'word', 'excel', 'ppt'];