/** * Generate session titles using a smol, fast model. */ import { dlopen, FFIType, ptr } from "bun:ffi"; import * as os from "node:os"; import * as path from "node:path"; import * as url from "node:url"; import { type Api, type AssistantMessage, completeSimple, type Message, type Model, retryTransientCompletion, } from "@oh-my-pi/pi-ai"; import { StreamMarkupHealing } from "@oh-my-pi/pi-ai/utils/stream-markup-healing"; import { writeTerminalSequence } from "@oh-my-pi/pi-tui"; import { isNativeRendering, onNativeRenderingChange } from "@oh-my-pi/pi-tui/native/state"; import { theme } from "@oh-my-pi/pi-tui/theme"; import { SPINNER_FRAMES } from "@oh-my-pi/pi-tui/theme/symbols"; import { $env, isTerminalHeadless, isWsl, logger, prompt } from "@oh-my-pi/pi-utils"; import type { ModelRegistry } from "../config/model-registry"; import { roleCandidatePool } from "../config/model-roles"; import { formatModelStringWithRouting } from "../config/model-resolver"; import { collectOnlineTinyCandidates, expandOnlineTinyModelFallbacks } from "../tiny/online-candidates"; import type { Settings } from "../config/settings"; import titleMarkerInstruction from "../prompts/system/title-marker-instruction.md" with { type: "text" }; import titleSystemPrompt from "../prompts/system/title-system.md" with { type: "text" }; import { formatTitleUserMessage } from "../tiny/message-preproc"; import { isLowSignalTitleInput, normalizeGeneratedTitle } from "../tiny/text"; import { tinyTitleClient } from "../tiny/title-client"; import { cfgRetryModelFallback } from "../session/settings"; import { DEFAULT_SPINNER_INTERVAL_MS, SPINNER_INTERVAL_STATIC } from "@oh-my-pi/pi-tui/spinner-clock"; const TITLE_SYSTEM_PROMPT = prompt.render(titleSystemPrompt); const TITLE_MARKER_INSTRUCTION = prompt.render(titleMarkerInstruction); // Plain π, not the nerd-font `icon.omp` glyph: window/tab titles render in the // OS UI font, which has no nerd-font PUA coverage. const DEFAULT_TERMINAL_TITLE = "π"; /** The native tab title without a session name. */ const NATIVE_TERMINAL_TITLE = "omp"; const TERMINAL_TITLE_CONTROL_CHARS = /[\u0000-\u001f\u007f-\u009f]/g; /** * Console titles longer than this read back truncated, so they always count * as drifted: harmless, it only re-sets the same title. */ const CONSOLE_TITLE_READ_CHARS = 1024; interface WindowsConsoleTitleApi { set(title: string): boolean; /** The live console title, or `undefined` when it cannot be read. */ get(): string | undefined; close(): void; } let windowsConsoleTitleApi: WindowsConsoleTitleApi | null | undefined; let lastTerminalTitle: string | undefined; function getWindowsConsoleTitleApi(): WindowsConsoleTitleApi | null { if (process.platform !== "win32") return null; if (windowsConsoleTitleApi !== undefined) return windowsConsoleTitleApi; try { const kernel32 = dlopen("kernel32.dll", { SetConsoleTitleW: { args: [FFIType.ptr], returns: FFIType.bool }, GetConsoleTitleW: { args: [FFIType.ptr, FFIType.u32], returns: FFIType.u32 }, }); const readBuffer = new Uint16Array(CONSOLE_TITLE_READ_CHARS); windowsConsoleTitleApi = { set(title) { const wideTitle = Buffer.from(`${title}\0`, "utf16le"); return kernel32.symbols.SetConsoleTitleW(ptr(wideTitle)); }, get() { readBuffer.fill(0); if (kernel32.symbols.GetConsoleTitleW(ptr(readBuffer), readBuffer.length) === 0) return undefined; const end = readBuffer.indexOf(0); return Buffer.from(readBuffer.buffer, 0, (end < 0 ? readBuffer.length : end) * 2).toString("utf16le"); }, close: () => kernel32.close(), }; } catch { windowsConsoleTitleApi = null; } return windowsConsoleTitleApi; } function setWindowsConsoleTitle(title: string): boolean { const api = getWindowsConsoleTitleApi(); if (!api) return false; try { return api.set(title); } catch { try { api.close(); } catch { // Ignore cleanup failures after the native title path has already failed. } windowsConsoleTitleApi = null; return false; } } /** * Whether the live console title differs from `title`, the one omp last set. * The console title is shared by every process attached to the console, so a * child (a worker, `browser-relay`, an npm `.cmd` shim's `title`) can replace * it behind the dedupe cache. Unreadable titles and the latched OSC fallback * report no drift, keeping the dedupe in force. */ function windowsConsoleTitleDrifted(title: string): boolean { if (terminalTitleRuntime.nativeTitleFailed) return false; const api = getWindowsConsoleTitleApi(); if (!api) return false; try { const live = api.get(); return live !== undefined && live !== title; } catch { return false; } } function disposeWindowsConsoleTitleApi(): void { try { windowsConsoleTitleApi?.close(); } catch { // Terminal teardown must remain best-effort. } windowsConsoleTitleApi = undefined; } // Cover the "backend ignores `disableReasoning`" case unconditionally: the // static `model.reasoning` catalog flag can't distinguish a thinking model that // was declared with `reasoning: false` (e.g. Qwen3 served locally via llama.cpp, // whose bundled jinja chat template forces `enable_thinking: true`) from one // that never emits thinking. `maxTokens` is a hard cap, not a target — the // happy-path completion still returns in a handful of tokens, so raising the // ceiling costs nothing when thinking is genuinely suppressed and keeps the // `
: ` that Tern indexes
* parked panes by, else the cwd.
*/
export function setSessionTerminalTitle(sessionName: string | undefined, cwd?: string): void {
// An authoritative session title (rename, new session, focus swap) supersedes
// any extension override so the base title tracks the real session again.
//
// It does NOT release the teardown latch. Every caller here is a routine
// session update, and several arrive from async transitions that can resume
// AFTER teardown restored the shell's title (an extension `newSession()`
// continuing past its `await`, a collab host frame) — work `stop()` cannot
// cancel. Releasing here would let the emit below, and a re-armed spinner,
// write into the parent shell's tab. Only `initTerminalTitleState()`, the
// explicit terminal-ownership path, releases the latch.
terminalTitleRuntime.extensionOverride = undefined;
terminalTitleRuntime.sessionName = sanitizeTerminalTitlePart(sessionName);
terminalTitleRuntime.label = terminalTitleRuntime.sessionName ?? getFallbackTerminalTitle(cwd);
emitTerminalTitle();
reportTernSession();
}
/**
* Whether the effective symbol preset is `nerd`: under `nf+emoji` title icons,
* the title fork then asks the model for a Nerd Fonts glyph to head the title.
*/
export function nerdGlyphsActive(): boolean {
return typeof theme !== "undefined" && theme.getSymbolPreset() === "nerd";
}
/** The OSC 1337 user variable Tern reads the session file from. */
const TERN_SESSION_FILE_VAR = "omp_session_file";
/**
* The OSC 1337 user variable telling Tern this omp understands `--restore`, set to
* `1` before each session file report: Tern's daemon then relaunches it after a
* crash with `--restore ` instead of `--resume `.
*/
const TERN_RESTORE_VAR = "omp_restore";
/** The live session as Tern hears about it (read from the interactive session manager). */
export interface TerminalSessionSource {
/** The session file, if the session persists to one. */
file(): string | undefined;
/** The session's working directory. */
cwd(): string;
}
/** Where the current session's file and directory are read from. */
let sessionSource: TerminalSessionSource | undefined;
/** The session file Tern was last told about. */
let reportedSessionFile: string | undefined;
/** The working directory Tern was last told about. */
let reportedCwd: string | undefined;
/**
* Name the live session's source. Every session title update (start, new
* session, resume, cwd switch) and {@link reportTernSession} re-read it and, in
* Tern, report what changed: the file, so Tern's daemon can relaunch
* `omp --resume ` after it restarts, and the directory, which Tern names
* in omp's composer bar.
*/
export function setTerminalSessionSource(source: TerminalSessionSource | undefined): void {
sessionSource = source;
reportTernSession();
}
/**
* Tell Tern (`TERM_PROGRAM=tern`, nowhere else) what changed about the session:
* the file as an OSC 1337 user variable holding its absolute path in base64 (no
* file removes the variable), preceded by {@link TERN_RESTORE_VAR}, and the working
* directory as OSC 7 (`file://host/path`), as a shell reports it at each prompt.
*/
export function reportTernSession(): void {
if (terminalTitleRuntime.disposed || $env.TERM_PROGRAM?.toLowerCase() !== "tern") return;
if (!process.stdout.isTTY || isTerminalHeadless()) return;
const file = sessionSource?.file();
const resolvedFile = file ? path.resolve(file) : undefined;
if (resolvedFile !== reportedSessionFile) {
reportedSessionFile = resolvedFile;
const value = resolvedFile ? `=${Buffer.from(resolvedFile).toString("base64")}` : "";
if (resolvedFile) writeTerminalSequence(`\x1b]1337;SetUserVar=${TERN_RESTORE_VAR}=MQ==\x07`);
writeTerminalSequence(`\x1b]1337;SetUserVar=${TERN_SESSION_FILE_VAR}${value}\x07`);
}
// Without a session the shell takes the directory back at its next prompt.
const cwd = sessionSource ? path.resolve(sessionSource.cwd()) : undefined;
if (cwd && cwd !== reportedCwd) {
writeTerminalSequence(`\x1b]7;file://${os.hostname()}${url.pathToFileURL(cwd).pathname}\x07`);
}
reportedCwd = cwd;
}
/**
* The branch's pull request number for the native tab title, which carries it
* after the session name (a TSP terminal has no status strip to show it in).
*/
export function setTerminalTitlePullRequest(pr: number | undefined): void {
terminalTitleRuntime.pullRequest = pr;
emitTerminalTitle();
}
/**
* Set a terminal title from an extension's `setTitle()`. Unlike the session base
* title, this owns the terminal verbatim: periodic and run-state updates will not
* rewrite it. Cleared when the app next sets an authoritative session title via
* {@link setSessionTerminalTitle}, or when the extension passes an empty or blank
* title to release its claim.
*/
export function setExtensionTerminalTitle(title: string): void {
// A title that renders to nothing RELEASES the override rather than owning the
// terminal with it: `emitTerminalTitle` falls through on nullish only, so a
// latched blank would strand the title at the bare brand and silence every
// subsequent run-state change. Reuse the sink's own emptiness predicate so
// "releases its claim" means the same thing here as it does at the sink, and
// so the stored override is the value that will actually render.
terminalTitleRuntime.extensionOverride = sanitizeTerminalTitlePart(title);
emitTerminalTitle();
}
export type TerminalTitleState = "idle" | "working" | "attention";
export type TerminalTitleSpinnerStyle = "braille" | "pulse" | "dots" | "line";
/**
* Working-state spinner frames per `tui.titleSpinner` style. `braille` is the
* historical default; `pulse` fills and empties a moon; `dots` cycles single
* braille dots; `line` is plain ASCII (`- \ | /`) for fonts without braille
* coverage. Every frame is a single column so the separator never reflows the
* title.
*/
export const TERMINAL_TITLE_SPINNER_STYLES: Record = {
braille: SPINNER_FRAMES.unicode.activity,
pulse: ["○", "◔", "◑", "◕", "●", "◕", "◑", "◔"],
dots: ["⠁", "⠂", "⠄", "⠠", "⠐", "⠈"],
line: ["-", "\\", "|", "/"],
};
/** WSL stdout still crosses ConPTY at the `wslhost` boundary, so its working title stays static (`:`). */
const isStaticTitleHost = (
platform: NodeJS.Platform = process.platform,
env: NodeJS.ProcessEnv = $env as NodeJS.ProcessEnv,
): boolean => isWsl(platform, env);
const STATIC_TITLE_WORKING_SEPARATOR = ":";
/** The user's turn: the title reads like a shell prompt awaiting input. */
const TITLE_IDLE_SEPARATOR = ">";
/** Agent blocked on the user (ask / approval prompt). */
const TITLE_ATTENTION_SEPARATOR = "!";
const terminalTitleRuntime: {
/** The classic title's label: the session name, else the cwd. */
label: string | undefined;
/** The session's own name, without the cwd fallback `label` uses. */
sessionName: string | undefined;
/** The branch's pull request, shown in the native title only. */
pullRequest: number | undefined;
/** Unsubscribes the native-rendering watch taken by `initTerminalTitleState()`. */
unwatchNative: (() => void) | undefined;
state: TerminalTitleState;
frame: number;
enabled: boolean;
style: TerminalTitleSpinnerStyle;
/** Spinner period in ms; `SPINNER_INTERVAL_STATIC` (0) pins the working separator to `:`. */
intervalMs: number;
timer: NodeJS.Timeout | undefined;
/** A title an extension set via `setTitle()`. While set, it owns the terminal
* title verbatim: the run-state separator never rewrites it. Cleared when the
* app next establishes an authoritative session title (rename, new session,
* focus swap) via `setSessionTerminalTitle`. */
extensionOverride: string | undefined;
/** Set by `disposeTerminalTitleState()` at teardown. While set, nothing may
* re-arm the spinner or emit an OSC title — teardown restores the shell's own
* title, so a later write would land in the parent shell's tab. Cleared only
* by `initTerminalTitleState()`, when the app takes the terminal over again. */
disposed: boolean;
/** Latched the first time the sink falls back to OSC on a Windows console.
* The spinner interval is only cheap through `SetConsoleTitleW`; once
* the native path fails, every new frame would cross ConPTY as OSC and
* reintroduce the write-loop CPU cost the static separator exists to avoid.
* While latched the working separator stays `:` and no interval is
* scheduled. */
nativeTitleFailed: boolean;
/** The console title `initTerminalTitleState()` read on a Windows console.
* The console has no title stack, so teardown puts it back through
* `SetConsoleTitleW`. */
initialWindowsConsoleTitle: string | undefined;
/** Set when teardown restored `initialWindowsConsoleTitle` natively. ConPTY
* turns OSC 0 into a console-title set, so `popTerminalTitle()` must then
* skip its clear or it blanks the title just restored. */
nativeTitleRestored: boolean;
} = {
label: undefined,
sessionName: undefined,
pullRequest: undefined,
unwatchNative: undefined,
state: "idle",
frame: 0,
enabled: true,
style: "braille",
intervalMs: DEFAULT_SPINNER_INTERVAL_MS,
timer: undefined,
extensionOverride: undefined,
disposed: false,
nativeTitleFailed: false,
initialWindowsConsoleTitle: undefined,
nativeTitleRestored: false,
};
/**
* Compose the terminal title from the `π` brand, a state-carrying separator, and
* the session label. Pure (no I/O) so the state→separator contract is testable:
* - `idle` (user's turn): `π > label`;
* - `working`: `π ⠋ label` (static `π : label` under WSL, or when `staticSpinner` is set);
* - `attention`: `π ! label`;
* - disabled: `π: label`.
* Without a label the separator trails the brand (`π >`) so the state stays visible.
* The `working` separator cycles `TERMINAL_TITLE_SPINNER_STYLES[style]`; `style`
* defaults to `braille` so existing 5-arg callers keep the historical frames.
* `staticSpinner` is the caller's own reasons to pin the working separator: the
* Windows native-title path has failed, or `tui.spinnerInterval` is `0`.
*/
export function buildTerminalTitleWithState(
label: string | undefined,
state: TerminalTitleState,
frame: number,
enabled: boolean,
platform: NodeJS.Platform = process.platform,
style: TerminalTitleSpinnerStyle = "braille",
env: NodeJS.ProcessEnv = $env as NodeJS.ProcessEnv,
staticSpinner = false,
): string {
if (!enabled) return label ? `${DEFAULT_TERMINAL_TITLE}: ${label}` : DEFAULT_TERMINAL_TITLE;
const frames = TERMINAL_TITLE_SPINNER_STYLES[style] ?? TERMINAL_TITLE_SPINNER_STYLES.braille;
const staticHost = staticSpinner || isStaticTitleHost(platform, env);
const separator =
state === "working"
? staticHost
? STATIC_TITLE_WORKING_SEPARATOR
: frames[frame % frames.length]
: state === "attention"
? TITLE_ATTENTION_SEPARATOR
: TITLE_IDLE_SEPARATOR;
return label ? `${DEFAULT_TERMINAL_TITLE} ${separator} ${label}` : `${DEFAULT_TERMINAL_TITLE} ${separator}`;
}
/**
* The tab title while a TSP terminal renders: the session name (`omp` before
* there is one) and the branch's pull request. The terminal shows run state
* itself, so there is no brand or state separator.
*/
export function buildNativeTerminalTitle(sessionName: string | undefined, pullRequest: number | undefined): string {
const name = sessionName ?? NATIVE_TERMINAL_TITLE;
return pullRequest === undefined ? name : `${name} · #${pullRequest}`;
}
function emitTerminalTitle(): void {
// The teardown latch lives at the sink (`writeTerminalTitle`), so every path
// here is covered without a second check.
// An extension override owns the terminal verbatim; the terminal sink
// deduplicates repeated state updates.
const native = isNativeRendering();
const next =
terminalTitleRuntime.extensionOverride ??
(native
? buildNativeTerminalTitle(terminalTitleRuntime.sessionName, terminalTitleRuntime.pullRequest)
: buildTerminalTitleWithState(
terminalTitleRuntime.label,
terminalTitleRuntime.state,
terminalTitleRuntime.frame,
terminalTitleRuntime.enabled,
process.platform,
terminalTitleRuntime.style,
$env as NodeJS.ProcessEnv,
(process.platform === "win32" && terminalTitleRuntime.nativeTitleFailed) ||
terminalTitleRuntime.intervalMs === SPINNER_INTERVAL_STATIC,
));
// The composed working title is the only write that can fail into an
// animated OSC frame: on native failure it re-pins static (`:`), while a
// direct `setTerminalTitle` preserves its caller's title verbatim.
const recomposeStaticOnFailure =
!native &&
terminalTitleRuntime.extensionOverride === undefined &&
terminalTitleRuntime.state === "working" &&
terminalTitleRuntime.enabled &&
terminalTitleRuntime.intervalMs !== SPINNER_INTERVAL_STATIC &&
!isStaticTitleHost();
writeTerminalTitle(next, recomposeStaticOnFailure);
}
function stopTerminalTitleSpinner(): void {
clearInterval(terminalTitleRuntime.timer);
terminalTitleRuntime.timer = undefined;
}
function startTerminalTitleSpinner(): void {
if (
isNativeRendering() ||
isStaticTitleHost() ||
terminalTitleRuntime.disposed ||
terminalTitleRuntime.timer ||
terminalTitleRuntime.nativeTitleFailed ||
terminalTitleRuntime.intervalMs === SPINNER_INTERVAL_STATIC ||
!process.stdout.isTTY
)
return;
terminalTitleRuntime.timer = setInterval(() => {
terminalTitleRuntime.frame =
(terminalTitleRuntime.frame + 1) % TERMINAL_TITLE_SPINNER_STYLES[terminalTitleRuntime.style].length;
// An extension override is frame-independent; the sink would dedupe it anyway.
if (terminalTitleRuntime.extensionOverride === undefined) emitTerminalTitle();
}, terminalTitleRuntime.intervalMs);
// Never keep the event loop alive for a cosmetic animation.
terminalTitleRuntime.timer.unref?.();
}
/**
* Reflect the agent run state in the terminal title's separator: `working`
* animates (static `:` under WSL), `idle` shows `>` (your turn), and
* `attention` shows `!` (agent blocked on you). Gated off by `tui.titleState`.
*/
export function setTerminalTitleState(state: TerminalTitleState): void {
terminalTitleRuntime.state = state;
if (state === "working" && terminalTitleRuntime.enabled) startTerminalTitleSpinner();
else stopTerminalTitleSpinner();
emitTerminalTitle();
}
/** Enable/disable the run-state separator (driven by the `tui.titleState` setting). */
export function setTerminalTitleStateEnabled(enabled: boolean): void {
terminalTitleRuntime.enabled = enabled;
if (enabled && terminalTitleRuntime.state === "working") startTerminalTitleSpinner();
else stopTerminalTitleSpinner();
emitTerminalTitle();
}
/**
* Select the working-state spinner glyph set (driven by `tui.titleSpinner`).
* Unknown values fall back to `braille`; switching style resets the frame so a
* shorter set never indexes out of range, and re-arms the live interval when
* `working` so the tick cadence stays on the new frames.
*/
export function setTerminalTitleSpinnerStyle(style: string | undefined): void {
const next: TerminalTitleSpinnerStyle =
style === "braille" || style === "pulse" || style === "dots" || style === "line" ? style : "braille";
if (next === terminalTitleRuntime.style) return;
terminalTitleRuntime.style = next;
terminalTitleRuntime.frame = 0;
if (terminalTitleRuntime.state === "working" && terminalTitleRuntime.enabled) {
stopTerminalTitleSpinner();
startTerminalTitleSpinner();
}
emitTerminalTitle();
}
/**
* Set the working-state spinner period (driven by `tui.spinnerInterval` /
* `--spinner-interval`, already validated by the setting layer). `0` pins
* the working separator to a static `:` and schedules no interval. Re-arms a
* live spinner so the new cadence applies immediately.
*/
export function setTerminalTitleSpinnerInterval(intervalMs: number): void {
if (intervalMs === terminalTitleRuntime.intervalMs) return;
terminalTitleRuntime.intervalMs = intervalMs;
terminalTitleRuntime.frame = 0;
if (terminalTitleRuntime.state === "working" && terminalTitleRuntime.enabled) {
stopTerminalTitleSpinner();
startTerminalTitleSpinner();
}
emitTerminalTitle();
}
/**
* Take ownership of the terminal title: the counterpart to
* {@link disposeTerminalTitleState}, called once when the UI claims the terminal.
* This is the ONLY release of the teardown latch. Routine updates — session
* rename, cwd change, focus swap, collab host state — must not release it: they
* can arrive from an async transition that resumes after teardown already handed
* the tab back to the shell.
*/
export function initTerminalTitleState(): void {
terminalTitleRuntime.disposed = false;
// The native-failure latch is claim-scoped like the spinner timer: a fresh
// owner gets a re-probed native path, so a transient SetConsoleTitleW
// failure in one session must not pin every later session static. The next
// write re-latches only if the native path still fails. Reset the cached
// binding too — tests swap the dlopen double per case via beforeEach, and a
// stale failure-shaped binding would otherwise survive the reset.
disposeWindowsConsoleTitleApi();
terminalTitleRuntime.nativeTitleFailed = false;
terminalTitleRuntime.nativeTitleRestored = false;
// `??=` keeps the shell's title across a repeat claim, which would otherwise
// read omp's own title back.
try {
terminalTitleRuntime.initialWindowsConsoleTitle ??= getWindowsConsoleTitleApi()?.get();
} catch {
// Unreadable: teardown falls back to the OSC clear in `popTerminalTitle()`.
}
// A fresh claim starts from the shell's title, not whatever the previous
// session last emitted: the dedupe cache must not swallow the first write.
// Releasing the latch alone would leave a stopped timer behind a `working`
// state — a frozen spinner frame. Mirror the enable path and re-arm.
if (terminalTitleRuntime.state === "working" && terminalTitleRuntime.enabled) startTerminalTitleSpinner();
// A TSP terminal takes the plain native title while it renders; the
// classic `π > label` (and its spinner) comes back when it stops.
terminalTitleRuntime.unwatchNative ??= onNativeRenderingChange(native => {
if (native) stopTerminalTitleSpinner();
else if (terminalTitleRuntime.state === "working" && terminalTitleRuntime.enabled) startTerminalTitleSpinner();
emitTerminalTitle();
});
}
/**
* Stop the spinner timer and latch the runtime off; call on session/UI teardown.
* The latch is the load-bearing half: `shutdown()` disposes and restores the shell
* title BEFORE it unsubscribes the session, so a live `#handleAgentStart` in that
* window would otherwise re-arm the spinner and write `π ⠋ …` into the parent
* shell's tab. Released only by {@link initTerminalTitleState}.
*/
export function disposeTerminalTitleState(): void {
// The session ends with the UI: Tern must not resume it in this pane.
sessionSource = undefined;
reportTernSession();
terminalTitleRuntime.disposed = true;
terminalTitleRuntime.unwatchNative?.();
terminalTitleRuntime.unwatchNative = undefined;
// `popTerminalTitle()` hands the terminal back to the shell, so the runtime no
// longer knows what is on screen: the stale dedupe cache (`lastTerminalTitle`,
// cleared below) must not swallow the first write after the latch releases.
stopTerminalTitleSpinner();
const initialTitle = terminalTitleRuntime.initialWindowsConsoleTitle;
if (initialTitle !== undefined) {
terminalTitleRuntime.nativeTitleRestored = setWindowsConsoleTitle(initialTitle);
terminalTitleRuntime.initialWindowsConsoleTitle = undefined;
}
disposeWindowsConsoleTitleApi();
lastTerminalTitle = undefined;
}
/**
* Save the current terminal title on terminals that support xterm window ops.
*/
export function pushTerminalTitle(): void {
if (!process.stdout.isTTY || isTerminalHeadless()) return;
writeTerminalSequence("\x1b[22;2t");
}
/**
* Hand the terminal title back to the shell on teardown, after
* {@link disposeTerminalTitleState}.
*
* Clears the title (OSC 0) before popping the one {@link pushTerminalTitle} saved
* (CSI 23;2t): a terminal with a title stack (xterm, iTerm2, VTE, mintty) restores
* the saved title over the clear, and one without a stack ignores the pop and
* keeps the clear instead of omp's title. Writes nothing when teardown already
* restored a Windows console title natively.
*/
export function popTerminalTitle(): void {
if (!process.stdout.isTTY || isTerminalHeadless()) return;
if (terminalTitleRuntime.nativeTitleRestored) {
terminalTitleRuntime.nativeTitleRestored = false;
return;
}
writeTerminalSequence("\x1b]0;\x07\x1b[23;2t");
}