diff --git a/CLAUDE.md b/CLAUDE.md
index 989631fd..e1ddb3cb 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -38,7 +38,7 @@ Claudeman is a Claude Code session manager with a web interface and autonomous R
When adding new features, always ask: "Will this maintain responsiveness with 20 sessions and 50 agent windows?"
-**Version**: 0.1392 (must match `package.json`)
+**Version**: 0.1393 (must match `package.json`)
**Tech Stack**: TypeScript (ES2022/NodeNext, strict mode), Node.js, Fastify, Server-Sent Events, node-pty
diff --git a/package.json b/package.json
index bc587ce3..8b1e5c7b 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "claudeman",
- "version": "0.1392",
+ "version": "0.1393",
"description": "The missing control plane for Claude Code - run 20 autonomous agents with real-time monitoring and session persistence",
"type": "module",
"main": "dist/index.js",
diff --git a/src/web/public/app.js b/src/web/public/app.js
index e14e416f..7412ce0f 100644
--- a/src/web/public/app.js
+++ b/src/web/public/app.js
@@ -2332,6 +2332,16 @@ class ClaudemanApp {
// Update subagent window visibility for active session
this.updateSubagentWindowVisibility();
+ // Load file browser if enabled
+ const settings = this.loadAppSettingsFromStorage();
+ if (settings.showFileBrowser) {
+ const fileBrowserPanel = this.$('fileBrowserPanel');
+ if (fileBrowserPanel) {
+ fileBrowserPanel.classList.add('visible');
+ this.loadFileBrowser(sessionId);
+ }
+ }
+
this.terminal.focus();
this.terminal.scrollToBottom();
} catch (err) {
@@ -7074,6 +7084,313 @@ class ClaudemanApp {
}
}
+ // ========== File Browser Panel ==========
+
+ // File tree data and state
+ fileBrowserData = null;
+ fileBrowserExpandedDirs = new Set();
+ fileBrowserFilter = '';
+ fileBrowserAllExpanded = false;
+ filePreviewContent = '';
+
+ async loadFileBrowser(sessionId) {
+ if (!sessionId) return;
+
+ const treeEl = this.$('fileBrowserTree');
+ const statusEl = this.$('fileBrowserStatus');
+ if (!treeEl) return;
+
+ // Show loading state
+ treeEl.innerHTML = '
Loading files...
';
+
+ try {
+ const res = await fetch(`/api/sessions/${sessionId}/files?depth=5&showHidden=false`);
+ if (!res.ok) throw new Error('Failed to load files');
+
+ const result = await res.json();
+ if (!result.success) throw new Error(result.error || 'Failed to load files');
+
+ this.fileBrowserData = result.data;
+ this.renderFileBrowserTree();
+
+ // Update status
+ if (statusEl) {
+ const { totalFiles, totalDirectories, truncated } = result.data;
+ statusEl.textContent = `${totalFiles} files, ${totalDirectories} dirs${truncated ? ' (truncated)' : ''}`;
+ }
+ } catch (err) {
+ console.error('Failed to load file browser:', err);
+ treeEl.innerHTML = `Failed to load files: ${err.message}
`;
+ }
+ }
+
+ renderFileBrowserTree() {
+ const treeEl = this.$('fileBrowserTree');
+ if (!treeEl || !this.fileBrowserData) return;
+
+ const { tree } = this.fileBrowserData;
+ if (!tree || tree.length === 0) {
+ treeEl.innerHTML = 'No files found
';
+ return;
+ }
+
+ const html = [];
+ const filter = this.fileBrowserFilter.toLowerCase();
+
+ const renderNode = (node, depth) => {
+ const isDir = node.type === 'directory';
+ const isExpanded = this.fileBrowserExpandedDirs.has(node.path);
+ const matchesFilter = !filter || node.name.toLowerCase().includes(filter);
+
+ // For directories, check if any children match
+ let hasMatchingChildren = false;
+ if (isDir && filter && node.children) {
+ hasMatchingChildren = this.hasMatchingChild(node, filter);
+ }
+
+ const shouldShow = matchesFilter || hasMatchingChildren;
+ const hiddenClass = !shouldShow && filter ? ' hidden-by-filter' : '';
+
+ const icon = isDir
+ ? (isExpanded ? '\uD83D\uDCC2' : '\uD83D\uDCC1')
+ : this.getFileIcon(node.extension);
+
+ const expandIcon = isDir
+ ? `\u25B6`
+ : '';
+
+ const sizeStr = !isDir && node.size !== undefined
+ ? `${this.formatFileSize(node.size)}`
+ : '';
+
+ const nameClass = isDir ? 'file-tree-name directory' : 'file-tree-name';
+
+ html.push(`
+
+ ${expandIcon}
+ ${icon}
+ ${this.escapeHtml(node.name)}
+ ${sizeStr}
+
+ `);
+
+ // Render children if directory is expanded
+ if (isDir && isExpanded && node.children) {
+ for (const child of node.children) {
+ renderNode(child, depth + 1);
+ }
+ }
+ };
+
+ for (const node of tree) {
+ renderNode(node, 0);
+ }
+
+ treeEl.innerHTML = html.join('');
+
+ // Add click handlers
+ treeEl.querySelectorAll('.file-tree-item').forEach(item => {
+ item.addEventListener('click', () => {
+ const path = item.dataset.path;
+ const type = item.dataset.type;
+
+ if (type === 'directory') {
+ this.toggleFileBrowserFolder(path);
+ } else {
+ this.openFilePreview(path);
+ }
+ });
+ });
+ }
+
+ hasMatchingChild(node, filter) {
+ if (!node.children) return false;
+ for (const child of node.children) {
+ if (child.name.toLowerCase().includes(filter)) return true;
+ if (child.type === 'directory' && this.hasMatchingChild(child, filter)) return true;
+ }
+ return false;
+ }
+
+ toggleFileBrowserFolder(path) {
+ if (this.fileBrowserExpandedDirs.has(path)) {
+ this.fileBrowserExpandedDirs.delete(path);
+ } else {
+ this.fileBrowserExpandedDirs.add(path);
+ }
+ this.renderFileBrowserTree();
+ }
+
+ filterFileBrowser(value) {
+ this.fileBrowserFilter = value;
+ // Auto-expand all if filtering
+ if (value) {
+ this.expandAllDirectories(this.fileBrowserData?.tree || []);
+ }
+ this.renderFileBrowserTree();
+ }
+
+ expandAllDirectories(nodes) {
+ for (const node of nodes) {
+ if (node.type === 'directory') {
+ this.fileBrowserExpandedDirs.add(node.path);
+ if (node.children) {
+ this.expandAllDirectories(node.children);
+ }
+ }
+ }
+ }
+
+ collapseAllDirectories() {
+ this.fileBrowserExpandedDirs.clear();
+ }
+
+ toggleFileBrowserExpand() {
+ this.fileBrowserAllExpanded = !this.fileBrowserAllExpanded;
+ const btn = this.$('fileBrowserExpandBtn');
+
+ if (this.fileBrowserAllExpanded) {
+ this.expandAllDirectories(this.fileBrowserData?.tree || []);
+ if (btn) btn.innerHTML = '\u229F'; // Collapse icon
+ } else {
+ this.collapseAllDirectories();
+ if (btn) btn.innerHTML = '\u229E'; // Expand icon
+ }
+ this.renderFileBrowserTree();
+ }
+
+ refreshFileBrowser() {
+ if (this.activeSessionId) {
+ this.fileBrowserExpandedDirs.clear();
+ this.fileBrowserFilter = '';
+ this.fileBrowserAllExpanded = false;
+ const searchInput = this.$('fileBrowserSearch');
+ if (searchInput) searchInput.value = '';
+ this.loadFileBrowser(this.activeSessionId);
+ }
+ }
+
+ closeFileBrowserPanel() {
+ const panel = this.$('fileBrowserPanel');
+ if (panel) {
+ panel.classList.remove('visible');
+ }
+ // Save setting
+ const settings = this.loadAppSettingsFromStorage();
+ settings.showFileBrowser = false;
+ localStorage.setItem('claudeman-app-settings', JSON.stringify(settings));
+ }
+
+ async openFilePreview(filePath) {
+ if (!this.activeSessionId || !filePath) return;
+
+ const overlay = this.$('filePreviewOverlay');
+ const titleEl = this.$('filePreviewTitle');
+ const bodyEl = this.$('filePreviewBody');
+ const footerEl = this.$('filePreviewFooter');
+
+ if (!overlay || !bodyEl) return;
+
+ // Show overlay with loading state
+ overlay.classList.add('visible');
+ titleEl.textContent = filePath;
+ bodyEl.innerHTML = 'Loading...
';
+ footerEl.textContent = '';
+
+ try {
+ const res = await fetch(`/api/sessions/${this.activeSessionId}/file-content?path=${encodeURIComponent(filePath)}&lines=500`);
+ if (!res.ok) throw new Error('Failed to load file');
+
+ const result = await res.json();
+ if (!result.success) throw new Error(result.error || 'Failed to load file');
+
+ const data = result.data;
+
+ if (data.type === 'image') {
+ bodyEl.innerHTML = `
`;
+ footerEl.textContent = `${this.formatFileSize(data.size)} \u2022 ${data.extension}`;
+ } else if (data.type === 'video') {
+ bodyEl.innerHTML = ``;
+ footerEl.textContent = `${this.formatFileSize(data.size)} \u2022 ${data.extension}`;
+ } else if (data.type === 'binary') {
+ bodyEl.innerHTML = `Binary file (${this.formatFileSize(data.size)})
Cannot preview
`;
+ footerEl.textContent = data.extension || 'binary';
+ } else {
+ // Text content
+ this.filePreviewContent = data.content;
+ bodyEl.innerHTML = `${this.escapeHtml(data.content)}
`;
+ const truncNote = data.truncated ? ` (showing 500/${data.totalLines} lines)` : '';
+ footerEl.textContent = `${data.totalLines} lines \u2022 ${this.formatFileSize(data.size)}${truncNote}`;
+ }
+ } catch (err) {
+ console.error('Failed to preview file:', err);
+ bodyEl.innerHTML = `Error: ${err.message}
`;
+ }
+ }
+
+ closeFilePreview() {
+ const overlay = this.$('filePreviewOverlay');
+ if (overlay) {
+ overlay.classList.remove('visible');
+ }
+ this.filePreviewContent = '';
+ }
+
+ copyFilePreviewContent() {
+ if (this.filePreviewContent) {
+ navigator.clipboard.writeText(this.filePreviewContent).then(() => {
+ this.showToast('Copied to clipboard', 'success');
+ }).catch(() => {
+ this.showToast('Failed to copy', 'error');
+ });
+ }
+ }
+
+ getFileIcon(ext) {
+ if (!ext) return '\uD83D\uDCC4'; // Default file
+
+ const icons = {
+ // TypeScript/JavaScript
+ 'ts': '\uD83D\uDCD8', 'tsx': '\uD83D\uDCD8', 'js': '\uD83D\uDCD2', 'jsx': '\uD83D\uDCD2',
+ 'mjs': '\uD83D\uDCD2', 'cjs': '\uD83D\uDCD2',
+ // Python
+ 'py': '\uD83D\uDC0D', 'pyx': '\uD83D\uDC0D', 'pyw': '\uD83D\uDC0D',
+ // Rust/Go/C
+ 'rs': '\uD83E\uDD80', 'go': '\uD83D\uDC39', 'c': '\u2699\uFE0F', 'cpp': '\u2699\uFE0F',
+ 'h': '\u2699\uFE0F', 'hpp': '\u2699\uFE0F',
+ // Web
+ 'html': '\uD83C\uDF10', 'htm': '\uD83C\uDF10', 'css': '\uD83C\uDFA8', 'scss': '\uD83C\uDFA8',
+ 'sass': '\uD83C\uDFA8', 'less': '\uD83C\uDFA8',
+ // Data
+ 'json': '\uD83D\uDCCB', 'yaml': '\uD83D\uDCCB', 'yml': '\uD83D\uDCCB', 'xml': '\uD83D\uDCCB',
+ 'toml': '\uD83D\uDCCB', 'csv': '\uD83D\uDCCB',
+ // Docs
+ 'md': '\uD83D\uDCDD', 'markdown': '\uD83D\uDCDD', 'txt': '\uD83D\uDCDD', 'rst': '\uD83D\uDCDD',
+ // Images
+ 'png': '\uD83D\uDDBC\uFE0F', 'jpg': '\uD83D\uDDBC\uFE0F', 'jpeg': '\uD83D\uDDBC\uFE0F',
+ 'gif': '\uD83D\uDDBC\uFE0F', 'svg': '\uD83D\uDDBC\uFE0F', 'webp': '\uD83D\uDDBC\uFE0F',
+ 'ico': '\uD83D\uDDBC\uFE0F', 'bmp': '\uD83D\uDDBC\uFE0F',
+ // Video/Audio
+ 'mp4': '\uD83C\uDFAC', 'webm': '\uD83C\uDFAC', 'mov': '\uD83C\uDFAC',
+ 'mp3': '\uD83C\uDFB5', 'wav': '\uD83C\uDFB5', 'ogg': '\uD83C\uDFB5',
+ // Config/Shell
+ 'sh': '\uD83D\uDCBB', 'bash': '\uD83D\uDCBB', 'zsh': '\uD83D\uDCBB',
+ 'env': '\uD83D\uDD10', 'gitignore': '\uD83D\uDEAB', 'dockerfile': '\uD83D\uDC33',
+ // Lock files
+ 'lock': '\uD83D\uDD12',
+ };
+
+ return icons[ext.toLowerCase()] || '\uD83D\uDCC4';
+ }
+
+ formatFileSize(bytes) {
+ if (bytes === undefined || bytes === null) return '';
+ if (bytes < 1024) return `${bytes} B`;
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
+ if (bytes < 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
+ return `${(bytes / (1024 * 1024 * 1024)).toFixed(1)} GB`;
+ }
+
// ========== Log Viewer Windows (Floating File Streamers) ==========
openLogViewerWindow(filePath, sessionId) {