diff --git a/console/index.html b/console/index.html
index 5274b90..f3321b1 100644
--- a/console/index.html
+++ b/console/index.html
@@ -164,6 +164,11 @@
+
+
edit fleets.toml
diff --git a/console/src/main.ts b/console/src/main.ts
index a3ad2cb..726840a 100644
--- a/console/src/main.ts
+++ b/console/src/main.ts
@@ -20,6 +20,7 @@ import { initDeployPanel, type DeployPanelHandle, type DeployedInfo } from "./de
import { createPane, bindBackend, type Level } from "./log";
import { initThemeToggle } from "./theme";
import { initSplitPane } from "./splitPane";
+import { initRowResize, applySavedRowHeight } from "./rowResize";
import { EditorView, basicSetup } from "codemirror";
import { EditorState } from "@codemirror/state";
import { StreamLanguage } from "@codemirror/language";
@@ -49,6 +50,8 @@ const configEl = document.getElementById("config");
const fleetDetailEl = document.getElementById("fleet-detail");
const fdHeaderEl = document.getElementById("fd-header");
const remoteEl = document.getElementById("remote");
+const drilldownRowEl = document.getElementById("drilldown-row");
+const rowResizerEl = document.getElementById("row-resizer");
const editorSection = document.getElementById("config-editor");
const editorMount = document.getElementById("cfg-editor-mount");
const editorError = document.getElementById("cfg-editor-error");
@@ -400,6 +403,18 @@ function showEditorError(msg: string | null): void {
editorError.hidden = !msg;
}
+// Floors the row's height at the window's bottom edge — the default, "not
+// split" mode (`.drilldown-row` without `.is-split`). While the editor is
+// open, `openEditor`/`closeEditor` switch the row into a fixed, user-
+// draggable height instead (`rowResize.ts`'s `applySavedRowHeight`/
+// `initRowResize`) and this is what `closeEditor` restores on the way out.
+function syncDrilldownHeight(): void {
+ if (!drilldownRowEl) return;
+ const top = drilldownRowEl.getBoundingClientRect().top;
+ const h = Math.max(200, window.innerHeight - top - 16);
+ drilldownRowEl.style.setProperty("--drilldown-row-h", `${h}px`);
+}
+
// Fill the editor down to the window's bottom edge (Brett: it capped at a
// fixed height with a chunk of blank space below) instead of a flat number —
// the space above it (the Fleets row's height) isn't constant, so this is
@@ -448,6 +463,14 @@ async function openEditor(target: EditorTarget): Promise {
}),
});
editorSection.hidden = false;
+ // The row switches from auto-fill into a fixed, draggable height once
+ // there's something below it to divide space against (Brett wanted the
+ // row's own height adjustable, not just the left/right split).
+ if (drilldownRowEl) {
+ drilldownRowEl.classList.add("is-split");
+ applySavedRowHeight(drilldownRowEl, syncEditorHeight);
+ }
+ if (rowResizerEl) rowResizerEl.hidden = false;
syncEditorHeight();
editorView.focus();
}
@@ -456,6 +479,11 @@ function closeEditor(): void {
editorView?.destroy();
editorView = null;
if (editorSection) editorSection.hidden = true;
+ if (rowResizerEl) rowResizerEl.hidden = true;
+ if (drilldownRowEl) {
+ drilldownRowEl.classList.remove("is-split");
+ syncDrilldownHeight();
+ }
showEditorError(null);
}
@@ -963,15 +991,15 @@ async function boot(): Promise {
// columns match it. `.drilldown-row`'s own top is fixed (topbar height +
// `.content`'s padding, neither content-dependent), so this only needs
// `resize`, not a ResizeObserver on the row itself.
- const drilldownRowEl = document.getElementById("drilldown-row");
- const syncDrilldownHeight = (): void => {
- if (!drilldownRowEl) return;
- const top = drilldownRowEl.getBoundingClientRect().top;
- const h = Math.max(200, window.innerHeight - top - 16);
- drilldownRowEl.style.setProperty("--drilldown-row-h", `${h}px`);
- };
syncDrilldownHeight();
window.addEventListener("resize", syncDrilldownHeight);
+ // Drag-resize between the row and the editor, shown only while the editor
+ // is open (`openEditor`/`closeEditor` toggle `rowResizerEl.hidden` and the
+ // row's `is-split` mode) — Brett wanted the row's own height adjustable
+ // too, not just auto-filled.
+ if (rowResizerEl && drilldownRowEl) {
+ initRowResize(rowResizerEl, drilldownRowEl, syncEditorHeight);
+ }
// Keep the fleets.toml/agents.toml editor filling the window as it's
// resized (`syncEditorHeight` no-ops while closed).
window.addEventListener("resize", syncEditorHeight);
diff --git a/console/src/rowResize.test.ts b/console/src/rowResize.test.ts
new file mode 100644
index 0000000..2a493db
--- /dev/null
+++ b/console/src/rowResize.test.ts
@@ -0,0 +1,26 @@
+import { describe, it, expect } from "vitest";
+import { clampHeight, MIN_HEIGHT, MAX_HEIGHT, DEFAULT_HEIGHT } from "./rowResize";
+
+describe("clampHeight", () => {
+ it("passes a value already inside the range through unchanged", () => {
+ expect(clampHeight(500)).toBe(500);
+ });
+
+ it("floors to MIN_HEIGHT", () => {
+ expect(clampHeight(0)).toBe(MIN_HEIGHT);
+ expect(clampHeight(-100)).toBe(MIN_HEIGHT);
+ });
+
+ it("ceils to MAX_HEIGHT", () => {
+ expect(clampHeight(5000)).toBe(MAX_HEIGHT);
+ });
+
+ it("rounds fractional pixels", () => {
+ expect(clampHeight(500.6)).toBe(501);
+ });
+
+ it("falls back to DEFAULT_HEIGHT for non-finite input", () => {
+ expect(clampHeight(NaN)).toBe(DEFAULT_HEIGHT);
+ expect(clampHeight(Infinity)).toBe(DEFAULT_HEIGHT);
+ });
+});
diff --git a/console/src/rowResize.ts b/console/src/rowResize.ts
new file mode 100644
index 0000000..68169d2
--- /dev/null
+++ b/console/src/rowResize.ts
@@ -0,0 +1,74 @@
+// Drag-resize between the drilldown row (Fleets/chat) and whatever sits below
+// it — today just the fleets.toml/agents.toml editor, the only other
+// standing section. Shown only while that's open: with nothing below there's
+// nothing to divide the row's height against, so the row just auto-fills the
+// window instead (`main.ts`'s `syncDrilldownHeight`). Same pure
+// clamp/persist + DOM-wiring split as splitPane.ts (the left/right version).
+
+const STORAGE_KEY = "oab-studio.drilldown-row-height";
+export const MIN_HEIGHT = 160;
+export const MAX_HEIGHT = 2000;
+export const DEFAULT_HEIGHT = 360;
+
+// Pure: clamp a candidate row height (px) into the allowed range.
+export function clampHeight(px: number): number {
+ if (!Number.isFinite(px)) return DEFAULT_HEIGHT;
+ return Math.min(MAX_HEIGHT, Math.max(MIN_HEIGHT, Math.round(px)));
+}
+
+// Read the saved height, tolerating a missing/garbage value.
+export function readHeight(): number {
+ try {
+ const raw = localStorage.getItem(STORAGE_KEY);
+ const n = raw === null ? NaN : Number(raw);
+ return clampHeight(n);
+ } catch {
+ return DEFAULT_HEIGHT;
+ }
+}
+
+export function saveHeight(px: number): void {
+ try {
+ localStorage.setItem(STORAGE_KEY, String(clampHeight(px)));
+ } catch {
+ /* storage unavailable — in-memory only for this session */
+ }
+}
+
+// Apply the saved (or default) height to `row` — called whenever the editor
+// opens, switching the row from auto-fill into its fixed, draggable mode.
+// `onChange` is the caller's hook to keep dependent layout (the editor's own
+// fill height) in sync with the row's new size.
+export function applySavedRowHeight(row: HTMLElement, onChange: () => void): void {
+ row.style.setProperty("--drilldown-row-h", `${readHeight()}px`);
+ onChange();
+}
+
+// Wire the drag handle: drag up/down to shrink/grow `row` (it sits above the
+// handle), clamping live and persisting once on release. `onChange` runs on
+// every live update too, not just at the end — the editor below needs to
+// reflow as the row's height changes, not just once it settles.
+export function initRowResize(handle: HTMLElement, row: HTMLElement, onChange: () => void): void {
+ let startY = 0;
+ let startHeight = 0;
+
+ const onMove = (e: MouseEvent): void => {
+ const next = clampHeight(startHeight + (e.clientY - startY));
+ row.style.setProperty("--drilldown-row-h", `${next}px`);
+ onChange();
+ };
+ const onUp = (e: MouseEvent): void => {
+ document.removeEventListener("mousemove", onMove);
+ document.removeEventListener("mouseup", onUp);
+ handle.classList.remove("is-dragging");
+ saveHeight(clampHeight(startHeight + (e.clientY - startY)));
+ };
+ handle.addEventListener("mousedown", (e) => {
+ startY = e.clientY;
+ startHeight = row.getBoundingClientRect().height;
+ handle.classList.add("is-dragging");
+ document.addEventListener("mousemove", onMove);
+ document.addEventListener("mouseup", onUp);
+ e.preventDefault();
+ });
+}
diff --git a/console/src/splitPane.ts b/console/src/splitPane.ts
index e5985f9..d119110 100644
--- a/console/src/splitPane.ts
+++ b/console/src/splitPane.ts
@@ -4,8 +4,11 @@
// (same split as theme.ts/theme toggle).
const STORAGE_KEY = "oab-studio.drilldown-side-width";
-export const MIN_WIDTH = 280;
-export const MAX_WIDTH = 900;
+// Brett: wanted more adjustment budget than 280–900 — widened both ends.
+// `.drilldown-main` gets its own 360px floor (styles.css) so the left column
+// can't get crushed unreadable when the side is dragged near MAX_WIDTH.
+export const MIN_WIDTH = 220;
+export const MAX_WIDTH = 1200;
export const DEFAULT_WIDTH = 420;
// Pure: clamp a candidate side-column width (px) into the allowed range.
diff --git a/console/src/styles.css b/console/src/styles.css
index d184f82..4ec4d6c 100644
--- a/console/src/styles.css
+++ b/console/src/styles.css
@@ -207,9 +207,21 @@ body {
.drilldown-row[hidden] {
display: none;
}
+/* While the editor below is open, the row's height becomes a fixed,
+ user-draggable value (`#row-resizer`, `rowResize.ts`) instead of an
+ auto-fill floor — content that doesn't fit scrolls inside the row rather
+ than pushing the editor further down the page. `main.ts` toggles this
+ class in `openEditor`/`closeEditor`. */
+.drilldown-row.is-split {
+ height: var(--drilldown-row-h, auto);
+ min-height: 0;
+ overflow-y: auto;
+}
.drilldown-main {
flex: 1 1 0;
- min-width: 0;
+ /* A floor so dragging .drilldown-side toward MAX_WIDTH (splitPane.ts)
+ can't crush this column unreadable. */
+ min-width: 360px;
display: flex;
flex-direction: column;
}
@@ -256,10 +268,38 @@ body {
.drilldown-resizer.is-dragging::after {
background: var(--s-starting);
}
+/* Drag handle between the row and the editor below — the vertical twin of
+ .drilldown-resizer above, wired by main.ts (src/rowResize.ts owns the
+ pure clamp/persist logic). Hidden by default; `openEditor`/`closeEditor`
+ (main.ts) show/hide it alongside the editor itself. */
+.row-resizer {
+ height: 13px;
+ margin: -2px 0;
+ cursor: row-resize;
+ position: relative;
+ z-index: 1;
+}
+.row-resizer::after {
+ content: "";
+ position: absolute;
+ left: 4px;
+ right: 4px;
+ top: 50%;
+ height: 2px;
+ border-radius: 2px;
+ background: var(--border);
+ transform: translateY(-50%);
+}
+.row-resizer:hover::after,
+.row-resizer.is-dragging::after {
+ background: var(--s-starting);
+}
.drilldown-side {
flex: 0 0 var(--drilldown-side-w, 420px);
- min-width: 280px;
- max-width: 70vw;
+ /* Mirrors splitPane.ts's MIN_WIDTH/MAX_WIDTH — a vw-relative backstop for
+ window sizes those px bounds don't anticipate, not the primary limit. */
+ min-width: 220px;
+ max-width: 85vw;
display: flex;
flex-direction: column;
gap: 12px;