fix(app): working scrollback search + stop prompt duplication on focus

Search fixes:
- TerminalView sets allowProposedApi (the search addon's match decorations
  use registerMarker/registerDecoration); without it findNext threw before
  firing results, so the counter was stuck at 0/0.
- The search bar now renders inside the panel it targets (in the header)
  instead of a global top-right overlay, so it's obvious which panel is
  searched.
- Search is anchored to the panel it was opened on (searchSurfaceId) and no
  longer follows focus — opening it in one panel and switching away no longer
  shows it open elsewhere.

Prompt duplication:
- The focus border was 1px when unfocused, 2px when focused; with border-box
  that resized the content on every focus switch, firing ResizeObserver -> fit
  -> PTY SIGWINCH and making zsh/powerlevel10k reprint its prompt. The border
  is now a constant 2px, color-only on focus.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-14 08:34:43 +07:00
parent a30ec1cc7f
commit 04ac7cdec2
4 changed files with 43 additions and 18 deletions
+20 -4
View File
@@ -1,6 +1,7 @@
import { useEffect, useRef, useState } from "react";
import { Maximize2, Minimize2, RotateCw, GripVertical } from "lucide-react";
import { TerminalView } from "./TerminalView";
import { SearchBar } from "./SearchBar";
import { StatusRing } from "./StatusRing";
import { COLORS, FONT, STATE_COLOR } from "./theme";
import type { LayoutNode, SurfaceState, SurfaceView } from "./layoutTypes";
@@ -16,6 +17,11 @@ interface Props {
focusedId: string | null;
onFocus: (id: string) => void;
zoomed: string | null;
/** The surface whose scrollback search bar is open, or null. Anchored to the
* panel it was opened on — it does NOT follow focus. */
searchSurfaceId: string | null;
searchNonce: number;
onCloseSearch: () => void;
}
type Edge = "left" | "right" | "top" | "bottom";
@@ -34,7 +40,7 @@ function shortPath(cwd: string): string {
return leaf ? `~/${leaf}` : cwd;
}
export function LayoutEngine({ workspaceId, layout, running, states, surfaces, focusedId, onFocus, zoomed }: Props) {
export function LayoutEngine({ workspaceId, layout, running, states, surfaces, focusedId, onFocus, zoomed, searchSurfaceId, searchNonce, onCloseSearch }: Props) {
// Panel drag-to-reorder. Implemented with raw pointer events rather than the
// HTML5 drag API, which is unreliable in the macOS WKWebView Tauri uses.
const [drop, setDrop] = useState<DropTarget | null>(null);
@@ -72,7 +78,7 @@ export function LayoutEngine({ workspaceId, layout, running, states, surfaces, f
if (!layout) {
return <div style={{ color: COLORS.textMuted, padding: 24 }}>Empty workspace apply a preset to add panels.</div>;
}
const shared = { workspaceId, running, states, surfaces, focusedId, onFocus, zoomed, drop, onStartPanelDrag: startPanelDrag };
const shared = { workspaceId, running, states, surfaces, focusedId, onFocus, zoomed, drop, onStartPanelDrag: startPanelDrag, searchSurfaceId, searchNonce, onCloseSearch };
if (zoomed) {
return (
<div style={{ width: "100%", height: "100%", padding: 12, boxSizing: "border-box" }}>
@@ -94,6 +100,9 @@ interface NodeProps {
zoomed: string | null;
drop: DropTarget | null;
onStartPanelDrag: (srcId: string, e: React.MouseEvent) => void;
searchSurfaceId: string | null;
searchNonce: number;
onCloseSearch: () => void;
}
function Node({ node, path, ...rest }: NodeProps) {
@@ -103,7 +112,7 @@ function Node({ node, path, ...rest }: NodeProps) {
return <SplitView split={node.split} path={path} {...rest} />;
}
function Leaf({ id, workspaceId, running, states, surfaces, focusedId, onFocus, zoomed, drop, onStartPanelDrag }: Omit<NodeProps, "node" | "path"> & { id: string }) {
function Leaf({ id, workspaceId, running, states, surfaces, focusedId, onFocus, zoomed, drop, onStartPanelDrag, searchSurfaceId, searchNonce, onCloseSearch }: Omit<NodeProps, "node" | "path"> & { id: string }) {
const focused = focusedId === id;
const dropEdge = drop && drop.id === id ? drop.edge : null;
@@ -114,7 +123,11 @@ function Leaf({ id, workspaceId, running, states, surfaces, focusedId, onFocus,
style={{
position: "relative", display: "flex", flexDirection: "column", width: "100%", height: "100%",
background: COLORS.bgPanel, borderRadius: 8, overflow: "hidden",
border: focused ? `2px solid ${COLORS.accent}` : `1px solid ${COLORS.borderSubtle}`,
// Constant 2px border, color-only on focus. A width change (1px<->2px)
// would resize the inner content box, fire ResizeObserver -> fit -> PTY
// SIGWINCH, making zsh/powerlevel10k reprint its prompt on every focus
// switch (the "stacked prompts" bug).
border: `2px solid ${focused ? COLORS.accent : COLORS.borderSubtle}`,
boxSizing: "border-box",
}}
>
@@ -170,6 +183,9 @@ function Leaf({ id, workspaceId, running, states, surfaces, focusedId, onFocus,
<div style={{ flex: 1, minHeight: 0 }}>
<TerminalView key={id} surfaceId={id} />
</div>
{searchSurfaceId === id && (
<SearchBar surfaceId={id} reopenNonce={searchNonce} onClose={onCloseSearch} />
)}
</>
);
}