diff --git a/.changeset/terminal-right-dock-bottom-grabber.md b/.changeset/terminal-right-dock-bottom-grabber.md
new file mode 100644
index 00000000..d2920b64
--- /dev/null
+++ b/.changeset/terminal-right-dock-bottom-grabber.md
@@ -0,0 +1,5 @@
+---
+"@inkeep/open-knowledge": patch
+---
+
+The bottom resize grabber no longer lingers when the terminal is docked to the right column. Previously the grabber stayed visible and could be dragged up to reveal an empty bottom panel; it now disappears while the terminal is right-docked and returns when you dock the terminal back to the bottom.
diff --git a/packages/app/src/components/TerminalDock.dom.test.tsx b/packages/app/src/components/TerminalDock.dom.test.tsx
index 76a33cb7..8a3d0edc 100644
--- a/packages/app/src/components/TerminalDock.dom.test.tsx
+++ b/packages/app/src/components/TerminalDock.dom.test.tsx
@@ -4,6 +4,7 @@ import userEvent from '@testing-library/user-event';
import { useEffect, useRef, useState } from 'react';
import { TooltipProvider } from '@/components/ui/tooltip';
import type { OkDesktopBridge } from '@/lib/desktop-bridge-types';
+import type { TerminalDockPosition } from '@/lib/terminal-dock-store';
import { requestActiveTerminalInput } from './handoff/terminal-input-events';
const TERMINAL_PANEL_ID = 'terminal-dock-panel';
@@ -46,10 +47,11 @@ mock.module('@/components/ui/resizable', () => ({
);
},
// biome-ignore lint/suspicious/noExplicitAny: test stub
- ResizableHandle: ({ onPointerDown, disabled }: any) => (
+ ResizableHandle: ({ onPointerDown, disabled, withHandle }: any) => (
),
@@ -153,7 +155,7 @@ function makeBridge() {
}
// biome-ignore lint/suspicious/noExplicitAny: test harness props
-function DockHarness({ v, l, onVisibleChange, bridge, onReveal }: any) {
+function DockHarness({ v, l, onVisibleChange, bridge, onReveal, dock = 'bottom' }: any) {
const [bottomContainer, setBottomContainer] = useState(null);
const [editorRegionEl, setEditorRegionEl] = useState(null);
return (
@@ -161,7 +163,7 @@ function DockHarness({ v, l, onVisibleChange, bridge, onReveal }: any) {
editorRegionEl?.focus()}
- dockPosition="bottom"
+ dockPosition={dock}
onToggleDock={() => {}}
/>
@@ -190,13 +192,18 @@ function renderDock(
) {
const onVisibleChange = mock((_v: boolean) => {});
const { bridge, create, kill, input, viewMenuPushes, dispatchMenuAction } = makeBridge();
- const ui = (v: boolean, l?: { prompt: string; nonce: number; cli?: string } | null) => (
+ const ui = (
+ v: boolean,
+ l?: { prompt: string; nonce: number; cli?: string } | null,
+ dock?: TerminalDockPosition,
+ ) => (
);
const utils = render(ui(visible, launch));
@@ -208,8 +215,11 @@ function renderDock(
input,
viewMenuPushes,
dispatchMenuAction,
- rerender: (v: boolean, l?: { prompt: string; nonce: number; cli?: string } | null) =>
- utils.rerender(ui(v, l)),
+ rerender: (
+ v: boolean,
+ l?: { prompt: string; nonce: number; cli?: string } | null,
+ dock?: TerminalDockPosition,
+ ) => utils.rerender(ui(v, l, dock)),
};
}
@@ -796,6 +806,18 @@ describe('TerminalDock multi-session', () => {
'false',
);
});
+
+ test('hides the grabber while right-docked and restores it on return to bottom', () => {
+ const view = renderDock(true);
+ act(() => view.rerender(true, null, 'right'));
+ const handle = () => screen.getByTestId('terminal-resize-handle');
+ expect(handle().getAttribute('data-disabled')).toBe('true');
+ expect(handle().getAttribute('data-with-handle')).toBe('false');
+
+ act(() => view.rerender(true, null, 'bottom'));
+ expect(handle().getAttribute('data-disabled')).toBe('false');
+ expect(handle().getAttribute('data-with-handle')).toBe('true');
+ });
});
describe('TerminalSessionsHost focus-return gating across a dock move', () => {
diff --git a/packages/app/src/components/TerminalDock.tsx b/packages/app/src/components/TerminalDock.tsx
index 4447b30a..515347ce 100644
--- a/packages/app/src/components/TerminalDock.tsx
+++ b/packages/app/src/components/TerminalDock.tsx
@@ -33,6 +33,7 @@ export function TerminalDock({
const panelRef = usePanelRef();
const [isCollapsed, setIsCollapsed] = useState(!visible);
const xtermBackground = xtermThemeForMode(resolvedTheme).background;
+ const bottomOpen = visible && dockPosition === 'bottom';
const showBottomRevealTab = !visible && dockPosition === 'bottom' && onReveal != null;
const [initialHeightPx] = useState(() => getInitialTerminalHeight());
@@ -64,12 +65,12 @@ export function TerminalDock({
useEffect(() => {
const panel = panelRef.current;
if (panel == null) return;
- if (visible && dockPosition === 'bottom') {
+ if (bottomOpen) {
panel.resize(`${heightPxRef.current}px`);
} else {
panel.collapse();
}
- }, [visible, panelRef, dockPosition]);
+ }, [bottomOpen, panelRef]);
return (
- {/* The handle drags only while the terminal is open: you can resize it, and
- drag all the way down to collapse (hide). Once hidden it is disabled — the
- reveal tab is the single open mechanism, so there is no drag-up-to-open
- (which would be a second, redundant way in). Gating on the controlled
- `visible` prop (not `isCollapsed`) means an in-progress drag-to-collapse
+ {/* The handle drags only while the bottom panel is open: you can resize it,
+ and drag all the way down to collapse (hide). Otherwise it is disabled —
+ when bottom-docked-and-hidden the reveal tab rendered above is the single
+ way back in, and when right-docked the ways in live outside this file
+ (EditorArea's reveal tab; the tab strip's dock-toggle) — so there is no
+ drag-up-to-open (which would be a second, redundant way in). Gating on
+ controlled props (not `isCollapsed`) means an in-progress drag-to-collapse
completes before the handle disables on the next commit. */}
{
- if (!visible) return;
+ if (!bottomOpen) return;
setIsDragging(true);
isDraggingRef.current = true;
const handleUp = () => {
@@ -123,7 +126,7 @@ export function TerminalDock({
id={TERMINAL_PANEL_ID}
style={{ backgroundColor: xtermBackground }}
panelRef={panelRef}
- defaultSize={visible && dockPosition === 'bottom' ? `${initialHeightPx}px` : 0}
+ defaultSize={bottomOpen ? `${initialHeightPx}px` : 0}
minSize="120px"
maxSize="95%"
collapsible