Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions src/web-ui/src/app/components/NavPanel/MainNav.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -526,8 +526,7 @@ const MainNav: React.FC<MainNavProps> = ({
aria-label={t('nav.items.persona')}
title={t('nav.items.persona')}
>
<img className="bitfun-nav-panel__workspace-bot-default" src="/panda_1.png" alt="" />
<img className="bitfun-nav-panel__workspace-bot-hover" src="/panda_2.png" alt="" />
<img className="bitfun-nav-panel__workspace-bot-logo" src="/Logo-ICON.png" alt="" />
</button>
<div className="bitfun-nav-panel__workspace-create-group">
<button
Expand Down
14 changes: 3 additions & 11 deletions src/web-ui/src/app/components/NavPanel/NavPanel.scss
Original file line number Diff line number Diff line change
Expand Up @@ -161,18 +161,10 @@ $_section-header-height: 24px;
left: 0;
}

&-default {
&-logo {
z-index: 1;
}

&-hover {
z-index: 2;
opacity: 0;
transition: opacity $motion-fast $easing-standard;
}

&:hover &-hover {
opacity: 1;
object-fit: contain !important;
padding: 6px;
}
}

Expand Down
4 changes: 2 additions & 2 deletions src/web-ui/src/app/components/panels/base/FlexiblePanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -247,7 +247,7 @@ const FlexiblePanel: React.FC<ExtendedFlexiblePanelProps> = memo(({
const markdownWorkspacePath = markdownEditorData.workspacePath || workspacePath;
const markdownJumpToLine = markdownEditorData.jumpToLine;
const markdownJumpToColumn = markdownEditorData.jumpToColumn;

return (
<div className="bitfun-flexible-panel__markdown-editor">
{markdownFilePath || markdownInitialContent !== undefined ? (
Expand Down Expand Up @@ -435,7 +435,7 @@ const FlexiblePanel: React.FC<ExtendedFlexiblePanelProps> = memo(({
const fileName = editorData.fileName || content.title;
const editorLanguage = editorData.language;
const editorWorkspacePath = editorData.workspacePath || workspacePath;

return (
<CodeEditor
filePath={filePath}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import { useTranslation } from 'react-i18next';
import { TabBar } from '../tab-bar';
import { DropZone } from './DropZone';
import FlexiblePanel from '../../base/FlexiblePanel';
import { createLogger } from '@/shared/utils/logger';
const _dbgLog = createLogger('EditorGroup');
import type {
EditorGroupId,
EditorGroupState,
Expand Down Expand Up @@ -98,10 +100,11 @@ export const EditorGroup: React.FC<EditorGroupProps> = ({

// Tabs to render (active + cached)
const tabsToRender = useMemo(() => {
return group.tabs.filter(t =>
const result = group.tabs.filter(t =>
!t.isHidden &&
(t.id === group.activeTabId || cachedTabsRef.current.has(t.id))
);
return result;
}, [group.tabs, group.activeTabId]);

const handleContentChange = useCallback((content: PanelContent | null) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ import type { EditorGroupId, PanelContent, CreateTabEventDetail } from '../types
import { TAB_EVENTS } from '../types';
import { useI18n } from '@/infrastructure/i18n';
import { drainPendingTabs } from '@/shared/services/pendingTabQueue';
import { createLogger } from '@/shared/utils/logger';
const _dbgLog = createLogger('useTabLifecycle');
interface UseTabLifecycleOptions {
/** App mode / target canvas */
mode?: 'agent' | 'project' | 'git';
Expand Down Expand Up @@ -252,7 +254,7 @@ export const useTabLifecycle = (options: UseTabLifecycleOptions = {}): UseTabLif
const existing = findTabByMetadata({ duplicateCheckKey });
if (existing) {
const hasJumpInfo = data?.jumpToRange || data?.jumpToLine || data?.jumpToColumn;

if (replaceExisting || hasJumpInfo) {
// Update content
updateTabContent(existing.tab.id, existing.groupId, content);
Expand All @@ -269,7 +271,7 @@ export const useTabLifecycle = (options: UseTabLifecycleOptions = {}): UseTabLif

// Determine target group: use specified group when split enabled, otherwise active group
const groupId = (enableSplitView && targetGroup) ? targetGroup : (targetGroup || activeGroupId);

// Open all tabs in active state by default (no preview replacement)
addTab(content, 'active', groupId);

Expand Down
32 changes: 29 additions & 3 deletions src/web-ui/src/app/layout/WorkspaceBody.scss
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ $_nav-collapsed-width: 80px;
.bitfun-workspace-body__nav-area {
display: flex;
flex-direction: column;
width: $_nav-width;
width: var(--nav-width, $_nav-width);
flex-shrink: 0;
height: 100%;
overflow: hidden;
Expand All @@ -46,6 +46,10 @@ $_nav-collapsed-width: 80px;
}
}

.bitfun-is-resizing-nav .bitfun-workspace-body__nav-area {
transition: none;
}

.bitfun-workspace-body.is-entering .bitfun-workspace-body__nav-area:not(.is-collapsed) {
animation: wb-nav-slide-in 560ms cubic-bezier(0.22, 1, 0.36, 1) both;
}
Expand All @@ -71,6 +75,28 @@ $_nav-collapsed-width: 80px;
height: 100%;
cursor: ew-resize;
z-index: 2;

&::after {
content: '';
position: absolute;
top: 0;
left: 50%;
transform: translateX(-50%);
width: 2px;
height: 100%;
background: var(--color-primary);
opacity: 0;
border-radius: 1px;
transition: opacity $motion-fast $easing-standard;
}

&:hover::after {
opacity: 0.35;
}
}

.bitfun-is-resizing-nav .bitfun-workspace-body__nav-divider::after {
opacity: 0.6;
}

// ── Right column: rounded scene card (SceneBar + SceneViewport) ───
Expand Down Expand Up @@ -128,14 +154,14 @@ $_nav-collapsed-width: 80px;
opacity: 0;
}
to {
width: $_nav-width;
width: var(--nav-width, $_nav-width);
opacity: 1;
}
}

@keyframes wb-nav-slide-out {
from {
width: $_nav-width;
width: var(--nav-width, $_nav-width);
opacity: 1;
}
to {
Expand Down
40 changes: 28 additions & 12 deletions src/web-ui/src/app/layout/WorkspaceBody.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
* SceneViewport (flex:1 — active scene content)
*/

import React, { useCallback, useRef } from 'react';
import React, { useCallback, useRef, useState } from 'react';
import { useCurrentWorkspace } from '../../infrastructure/contexts/WorkspaceContext';
import { NavBar } from '../components/NavBar';
import NavPanel from '../components/NavPanel/NavPanel';
Expand All @@ -19,6 +19,11 @@ import { SceneViewport } from '../scenes';
import { useApp } from '../hooks/useApp';
import './WorkspaceBody.scss';

const NAV_DEFAULT_WIDTH = 240;
const NAV_MIN_WIDTH = 240;
const NAV_MAX_WIDTH = 480;
const COLLAPSE_THRESHOLD = 64;

interface WorkspaceBodyProps {
className?: string;
isEntering?: boolean;
Expand All @@ -42,40 +47,48 @@ const WorkspaceBody: React.FC<WorkspaceBodyProps> = ({
}) => {
const { workspace: currentWorkspace } = useCurrentWorkspace();
const { state, toggleLeftPanel } = useApp();
const collapseDragRef = useRef<{ startX: number; hasCollapsed: boolean } | null>(null);
const isNavCollapsed = state.layout.leftPanelCollapsed;
const [navWidth, setNavWidth] = useState(NAV_DEFAULT_WIDTH);

const handleNavCollapseDragStart = useCallback((event: React.MouseEvent<HTMLDivElement>) => {
if (event.button !== 0 || isNavCollapsed) return;
event.preventDefault();

const COLLAPSE_THRESHOLD = 64;
collapseDragRef.current = { startX: event.clientX, hasCollapsed: false };
const startX = event.clientX;
const startWidth = navWidth;
let hasCollapsed = false;

document.body.classList.add('bitfun-is-dragging-nav-collapse');
document.body.classList.add('bitfun-is-resizing-nav');

const cleanup = () => {
collapseDragRef.current = null;
document.body.classList.remove('bitfun-is-dragging-nav-collapse');
document.body.classList.remove('bitfun-is-resizing-nav');
window.removeEventListener('mousemove', handleMouseMove);
window.removeEventListener('mouseup', handleMouseUp);
};

const handleMouseMove = (moveEvent: MouseEvent) => {
const dragState = collapseDragRef.current;
if (!dragState || dragState.hasCollapsed) return;
const deltaX = moveEvent.clientX - dragState.startX;
if (deltaX <= -COLLAPSE_THRESHOLD) {
dragState.hasCollapsed = true;
if (hasCollapsed) return;
const deltaX = moveEvent.clientX - startX;
const rawWidth = startWidth + deltaX;

// Collapse only after the width hits minimum AND continues left by COLLAPSE_THRESHOLD
if (rawWidth <= NAV_MIN_WIDTH - COLLAPSE_THRESHOLD) {
hasCollapsed = true;
toggleLeftPanel();
cleanup();
return;
}
const newWidth = Math.min(NAV_MAX_WIDTH, Math.max(NAV_MIN_WIDTH, rawWidth));
setNavWidth(newWidth);
};

const handleMouseUp = () => cleanup();

window.addEventListener('mousemove', handleMouseMove);
window.addEventListener('mouseup', handleMouseUp);
}, [isNavCollapsed, toggleLeftPanel]);
}, [isNavCollapsed, navWidth, toggleLeftPanel]);

return (
<div className={`bitfun-workspace-body${isEntering ? ' is-entering' : ''}${isExiting ? ' is-exiting' : ''} ${className}`}>
Expand All @@ -86,7 +99,10 @@ const WorkspaceBody: React.FC<WorkspaceBodyProps> = ({
)}

{/* Left: nav history bar + navigation sidebar — always rendered for slide animation */}
<div className={`bitfun-workspace-body__nav-area${isNavCollapsed ? ' is-collapsed' : ''}`}>
<div
className={`bitfun-workspace-body__nav-area${isNavCollapsed ? ' is-collapsed' : ''}`}
style={isNavCollapsed ? undefined : { '--nav-width': `${navWidth}px` } as React.CSSProperties}
>
<NavBar onExpandNav={toggleLeftPanel} onMaximize={onMaximize} />
<NavPanel className="bitfun-workspace-body__nav-panel" />
{!isNavCollapsed && (
Expand Down
13 changes: 13 additions & 0 deletions src/web-ui/src/shared/services/ide-control/PanelController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,19 @@ export class PanelController implements IdeController {
},
};

case 'code-editor':
case 'file-viewer':
case 'markdown-editor':
case 'plan-viewer':
return {
...baseDetail,
data: {
...baseDetail.data,
filePath: config.file_path,
workspacePath: config.workspace_path,
},
};

default:
return baseDetail;
}
Expand Down
23 changes: 19 additions & 4 deletions src/web-ui/src/tools/editor/components/CodeEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -926,7 +926,10 @@ const CodeEditor: React.FC<CodeEditorProps> = ({

// Load file content
const loadFileContent = useCallback(async () => {
if (!filePath) return;
if (!filePath) {
setLoading(false);
return;
}

// If Model already has content, skip file loading to avoid overwriting unsaved changes (e.g. switching back to open tab)
if (modelRef.current && modelRef.current.getValue()) {
Expand All @@ -949,7 +952,10 @@ const CodeEditor: React.FC<CodeEditorProps> = ({
setHasChanges(false);
hasChangesRef.current = false;

onContentChange?.(fileContent, false);
// NOTE: Do NOT call onContentChange here during initial load.
// Calling it triggers parent re-render which unmounts this component,
// causing an infinite loop. onContentChange should only be called
// when user actually edits the content.

// Sync versionId after Model update
queueMicrotask(() => {
Expand Down Expand Up @@ -1124,9 +1130,18 @@ const CodeEditor: React.FC<CodeEditorProps> = ({
}
}, [filePath, hasChanges, monacoReady]);

// Initial file load
// Initial file load - only run once when filePath changes
const loadFileContentCalledRef = useRef(false);
useEffect(() => {
loadFileContent();
// Reset the flag when filePath changes
loadFileContentCalledRef.current = false;
}, [filePath]);

useEffect(() => {
if (!loadFileContentCalledRef.current) {
loadFileContentCalledRef.current = true;
loadFileContent();
}
}, [loadFileContent]);

// Periodic file modification check
Expand Down
29 changes: 18 additions & 11 deletions src/web-ui/src/tools/editor/components/MarkdownEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -109,9 +109,9 @@ const MarkdownEditor: React.FC<MarkdownEditorProps> = ({
setTimeout(() => {
editorRef.current?.setInitialContent?.(fileContent);
}, 0);
if (onContentChangeRef.current) {
onContentChangeRef.current(fileContent, false);
}
// NOTE: Do NOT call onContentChange here during initial load.
// Calling it triggers parent re-render which unmounts this component,
// causing an infinite loop.
}
} catch (err) {
if (!isUnmountedRef.current) {
Expand All @@ -132,18 +132,28 @@ const MarkdownEditor: React.FC<MarkdownEditorProps> = ({
}
}, [filePath, t]);

// Initial file load - only run once when filePath changes
const loadFileContentCalledRef = useRef(false);
useEffect(() => {
// Reset the flag when filePath changes
loadFileContentCalledRef.current = false;
}, [filePath]);

useEffect(() => {
if (filePath) {
loadFileContent();
if (!loadFileContentCalledRef.current) {
loadFileContentCalledRef.current = true;
loadFileContent();
}
} else if (initialContent !== undefined) {
setContent(initialContent);
setHasChanges(false);
setTimeout(() => {
editorRef.current?.setInitialContent?.(initialContent);
}, 0);
if (onContentChangeRef.current) {
onContentChangeRef.current(initialContent, false);
}
// NOTE: Do NOT call onContentChange here during initial load.
// Calling it triggers parent re-render which unmounts this component,
// causing an infinite loop.
}
}, [filePath, initialContent, loadFileContent]);

Expand Down Expand Up @@ -195,10 +205,7 @@ const MarkdownEditor: React.FC<MarkdownEditorProps> = ({

const handleDirtyChange = useCallback((isDirty: boolean) => {
setHasChanges(isDirty);
if (onContentChangeRef.current) {
onContentChangeRef.current(content, isDirty);
}
}, [content]);
}, []);

const handleSave = useCallback((_value: string) => {
saveFileContent();
Expand Down
5 changes: 4 additions & 1 deletion src/web-ui/src/tools/editor/components/PlanViewer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,10 @@ const PlanViewer: React.FC<PlanViewerProps> = ({
}, []);

const loadFileContent = useCallback(async () => {
if (!filePath || isUnmountedRef.current) return;
if (!filePath || isUnmountedRef.current) {
if (!isUnmountedRef.current) setLoading(false);
return;
}

if (planBuildStateService.isFileWriting(filePath)) {
return;
Expand Down
Loading
Loading