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
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ export interface ContentCanvasProps {
workspacePath?: string;
/** App mode */
mode?: 'agent' | 'project' | 'git';
/** Whether the containing scene is currently visible */
isSceneActive?: boolean;
/** Interaction callback */
onInteraction?: (itemId: string, userInput: string) => Promise<void>;
/** Before-close callback */
Expand All @@ -29,6 +31,7 @@ export interface ContentCanvasProps {
export const ContentCanvas: React.FC<ContentCanvasProps> = ({
workspacePath,
mode = 'agent',
isSceneActive = true,
onInteraction,
disablePopOut = false,
}) => {
Expand Down Expand Up @@ -114,6 +117,7 @@ export const ContentCanvas: React.FC<ContentCanvasProps> = ({
<div className="canvas-content-canvas__editor">
<EditorArea
workspacePath={workspacePath}
isSceneActive={isSceneActive}
onOpenMissionControl={handleOpenMissionControl}
onInteraction={onInteraction}
onTabCloseWithDirtyCheck={handleCloseWithDirtyCheck}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import type {
import './EditorArea.scss';
export interface EditorAreaProps {
workspacePath?: string;
isSceneActive?: boolean;
onOpenMissionControl?: () => void;
onInteraction?: (itemId: string, userInput: string) => Promise<void>;
onTabCloseWithDirtyCheck?: (tabId: string, groupId: EditorGroupId) => Promise<boolean>;
Expand All @@ -20,6 +21,7 @@ export interface EditorAreaProps {

export const EditorArea: React.FC<EditorAreaProps> = ({
workspacePath,
isSceneActive = true,
onOpenMissionControl,
onInteraction,
onTabCloseWithDirtyCheck,
Expand Down Expand Up @@ -125,6 +127,7 @@ export const EditorArea: React.FC<EditorAreaProps> = ({
groupId={groupId}
group={group}
isActive={activeGroupId === groupId}
isSceneActive={isSceneActive}
draggingTabId={draggingTabId}
draggingFromGroupId={draggingFromGroupId}
splitMode={layout.splitMode}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ export interface EditorGroupProps {
groupId: EditorGroupId;
group: EditorGroupState;
isActive: boolean;
isSceneActive?: boolean;
draggingTabId: string | null;
draggingFromGroupId: EditorGroupId | null;
splitMode: SplitMode;
Expand All @@ -50,6 +51,7 @@ export const EditorGroup: React.FC<EditorGroupProps> = ({
groupId,
group,
isActive,
isSceneActive = true,
draggingTabId,
draggingFromGroupId,
splitMode,
Expand Down Expand Up @@ -174,7 +176,7 @@ export const EditorGroup: React.FC<EditorGroupProps> = ({
>
<FlexiblePanel
content={tab.content as any}
isActive={group.activeTabId === tab.id}
isActive={isSceneActive && group.activeTabId === tab.id}
onContentChange={group.activeTabId === tab.id ? handleContentChange : undefined}
onDirtyStateChange={group.activeTabId === tab.id ? handleDirtyStateChange : undefined}
onFileMissingFromDiskChange={
Expand Down
48 changes: 44 additions & 4 deletions src/web-ui/src/app/hooks/useWindowControls.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { getCurrentWindow } from '@tauri-apps/api/window';
import { useWorkspaceContext } from '../../infrastructure/contexts/WorkspaceContext';
import { notificationService } from '@/shared/notification-system';
import { createLogger } from '@/shared/utils/logger';
import { sendDebugProbe } from '@/shared/utils/debugProbe';
import { useI18n } from '@/infrastructure/i18n';

const log = createLogger('useWindowControls');
Expand Down Expand Up @@ -90,15 +91,54 @@ export const useWindowControls = (options?: { isToolbarMode?: boolean }) => {
}

if (document.visibilityState === 'visible') {
sendDebugProbe(
'useWindowControls.ts:handleVisibilityChange',
'Window became visible',
{
isToolbarMode,
}
);
try {
const appWindow = getCurrentWindow();
// Delay update until window fully restores
setTimeout(async () => {
await updateWindowState(appWindow);
await restoreMacOSOverlayTitlebar(appWindow);
const startedAt = typeof performance !== 'undefined' ? performance.now() : Date.now();
try {
await updateWindowState(appWindow);
await restoreMacOSOverlayTitlebar(appWindow);
sendDebugProbe(
'useWindowControls.ts:handleVisibilityChange',
'Window restore sync completed',
{
durationMs:
Math.round(
((typeof performance !== 'undefined' ? performance.now() : Date.now()) -
startedAt) *
10
) / 10,
isToolbarMode,
}
);
} catch (error) {
sendDebugProbe(
'useWindowControls.ts:handleVisibilityChange',
'Window restore sync failed',
{
error: formatErrorMessage(error),
isToolbarMode,
}
);
}
}, 300);
} catch (_error) {
// Ignore errors
} catch (error) {
sendDebugProbe(
'useWindowControls.ts:handleVisibilityChange',
'Window restore setup failed',
{
error: formatErrorMessage(error),
isToolbarMode,
}
);
}
}
};
Expand Down
17 changes: 11 additions & 6 deletions src/web-ui/src/app/scenes/SceneViewport.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ const SceneViewport: React.FC<SceneViewportProps> = ({ workspacePath, isEntering
) : null
}
>
{renderScene(tab.id, workspacePath, isEntering)}
{renderScene(tab.id, workspacePath, isEntering, isActive)}
</Suspense>
</div>
);
Expand All @@ -94,16 +94,21 @@ const SceneViewport: React.FC<SceneViewportProps> = ({ workspacePath, isEntering
);
};

function renderScene(id: SceneTabId, workspacePath?: string, isEntering?: boolean) {
function renderScene(
id: SceneTabId,
workspacePath?: string,
isEntering?: boolean,
isActive: boolean = false
) {
switch (id) {
case 'welcome':
return <WelcomeScene />;
case 'session':
return <SessionScene workspacePath={workspacePath} isEntering={isEntering} />;
return <SessionScene workspacePath={workspacePath} isEntering={isEntering} isActive={isActive} />;
case 'terminal':
return <TerminalScene />;
return <TerminalScene isActive={isActive} />;
case 'git':
return <GitScene workspacePath={workspacePath} />;
return <GitScene workspacePath={workspacePath} isActive={isActive} />;
case 'settings':
return <SettingsScene />;
case 'file-viewer':
Expand All @@ -125,7 +130,7 @@ function renderScene(id: SceneTabId, workspacePath?: string, isEntering?: boolea
case 'insights':
return <InsightsScene />;
case 'shell':
return <ShellScene />;
return <ShellScene isActive={isActive} />;
case 'panel-view':
return <PanelViewScene workspacePath={workspacePath} />;
default:
Expand Down
41 changes: 26 additions & 15 deletions src/web-ui/src/app/scenes/git/GitScene.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,13 @@ import './GitScene.scss';

interface GitSceneProps {
workspacePath?: string;
isActive?: boolean;
}

const GitScene: React.FC<GitSceneProps> = ({ workspacePath: workspacePathProp }) => {
const GitScene: React.FC<GitSceneProps> = ({
workspacePath: workspacePathProp,
isActive = true,
}) => {
const { workspace } = useCurrentWorkspace();
const workspacePath = workspacePathProp ?? workspace?.rootPath ?? '';
const { t } = useTranslation('panels/git');
Expand All @@ -33,13 +37,16 @@ const GitScene: React.FC<GitSceneProps> = ({ workspacePath: workspacePathProp })
refresh,
} = useGitState({
repositoryPath: workspacePath,
isActive: true,
isActive,
refreshOnMount: true,
layers: ['basic', 'status'],
});

const repoLoading = statusLoading && !isRepository;
const handleRefresh = useCallback(() => refresh({ force: true, layers: ['basic', 'status'], reason: 'manual' }), [refresh]);
const handleRefresh = useCallback(
() => refresh({ force: true, layers: ['basic', 'status'], reason: 'manual' }),
[refresh]
);

useEffect(() => {
if (repoLoading || statusLoading) {
Expand All @@ -65,6 +72,22 @@ const GitScene: React.FC<GitSceneProps> = ({ workspacePath: workspacePathProp })
globalEventBus.emit('fill-chat-input', { content: t('init.chatPrompt') });
}, [t]);

const renderView = useCallback(() => {
switch (activeView) {
case 'branches':
return <BranchesView workspacePath={workspacePath} />;
case 'graph':
return <GraphView workspacePath={workspacePath} />;
case 'working-copy':
default:
return <WorkingCopyView workspacePath={workspacePath} isActive={isActive} />;
}
}, [activeView, isActive, workspacePath]);

if (!isActive) {
return <div className="bitfun-git-scene" aria-hidden="true" />;
}

if (!repoLoading && !isRepository) {
return (
<div className="bitfun-git-scene bitfun-git-scene--not-repository">
Expand Down Expand Up @@ -120,18 +143,6 @@ const GitScene: React.FC<GitSceneProps> = ({ workspacePath: workspacePathProp })
);
}

const renderView = () => {
switch (activeView) {
case 'branches':
return <BranchesView workspacePath={workspacePath} />;
case 'graph':
return <GraphView workspacePath={workspacePath} />;
case 'working-copy':
default:
return <WorkingCopyView workspacePath={workspacePath} />;
}
};

return <div className="bitfun-git-scene">{renderView()}</div>;
};

Expand Down
8 changes: 6 additions & 2 deletions src/web-ui/src/app/scenes/git/views/WorkingCopyView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,9 +52,13 @@ const FILE_LIST_WIDTH_MAX = 560;

interface WorkingCopyViewProps {
workspacePath?: string;
isActive?: boolean;
}

const WorkingCopyView: React.FC<WorkingCopyViewProps> = ({ workspacePath }) => {
const WorkingCopyView: React.FC<WorkingCopyViewProps> = ({
workspacePath,
isActive = true,
}) => {
const { t } = useTranslation('panels/git');
const notification = useNotification();

Expand All @@ -76,7 +80,7 @@ const WorkingCopyView: React.FC<WorkingCopyViewProps> = ({ workspacePath }) => {
refresh,
} = useGitState({
repositoryPath: workspacePath ?? '',
isActive: true,
isActive,
refreshOnMount: true,
layers: ['basic', 'status'],
});
Expand Down
4 changes: 3 additions & 1 deletion src/web-ui/src/app/scenes/session/AuxPane.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,11 @@ export interface AuxPaneRef {

interface AuxPaneProps {
workspacePath?: string;
isSceneActive?: boolean;
}

const AuxPane = forwardRef<AuxPaneRef, AuxPaneProps>(
({ workspacePath }, ref) => {
({ workspacePath, isSceneActive = true }, ref) => {
const {
addTab,
switchToTab,
Expand Down Expand Up @@ -124,6 +125,7 @@ const AuxPane = forwardRef<AuxPaneRef, AuxPaneProps>(
<ContentCanvas
workspacePath={workspacePath}
mode="agent"
isSceneActive={isSceneActive}
onInteraction={handleInteraction}
onBeforeClose={handleBeforeClose}
/>
Expand Down
3 changes: 3 additions & 0 deletions src/web-ui/src/app/scenes/session/SessionScene.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,11 +36,13 @@ import './SessionScene.scss';
interface SessionSceneProps {
workspacePath?: string;
isEntering?: boolean;
isActive?: boolean;
}

const SessionScene: React.FC<SessionSceneProps> = ({
workspacePath,
isEntering = false,
isActive = true,
}) => {
const { t } = useTranslation('flow-chat');
const { state, updateRightPanelWidth, toggleRightPanel } = useApp();
Expand Down Expand Up @@ -296,6 +298,7 @@ const SessionScene: React.FC<SessionSceneProps> = ({
<AuxPane
ref={auxPaneRef}
workspacePath={workspacePath}
isSceneActive={isActive}
/>
</div>
</div>
Expand Down
8 changes: 6 additions & 2 deletions src/web-ui/src/app/scenes/shell/ShellScene.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,14 @@ import './ShellScene.scss';

const TerminalScene = lazy(() => import('../terminal/TerminalScene'));

const ShellScene: React.FC = () => (
interface ShellSceneProps {
isActive?: boolean;
}

const ShellScene: React.FC<ShellSceneProps> = ({ isActive = true }) => (
<div className="bitfun-shell-scene">
<Suspense fallback={<div className="bitfun-shell-scene__loading" />}>
<TerminalScene />
<TerminalScene isActive={isActive} />
</Suspense>
</div>
);
Expand Down
10 changes: 9 additions & 1 deletion src/web-ui/src/app/scenes/terminal/TerminalScene.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,11 @@ import { useTerminalSceneStore } from '../../stores/terminalSceneStore';
import ConnectedTerminal from '../../../tools/terminal/components/ConnectedTerminal';
import './TerminalScene.scss';

const TerminalScene: React.FC = () => {
interface TerminalSceneProps {
isActive?: boolean;
}

const TerminalScene: React.FC<TerminalSceneProps> = ({ isActive = true }) => {
const { activeSessionId, setActiveSession } = useTerminalSceneStore();
const { t } = useTranslation('panels/terminal');

Expand All @@ -25,6 +29,10 @@ const TerminalScene: React.FC = () => {
setActiveSession(null);
}, [setActiveSession]);

if (!isActive) {
return <div className="bitfun-terminal-scene" aria-hidden="true" />;
}

return (
<div className="bitfun-terminal-scene">
{activeSessionId ? (
Expand Down
Loading
Loading