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
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,12 @@ parallel copies under `docs/` or `scripts/notes/`. At cut time: rename

## [Unreleased]

### TUI

- `ask_operator` is an inset overlay again so the transcript stays visible while
the operator answers.
- The `full_shell` overlay mode is removed; every overlay is inset.

## [0.3.7] - 2026-08-27

### Fixed
Expand Down
12 changes: 0 additions & 12 deletions src/tui/geometry.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -438,18 +438,6 @@ describe("resolveGeometry — overlay modes", () => {
});
expect(layout.overlayHeight).toBeGreaterThanOrEqual(5);
});

test("full_shell hides transcript and gives residual to overlay_host", () => {
const layout = idle80x24({
overlay: { mode: "full_shell", bodyRows: 20 },
});
expect(layout.overlayMode).toBe("full_shell");
expect(layout.transcriptHeight).toBe(0);
expect(layout.heights.prompt).toBe(0);
expect(layout.heights.notice).toBe(0);
expect(layout.overlayHeight).toBeGreaterThan(0);
expect(layout.overlayHeight + layout.chromeHeight).toBe(24);
});
});

describe("resolveGeometry — resize / residual", () => {
Expand Down
42 changes: 1 addition & 41 deletions src/tui/geometry/resolve.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export interface TerminalSize {
readonly rows: number;
}

export type OverlayMode = "closed" | "inset" | "full_shell";
export type OverlayMode = "closed" | "inset";

export interface OverlayInput {
readonly mode: OverlayMode;
Expand Down Expand Up @@ -173,7 +173,6 @@ function sumChrome(heights: MutableHeights): number {
}

function transcriptFloorFor(mode: OverlayMode, terminalRows: number): number {
if (mode === "full_shell") return 0;
if (mode === "inset") {
// Proposed ≥ 8 on 24-row; scale gently on shorter terminals.
if (terminalRows < 24)
Expand All @@ -199,11 +198,6 @@ function desiredOverlayHeight(
const requested = input.overlay?.bodyRows ?? Math.floor(rows * 0.4);
const fracCap = Math.floor(rows * OVERLAY_MAX_FRACTION);

if (mode === "full_shell") {
// Overlay owns residual after any remaining chrome (usually 0 after hide).
return Math.max(0, rows - chrome);
}

// inset: leave transcript floor; never exceed fraction cap
const floorSafe = Math.max(0, rows - chrome - floor);
return clamp(requested, 0, Math.min(fracCap, floorSafe));
Expand Down Expand Up @@ -326,40 +320,6 @@ export function resolveGeometry(input: GeometryInput): GeometryLayout {
const sideMargin = resolveSideMargin(terminal.columns);
const layoutMode: LayoutMode = "stack";

// Full-shell modal: hide transcript and bottom chrome; overlay owns residual.
if (mode === "full_shell") {
heights.transcript = 0;
heights.task = 0;
heights.agents = 0;
heights.plugin_banner = 0;
heights.command_banner = 0;
heights.settings_notice = 0;
heights.progress = 0;
heights.progress_divider = 0;
heights.notice = 0;
heights.prompt = 0;
const chrome = sumChrome(heights);
heights.overlay_host = Math.max(0, terminal.rows - chrome);
const regions = assignRects(heights, terminal);
return {
terminal,
transcriptHeight: 0,
chromeHeight: chrome,
overlayHeight: heights.overlay_host,
regions,
heights,
collapsed,
overlayMode: mode,
transcriptFloor: floor,
sideMargin,
contentWidth,
layoutMode,
chatWidth: contentWidth,
railWidth: 0,
railGutter: 0,
};
}

// Cap prompt growth against floor before overlay allocation.
const promptCap = Math.max(PROMPT_BASE_ROWS, Math.floor(terminal.rows * PROMPT_CAP_FRACTION));
if (heights.prompt > promptCap) heights.prompt = promptCap;
Expand Down
5 changes: 1 addition & 4 deletions src/tui/landing.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -432,8 +432,7 @@ describe("landing screen", () => {
// Heavy inset permission overlay: many choices plus a multi-line body
// so the float must take real headroom from the landing split. A
// three-item empty body leaves message delta 0 and would pass even if
// the split never slid. Operator asks use full_shell (CL-7067) and
// hide the landing instead — that path is covered in overlays.test.ts.
// the split never slid.
const heavyBody = [
"run_shell",
"Run shell command",
Expand Down Expand Up @@ -479,8 +478,6 @@ describe("landing screen", () => {
// float only asked the split for one choice row of headroom. It now asks for
// the overlay's real, already fraction-capped content height, so a terminal
// tall enough for that content shows every choice without scrolling.
// Operator full_shell hides the landing (CL-7067); this coverage stays on
// the inset permission path that still floats over landing.
test("a landing overlay with many choices shows them all when there is room", async () => {
await withTestRenderer(
async (h) => {
Expand Down
65 changes: 2 additions & 63 deletions src/tui/overlays.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -192,7 +192,8 @@ describe("operator question overlay", () => {
try {
openOperatorOverlay(shell);
expect(shell.overlayKind).toBe("operator");
expect(shell.layout.overlayMode).toBe("full_shell");
expect(shell.layout.overlayMode).toBe("inset");
expect(shell.layout.transcriptHeight).toBeGreaterThanOrEqual(OVERLAY_TRANSCRIPT_FLOOR);
expect(shell.overlayBodyLines.length).toBeGreaterThan(0);
expect(shell.overlayItems.length).toBeGreaterThan(3);
expect(focusOwner(shell.focus)).toBe("overlay");
Expand Down Expand Up @@ -223,68 +224,6 @@ describe("operator question overlay", () => {
{ width: 80, height: 24 },
);
});

test("full_shell decisionContext budget shows more body rows than inset", async () => {
// Same long body under both modes: inset stays capped at DECISION_CONTEXT_ROWS
// (8), while full_shell raises the cap with terminal height so a long ask
// stays readable (CL-7067). Mode-only asserts are not enough — pin the
// budget branch that actually shapes overlayBodyLines.
const longBody = [
"Should we proceed with the destructive reset of the working tree?",
...Array.from({ length: 24 }, (_, i) => `Context line ${i + 1}.`),
].join("\n");
const choices = [
"Cancel — keep working tree",
"Allow this once",
"Allow for this session",
] as const;
const size = { width: 80, height: 40 } as const;

async function bodyLineCount(mode: "inset" | "full_shell"): Promise<{
readonly lines: number;
readonly body: readonly string[];
readonly frame: string;
}> {
return withTestRenderer(async (h) => {
const shell = createAppShell(h.renderer, {
terminal: { columns: size.width, rows: size.height },
wireKeys: false,
run: "idle",
});
try {
openListOverlay(shell, {
kind: "operator",
title: "",
body: longBody,
items: [...choices],
overlayMode: mode,
frameId: `overlay-operator-${mode}`,
});
expect(shell.layout.overlayMode).toBe(mode);
await h.renderOnce();
await h.renderOnce();
return {
lines: shell.overlayBodyLines.length,
body: shell.overlayBodyLines,
frame: h.captureCharFrame(),
};
} finally {
shell.dispose();
}
}, size);
}

const inset = await bodyLineCount("inset");
const full = await bodyLineCount("full_shell");
expect(full.lines).toBeGreaterThan(inset.lines);
// Short, non-wrapping context lines map 1:1 to the budget. Inset caps at
// eight context rows (then dither + tail); full_shell's raised cap keeps a
// mid-body line the inset path drops.
expect(full.body.some((line) => line.includes("Context line 12."))).toBe(true);
expect(inset.body.some((line) => line.includes("Context line 12."))).toBe(false);
expect(full.frame).toContain("Context line 12.");
expect(inset.frame).not.toContain("Context line 12.");
});
});

describe("model / provider picker", () => {
Expand Down
4 changes: 1 addition & 3 deletions src/tui/overlays.ts
Original file line number Diff line number Diff line change
Expand Up @@ -148,9 +148,7 @@ export function openOperatorOverlay(shell: AppShell, opts?: OpenOperatorOpts): v
items: choices,
activeIndex: opts?.activeIndex ?? 0,
frameId: "overlay-operator",
// Full shell so long questions and option lists stay readable (CL-7067).
// Permission gates keep the default inset path.
overlayMode: "full_shell",
// Chat-first: keep the transcript visible while the operator answers.
...(opts?.itemIds !== undefined ? { itemIds: opts.itemIds } : {}),
...(opts?.onAccept !== undefined ? { onAccept: opts.onAccept } : {}),
...(opts?.onTextAnswer !== undefined ? { onTextAnswer: opts.onTextAnswer } : {}),
Expand Down
35 changes: 3 additions & 32 deletions src/tui/shell.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1282,12 +1282,8 @@ function relayoutOverlayHost(shell: AppShell, itemCount: number): void {
const perItem = overlayRowsPerItem(shell.overlayKind);
const hostRows = overlayHostRows(shell, shell.overlayBodyLines.length, itemCount * perItem);
const minHostRows = overlayMinHostRows(shell, shell.overlayBodyLines.length, itemCount > 0);
// Preserve the mode the open chose (operator uses full_shell; others inset).
// Hardcoding inset here would collapse a full_shell ask on every list refresh.
const bag = internals.get(shell);
const mode: OverlayMode = bag?.overlayMode === "full_shell" ? "full_shell" : "inset";
relayout(shell, {
overlayMode: mode,
overlayMode: "inset",
overlayBodyRows: hostRows,
overlayMinBodyRows: minHostRows,
});
Expand Down Expand Up @@ -1941,8 +1937,6 @@ interface PriorOverlaySnapshot {
readonly onCancel: (() => void) | null;
readonly addProviderHint: boolean;
readonly setDefaultHint: boolean;
/** Geometry mode the primary used before the palette stacked over it. */
readonly overlayMode: OverlayMode;
}

interface ShellInternals {
Expand Down Expand Up @@ -3435,16 +3429,6 @@ function decisionContextBudget(
): number {
const fixedChrome =
OVERLAY_HOST_BORDER_ROWS + overlayTitleRows(kind) + DECISION_HEADER_AND_TRAILER_ROWS;
// full_shell owns residual after chrome — drop the inset fraction cap so a
// longer question can use the extra rows instead of staying clipped at eight.
const bag = internals.get(shell);
if (bag?.overlayMode === "full_shell") {
const maxOverlayRows = Math.max(0, terminalHeight - PROMPT_BASE_ROWS);
const baseline =
maxOverlayRows - DECISION_CHOICE_ROWS - fixedChrome - DECISION_CONTEXT_BLANK_ROWS;
const fullCap = Math.max(DECISION_CONTEXT_ROWS, Math.floor(terminalHeight / 2));
return Math.max(0, Math.min(fullCap, baseline));
}
// The resolver never lets the overlay host past the fraction cap even when
// the transcript floor and every other zone have already given up their
// rows, so that cap — not just the prompt floor — bounds how much context
Expand Down Expand Up @@ -3570,12 +3554,6 @@ export interface OpenListOverlayOpts {
* server needs authorization.
*/
readonly echoChoice?: boolean;
/**
* Geometry mode for this open. Defaults to inset. Operator asks use
* full_shell so long questions and option lists stay readable; permission
* gates stay inset unless a caller opts in.
*/
readonly overlayMode?: "inset" | "full_shell";
/**
* Claim printable keys for a `>` filter row so the list narrows as you type.
* Opt-in per open (model picker, palette). Overlays without it keep j/k
Expand Down Expand Up @@ -3630,7 +3608,6 @@ export function openListOverlay(shell: AppShell, opts?: OpenListOverlayOpts): vo
onCancel: bag.overlayOnCancel,
addProviderHint: bag.overlayAddProviderHint,
setDefaultHint: bag.overlaySetDefaultHint,
overlayMode: bag.overlayMode === "full_shell" ? "full_shell" : "inset",
};
}
// Leave prior overlay focus frame; palette will stack above it.
Expand All @@ -3650,9 +3627,6 @@ export function openListOverlay(shell: AppShell, opts?: OpenListOverlayOpts): vo

const bag = internals.get(shell);
if (bag) {
// Mode must land before applyOverlayBodyText so decisionContextBudget can
// size the body against full_shell vs inset. Palette is always inset.
bag.overlayMode = !isPalette && opts?.overlayMode === "full_shell" ? "full_shell" : "inset";
// Palette open does not own primary accept; leave prior snapshot's callback.
if (!isPalette) {
bag.overlayItemIds = opts?.itemIds ? [...opts.itemIds] : [];
Expand Down Expand Up @@ -4131,7 +4105,6 @@ export function closeInsetOverlay(shell: AppShell): void {
bag.overlayOnCancel = prior.onCancel;
bag.overlayAddProviderHint = prior.addProviderHint;
bag.overlaySetDefaultHint = prior.setDefaultHint;
bag.overlayMode = prior.overlayMode === "full_shell" ? "full_shell" : "inset";
// If focus was not stacked (edge case), re-open overlay frame.
if (focusOwner(shell.focus) !== "overlay") {
shell.focus = openOverlay(shell.focus, OVERLAY_FRAME_ID, {
Expand All @@ -4143,7 +4116,7 @@ export function closeInsetOverlay(shell: AppShell): void {
const hostRows = overlayHostRows(shell, prior.bodyLines.length, listH);
const minHostRows = overlayMinHostRows(shell, prior.bodyLines.length, prior.list.count > 0);
relayout(shell, {
overlayMode: bag.overlayMode,
overlayMode: "inset",
overlayBodyRows: hostRows,
overlayMinBodyRows: minHostRows,
});
Expand Down Expand Up @@ -4252,10 +4225,8 @@ export function setOverlayBody(shell: AppShell, text: string, maxLines = 8): voi
shell.overlayBodyLines.length,
shell.overlayItems.length > 0,
);
const bag = internals.get(shell);
const mode: OverlayMode = bag?.overlayMode === "full_shell" ? "full_shell" : "inset";
relayout(shell, {
overlayMode: mode,
overlayMode: "inset",
overlayBodyRows: hostRows,
overlayMinBodyRows: minHostRows,
});
Expand Down
Loading