Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions console/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,11 @@
</section>
</div>
</div>
<!-- Drag-resize between the row above and the editor below (rowResize.ts)
— shown only while the editor is open; with nothing below the row
there's nothing to divide its height against, so it just
auto-fills the window instead. -->
<div class="row-resizer" id="row-resizer" title="Drag to resize" hidden></div>
<section id="config-editor" class="cfg-editor-wrap" hidden>
<div class="cfg-editor-head">
<span class="cfg-label" id="cfg-editor-title">edit fleets.toml</span>
Expand Down
42 changes: 35 additions & 7 deletions console/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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");
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -448,6 +463,14 @@ async function openEditor(target: EditorTarget): Promise<void> {
}),
});
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();
}
Expand All @@ -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);
}

Expand Down Expand Up @@ -963,15 +991,15 @@ async function boot(): Promise<void> {
// 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);
Expand Down
26 changes: 26 additions & 0 deletions console/src/rowResize.test.ts
Original file line number Diff line number Diff line change
@@ -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);
});
});
74 changes: 74 additions & 0 deletions console/src/rowResize.ts
Original file line number Diff line number Diff line change
@@ -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();
});
}
7 changes: 5 additions & 2 deletions console/src/splitPane.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
46 changes: 43 additions & 3 deletions console/src/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down Expand Up @@ -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;
Expand Down
Loading