From f5e27e1cc51fad14a4849f2f604c56ba295ed8f5 Mon Sep 17 00:00:00 2001 From: Sergey Linnik Date: Sun, 15 Mar 2026 12:24:59 +0300 Subject: [PATCH] feat(layout): add flex-stretch panels and focus mode Task panels now stretch to fill available width using flex-grow instead of fixed pixel sizes. Added a focus mode toggle button in the task title bar that shows only the active task at full width. In focus mode, hidden panels use visibility:hidden (not display:none) so terminals keep their correct dimensions. Focus mode state is persisted across app restarts. --- src/components/TaskPanel.tsx | 19 ++++++++ src/components/TilingLayout.tsx | 85 ++++++++++++++++++++++++--------- src/store/core.ts | 1 + src/store/persistence.ts | 3 ++ src/store/store.ts | 1 + src/store/types.ts | 2 + src/store/ui.ts | 4 ++ 7 files changed, 93 insertions(+), 22 deletions(-) diff --git a/src/components/TaskPanel.tsx b/src/components/TaskPanel.tsx index 71ea16ee..4fc5834e 100644 --- a/src/components/TaskPanel.tsx +++ b/src/components/TaskPanel.tsx @@ -28,6 +28,7 @@ import { clearPendingAction, showNotification, collapseTask, + toggleFocusMode, } from '../store/store'; import { ResizablePanel, type PanelChild } from './ResizablePanel'; import { EditableText, type EditableTextHandle } from './EditableText'; @@ -344,6 +345,24 @@ export function TaskPanel(props: TaskPanelProps) { + + + + ) : ( + + + + ) + } + onClick={() => { + if (!store.focusMode) setActiveTask(props.task.id); + toggleFocusMode(); + }} + title={store.focusMode ? 'Exit focus mode' : 'Focus on this task'} + /> diff --git a/src/components/TilingLayout.tsx b/src/components/TilingLayout.tsx index 54f431c5..8caf5f09 100644 --- a/src/components/TilingLayout.tsx +++ b/src/components/TilingLayout.tsx @@ -1,26 +1,16 @@ -import { Show, createMemo, createEffect, onMount, onCleanup, ErrorBoundary } from 'solid-js'; +import { Show, For, createMemo, createEffect, ErrorBoundary } from 'solid-js'; import { store, pickAndAddProject, closeTerminal } from '../store/store'; import { closeTask } from '../store/tasks'; -import { ResizablePanel, type PanelChild, type ResizablePanelHandle } from './ResizablePanel'; +import type { PanelChild } from './ResizablePanel'; import { TaskPanel } from './TaskPanel'; import { TerminalPanel } from './TerminalPanel'; import { NewTaskPlaceholder } from './NewTaskPlaceholder'; +import { markDirty } from '../lib/terminalFitManager'; import { theme } from '../lib/theme'; import { mod } from '../lib/platform'; -import { createCtrlShiftWheelResizeHandler } from '../lib/wheelZoom'; export function TilingLayout() { let containerRef: HTMLDivElement | undefined; - let panelHandle: ResizablePanelHandle | undefined; - - onMount(() => { - if (!containerRef) return; - const handleWheel = createCtrlShiftWheelResizeHandler((deltaPx) => { - panelHandle?.resizeAll(deltaPx); - }); - containerRef.addEventListener('wheel', handleWheel, { passive: false }); - onCleanup(() => containerRef?.removeEventListener('wheel', handleWheel)); - }); // Scroll the active task panel into view when selection changes createEffect(() => { @@ -29,6 +19,20 @@ export function TilingLayout() { const el = containerRef.querySelector(`[data-task-id="${CSS.escape(activeId)}"]`); el?.scrollIntoView({ block: 'nearest', inline: 'nearest', behavior: 'instant' }); }); + + // When switching tasks in focus mode, mark all terminals of the newly active + // task as dirty so they re-fit to the correct size. + createEffect(() => { + const activeId = store.activeTaskId; + if (!store.focusMode || !activeId) return; + const task = store.tasks[activeId]; + if (task) { + for (const agentId of task.agentIds) markDirty(agentId); + for (const shellId of task.shellAgentIds) markDirty(shellId); + } + const terminal = store.terminals[activeId]; + if (terminal) markDirty(terminal.agentId); + }); // Cache PanelChild objects by ID so sees stable references // and doesn't unmount/remount panels when taskOrder changes. const panelCache = new Map(); @@ -48,7 +52,7 @@ export function TilingLayout() { cached = { id: panelId, initialSize: 520, - minSize: 300, + minSize: 520, content: () => { const task = store.tasks[panelId]; const terminal = store.terminals[panelId]; @@ -334,15 +338,52 @@ export function TilingLayout() { } > - { - panelHandle = h; +
+ > + + {(child) => { + const isPlaceholder = child.id === '__placeholder'; + const hidden = () => + store.focusMode && !isPlaceholder && child.id !== store.activeTaskId; + const hidePlaceholder = () => store.focusMode && isPlaceholder; + return ( +
+ {child.content()} +
+ ); + }} +
+
); diff --git a/src/store/core.ts b/src/store/core.ts index fb7425a9..177c7fd2 100644 --- a/src/store/core.ts +++ b/src/store/core.ts @@ -56,6 +56,7 @@ export const [store, setStore] = createStore({ connectedClients: 0, }, showArena: false, + focusMode: false, }); export function updateWindowTitle(_taskName?: string): void { diff --git a/src/store/persistence.ts b/src/store/persistence.ts index dfe4babf..f9b335cf 100644 --- a/src/store/persistence.ts +++ b/src/store/persistence.ts @@ -43,6 +43,7 @@ export async function saveState(): Promise { inactiveColumnOpacity: store.inactiveColumnOpacity, editorCommand: store.editorCommand || undefined, customAgents: store.customAgents.length > 0 ? [...store.customAgents] : undefined, + focusMode: store.focusMode || undefined, }; for (const taskId of store.taskOrder) { @@ -175,6 +176,7 @@ interface LegacyPersistedState { editorCommand?: unknown; customAgents?: unknown; terminals?: unknown; + focusMode?: unknown; } export async function loadState(): Promise { @@ -280,6 +282,7 @@ export async function loadState(): Promise { const rawEditorCommand = raw.editorCommand; s.editorCommand = typeof rawEditorCommand === 'string' ? rawEditorCommand.trim() : ''; + s.focusMode = raw.focusMode === true; // Restore custom agents if (Array.isArray(raw.customAgents)) { diff --git a/src/store/store.ts b/src/store/store.ts index af9079d5..813c0b4f 100644 --- a/src/store/store.ts +++ b/src/store/store.ts @@ -92,6 +92,7 @@ export { setPanelSizes, toggleSidebar, toggleArena, + toggleFocusMode, setTerminalFont, setThemePreset, setAutoTrustFolders, diff --git a/src/store/types.ts b/src/store/types.ts index 3aa0df36..e69c1667 100644 --- a/src/store/types.ts +++ b/src/store/types.ts @@ -117,6 +117,7 @@ export interface PersistedState { inactiveColumnOpacity?: number; editorCommand?: string; customAgents?: AgentDef[]; + focusMode?: boolean; } // Panel cell IDs. Shell terminals use "shell:0", "shell:1", etc. @@ -183,4 +184,5 @@ export interface AppStore { missingProjectIds: Record; remoteAccess: RemoteAccess; showArena: boolean; + focusMode: boolean; } diff --git a/src/store/ui.ts b/src/store/ui.ts index a50a3e8f..06a75c62 100644 --- a/src/store/ui.ts +++ b/src/store/ui.ts @@ -99,6 +99,10 @@ export function toggleArena(show?: boolean): void { setStore('showArena', show ?? !store.showArena); } +export function toggleFocusMode(on?: boolean): void { + setStore('focusMode', on ?? !store.focusMode); +} + export function setWindowState(windowState: PersistedWindowState): void { const current = store.windowState; if (