Skip to content

Commit 664f92a

Browse files
Merge pull request #709 from corbitsdev/cl-7176-revert-ask_operator-to-an-inset-overlay
Show operator questions as an inset overlay
2 parents 56e7c6e + 659edec commit 664f92a

7 files changed

Lines changed: 14 additions & 155 deletions

File tree

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,12 @@ parallel copies under `docs/` or `scripts/notes/`. At cut time: rename
1313

1414
## [Unreleased]
1515

16+
### TUI
17+
18+
- `ask_operator` is an inset overlay again so the transcript stays visible while
19+
the operator answers.
20+
- The `full_shell` overlay mode is removed; every overlay is inset.
21+
1622
## [0.3.7] - 2026-08-27
1723

1824
### Fixed

src/tui/geometry.test.ts

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -438,18 +438,6 @@ describe("resolveGeometry — overlay modes", () => {
438438
});
439439
expect(layout.overlayHeight).toBeGreaterThanOrEqual(5);
440440
});
441-
442-
test("full_shell hides transcript and gives residual to overlay_host", () => {
443-
const layout = idle80x24({
444-
overlay: { mode: "full_shell", bodyRows: 20 },
445-
});
446-
expect(layout.overlayMode).toBe("full_shell");
447-
expect(layout.transcriptHeight).toBe(0);
448-
expect(layout.heights.prompt).toBe(0);
449-
expect(layout.heights.notice).toBe(0);
450-
expect(layout.overlayHeight).toBeGreaterThan(0);
451-
expect(layout.overlayHeight + layout.chromeHeight).toBe(24);
452-
});
453441
});
454442

