+ {children.map((child, i) => (
+
+
+ {i < children.length - 1 && (
+
startDrag(i, e)}
+ style={{
+ position: "absolute", zIndex: 4,
+ ...(orient === "h"
+ ? { top: 0, bottom: 0, right: -5, width: 10, cursor: "col-resize" }
+ : { left: 0, right: 0, bottom: -5, height: 10, cursor: "row-resize" }),
+ }} />
+ )}
+
+ ))}
+
);
}
diff --git a/app/src/SearchBar.tsx b/app/src/SearchBar.tsx
index eac81fa..1956a51 100644
--- a/app/src/SearchBar.tsx
+++ b/app/src/SearchBar.tsx
@@ -45,16 +45,17 @@ export function SearchBar({
};
}, [surfaceId]);
- function run(forward: boolean) {
+ function run(forward: boolean, override?: string) {
if (!surfaceId) return;
const addon = getSearch(surfaceId);
- if (!addon || !term) {
+ const query = override ?? term;
+ if (!addon || !query) {
addon?.clearDecorations();
setCount({ index: -1, total: 0 });
return;
}
- if (forward) addon.findNext(term, SEARCH_OPTS);
- else addon.findPrevious(term, SEARCH_OPTS);
+ if (forward) addon.findNext(query, SEARCH_OPTS);
+ else addon.findPrevious(query, SEARCH_OPTS);
}
return (
@@ -79,9 +80,14 @@ export function SearchBar({
ref={inputRef}
value={term}
onChange={(e) => {
- setTerm(e.target.value);
- setCount({ index: -1, total: 0 });
- if (surfaceId) getSearch(surfaceId)?.clearDecorations();
+ const value = e.target.value;
+ setTerm(value);
+ if (!value) {
+ setCount({ index: -1, total: 0 });
+ if (surfaceId) getSearch(surfaceId)?.clearDecorations();
+ } else {
+ run(true, value); // search-as-you-type
+ }
}}
onKeyDown={(e) => {
if (e.key === "Enter") {
diff --git a/app/src/TerminalView.tsx b/app/src/TerminalView.tsx
index 52e0116..661ca68 100644
--- a/app/src/TerminalView.tsx
+++ b/app/src/TerminalView.tsx
@@ -2,6 +2,7 @@ import { useEffect, useRef } from "react";
import { Terminal } from "@xterm/xterm";
import { WebglAddon } from "@xterm/addon-webgl";
import { SearchAddon } from "@xterm/addon-search";
+import { FitAddon } from "@xterm/addon-fit";
import { attachSurface, detachSurface, sendInput, resizeSurface } from "./socketBridge";
import { registerSearch, unregisterSearch } from "./searchRegistry";
@@ -25,6 +26,27 @@ export function TerminalView({ surfaceId }: { surfaceId: string }) {
term.loadAddon(search);
registerSearch(surfaceId, search);
+ const fit = new FitAddon();
+ term.loadAddon(fit);
+
+ // Fit the grid to the container and tell the daemon the new size. Coalesced
+ // through rAF so a burst of resize callbacks yields one resize per frame.
+ let rafId = 0;
+ let lastCols = 0, lastRows = 0;
+ const doFit = () => {
+ rafId = 0;
+ try { fit.fit(); } catch { return; }
+ if (term.cols !== lastCols || term.rows !== lastRows) {
+ lastCols = term.cols;
+ lastRows = term.rows;
+ void resizeSurface(surfaceId, term.cols, term.rows);
+ }
+ };
+ const scheduleFit = () => { if (!rafId) rafId = requestAnimationFrame(doFit); };
+
+ const ro = new ResizeObserver(scheduleFit);
+ ro.observe(ref.current);
+
// Input → daemon.
const inputDisposable = term.onData((data) => {
void sendInput(surfaceId, encoder.encode(data));
@@ -38,14 +60,15 @@ export function TerminalView({ surfaceId }: { surfaceId: string }) {
}).then((res) => {
if (disposed) return;
if (res.snapshot) term.write(res.snapshot);
- if (res.cols && res.rows) {
- term.resize(res.cols, res.rows);
- void resizeSurface(surfaceId, res.cols, res.rows);
- }
+ // Fit to the actual container rather than the daemon's stored geometry,
+ // then push the resulting size back so the PTY reflows to match.
+ scheduleFit();
});
return () => {
disposed = true;
+ if (rafId) cancelAnimationFrame(rafId);
+ ro.disconnect();
inputDisposable.dispose();
void detachSurface(surfaceId);
unregisterSearch(surfaceId);
diff --git a/app/src/TopBar.tsx b/app/src/TopBar.tsx
index 72e3342..df90abc 100644
--- a/app/src/TopBar.tsx
+++ b/app/src/TopBar.tsx
@@ -1,4 +1,4 @@
-import { FolderGit2, PanelRight, Search, Bell, Settings, ChevronDown } from "lucide-react";
+import { FolderGit2, PanelLeft, PanelRight, Search, Bell, Settings, ChevronDown } from "lucide-react";
import { COLORS, FONT } from "./theme";
import type { WorkspaceView } from "./layoutTypes";
import { leafIds } from "./layoutTypes";
@@ -29,11 +29,14 @@ function IconBtn({ icon, onClick, active, title }: { icon: React.ReactNode; onCl
}
export function TopBar({
- active, eventsOpen, onToggleEvents, unread,
+ active, eventsOpen, onToggleEvents, onShowEvents, sidebarOpen, onToggleSidebar, unread,
}: {
active: WorkspaceView | null;
eventsOpen: boolean;
onToggleEvents: () => void;
+ onShowEvents: () => void;
+ sidebarOpen: boolean;
+ onToggleSidebar: () => void;
unread: number;
}) {
return (
@@ -44,8 +47,8 @@ export function TopBar({
borderBottom: `1px solid ${COLORS.borderSubtle}`,
}}
>
- {/* macOS traffic-light spacer — real lights are drawn by the window chrome. */}
-
+ {/* Left: sidebar toggle, flush to the left edge. */}
+
} onClick={onToggleSidebar} active={sidebarOpen} title="Toggle Sidebar" />
{/* Workspace breadcrumb */}
@@ -67,10 +70,9 @@ export function TopBar({
{/* Right cluster */}
-
} onClick={onToggleEvents} active={eventsOpen} title="Toggle Event Center" />
} title="Search (mock)" />
- } title="Notifications (mock)" />
+ } onClick={onShowEvents} active={eventsOpen} title="Open activity log" />
{unread > 0 && (
)}
+
} onClick={onToggleEvents} active={eventsOpen} title="Toggle Event Center" />
} title="Settings (mock)" />