fix(coding-agent): recompute interactive bash preview width closes #2569
This commit is contained in:
@@ -4,6 +4,7 @@
|
|||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
|
- Fixed interactive bash execution collapsed previews to recompute visual line wrapping at render time, so previews respect the current terminal width after resizes and split-pane width changes ([#2569](https://github.com/badlogic/pi-mono/issues/2569))
|
||||||
- Fixed RPC `get_session_stats` to expose `contextUsage`, so headless clients can read actual current context-window usage instead of deriving it from token totals ([#2550](https://github.com/badlogic/pi-mono/issues/2550))
|
- Fixed RPC `get_session_stats` to expose `contextUsage`, so headless clients can read actual current context-window usage instead of deriving it from token totals ([#2550](https://github.com/badlogic/pi-mono/issues/2550))
|
||||||
- Fixed `pi update` for git packages to fetch only the tracked target branch with `--no-tags`, reducing unrelated branch and tag noise while preserving force-push-safe updates ([#2548](https://github.com/badlogic/pi-mono/issues/2548))
|
- Fixed `pi update` for git packages to fetch only the tracked target branch with `--no-tags`, reducing unrelated branch and tag noise while preserving force-push-safe updates ([#2548](https://github.com/badlogic/pi-mono/issues/2548))
|
||||||
|
|
||||||
|
|||||||
@@ -28,12 +28,10 @@ export class BashExecutionComponent extends Container {
|
|||||||
private fullOutputPath?: string;
|
private fullOutputPath?: string;
|
||||||
private expanded = false;
|
private expanded = false;
|
||||||
private contentContainer: Container;
|
private contentContainer: Container;
|
||||||
private ui: TUI;
|
|
||||||
|
|
||||||
constructor(command: string, ui: TUI, excludeFromContext = false) {
|
constructor(command: string, ui: TUI, excludeFromContext = false) {
|
||||||
super();
|
super();
|
||||||
this.command = command;
|
this.command = command;
|
||||||
this.ui = ui;
|
|
||||||
|
|
||||||
// Use dim border for excluded-from-context commands (!! prefix)
|
// Use dim border for excluded-from-context commands (!! prefix)
|
||||||
const colorKey = excludeFromContext ? "dim" : "bashMode";
|
const colorKey = excludeFromContext ? "dim" : "bashMode";
|
||||||
@@ -147,15 +145,25 @@ export class BashExecutionComponent extends Container {
|
|||||||
const displayText = availableLines.map((line) => theme.fg("muted", line)).join("\n");
|
const displayText = availableLines.map((line) => theme.fg("muted", line)).join("\n");
|
||||||
this.contentContainer.addChild(new Text(`\n${displayText}`, 1, 0));
|
this.contentContainer.addChild(new Text(`\n${displayText}`, 1, 0));
|
||||||
} else {
|
} else {
|
||||||
// Use shared visual truncation utility
|
// Use shared visual truncation utility with width-aware caching
|
||||||
const styledOutput = previewLogicalLines.map((line) => theme.fg("muted", line)).join("\n");
|
const styledOutput = previewLogicalLines.map((line) => theme.fg("muted", line)).join("\n");
|
||||||
const { visualLines } = truncateToVisualLines(
|
const styledInput = `\n${styledOutput}`;
|
||||||
`\n${styledOutput}`,
|
let cachedWidth: number | undefined;
|
||||||
PREVIEW_LINES,
|
let cachedLines: string[] | undefined;
|
||||||
this.ui.terminal.columns,
|
this.contentContainer.addChild({
|
||||||
1, // padding
|
render: (width: number) => {
|
||||||
);
|
if (cachedLines === undefined || cachedWidth !== width) {
|
||||||
this.contentContainer.addChild({ render: () => visualLines, invalidate: () => {} });
|
const result = truncateToVisualLines(styledInput, PREVIEW_LINES, width, 1);
|
||||||
|
cachedLines = result.visualLines;
|
||||||
|
cachedWidth = width;
|
||||||
|
}
|
||||||
|
return cachedLines ?? [];
|
||||||
|
},
|
||||||
|
invalidate: () => {
|
||||||
|
cachedWidth = undefined;
|
||||||
|
cachedLines = undefined;
|
||||||
|
},
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
80
packages/coding-agent/test/bash-execution-width.test.ts
Normal file
80
packages/coding-agent/test/bash-execution-width.test.ts
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
/**
|
||||||
|
* Test that BashExecutionComponent's collapsed output respects the render-time width,
|
||||||
|
* not a stale captured width. Regression test for #2569.
|
||||||
|
*/
|
||||||
|
import { visibleWidth } from "@mariozechner/pi-tui";
|
||||||
|
import { beforeAll, describe, expect, it } from "vitest";
|
||||||
|
import { BashExecutionComponent } from "../src/modes/interactive/components/bash-execution.js";
|
||||||
|
import { initTheme } from "../src/modes/interactive/theme/theme.js";
|
||||||
|
|
||||||
|
/** Minimal TUI stub that only exposes terminal.columns */
|
||||||
|
function createTuiStub(columns: number): { columns: number; stub: any } {
|
||||||
|
const state = { columns };
|
||||||
|
const stub = {
|
||||||
|
terminal: {
|
||||||
|
get columns() {
|
||||||
|
return state.columns;
|
||||||
|
},
|
||||||
|
get rows() {
|
||||||
|
return 24;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// Loader calls ui.addInterval / ui.removeInterval
|
||||||
|
addInterval: (_cb: () => void, _ms: number) => ({ dispose: () => {} }),
|
||||||
|
removeInterval: () => {},
|
||||||
|
requestRender: () => {},
|
||||||
|
};
|
||||||
|
return { columns: state.columns, stub };
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("BashExecutionComponent width handling (#2569)", () => {
|
||||||
|
beforeAll(() => {
|
||||||
|
initTheme(undefined, false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("collapsed preview lines respect render-time width, not construction-time width", () => {
|
||||||
|
const wideWidth = 200;
|
||||||
|
const narrowWidth = 80;
|
||||||
|
|
||||||
|
const { stub } = createTuiStub(wideWidth);
|
||||||
|
const component = new BashExecutionComponent("pwd", stub);
|
||||||
|
|
||||||
|
// Add output with long lines that will wrap differently at different widths
|
||||||
|
const longLine = "x".repeat(150);
|
||||||
|
component.appendOutput(`${longLine}\n${longLine}\n`);
|
||||||
|
|
||||||
|
// Complete the command so it enters collapsed mode
|
||||||
|
component.setComplete(0, false);
|
||||||
|
|
||||||
|
// Render at the narrow width (simulating a resize or split pane)
|
||||||
|
const lines = component.render(narrowWidth);
|
||||||
|
|
||||||
|
// Every rendered line must fit within the narrow width
|
||||||
|
for (let i = 0; i < lines.length; i++) {
|
||||||
|
const w = visibleWidth(lines[i]);
|
||||||
|
expect(w, `Line ${i} visibleWidth=${w} > ${narrowWidth}`).toBeLessThanOrEqual(narrowWidth);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("re-computes lines when width changes between renders", () => {
|
||||||
|
const { stub } = createTuiStub(200);
|
||||||
|
const component = new BashExecutionComponent("echo hello", stub);
|
||||||
|
|
||||||
|
const longLine = "abcdefghij".repeat(20); // 200 chars
|
||||||
|
component.appendOutput(`${longLine}\n`);
|
||||||
|
component.setComplete(0, false);
|
||||||
|
|
||||||
|
// First render at width 200
|
||||||
|
const lines200 = component.render(200);
|
||||||
|
for (const line of lines200) {
|
||||||
|
expect(visibleWidth(line)).toBeLessThanOrEqual(200);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Second render at width 60 (split pane scenario)
|
||||||
|
const lines60 = component.render(60);
|
||||||
|
for (let i = 0; i < lines60.length; i++) {
|
||||||
|
const w = visibleWidth(lines60[i]);
|
||||||
|
expect(w, `Line ${i} visibleWidth=${w} > 60`).toBeLessThanOrEqual(60);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user