packages/desktop/src/renderer/pages/conversation/Workspace/README.en.md
The Workspace module is a core component in AionUi for managing conversation workspace files and folders. It provides complete file tree display, file operations (open, delete, rename, preview), file addition, and paste functionality. The module follows a React Hooks architecture, splitting business logic into multiple independent hooks, achieving high modularity and maintainability.
The Workspace module follows the Container Component Pattern:
Advantages of this architecture:
workspace/
├── index.tsx # Container component (550 lines) - Composes all hooks
├── hooks/ # Business logic hooks
│ ├── useWorkspaceTree.ts # Tree state management and selection logic
│ ├── useWorkspaceEvents.ts # Event listener management
│ ├── useWorkspaceFileOps.ts # File operations (open, delete, rename, preview)
│ ├── useWorkspaceModals.ts # Modal and menu state management
│ └── useWorkspacePaste.ts # File paste and add logic
├── utils/
│ └── treeHelpers.ts # Tree structure utility functions
└── types.ts # TypeScript type definitions
Responsibility: Manage workspace file tree state and selection logic
Main Features:
Core API:
const {
// State
files, // File tree data
loading, // Loading state (with debounce)
selected, // Selected node keys
expandedKeys, // Expanded node keys
selectedNodeRef, // Last selected folder node reference
// Actions
loadWorkspace, // Load workspace
refreshWorkspace, // Refresh workspace
ensureNodeSelected, // Ensure node is selected
clearSelection, // Clear selection
} = useWorkspaceTree({ workspace, conversation_id, eventPrefix });
Features:
Responsibility: Manage all event listeners
Events Listened:
tool_group, tool_callacp_tool_call${eventPrefix}.workspace.refresh${eventPrefix}.selected.file.clear (after sending message)Features:
Responsibility: Handle all file operation logic
Main Features:
handleOpenNode) - Open file/folder with system default handlerhandleRevealNode) - Show in system file explorerhandleDeleteNode, handleDeleteConfirm) - Delete with confirmationopenRenameModal, handleRenameConfirm) - Rename with timeout protectionhandlePreviewFile) - Support multiple format previewshandleAddToChat) - Add file/folder to conversationSupported Preview Formats:
.md, .markdown.diff, .patch.pdf, .ppt, .pptx, .doc, .docx, .xls, .xlsx, .csv.js, .ts, .tsx, .jsx, .py, .java, .go, .rs, .c, .cpp, .json, .xml, .yaml, etc..png, .jpg, .jpeg, .gif, .bmp, .webp, .svg, .ico, etc..html, .htmFeatures:
Responsibility: Manage all modal and menu states
Managed States:
contextMenu) - Position, visibility, target noderenameModal) - Visibility, input value, target node, loading statedeleteModal) - Visibility, target node, loading statepasteConfirm) - Visibility, file list, "do not ask again" optionCore API:
const {
// Context menu
contextMenu,
openContextMenu,
closeContextMenu,
// Rename modal
renameModal,
setRenameModal,
renameLoading,
closeRenameModal,
// Delete modal
deleteModal,
setDeleteModal,
closeDeleteModal,
// Paste confirm
pasteConfirm,
setPasteConfirm,
closePasteConfirm,
} = useWorkspaceModals();
Features:
Responsibility: Handle file paste and add logic
Main Features:
handleAddFiles) - Add from file pickerhandleFilesToAdd) - Add from system clipboardhandlePasteConfirm) - Handle paste confirmation dialogWorkflow:
User pastes files
↓
Check workspace.pasteConfirm config
↓
├─ Confirmation disabled → Copy directly to target folder
└─ Confirmation required → Show confirmation dialog
↓
User confirms → Copy files
↓
If "do not ask again" checked → Save config
Features:
usePasteService to capture global paste eventsworkspace.pasteConfirm)import ChatWorkspace from './workspace';
function ConversationPage() {
const [messageApi, messageContext] = Message.useMessage();
return (
<>
{messageContext}
<ChatWorkspace
conversation_id={conversationId}
workspace={workspacePath}
eventPrefix='gemini'
messageApi={messageApi}
/>
</>
);
}
import { emitter } from '@/renderer/utils/emitter';
import { useEffect } from 'react';
function MyComponent() {
useEffect(() => {
const handleFileSelected = (items: Array<{ path: string; name: string; isFile: boolean }>) => {
console.log('Selected files:', items);
};
emitter.on('gemini.selected.file', handleFileSelected);
return () => {
emitter.off('gemini.selected.file', handleFileSelected);
};
}, []);
}
import { emitter } from '@/renderer/utils/emitter';
function RefreshButton() {
const handleRefresh = () => {
emitter.emit('gemini.workspace.refresh');
};
return <button onClick={handleRefresh}>Refresh</button>;
}
import { emitter } from '@/renderer/utils/emitter';
function ClearButton() {
const handleClear = () => {
emitter.emit('gemini.selected.file.clear');
};
return <button onClick={handleClear}>Clear Selection</button>;
}
eventPrefix is used to distinguish different agent types, supports:
gemini - Gemini AI conversationacp - ACP (AI Code Partner) conversationcodex - Codex conversationEvent naming convention: ${eventPrefix}.${eventName}
Workspace depends on PreviewContext for file preview:
import { PreviewProvider } from '../preview';
function Layout() {
return (
<PreviewProvider>
<ChatWorkspace {...props} />
</PreviewProvider>
);
}
Control whether to show paste confirmation dialog via workspace.pasteConfirm config:
// Disable paste confirmation
await ConfigStorage.set('workspace.pasteConfirm', true);
// Enable paste confirmation (default)
await ConfigStorage.set('workspace.pasteConfirm', false);
Loading icon persists for at least 1 second to avoid flickering from rapid toggling:
if (Date.now() - lastLoadingTime.current > 1000) {
setLoading(false);
} else {
setTimeout(() => setLoading(false), 1000);
}
Search input uses useDebounce hook with 200ms delay to reduce unnecessary requests:
const onSearch = useDebounce(
(value: string) => {
void treeHook.loadWorkspace(workspace, value);
},
200,
[workspace, treeHook.loadWorkspace]
);
Uses useRef to store selection state, avoiding unnecessary re-renders:
const selectedKeysRef = useRef<string[]>([]);
const selectedNodeRef = useRef<SelectedNodeRef | null>(null);
All file operations include comprehensive error handling:
try {
const result = await operation();
if (!result.success) {
messageApi.error(result.msg || t('defaultErrorMessage'));
}
} catch (error) {
messageApi.error(t('unknownError'));
}
index.tsx (Container Component)
↓
├── useWorkspaceTree (Independent)
├── useWorkspaceModals (Independent)
├── useWorkspacePaste (Depends on: Tree, Modals)
├── useWorkspaceFileOps (Depends on: Tree, Modals, Preview)
└── useWorkspaceEvents (Depends on: Tree, Modals)
Add new extension detection in the handlePreviewFile function in useWorkspaceFileOps:
if (['new', 'ext'].includes(ext)) {
contentType = 'newType';
}
Modify the context menu rendering logic in index.tsx (lines 363-429).
The 200ms delay ensures the file system operation completes, avoiding reading stale data during refresh.
Users can check "do not ask again" in the paste confirmation dialog, or set it programmatically:
await ConfigStorage.set('workspace.pasteConfirm', true);