455443
describe("resolveGeometry — resize / residual", () => {

src/tui/geometry/resolve.ts

Lines changed: 1 addition & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ export interface TerminalSize {
2222
readonly rows: number;
2323
}
2424

25-
export type OverlayMode = "closed" | "inset" | "full_shell";
25+
export type OverlayMode = "closed" | "inset";
2626

2727
export interface OverlayInput {
2828
readonly mode: OverlayMode;
@@ -173,7 +173,6 @@ function sumChrome(heights: MutableHeights): number {
173173
}
174174

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

202-
if (mode === "full_shell") {
203-
// Overlay owns residual after any remaining chrome (usually 0 after hide).
204-
return Math.max(0, rows - chrome);
205-
}
206-
207201
// inset: leave transcript floor; never exceed fraction cap
208202
const floorSafe = Math.max(0, rows - chrome - floor);
209203
return clamp(requested, 0, Math.min(fracCap, floorSafe));
@@ -326,40 +320,6 @@ export function resolveGeometry(input: GeometryInput): GeometryLayout {
326320
const sideMargin = resolveSideMargin(terminal.columns);
327321
const layoutMode: LayoutMode = "stack";
328322

329-
// Full-shell modal: hide transcript and bottom chrome; overlay owns residual.
330-
if (mode === "full_shell") {
331-
heights.transcript = 0;
332-
heights.task = 0;
333-
heights.agents = 0;
334-
heights.plugin_banner = 0;
335-
heights.command_banner = 0;
336-
heights.settings_notice = 0;
337-
heights.progress = 0;
338-
heights.progress_divider = 0;
339-
heights.notice = 0;
340-
heights.prompt = 0;
341-
const chrome = sumChrome(heights);
342-
heights.overlay_host = Math.max(0, terminal.rows - chrome);
343-
const regions = assignRects(heights, terminal);
344-
return {
345-
terminal,
346-
transcriptHeight: 0,
347-
chromeHeight: chrome,
348-
overlayHeight: heights.overlay_host,
349-
regions,
350-
heights,
351-
collapsed,
352-
overlayMode: mode,
353-
transcriptFloor: floor,
354-
sideMargin,
355-
contentWidth,
356-
layoutMode,
357-
chatWidth: contentWidth,
358-
railWidth: 0,
359-
railGutter: 0,
360-
};
361-
}
362-
363323
// Cap prompt growth against floor before overlay allocation.
364324
const promptCap = Math.max(PROMPT_BASE_ROWS, Math.floor(terminal.rows * PROMPT_CAP_FRACTION));
365325
if (heights.prompt > promptCap) heights.prompt = promptCap;

src/tui/landing.test.ts

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -432,8 +432,7 @@ describe("landing screen", () => {
432432
// Heavy inset permission overlay: many choices plus a multi-line body
433433
// so the float must take real headroom from the landing split. A
434434
// three-item empty body leaves message delta 0 and would pass even if
435-
// the split never slid. Operator asks use full_shell (CL-7067) and
436-
// hide the landing instead — that path is covered in overlays.test.ts.
435+
// the split never slid.
437436
const heavyBody = [
438437
"run_shell",
439438
"Run shell command",
@@ -479,8 +478,6 @@ describe("landing screen", () => {
479478
// float only asked the split for one choice row of headroom. It now asks for
480479
// the overlay's real, already fraction-capped content height, so a terminal
481480
// tall enough for that content shows every choice without scrolling.
482-
// Operator full_shell hides the landing (CL-7067); this coverage stays on
483-
// the inset permission path that still floats over landing.
484481
test("a landing overlay with many choices shows them all when there is room", async () => {
485482
await withTestRenderer(
486483
async (h) => {

src/tui/overlays.test.ts

Lines changed: 2 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -192,7 +192,8 @@ describe("operator question overlay", () => {
192192
try {
193193
openOperatorOverlay(shell);
194194
expect(shell.overlayKind).toBe("operator");
195-
expect(shell.layout.overlayMode).toBe("full_shell");
195+
expect(shell.layout.overlayMode).toBe("inset");
196+
expect(shell.layout.transcriptHeight).toBeGreaterThanOrEqual(OVERLAY_TRANSCRIPT_FLOOR);
196197
expect(shell.overlayBodyLines.length).toBeGreaterThan(0);
197198
expect(shell.overlayItems.length).toBeGreaterThan(3);
198199
expect(focusOwner(shell.focus)).toBe("overlay");
@@ -223,68 +224,6 @@ describe("operator question overlay", () => {
223224
{ width: 80, height: 24 },
224225
);
225226
});
226-
227-
test("full_shell decisionContext budget shows more body rows than inset", async () => {
228-
// Same long body under both modes: inset stays capped at DECISION_CONTEXT_ROWS
229-
// (8), while full_shell raises the cap with terminal height so a long ask
230-
// stays readable (CL-7067). Mode-only asserts are not enough — pin the
231-
// budget branch that actually shapes overlayBodyLines.
232-
const longBody = [
233-
"Should we proceed with the destructive reset of the working tree?",
234-
...Array.from({ length: 24 }, (_, i) => `Context line ${i + 1}.`),
235-
].join("\n");
236-
const choices = [
237-
"Cancel — keep working tree",
238-
"Allow this once",
239-
"Allow for this session",
240-
] as const;
241-
const size = { width: 80, height: 40 } as const;
242-
243-
async function bodyLineCount(mode: "inset" | "full_shell"): Promise<{
244-
readonly lines: number;
245-
readonly body: readonly string[];
246-
readonly frame: string;
247-
}> {
248-
return withTestRenderer(async (h) => {
249-
const shell = createAppShell(h.renderer, {
250-
terminal: { columns: size.width, rows: size.height },
251-
wireKeys: false,
252-
run: "idle",
253-
});
254-
try {
255-
openListOverlay(shell, {
256-
kind: "operator",
257-
title: "",
258-
body: longBody,
259-
items: [...choices],
260-
overlayMode: mode,
261-
frameId: `overlay-operator-${mode}`,
262-
});
263-
expect(shell.layout.overlayMode).toBe(mode);
264-
await h.renderOnce();
265-
await h.renderOnce();
266-
return {
267-
lines: shell.overlayBodyLines.length,
268-
body: shell.overlayBodyLines,
269-
frame: h.captureCharFrame(),
270-
};
271-
} finally {
272-
shell.dispose();
273-
}
274-
}, size);
275-
}
276-
277-
const inset = await bodyLineCount("inset");
278-
const full = await bodyLineCount("full_shell");
279-
expect(full.lines).toBeGreaterThan(inset.lines);
280-
// Short, non-wrapping context lines map 1:1 to the budget. Inset caps at
281-
// eight context rows (then dither + tail); full_shell's raised cap keeps a
282-
// mid-body line the inset path drops.
283-
expect(full.body.some((line) => line.includes("Context line 12."))).toBe(true);
284-
expect(inset.body.some((line) => line.includes("Context line 12."))).toBe(false);
285-
expect(full.frame).toContain("Context line 12.");
286-
expect(inset.frame).not.toContain("Context line 12.");
287-
});
288227
});
289228

290229
describe("model / provider picker", () => {

src/tui/overlays.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -148,9 +148,7 @@ export function openOperatorOverlay(shell: AppShell, opts?: OpenOperatorOpts): v
148148
items: choices,
149149
activeIndex: opts?.activeIndex ?? 0,
150150
frameId: "overlay-operator",
151-
// Full shell so long questions and option lists stay readable (CL-7067).
152-
// Permission gates keep the default inset path.
153-
overlayMode: "full_shell",
151+
// Chat-first: keep the transcript visible while the operator answers.
154152
...(opts?.itemIds !== undefined ? { itemIds: opts.itemIds } : {}),
155153
...(opts?.onAccept !== undefined ? { onAccept: opts.onAccept } : {}),
156154
...(opts?.onTextAnswer !== undefined ? { onTextAnswer: opts.onTextAnswer } : {}),

src/tui/shell.ts

Lines changed: 3 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1282,12 +1282,8 @@ function relayoutOverlayHost(shell: AppShell, itemCount: number): void {
12821282
const perItem = overlayRowsPerItem(shell.overlayKind);
12831283
const hostRows = overlayHostRows(shell, shell.overlayBodyLines.length, itemCount * perItem);
12841284
const minHostRows = overlayMinHostRows(shell, shell.overlayBodyLines.length, itemCount > 0);
1285-
// Preserve the mode the open chose (operator uses full_shell; others inset).
1286-
// Hardcoding inset here would collapse a full_shell ask on every list refresh.
1287-
const bag = internals.get(shell);
1288-
const mode: OverlayMode = bag?.overlayMode === "full_shell" ? "full_shell" : "inset";
12891285
relayout(shell, {
1290-
overlayMode: mode,
1286+
overlayMode: "inset",
12911287
overlayBodyRows: hostRows,
12921288
overlayMinBodyRows: minHostRows,
12931289
});
@@ -1941,8 +1937,6 @@ interface PriorOverlaySnapshot {
19411937
readonly onCancel: (() => void) | null;
19421938
readonly addProviderHint: boolean;
19431939
readonly setDefaultHint: boolean;
1944-
/** Geometry mode the primary used before the palette stacked over it. */
1945-
readonly overlayMode: OverlayMode;
19461940
}
19471941

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

36513628
const bag = internals.get(shell);
36523629
if (bag) {
3653-
// Mode must land before applyOverlayBodyText so decisionContextBudget can
3654-
// size the body against full_shell vs inset. Palette is always inset.
3655-
bag.overlayMode = !isPalette && opts?.overlayMode === "full_shell" ? "full_shell" : "inset";
36563630
// Palette open does not own primary accept; leave prior snapshot's callback.
36573631
if (!isPalette) {
36583632
bag.overlayItemIds = opts?.itemIds ? [...opts.itemIds] : [];
@@ -4131,7 +4105,6 @@ export function closeInsetOverlay(shell: AppShell): void {
41314105
bag.overlayOnCancel = prior.onCancel;
41324106
bag.overlayAddProviderHint = prior.addProviderHint;
41334107
bag.overlaySetDefaultHint = prior.setDefaultHint;
4134-
bag.overlayMode = prior.overlayMode === "full_shell" ? "full_shell" : "inset";
41354108
// If focus was not stacked (edge case), re-open overlay frame.
41364109
if (focusOwner(shell.focus) !== "overlay") {
41374110
shell.focus = openOverlay(shell.focus, OVERLAY_FRAME_ID, {
@@ -4143,7 +4116,7 @@ export function closeInsetOverlay(shell: AppShell): void {
41434116
const hostRows = overlayHostRows(shell, prior.bodyLines.length, listH);
41444117
const minHostRows = overlayMinHostRows(shell, prior.bodyLines.length, prior.list.count > 0);
41454118
relayout(shell, {
4146-
overlayMode: bag.overlayMode,
4119+
overlayMode: "inset",
41474120
overlayBodyRows: hostRows,
41484121
overlayMinBodyRows: minHostRows,
41494122
});
@@ -4252,10 +4225,8 @@ export function setOverlayBody(shell: AppShell, text: string, maxLines = 8): voi
42524225
shell.overlayBodyLines.length,
42534226
shell.overlayItems.length > 0,
42544227
);
4255-
const bag = internals.get(shell);
4256-
const mode: OverlayMode = bag?.overlayMode === "full_shell" ? "full_shell" : "inset";
42574228
relayout(shell, {
4258-
overlayMode: mode,
4229+
overlayMode: "inset",
42594230
overlayBodyRows: hostRows,
42604231
overlayMinBodyRows: minHostRows,
42614232
});

0 commit comments

Comments
 (0)