import { useLayoutEffect } from "react";
import { t } from "../../../i18n";
import { useThemeColors } from "../../../theme/theme-context";
import { Box, Text, TextAttributes } from "../../../ui";
import { Button, Spinner } from "../../ui";
import type { ListScreenState } from "../list/model";
import { CommandBarListBody } from "../list/view";
import { CommandBarMultiSelectBody, isMultiSelectPickerRoute } from "../multi-select-picker";
import { ThemePicker } from "../theme-picker";
import { truncateText } from "../view-model";
import { CommandBarWorkflowBody } from "../workflow/body";
import type { CommandBarConfirmRoute, CommandBarRoute } from "../workflow/types";
import { NATIVE_COMMAND_SURFACE, nativeCommandSurfaceBorder } from "./native-surface";
import { useCommandBarPalette } from "./palette";
import { publishCommandBarPrompt } from "./prompt-binding";
import type { CommandBarPanelProps } from "./types";
const COMMAND_BAR_OVERLAY_Z_INDEX = 2_147_483_646;
const COMMAND_BAR_PANEL_Z_INDEX = 2_147_483_647;
/**
* The root screen keeps the closed prompt's own words. Opening the bar puts the
* caret in the same input the header was already showing, so changing the
* placeholder underneath it read as the control being swapped out. Nested
* screens are a different screen and say so.
*/
function resolvePromptPlaceholder(listState: ListScreenState): string {
if (listState.kind === "root") return t("Search or run a command");
if (listState.title === "Security Description") return t("Search tickers");
return t("Filter");
}
export function CommandBarPanel({
bodyHeight,
bodySlotKey,
committedThemeId,
contentPadding,
currentRoute,
getWorkflowInputRef,
hasChromeRow,
labelWidth,
listBodyHeight,
nativeListRows,
nativeListScrollRef,
nativeOccluderRect,
nativePaneChrome,
onBack,
onConfirmRoute,
onFieldFocus,
onFieldPickerOpen,
onFieldValueChange,
onListHoverIndex,
onListRowMouseDown,
onListScroll,
onMoveFieldFocus,
onMultiSelectCommit,
onMultiSelectSelect,
onMultiSelectToggle,
onNativeOccluderChange,
onSelectFieldRef,
onOverlayClose,
onQueryChange,
onThemeCommit,
onThemePreview,
onWorkflowActiveTextareaSync,
onWorkflowSubmit,
panelBounds,
queryDisplayWidth,
rootGhostSuffix,
rootShortcutFeedback,
selectedScrollRowIndex,
termHeight,
termWidth,
themePickerActive,
themePickerFilter,
themePickerRef,
trailingWidth,
visibleListState,
workflowScrollRef,
}: CommandBarPanelProps) {
const colors = useThemeColors();
const palette = useCommandBarPalette(nativePaneChrome);
useLayoutEffect(() => {
const scrollBox = nativeListScrollRef.current;
if (!scrollBox) return;
if (scrollBox.verticalScrollBar) scrollBox.verticalScrollBar.visible = false;
if (selectedScrollRowIndex < 0) return;
const viewportHeight = Math.max(1, scrollBox.viewport?.height ?? listBodyHeight);
if (selectedScrollRowIndex < scrollBox.scrollTop) {
scrollBox.scrollTo(selectedScrollRowIndex);
} else if (selectedScrollRowIndex >= scrollBox.scrollTop + viewportHeight) {
scrollBox.scrollTo(selectedScrollRowIndex - viewportHeight + 1);
}
}, [listBodyHeight, nativeListScrollRef, selectedScrollRowIndex, visibleListState?.kind, visibleListState?.query]);
// The header prompt is the bar's input while a list screen is showing. A
// workflow owns its own fields, so it publishes nothing and the prompt goes
// quiet rather than taking focus from them.
useLayoutEffect(() => {
if (!visibleListState) {
publishCommandBarPrompt(null);
return;
}
publishCommandBarPrompt({
screenKey: `${visibleListState.kind}:${visibleListState.title}`,
query: visibleListState.query,
placeholder: resolvePromptPlaceholder(visibleListState),
ghostSuffix: visibleListState.kind === "root" ? rootGhostSuffix : null,
onQueryChange,
});
}, [onQueryChange, rootGhostSuffix, visibleListState]);
useLayoutEffect(() => () => publishCommandBarPrompt(null), []);
useLayoutEffect(() => {
onNativeOccluderChange?.(nativeOccluderRect);
return () => {
onNativeOccluderChange?.(null);
};
}, [
nativeOccluderRect.height,
nativeOccluderRect.width,
nativeOccluderRect.x,
nativeOccluderRect.y,
onNativeOccluderChange,
]);
return (
// The click-away overlay starts where the sheet does: the header above it
// holds the bar's input, and a click there must reach it, not close the bar.
{
event.stopPropagation?.();
event.preventDefault?.();
onOverlayClose();
}}
>
{
event.stopPropagation?.();
}}
data-gloom-role="command-bar-panel"
style={nativePaneChrome ? {
// The lower half of the control the header input opens: rounded and
// bordered along its three free edges, open where the input sits.
border: `1px solid ${nativeCommandSurfaceBorder(colors)}`,
borderTopWidth: 0,
borderRadius: `0 0 ${NATIVE_COMMAND_SURFACE.radiusPx}px ${NATIVE_COMMAND_SURFACE.radiusPx}px`,
boxShadow: NATIVE_COMMAND_SURFACE.shadow,
overflow: "hidden",
padding: `${NATIVE_COMMAND_SURFACE.paddingYPx}px ${NATIVE_COMMAND_SURFACE.paddingXPx}px`,
} : undefined}
>
{/* The desktop sheet pads itself in CSS; the terminal spends a row. */}
{!nativePaneChrome && }
{/* Rows stop at the results column, so a selection bar on a wide window
does not run on past the text into empty sheet. */}
{/* The query itself is typed in the header prompt. This row only exists
when it has something to say: the way back from a nested screen, or
what a typed prefix resolved to. Its height is reserved in
panel/layout.ts, which is why the render is keyed on the same flag. */}
{hasChromeRow && (
<>
{currentRoute ? (
<>
{truncateText(t(getCommandBarPanelTitle(currentRoute)), Math.max(1, queryDisplayWidth - 8))}
>
) : rootShortcutFeedback ? (
{truncateText(rootShortcutFeedback, queryDisplayWidth)}
) : null}
>
)}
{themePickerActive && (
)}
{visibleListState && !themePickerActive && !isMultiSelectPickerRoute(currentRoute) && (
)}
{currentRoute?.kind === "workflow" && (
)}
{currentRoute?.kind === "confirm" && (
)}
{isMultiSelectPickerRoute(currentRoute) && (
)}
{!nativePaneChrome && }
);
}
function getCommandBarPanelTitle(route: CommandBarRoute): string {
if (route.kind === "mode") {
if (route.screen === "layout") return "Layout Actions";
return "Security Description";
}
if (route.kind === "picker") return route.title;
if (route.kind === "pane-settings") return "Pane Settings";
if (route.kind === "workflow") return route.title;
return route.title;
}
function CommandBarConfirmBody({
route,
bodyHeight,
contentPadding,
queryDisplayWidth,
onConfirm,
}: {
route: CommandBarConfirmRoute;
bodyHeight: number;
contentPadding: number;
queryDisplayWidth: number;
onConfirm: () => void;
}) {
const themeColors = useThemeColors();
const palette = useCommandBarPalette(false);
return (
{route.body.map((line, index) => (
{truncateText(t(line), queryDisplayWidth)}
))}
{route.error && (
{truncateText(route.error, queryDisplayWidth)}
)}
{route.pending && (
)}
);
}