{/* Sticky Input */}
diff --git a/frontend/src/components/ChatWindow.js b/frontend/src/components/ChatWindow.js
index 77f7988..c52c704 100644
--- a/frontend/src/components/ChatWindow.js
+++ b/frontend/src/components/ChatWindow.js
@@ -123,7 +123,7 @@
{message.reasoning}
@@ -239,9 +239,10 @@
};
// Main ChatWindow component with dynamic height calculation
-const ChatWindow = ({ chatHistory, maxHeight, onSynthesize, featureName, isStreamingPlaying, onAudioPlay }) => {
+const ChatWindow = ({ chatHistory, maxHeight, onSynthesize, featureName, isStreamingPlaying, onAudioPlay, autoCollapse = false }) => {
const containerRef = useRef(null);
const [activePlayingId, setActivePlayingId] = useState(null);
+ const [expandedIndices, setExpandedIndices] = useState({});
useEffect(() => {
// If a new stream starts playing, stop any ongoing historical audio
@@ -256,32 +257,93 @@
}
}, [chatHistory]);
+ // Handle auto-scroll when thought trace content changes (expanding or streaming)
+ useEffect(() => {
+ const container = containerRef.current;
+ if (!container) return;
+
+ let isNearBottom = true;
+ const handleScroll = () => {
+ const threshold = 150;
+ isNearBottom = (container.scrollHeight - container.scrollTop - container.clientHeight) < threshold;
+ };
+
+ container.addEventListener('scroll', handleScroll);
+
+ const observer = new ResizeObserver(() => {
+ if (isNearBottom) {
+ container.scrollTop = container.scrollHeight;
+ }
+ });
+
+ // Observe children for height changes
+ Array.from(container.children).forEach(child => observer.observe(child));
+
+ return () => {
+ container.removeEventListener('scroll', handleScroll);
+ observer.disconnect();
+ };
+ }, [chatHistory]);
+
return (
- {chatHistory.map((message, index) => (
-
- {
- setActivePlayingId(id);
- if (id && onAudioPlay) {
- onAudioPlay(); // Notify parent to stop streaming (to prevent overlap)
- }
- }}
- />
-
- ))}
+ {chatHistory.map((message, index) => {
+ const isLastMessage = index === chatHistory.length - 1;
+ const shouldCollapse = autoCollapse && !isLastMessage && !message.isUser && !expandedIndices[index];
+
+ return (
+
+ {shouldCollapse ? (
+
+ ) : (
+
+
+ {
+ setActivePlayingId(id);
+ if (id && onAudioPlay) {
+ onAudioPlay(); // Notify parent to stop streaming (to prevent overlap)
+ }
+ }}
+ />
+
+ {autoCollapse && !isLastMessage && !message.isUser && expandedIndices[index] && (
+
+ )}
+
+ )}
+
+ );
+ })}
);
};
diff --git a/frontend/src/components/FileSystemNavigator.js b/frontend/src/components/FileSystemNavigator.js
index 18de142..7589614 100644
--- a/frontend/src/components/FileSystemNavigator.js
+++ b/frontend/src/components/FileSystemNavigator.js
@@ -30,12 +30,36 @@
return data.files || [];
}, [nodeId, sessionId]);
+ const mergeFiles = (prev, newFiles, parentPath) => {
+ const parentPrefix = (parentPath === "." || parentPath === "/" || parentPath === "")
+ ? ""
+ : (parentPath.endsWith("/") ? parentPath : parentPath + "/");
+
+ const newPaths = new Set(newFiles.map(f => f.path));
+ const preserved = prev.filter(f => {
+ if (newPaths.has(f.path)) return false;
+ const rel = f.path.startsWith("/") ? f.path.slice(1) : f.path;
+ const pRel = parentPrefix.startsWith("/") ? parentPrefix.slice(1) : parentPrefix;
+
+ if (pRel === "") {
+ if (!rel.includes("/")) return false;
+ } else {
+ if (rel.startsWith(pRel)) {
+ const sub = rel.slice(pRel.length);
+ if (!sub.includes("/") && sub.length > 0) return false;
+ }
+ }
+ return true;
+ });
+ return [...newFiles, ...preserved];
+ };
+
const loadRoot = useCallback(async () => {
setLoading(true);
setError(null);
try {
const files = await fetchLevel(initialPath);
- setTree(files);
+ setTree(prev => mergeFiles(prev, files, initialPath));
} catch (err) {
setError(err.message || "Failed to connect to node filesystem.");
} finally {
@@ -47,8 +71,6 @@
if (nodeId) {
loadRoot();
- // Poll for updates if we are in a specific session (Coding Assistant)
- // or if explicitly requested via showSyncStatus
const shouldPoll = showSyncStatus || (sessionId && sessionId !== "__fs_explorer__");
if (shouldPoll) {
@@ -60,6 +82,37 @@
}
}, [nodeId, loadRoot, showSyncStatus, sessionId]);
+ // Keyboard Navigation for Media
+ useEffect(() => {
+ const handleKeyDown = (e) => {
+ if (!previewImage) return;
+
+ if (e.key === 'ArrowRight' || e.key === 'ArrowLeft') {
+ e.preventDefault();
+ // Get flat list of images in the current tree
+ const imageFiles = tree.filter(f => !f.is_dir && isImageFile(f.path))
+ .sort((a, b) => a.path.localeCompare(b.path));
+
+ if (imageFiles.length <= 1) return;
+
+ const currentIndex = imageFiles.findIndex(f => f.path === previewImage.path);
+ let nextIndex;
+ if (e.key === 'ArrowRight') {
+ nextIndex = (currentIndex + 1) % imageFiles.length;
+ } else {
+ nextIndex = (currentIndex - 1 + imageFiles.length) % imageFiles.length;
+ }
+ handleView(imageFiles[nextIndex].path);
+ } else if (e.key === 'Escape') {
+ if (previewImage.url) URL.revokeObjectURL(previewImage.url);
+ setPreviewImage(null);
+ }
+ };
+
+ window.addEventListener('keydown', handleKeyDown);
+ return () => window.removeEventListener('keydown', handleKeyDown);
+ }, [previewImage, tree]);
+
const toggleFolder = async (path) => {
const isExpanded = expanded[path];
if (!isExpanded) {
@@ -129,6 +182,8 @@
setError(null);
try {
await nodeFsRm(nodeId, path, sessionId);
+ // Optimistically remove from tree to force UI update
+ setTree(prev => prev.filter(f => !f.path.startsWith(path)));
setDeleteModal(null);
setTimeout(loadRoot, 500);
} catch (err) {
@@ -156,7 +211,11 @@
try {
const blob = await nodeFsDownloadBlob(nodeId, path, sessionId);
const url = URL.createObjectURL(blob);
- setPreviewImage({ path, url });
+
+ setPreviewImage(prev => {
+ if (prev && prev.url) URL.revokeObjectURL(prev.url);
+ return { path, url };
+ });
} catch (err) {
setError(`Failed to load image: ${err.message}`);
} finally {
diff --git a/frontend/src/components/MultiNodeConsole.js b/frontend/src/components/MultiNodeConsole.js
index f073a40..249ff64 100644
--- a/frontend/src/components/MultiNodeConsole.js
+++ b/frontend/src/components/MultiNodeConsole.js
@@ -226,7 +226,6 @@
if (stealthData) xterm.write(stealthData);
} else if (data) xterm.write(data);
break;
- case 'browser_event': xterm.write(`\x1b[90m${msg.data.type === 'console' ? 'š„ļø' : 'š'} ${msg.data.text || msg.data.url}\x1b[0m\r\n`); break;
}
// Always scroll to bottom on new output
xterm.scrollToBottom();
diff --git a/frontend/src/pages/NodesPage.js b/frontend/src/pages/NodesPage.js
index 9639d9e..ba51333 100644
--- a/frontend/src/pages/NodesPage.js
+++ b/frontend/src/pages/NodesPage.js
@@ -14,12 +14,11 @@
const [error, setError] = useState(null);
const [showCreateModal, setShowCreateModal] = useState(false);
const [nodeToDelete, setNodeToDelete] = useState(null);
- const [newNode, setNewNode] = useState({ node_id: '', display_name: '', description: '', skill_config: { shell: { enabled: true }, browser: { enabled: true }, sync: { enabled: true } } });
+ const [newNode, setNewNode] = useState({ node_id: '', display_name: '', description: '', skill_config: { shell: { enabled: true }, sync: { enabled: true } } });
const [expandedTerminals, setExpandedTerminals] = useState({}); // node_id -> boolean
const [expandedNodes, setExpandedNodes] = useState({}); // node_id -> boolean
const [expandedFiles, setExpandedFiles] = useState({}); // node_id -> boolean
const [editingNodeId, setEditingNodeId] = useState(null);
- const [provisionIncludeBrowsers, setProvisionIncludeBrowsers] = useState(true);
const [editForm, setEditForm] = useState({
display_name: '',
description: '',
@@ -613,56 +612,6 @@
)}
{/* SANDBOX POLICY CONFIGURATION ā New M6 Feature */}
{editingNodeId === node.node_id && editForm.skill_config?.shell?.enabled ? (
@@ -768,24 +717,12 @@