From 11b580b3a8a47e992b6a0b65b512c32e7d733443 Mon Sep 17 00:00:00 2001 From: cpendery Date: Wed, 8 Apr 2026 13:52:35 -0700 Subject: [PATCH 1/8] fix: leaking oscs Signed-off-by: cpendery --- shell/shellIntegration-rc.zsh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/shell/shellIntegration-rc.zsh b/shell/shellIntegration-rc.zsh index a5402b7..92f4066 100644 --- a/shell/shellIntegration-rc.zsh +++ b/shell/shellIntegration-rc.zsh @@ -6,11 +6,11 @@ if [[ -f $USER_ZDOTDIR/.zshrc ]]; then fi __is_prompt_start() { - builtin printf '\e]6973;PS\a' + builtin printf '\e]6973;PS\a' > /dev/tty } __is_prompt_end() { - builtin printf '\e]6973;PE\a' + builtin printf '\e]6973;PE\a' > /dev/tty } __is_escape_value() { From a2071b18e5fa82accab2d4c88de53ab77b2be7eb Mon Sep 17 00:00:00 2001 From: cpendery Date: Thu, 9 Apr 2026 13:38:49 -0700 Subject: [PATCH 2/8] fix: wrong parsing Signed-off-by: cpendery --- src/ui/ui-root.ts | 35 ++++++++++++++++++++--------------- 1 file changed, 20 insertions(+), 15 deletions(-) diff --git a/src/ui/ui-root.ts b/src/ui/ui-root.ts index fc838ff..72da534 100644 --- a/src/ui/ui-root.ts +++ b/src/ui/ui-root.ts @@ -26,10 +26,17 @@ const writeOutput = (data: string) => { process.stdout.write(data); }; +const _suggestionLayout = (term: ISTerm): { direction: "above" | "below"; lines: number } => { + const maxLines = getMaxLines(); + const { remainingLines, cursorY } = term.getCursorState(); + const direction = remainingLines >= maxLines ? "below" : cursorY >= maxLines ? "above" : remainingLines >= cursorY ? "below" : "above"; + const lines = direction === "above" ? Math.min(maxLines, cursorY) : Math.min(maxLines, remainingLines); + return { direction, lines }; +}; + const _render = (term: ISTerm, suggestionManager: SuggestionManager, data: string, handlingBackspace: boolean, handlingSuggestion: boolean): boolean => { - const direction = _direction(term); + const { direction, lines } = _suggestionLayout(term); const { hidden: cursorHidden, shift: cursorShift } = term.getCursorState(); - const linesOfInterest = getMaxLines(); const suggestion = suggestionManager.render(direction); const hasSuggestion = suggestion.length != 0; @@ -43,12 +50,12 @@ const _render = (term: ISTerm, suggestionManager: SuggestionManager, data: strin const commandState = term.getCommandState(); const cursorTerminated = handlingBackspace ? true : commandState.cursorTerminated ?? false; const showSuggestions = hasSuggestion && cursorTerminated && !commandState.hasOutput && !cursorShift && !!commandState.commandText; - const patch = term.getPatch(linesOfInterest, showSuggestions ? suggestion : [], direction); + const patch = term.getPatch(lines, showSuggestions ? suggestion : [], direction); const ansiCursorShow = cursorHidden ? "" : ansi.cursorShow; if (direction == "above") { writeOutput( - data + ansi.cursorHide + ansi.cursorSavePosition + ansi.cursorPrevLine.repeat(linesOfInterest) + patch + ansi.cursorRestorePosition + ansiCursorShow, + data + ansi.cursorHide + ansi.cursorSavePosition + ansi.cursorPrevLine.repeat(lines) + patch + ansi.cursorRestorePosition + ansiCursorShow, ); } else { writeOutput(ansi.cursorHide + ansi.cursorSavePosition + ansi.cursorNextLine + patch + ansi.cursorRestorePosition + ansiCursorShow + data); @@ -57,28 +64,26 @@ const _render = (term: ISTerm, suggestionManager: SuggestionManager, data: strin }; const _clear = (term: ISTerm): void => { - const clearDirection = _direction(term) == "above" ? "below" : "above"; // invert direction to clear what was previously rendered - const { hidden: cursorHidden } = term.getCursorState(); - const patch = term.getPatch(getMaxLines(), [], clearDirection); + const { direction } = _suggestionLayout(term); + const clearDirection = direction == "above" ? "below" : "above"; // invert direction to clear what was previously rendered + const { hidden: cursorHidden, cursorY, remainingLines } = term.getCursorState(); + const lines = clearDirection === "above" ? Math.min(getMaxLines(), cursorY) : Math.min(getMaxLines(), remainingLines); + const patch = term.getPatch(lines, [], clearDirection); const ansiCursorShow = cursorHidden ? "" : ansi.cursorShow; if (clearDirection == "above") { - writeOutput(ansi.cursorHide + ansi.cursorSavePosition + ansi.cursorPrevLine.repeat(getMaxLines()) + patch + ansi.cursorRestorePosition + ansiCursorShow); + writeOutput(ansi.cursorHide + ansi.cursorSavePosition + ansi.cursorPrevLine.repeat(lines) + patch + ansi.cursorRestorePosition + ansiCursorShow); } else { writeOutput(ansi.cursorHide + ansi.cursorSavePosition + ansi.cursorNextLine + patch + ansi.cursorRestorePosition + ansiCursorShow); } }; -const _direction = (term: ISTerm): "above" | "below" => { - return term.getCursorState().remainingLines > getMaxLines() ? "below" : "above"; -}; - export const render = async (program: Command, shell: Shell, underTest: boolean, login: boolean) => { const [isterm, { SuggestionManager }] = await Promise.all([import("../isterm/index.js"), import("./suggestionManager.js")]); const term = await isterm.default.spawn(program, { shell, rows: process.stdout.rows, cols: process.stdout.columns, underTest, login }); const suggestionManager = new SuggestionManager(term, shell); let hasSuggestion = false; - let direction = _direction(term); + let direction = _suggestionLayout(term).direction; let handlingBackspace = false; // backspace normally consistent of two data points (move back & delete), so on the first data point, we won't enforce the cursor terminated rule. this will help reduce flicker const stdinStartedInRawMode = process.stdin.isRaw; if (process.stdin.isTTY) process.stdin.setRawMode(true); @@ -94,7 +99,7 @@ export const render = async (program: Command, shell: Shell, underTest: boolean, term.onData(async (data) => { data = data.replace(enableWin32InputMode, ""); // remove win32-input-mode enable sequence if it comes through data - const handlingDirectionChange = direction != _direction(term); + const handlingDirectionChange = direction != _suggestionLayout(term).direction; // clear the previous suggestion if the direction has changed to avoid leftover suggestions if (handlingDirectionChange) { _clear(term); @@ -105,7 +110,7 @@ export const render = async (program: Command, shell: Shell, underTest: boolean, hasSuggestion = _render(term, suggestionManager, "", handlingBackspace, hasSuggestion); handlingBackspace = false; - direction = _direction(term); + direction = _suggestionLayout(term).direction; }); process.stdin.on("keypress", (...keyPress: KeyPressEvent) => { From b13342ad8bece73010f14045d872e92320bc43ea Mon Sep 17 00:00:00 2001 From: cpendery Date: Thu, 9 Apr 2026 13:49:31 -0700 Subject: [PATCH 3/8] fix: style Signed-off-by: cpendery --- src/ui/ui-root.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/ui/ui-root.ts b/src/ui/ui-root.ts index 72da534..cc82ee4 100644 --- a/src/ui/ui-root.ts +++ b/src/ui/ui-root.ts @@ -54,9 +54,7 @@ const _render = (term: ISTerm, suggestionManager: SuggestionManager, data: strin const ansiCursorShow = cursorHidden ? "" : ansi.cursorShow; if (direction == "above") { - writeOutput( - data + ansi.cursorHide + ansi.cursorSavePosition + ansi.cursorPrevLine.repeat(lines) + patch + ansi.cursorRestorePosition + ansiCursorShow, - ); + writeOutput(data + ansi.cursorHide + ansi.cursorSavePosition + ansi.cursorPrevLine.repeat(lines) + patch + ansi.cursorRestorePosition + ansiCursorShow); } else { writeOutput(ansi.cursorHide + ansi.cursorSavePosition + ansi.cursorNextLine + patch + ansi.cursorRestorePosition + ansiCursorShow + data); } From dcba77ae2ef4cb61f0f270cd7101ce206bc72bf1 Mon Sep 17 00:00:00 2001 From: cpendery Date: Thu, 9 Apr 2026 14:23:58 -0700 Subject: [PATCH 4/8] fix: fish query Signed-off-by: cpendery --- src/isterm/pty.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/isterm/pty.ts b/src/isterm/pty.ts index d8d1d99..93aecd1 100644 --- a/src/isterm/pty.ts +++ b/src/isterm/pty.ts @@ -390,8 +390,8 @@ const convertToPtyTarget = async (shell: Shell, underTest: boolean, login: boole case Shell.Fish: shellArgs = platform == "win32" - ? ["--init-command", `. "$(cygpath -u '${path.join(shellResourcesPath, "shellIntegration.fish")}')"`] - : ["--init-command", `. ${path.join(shellResourcesPath, "shellIntegration.fish").replace(/(\s+)/g, "\\$1")}`]; + ? ["--features", "no-query-term", "--init-command", `. "$(cygpath -u '${path.join(shellResourcesPath, "shellIntegration.fish")}')"`] + : ["--features", "no-query-term", "--init-command", `. ${path.join(shellResourcesPath, "shellIntegration.fish").replace(/(\s+)/g, "\\$1")}`]; break; case Shell.Xonsh: { const sharedConfig = os.platform() == "win32" ? path.join("C:\\ProgramData", "xonsh", "xonshrc") : path.join("etc", "xonsh", "xonshrc"); From 91d105f0964935d9adffaff52f10d85abb532886 Mon Sep 17 00:00:00 2001 From: cpendery Date: Thu, 9 Apr 2026 20:47:59 -0700 Subject: [PATCH 5/8] fix: pty issue Signed-off-by: cpendery --- src/isterm/pty.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/isterm/pty.ts b/src/isterm/pty.ts index 93aecd1..b0d04a9 100644 --- a/src/isterm/pty.ts +++ b/src/isterm/pty.ts @@ -90,6 +90,7 @@ export class ISTerm implements IPty { this.#shell = shell; this.#ptyEmitter = new EventEmitter(); + this.#term.onData((data) => this.#pty.write(data)); this.#pty.onData((data) => { const cursorY = this.#term.buffer.active.cursorY; this.#term.write(data, () => { @@ -390,8 +391,8 @@ const convertToPtyTarget = async (shell: Shell, underTest: boolean, login: boole case Shell.Fish: shellArgs = platform == "win32" - ? ["--features", "no-query-term", "--init-command", `. "$(cygpath -u '${path.join(shellResourcesPath, "shellIntegration.fish")}')"`] - : ["--features", "no-query-term", "--init-command", `. ${path.join(shellResourcesPath, "shellIntegration.fish").replace(/(\s+)/g, "\\$1")}`]; + ? ["--init-command", `. "$(cygpath -u '${path.join(shellResourcesPath, "shellIntegration.fish")}')"` ] + : ["--init-command", `. ${path.join(shellResourcesPath, "shellIntegration.fish").replace(/(\s+)/g, "\\$1")}`]; break; case Shell.Xonsh: { const sharedConfig = os.platform() == "win32" ? path.join("C:\\ProgramData", "xonsh", "xonshrc") : path.join("etc", "xonsh", "xonshrc"); From 199e3a47e67fd1631e44332b6ed89cabbbac5054 Mon Sep 17 00:00:00 2001 From: cpendery Date: Fri, 10 Apr 2026 01:08:19 -0700 Subject: [PATCH 6/8] fix: fish under test Signed-off-by: cpendery --- src/isterm/pty.ts | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/isterm/pty.ts b/src/isterm/pty.ts index b0d04a9..b6ea2d0 100644 --- a/src/isterm/pty.ts +++ b/src/isterm/pty.ts @@ -86,11 +86,19 @@ export class ISTerm implements IPty { this.#term.unicode.activeVersion = "11"; this.#term.parser.registerOscHandler(IsTermOscPs, (data) => this._handleIsSequence(data)); + // handling fish DA1 failing in e2e tests due to proxy issues - send fake response when under test + if (underTest) { + this.#term.parser.registerCsiHandler({ final: "c" }, (params) => { + if (params[0] === 0) { + this.#pty.write("\x1b[?1;2c"); + } + return false; + }); + } this.#commandManager = new CommandManager(this.#term, shell); this.#shell = shell; this.#ptyEmitter = new EventEmitter(); - this.#term.onData((data) => this.#pty.write(data)); this.#pty.onData((data) => { const cursorY = this.#term.buffer.active.cursorY; this.#term.write(data, () => { @@ -451,4 +459,4 @@ const convertToPtyEnv = (shell: Shell, underTest: boolean, login: boolean) => { } return env; -}; +}; \ No newline at end of file From aa38140538ac0bfd19576b9e3bd95b4a4a7cb2fe Mon Sep 17 00:00:00 2001 From: cpendery Date: Fri, 10 Apr 2026 01:11:33 -0700 Subject: [PATCH 7/8] fix: style Signed-off-by: cpendery --- src/isterm/pty.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/isterm/pty.ts b/src/isterm/pty.ts index b6ea2d0..ac1d095 100644 --- a/src/isterm/pty.ts +++ b/src/isterm/pty.ts @@ -399,7 +399,7 @@ const convertToPtyTarget = async (shell: Shell, underTest: boolean, login: boole case Shell.Fish: shellArgs = platform == "win32" - ? ["--init-command", `. "$(cygpath -u '${path.join(shellResourcesPath, "shellIntegration.fish")}')"` ] + ? ["--init-command", `. "$(cygpath -u '${path.join(shellResourcesPath, "shellIntegration.fish")}')"`] : ["--init-command", `. ${path.join(shellResourcesPath, "shellIntegration.fish").replace(/(\s+)/g, "\\$1")}`]; break; case Shell.Xonsh: { @@ -459,4 +459,4 @@ const convertToPtyEnv = (shell: Shell, underTest: boolean, login: boolean) => { } return env; -}; \ No newline at end of file +}; From 40cde7aa0d3326aa0468a5cff54eb6b6fec6e839 Mon Sep 17 00:00:00 2001 From: cpendery Date: Fri, 10 Apr 2026 02:38:16 -0700 Subject: [PATCH 8/8] fix: suppression Signed-off-by: cpendery --- shell/shellIntegration.fish | 1 + src/isterm/pty.ts | 9 --------- 2 files changed, 1 insertion(+), 9 deletions(-) diff --git a/shell/shellIntegration.fish b/shell/shellIntegration.fish index e1ed8f4..d5a59b8 100644 --- a/shell/shellIntegration.fish +++ b/shell/shellIntegration.fish @@ -17,6 +17,7 @@ function __is_update_cwd --on-event fish_prompt; set __is_cwd (__is_escape_value if [ "$ISTERM_TESTING" = "1" ] function is_user_prompt; printf '> '; end + set -Ua fish_features no-query-term end function fish_prompt; diff --git a/src/isterm/pty.ts b/src/isterm/pty.ts index ac1d095..d8d1d99 100644 --- a/src/isterm/pty.ts +++ b/src/isterm/pty.ts @@ -86,15 +86,6 @@ export class ISTerm implements IPty { this.#term.unicode.activeVersion = "11"; this.#term.parser.registerOscHandler(IsTermOscPs, (data) => this._handleIsSequence(data)); - // handling fish DA1 failing in e2e tests due to proxy issues - send fake response when under test - if (underTest) { - this.#term.parser.registerCsiHandler({ final: "c" }, (params) => { - if (params[0] === 0) { - this.#pty.write("\x1b[?1;2c"); - } - return false; - }); - } this.#commandManager = new CommandManager(this.#term, shell); this.#shell = shell;