Skip to content
Open
10 changes: 2 additions & 8 deletions frontend/app/view/term/term-model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -738,10 +738,7 @@ export class TermViewModel implements ViewModel {
}
this.triggerRestartAtom();
await RpcApi.ControllerDestroyCommand(TabRpcClient, this.blockId);
const termsize = {
rows: this.termRef.current?.terminal?.rows,
cols: this.termRef.current?.terminal?.cols,
};
const termsize = this.termRef.current?.getTermSize();
await RpcApi.ControllerResyncCommand(TabRpcClient, {
tabid: globalStore.get(atoms.staticTabId),
blockid: this.blockId,
Expand All @@ -756,10 +753,7 @@ export class TermViewModel implements ViewModel {
meta: { "term:durable": isDurable },
});
await RpcApi.ControllerDestroyCommand(TabRpcClient, this.blockId);
const termsize = {
rows: this.termRef.current?.terminal?.rows,
cols: this.termRef.current?.terminal?.cols,
};
const termsize = this.termRef.current?.getTermSize();
await RpcApi.ControllerResyncCommand(TabRpcClient, {
tabid: globalStore.get(atoms.staticTabId),
blockid: this.blockId,
Expand Down
66 changes: 57 additions & 9 deletions frontend/app/view/term/termwrap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import { SearchAddon } from "@xterm/addon-search";
import { SerializeAddon } from "@xterm/addon-serialize";
import { WebLinksAddon } from "@xterm/addon-web-links";
import { WebglAddon } from "@xterm/addon-webgl";
import { ImageAddon } from "@xterm/addon-image";
import * as TermTypes from "@xterm/xterm";
import { Terminal } from "@xterm/xterm";
import debug from "debug";
Expand Down Expand Up @@ -61,6 +62,7 @@ let loggedWebGL = false;
type TermWrapOptions = {
keydownHandler?: (e: KeyboardEvent) => boolean;
useWebGl?: boolean;
useSixel?: boolean;
sendDataHandler?: (data: string) => void;
nodeModel?: BlockNodeModel;
};
Expand Down Expand Up @@ -177,6 +179,19 @@ export class TermWrap {
loggedWebGL = true;
}
}
if (waveOptions.useSixel ?? true) {
try {
this.terminal.loadAddon(
new ImageAddon({
enableSizeReports: true,
sixelSupport: true,
iipSupport: false,
})
);
} catch (e) {
console.error("failed to load image addon for sixel support", e);
}
}
// Register OSC handlers
this.terminal.parser.registerOscHandler(7, (data: string) => {
return handleOsc7Command(data, this.blockId, this.loaded);
Expand Down Expand Up @@ -433,6 +448,35 @@ export class TermWrap {
return prtn;
}

private getTerminalPixelSize(): { xpixel: number; ypixel: number } {
const screenElem = this.connectElem.querySelector(".xterm-screen") as HTMLElement | null;
const targetElem = screenElem ?? this.connectElem;
const rect = targetElem.getBoundingClientRect();
return {
xpixel: Math.max(0, Math.floor(rect.width)),
ypixel: Math.max(0, Math.floor(rect.height)),
};
}

private areTermSizesEqual(a: TermSize, b: TermSize): boolean {
return (
a.rows === b.rows &&
a.cols === b.cols &&
(a.xpixel ?? 0) === (b.xpixel ?? 0) &&
(a.ypixel ?? 0) === (b.ypixel ?? 0)
);
}

getTermSize(): TermSize {
const { xpixel, ypixel } = this.getTerminalPixelSize();
const termSize: TermSize = { rows: this.terminal.rows, cols: this.terminal.cols };
if (xpixel > 0 && ypixel > 0) {
termSize.xpixel = xpixel;
termSize.ypixel = ypixel;
}
return termSize;
}

async loadInitialTerminalData(): Promise<void> {
const startTs = Date.now();
const zoneId = this.getZoneId();
Expand All @@ -441,7 +485,7 @@ export class TermWrap {
if (cacheFile != null) {
ptyOffset = cacheFile.meta["ptyoffset"] ?? 0;
if (cacheData.byteLength > 0) {
const curTermSize: TermSize = { rows: this.terminal.rows, cols: this.terminal.cols };
const curTermSize: TermSize = this.getTermSize();
const fileTermSize: TermSize = cacheFile.meta["termsize"];
let didResize = false;
if (
Expand Down Expand Up @@ -469,7 +513,7 @@ export class TermWrap {

async resyncController(reason: string) {
dlog("resync controller", this.blockId, reason);
const rtOpts: RuntimeOpts = { termsize: { rows: this.terminal.rows, cols: this.terminal.cols } };
const rtOpts: RuntimeOpts = { termsize: this.getTermSize() };
try {
await RpcApi.ControllerResyncCommand(TabRpcClient, {
tabid: this.tabId,
Expand Down Expand Up @@ -499,18 +543,22 @@ export class TermWrap {
}

handleResize() {
const oldRows = this.terminal.rows;
const oldCols = this.terminal.cols;
const oldTermSize = this.getTermSize();
const atBottom = this.cachedAtBottomForResize ?? this.wasRecentlyAtBottom();
if (!atBottom) {
this.cachedAtBottomForResize = null;
}
this.fitAddon.fit();
if (oldRows !== this.terminal.rows || oldCols !== this.terminal.cols) {
const termSize: TermSize = { rows: this.terminal.rows, cols: this.terminal.cols };
RpcApi.ControllerInputCommand(TabRpcClient, { blockid: this.blockId, termsize: termSize });
const newTermSize = this.getTermSize();
if (!this.areTermSizesEqual(oldTermSize, newTermSize)) {
RpcApi.ControllerInputCommand(TabRpcClient, { blockid: this.blockId, termsize: newTermSize });
}
Comment on lines +619 to 636
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Pixel-only resizes can be skipped due to current “old size” capture.

At Line 546, oldTermSize is measured from the current DOM before fit(). If pixel size changes but rows/cols do not, Line 553 sees no diff and Line 554 won’t send ControllerInputCommand, leaving backend ws_xpixel/ws_ypixel stale.

💡 Proposed fix
@@
 export class TermWrap {
+    private lastReportedTermSize: TermSize | null = null;
@@
     handleResize() {
-        const oldTermSize = this.getTermSize();
+        const oldTermSize = this.lastReportedTermSize;
         const atBottom = this.cachedAtBottomForResize ?? this.wasRecentlyAtBottom();
@@
         this.fitAddon.fit();
         const newTermSize = this.getTermSize();
-        if (!this.areTermSizesEqual(oldTermSize, newTermSize)) {
+        if (oldTermSize == null || !this.areTermSizesEqual(oldTermSize, newTermSize)) {
             RpcApi.ControllerInputCommand(TabRpcClient, { blockid: this.blockId, termsize: newTermSize });
+            this.lastReportedTermSize = { ...newTermSize };
         }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/app/view/term/termwrap.ts` around lines 546 - 555, The current logic
uses a transient oldTermSize (measured immediately before fit) so pixel-only
changes made by fit() are missed; instead, compare the new size against the
last-known/sent terminal size stored on the instance. Replace the transient
oldTermSize usage with a persistent property (e.g., this.lastSentTermSize) so
that you compute newTermSize after this.fitAddon.fit(), compare newTermSize to
this.lastSentTermSize using areTermSizesEqual, and if different call
RpcApi.ControllerInputCommand(TabRpcClient, { blockid: this.blockId, termsize:
newTermSize }) and then update this.lastSentTermSize = newTermSize; also
preserve the cachedAtBottomForResize logic and any fallbacks to
this.getTermSize() when lastSentTermSize is unset.

dlog("resize", `${this.terminal.rows}x${this.terminal.cols}`, `${oldRows}x${oldCols}`, this.hasResized);
dlog(
"resize",
`${newTermSize.rows}x${newTermSize.cols}`,
`${oldTermSize.rows}x${oldTermSize.cols}`,
this.hasResized
);
if (!this.hasResized) {
this.hasResized = true;
this.resyncController("initial resize");
Expand All @@ -529,7 +577,7 @@ export class TermWrap {
return;
}
const serializedOutput = this.serializeAddon.serialize();
const termSize: TermSize = { rows: this.terminal.rows, cols: this.terminal.cols };
const termSize: TermSize = this.getTermSize();
console.log("idle timeout term", this.dataBytesProcessed, serializedOutput.length, termSize);
fireAndForget(() =>
services.BlockService.SaveTerminalState(this.blockId, serializedOutput, "full", this.ptyOffset, termSize)
Expand Down
2 changes: 2 additions & 0 deletions frontend/types/gotypes.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1568,6 +1568,8 @@ declare global {
type TermSize = {
rows: number;
cols: number;
xpixel?: number;
ypixel?: number;
};

// wconfig.TermThemeType
Expand Down
Loading