feat(agent): detect coding agents on Windows via shell-integration command capture

ConPTY has no foreground process group, so the Unix process-table poll
(pgid -> argv) has no Windows equivalent and foreground_agent was a stub
returning None. Follow Warp's approach instead: the shell integration
captures the submitted command line at preexec and carries it
percent-encoded on the OSC 133;C mark; the daemon detects the agent from
that string.

- shell_integration: all four bodies (zsh/bash/fish/PowerShell) append
  the submitted line to 133;C, truncated to 512 chars and escaped
  (% ESC BEL CR NL). PowerShell guards the surrogate-splitting truncation
  and wraps the emission in try/catch (EscapeDataString throws on lone
  surrogates under .NET Framework / PS 5.1).
- pane: ShellState.command stores the capture (cleared on D only, so a
  stray foreign A/B mid-command can't wipe the chip); on Windows
  apply_signals feeds it through the new detect_from_command_with.
- cli_agent: detect_from_command_with tokenizes a typed command line
  (quotes, & call operator, case-insensitive) and reuses the argv
  detection; base_stem handles backslash paths and .exe/.cmd/.bat/.ps1.
- ForegroundProbes::agent is now Option<Option<CLIAgent>>: None means
  'no process-table view' (native SSH, Windows) and is never applied,
  fixing the 0.5s poll wiping event-branded agents on native-SSH panes.
This commit is contained in:
l0ng-ai
2026-07-16 20:23:47 +08:00
parent 0603aa99b8
commit 775f0b7024
3 changed files with 315 additions and 30 deletions
+119 -11
View File
@@ -6,12 +6,15 @@
//! deliberately *not* tty7's own agent: it only observes and enriches whatever
//! agent the user launched.
//!
//! Detection is command-based: the daemon already
//! Detection is command-based: on macOS/Linux the daemon already
//! reads the foreground process's `argv` for SSH-context sniffing, so we reuse
//! that to match the invoked command against a known agent. Matching is a pure
//! function over `argv` — [`CLIAgent::detect_from_argv`] — kept here in `core`
//! (framework-light, unit-tested) and called daemon-side, with the resulting
//! `Option<CLIAgent>` streamed to the client for the UI.
//! `Option<CLIAgent>` streamed to the client for the UI. On Windows ConPTY
//! exposes no foreground process group, so the input is the *typed command
//! line* the shell integration captures at preexec and carries on the `133;C`
//! mark — [`CLIAgent::detect_from_command_with`] matches it the same way.
//!
//! The enum is serialized across the daemon↔client protocol, so its variants
//! are the wire contract; add new agents at the end.
@@ -25,7 +28,6 @@
//! the client.
use std::collections::HashMap;
use std::path::Path;
use serde::{Deserialize, Serialize};
@@ -310,7 +312,7 @@ impl CLIAgent {
if arg.starts_with('-') {
continue;
}
for segment in arg.split('/') {
for segment in arg.split(['/', '\\']) {
if let Some(agent) =
CLIAgent::match_token(&base_stem(segment).to_ascii_lowercase())
{
@@ -322,6 +324,37 @@ impl CLIAgent {
None
}
/// [`detect_from_argv_with`](Self::detect_from_argv_with) over a *typed
/// command line* rather than a live process `argv` — the Windows detection
/// input. ConPTY has no foreground process group to resolve to an argv, so
/// there the daemon learns what runs from the shell integration instead:
/// PowerShell's `PSConsoleHostReadLine` wrapper reports the submitted line
/// on the `133;C` mark (the same capture Warp's Windows integration uses),
/// and this matches it like an argv.
///
/// The tokenization is deliberately naive — whitespace split, surrounding
/// quotes trimmed, a leading PowerShell call operator (`&`) dropped, and
/// everything lowercased (Windows commands are case-insensitive). A quoted
/// launcher path containing spaces splits wrong and misses — notably
/// PSReadLine tab-completion's `& 'C:\Program Files\…\claude.exe'` — the
/// accepted trade-off for not writing a shell parser; the dominant shapes
/// (a bare shim on PATH, `npx …`) tokenize fine, and `agent_commands`
/// rules cover personal wrappers.
pub fn detect_from_command_with(
command: &str,
custom: &HashMap<String, String>,
) -> Option<CLIAgent> {
let mut argv: Vec<String> = command
.split_whitespace()
.map(|t| t.trim_matches(['"', '\'']).to_ascii_lowercase())
.filter(|t| !t.is_empty())
.collect();
if argv.first().is_some_and(|t| t == "&") {
argv.remove(0);
}
Self::detect_from_argv_with(&argv, custom)
}
}
/// A `KEY=value` shell environment assignment prefix (`FOO=bar cmd`). The `KEY`
@@ -345,14 +378,24 @@ fn is_env_assignment(token: &str) -> bool {
/// The final path component with a leading dir and a trailing script extension
/// stripped, lowercased-ready but case preserved (callers lowercase when they
/// match interpreter args). `/usr/bin/claude` → `claude`, `cli.js` → `cli`.
/// Splits on both separators by hand (not [`Path`]) so a Windows path in a
/// captured command line (`C:\…\claude.cmd`) resolves the same on every
/// platform — including in tests run on Unix.
fn base_stem(token: &str) -> &str {
let name = Path::new(token)
.file_name()
.and_then(|n| n.to_str())
.unwrap_or(token);
// Strip one known script extension; leave unknown suffixes intact so
// `claude-code` stays whole.
for ext in [".js", ".mjs", ".cjs", ".ts", ".py", ".rb", ".sh"] {
// Trailing separators are dropped first (`claude/` → `claude`, matching
// the old `Path::file_name` behavior), then everything up to the last
// separator.
let trimmed = token.trim_end_matches(['/', '\\']);
let name = match trimmed.rfind(['/', '\\']) {
Some(i) => &trimmed[i + 1..],
None => trimmed,
};
// Strip one known script/launcher extension; leave unknown suffixes intact
// so `claude-code` stays whole. The Windows set covers npm's shim trio
// (`claude.cmd` / `claude.ps1` / `claude.exe`).
for ext in [
".js", ".mjs", ".cjs", ".ts", ".py", ".rb", ".sh", ".exe", ".cmd", ".bat", ".ps1",
] {
if let Some(stem) = name.strip_suffix(ext) {
return stem;
}
@@ -617,6 +660,11 @@ mod tests {
CLIAgent::detect_from_argv(&argv(&["cursor-agent"])),
Some(CLIAgent::Cursor)
);
// A trailing separator is tolerated, matching Path::file_name.
assert_eq!(
CLIAgent::detect_from_argv(&argv(&["claude/"])),
Some(CLIAgent::Claude)
);
}
#[test]
@@ -737,6 +785,66 @@ mod tests {
);
}
#[test]
fn detects_from_typed_command_lines() {
let none = HashMap::new();
// Plain invocations, flags in tow.
assert_eq!(
CLIAgent::detect_from_command_with("claude --resume abc", &none),
Some(CLIAgent::Claude)
);
// Windows launcher shapes: npm shims, absolute backslash paths, and
// case-insensitive names.
assert_eq!(
CLIAgent::detect_from_command_with("claude.exe", &none),
Some(CLIAgent::Claude)
);
assert_eq!(
CLIAgent::detect_from_command_with(
r"C:\Users\x\AppData\Roaming\npm\claude.cmd --model opus",
&none
),
Some(CLIAgent::Claude)
);
assert_eq!(
CLIAgent::detect_from_command_with("CLAUDE", &none),
Some(CLIAgent::Claude)
);
// PowerShell call operator + a quoted (space-free) path.
assert_eq!(
CLIAgent::detect_from_command_with(r#"& "C:\tools\codex.exe""#, &none),
Some(CLIAgent::Codex)
);
// Interpreter-wrapped, Windows separators in the script path.
assert_eq!(
CLIAgent::detect_from_command_with(
r"node C:\x\node_modules\@anthropic-ai\claude-code\cli.js",
&none
),
Some(CLIAgent::Claude)
);
assert_eq!(
CLIAgent::detect_from_command_with("npx.cmd @google/gemini-cli", &none),
Some(CLIAgent::Gemini)
);
// Non-interpreter launchers never match on their arguments.
assert_eq!(
CLIAgent::detect_from_command_with("notepad claude.txt", &none),
None
);
assert_eq!(
CLIAgent::detect_from_command_with("cat codex.md", &none),
None
);
assert_eq!(CLIAgent::detect_from_command_with("", &none), None);
// Custom rules apply to the typed launcher too.
let custom: HashMap<String, String> = [("cc".to_string(), "claude".to_string())].into();
assert_eq!(
CLIAgent::detect_from_command_with("cc -c", &custom),
Some(CLIAgent::Claude)
);
}
#[test]
fn parses_sentinel_events() {
let ev = parse_agent_event(
+135 -12
View File
@@ -404,7 +404,13 @@ enum PaneBackend {
/// keeps the reader's signature readable.
struct ForegroundProbes {
remote: Box<dyn Fn() -> Option<RemoteContext> + Send>,
agent: Box<dyn Fn() -> Option<crate::core::cli_agent::CLIAgent> + Send>,
/// Outer `None` means this backend has no process-table view of the PTY
/// foreground at all (native SSH; Windows, where ConPTY has no foreground
/// process group) — "no opinion", never applied, so it can't wipe an agent
/// identified another way (sentinel events, the Windows `133;C;<cmd>`
/// mark). `Some(answer)` is a real poll result; its inner `None` ("polled,
/// no agent") clears the chip.
agent: Box<dyn Fn() -> Option<Option<crate::core::cli_agent::CLIAgent>> + Send>,
}
/// The local-PTY backend: the same handles `DaemonPane` has always owned.
@@ -715,6 +721,9 @@ impl DaemonPane {
// gate closures answer "nothing local": OSC 133 marks from the remote
// shell are trusted verbatim (correct — the remote shell *is* the session),
// and no process-table SSH detection runs (this pane already *is* SSH).
// The agent probe's `None` is "no opinion" (never applied), so an agent
// identified from its sentinel events keeps its chip — the poll used to
// wipe it within half a second.
let reader = Self::spawn_reader(
state,
shutting_down,
@@ -895,7 +904,11 @@ impl DaemonPane {
} else {
None
};
let agent = poll_now.then(&foreground_agent_fn);
// Flattened: a fired poll whose probe has no
// process-table view (native SSH, Windows) folds to
// "no opinion" and is never applied — see
// [`ForegroundProbes::agent`].
let agent = poll_now.then(&foreground_agent_fn).flatten();
let tr1 = trace.then(std::time::Instant::now);
let mut st = state.lock().unwrap();
@@ -1604,6 +1617,18 @@ fn apply_signals(st: &mut PaneState, signals: SniffSignals) {
}
}
if let Some(shell) = signals.shell {
// Windows: agent identity rides the C mark's command capture — ConPTY
// has no foreground process group for the Unix 0.5 s poll to read an
// argv from. `C;<cmd>` detects, the prompt marks (`A`/`B`/`D`) cleared
// `command` so they apply `None` and clear the chip. Unix keeps the
// poll (it sees through scripts and wrappers) and never consults the
// mark. Applied before the sentinel events below so an event naming
// the agent can still re-brand within the same chunk.
#[cfg(windows)]
apply_agent(
st,
agent_from_shell_mark(&shell, crate::core::config::agent_commands_cached()),
);
st.shell = shell.clone();
if let Some(sub) = &st.subscriber {
let _ = sub.send(DaemonMsg::Prompt {
@@ -1616,6 +1641,23 @@ fn apply_signals(st: &mut PaneState, signals: SniffSignals) {
apply_agent_signals(st, signals.agent_events, signals.notification);
}
/// The coding agent named by the shell's last `133;C;<command>` capture — the
/// Windows detection input ([`apply_signals`] applies it there on every shell
/// mark). `None` both at the prompt (`D` cleared `command`) and for an
/// unrecognized command, so applying the answer verbatim also clears
/// the chip when the command ends. Compiled on every platform so the unit
/// tests cover it from Unix dev machines; only the Windows build calls it.
#[cfg_attr(not(windows), allow(dead_code))]
fn agent_from_shell_mark(
shell: &ShellState,
custom: &std::collections::HashMap<String, String>,
) -> Option<crate::core::cli_agent::CLIAgent> {
shell
.command
.as_deref()
.and_then(|cmd| crate::core::cli_agent::CLIAgent::detect_from_command_with(cmd, custom))
}
/// Fold the chunk's agent signals into the pane's session state and push any
/// resulting change. Called with the state lock held.
///
@@ -1758,22 +1800,32 @@ fn foreground_remote_context(_master: &Mutex<Box<dyn MasterPty + Send>>) -> Opti
/// Identify the third-party CLI coding agent (Claude Code, Codex, …) owning the
/// PTY foreground, from its `argv`. Same process-table read as
/// [`foreground_remote_context`]; runs off the hot path on the 0.5 s poll.
/// Always `Some(answer)` — this platform *has* the process-table view, so even
/// "no agent" is a real answer that must apply (it clears the chip when the
/// agent exits). See [`ForegroundProbes::agent`] for the outer option's contract.
#[cfg(any(target_os = "macos", target_os = "linux"))]
fn foreground_agent(
master: &Mutex<Box<dyn MasterPty + Send>>,
) -> Option<crate::core::cli_agent::CLIAgent> {
let pid = master.lock().ok().and_then(|m| m.process_group_leader())?;
let argv = crate::daemon::remote::foreground_argv(pid)?;
crate::core::cli_agent::CLIAgent::detect_from_argv_with(
&argv,
crate::core::config::agent_commands_cached(),
)
) -> Option<Option<crate::core::cli_agent::CLIAgent>> {
let detect = || {
let pid = master.lock().ok().and_then(|m| m.process_group_leader())?;
let argv = crate::daemon::remote::foreground_argv(pid)?;
crate::core::cli_agent::CLIAgent::detect_from_argv_with(
&argv,
crate::core::config::agent_commands_cached(),
)
};
Some(detect())
}
/// Windows: ConPTY has no foreground process group, so there is no process
/// table to poll — "no opinion" (`None`), never applied. Agent identity comes
/// from the shell integration's `133;C;<command>` capture instead, applied in
/// [`apply_signals`] via [`agent_from_shell_mark`].
#[cfg(not(any(target_os = "macos", target_os = "linux")))]
fn foreground_agent(
_master: &Mutex<Box<dyn MasterPty + Send>>,
) -> Option<crate::core::cli_agent::CLIAgent> {
) -> Option<Option<crate::core::cli_agent::CLIAgent>> {
None
}
@@ -1791,6 +1843,11 @@ struct ShellState {
active: bool,
at_prompt: bool,
last_exit_code: Option<i32>,
/// The command line the shell reported on its last `133;C;<cmd>` mark
/// (percent-decoded), cleared when the command finishes (`D`). All of
/// tty7's shell integrations carry the payload; it is the Windows
/// coding-agent detection input (see [`agent_from_shell_mark`]).
command: Option<String>,
}
/// Changes a `feed` call produced, if any.
@@ -1869,10 +1926,26 @@ fn handle_osc133(shell: &mut ShellState, rest: &[u8]) -> bool {
// grid) instead of the editor — the "un-deletable char / doubled prompt"
// glitch. Setting it as early as `D`/`A` closes that window.
match rest.first() {
// A/B deliberately leave `command` alone: every tty7 integration emits
// D *before* A at a real prompt (so it's already cleared there), while
// a stray A/B from a foreign integration mid-command (a nested or
// remote shell drawing its own prompt — Windows has no pgid gate to
// reject it with, cf. issue #26) must not wipe the agent chip.
Some(b'A') | Some(b'B') => shell.at_prompt = true,
Some(b'C') => shell.at_prompt = false,
Some(b'C') => {
shell.at_prompt = false;
// tty7 extension: our shell integrations append the submitted
// command line, percent-encoded — the Windows agent-detection
// input (see [`agent_from_shell_mark`]). Bare `C` (a foreign
// shell's own integration) leaves it `None`.
shell.command = rest
.strip_prefix(b"C;")
.map(|c| String::from_utf8_lossy(&percent_decode(c)).into_owned())
.filter(|s| !s.trim().is_empty());
}
Some(b'D') => {
shell.at_prompt = true;
shell.command = None;
shell.last_exit_code = rest
.strip_prefix(b"D;")
.and_then(|c| std::str::from_utf8(c).ok())
@@ -2041,7 +2114,9 @@ mod tests {
// hanging CI.
let mut detected = None;
for _ in 0..200 {
if let Some(agent) = foreground_agent(&master) {
// Flatten: the outer Some is just "this platform has a process
// table"; the poll keeps going until detection actually answers.
if let Some(agent) = foreground_agent(&master).flatten() {
detected = Some(agent);
break;
}
@@ -2406,6 +2481,53 @@ mod tests {
assert_eq!(d.shell.as_ref().unwrap().last_exit_code, Some(130));
}
/// The C mark's command capture (tty7 extension, PowerShell integration) —
/// the Windows agent-detection input: `C;<cmd>` records the submitted line
/// percent-decoded, every prompt mark clears it, and
/// [`agent_from_shell_mark`] turns it into the chip's agent.
#[test]
fn sniff_osc133_command_capture_drives_agent_detection() {
let custom = std::collections::HashMap::new();
let mut s = OscSniffer::new();
// A submitted `claude --help` (space percent-encoded, as the
// PowerShell body emits it).
let c = s.feed(b"\x1b]133;C;claude%20--help\x07");
let shell = c.shell.as_ref().unwrap();
assert!(!shell.at_prompt);
assert_eq!(shell.command.as_deref(), Some("claude --help"));
assert_eq!(
agent_from_shell_mark(shell, &custom),
Some(crate::core::cli_agent::CLIAgent::Claude)
);
// The command finishing (D) clears the capture → the agent clears.
let d = s.feed(b"\x1b]133;D;0\x07");
let shell = d.shell.as_ref().unwrap();
assert_eq!(shell.command, None);
assert_eq!(agent_from_shell_mark(shell, &custom), None);
// A non-agent command sets the capture but detects nothing.
let c = s.feed(b"\x1b]133;C;git%20status\x07");
let shell = c.shell.as_ref().unwrap();
assert_eq!(shell.command.as_deref(), Some("git status"));
assert_eq!(agent_from_shell_mark(shell, &custom), None);
// A bare `C` (a foreign shell integration) leaves no capture.
let c = s.feed(b"\x1b]133;C\x07");
assert_eq!(c.shell.as_ref().unwrap().command, None);
// A stray A/B mid-command (a nested/remote shell drawing its own
// prompt) must NOT wipe the capture — only D (command finished) does.
// Windows has no pgid gate to reject foreign marks with, so this is
// what keeps the agent chip alive while the agent runs.
let _ = s.feed(b"\x1b]133;C;codex\x07");
let a = s.feed(b"\x1b]133;A\x1b]133;B\x07");
assert_eq!(a.shell.as_ref().unwrap().command.as_deref(), Some("codex"));
let d = s.feed(b"\x1b]133;D;0\x07");
assert_eq!(d.shell.as_ref().unwrap().command, None);
}
#[test]
fn sniff_osc133_edit_mode_does_not_emit_prompt_state() {
let mut s = OscSniffer::new();
@@ -3034,6 +3156,7 @@ mod tests {
active: true,
at_prompt: true,
last_exit_code: Some(0),
command: None,
}),
..SniffSignals::default()
},
+61 -7
View File
@@ -7,9 +7,12 @@
//! interoperates with the wider ecosystem rather than a bespoke scheme:
//! - `OSC 133 ; A ST` prompt start
//! - `OSC 133 ; B ST` prompt end / command input begins
//! - `OSC 133 ; C ST` command output begins (command executing)
//! - `OSC 133 ; C [; <cmd>] ST` command output begins; all four integrations
//! append the submitted command line percent-encoded (tty7 extension — the
//! Windows coding-agent detection input, see `core::cli_agent`)
//! - `OSC 133 ; D ; <exit> ST` command finished, with its exit code
//! - `OSC 133 ; V ; 0/1 ST` tty7 extension: shell edit mode
//!
//! plus `OSC 7` to report the cwd precisely (many login shells don't emit it
//! unless they think they're in Terminal.app).
//!
@@ -106,10 +109,21 @@ if [[ -o interactive ]] && [[ -z "$TTY7_SHELL_INTEGRATION" ]]; then
# preexec runs after the user hits Enter, before the command runs: mark the
# start of command output (C). We track an "active" flag so the very first
# prompt (no command yet) doesn't emit a bogus D.
# prompt (no command yet) doesn't emit a bogus D. The C mark carries the
# submitted line ($1), truncated (detection only reads the front) and with
# the bytes that would break OSC framing or the daemon's percent-decode
# escaped (% ESC BEL CR NL) — the coding-agent detection input on Windows,
# where ConPTY has no process table to poll (see core::cli_agent).
__tty7_preexec() {
__tty7_cmd_active=1
__tty7_osc "133;C"
local cmd=$1
cmd=${cmd[1,512]}
cmd=${cmd//\%/%25}
cmd=${cmd//$'\e'/%1B}
cmd=${cmd//$'\a'/%07}
cmd=${cmd//$'\r'/%0D}
cmd=${cmd//$'\n'/%0A}
__tty7_osc "133;C;$cmd"
}
autoload -Uz add-zsh-hook
@@ -177,9 +191,16 @@ if status is-interactive; and test -z "$TTY7_SHELL_INTEGRATION"
printf '\e]7;file://%s%s\a' (hostname) (string replace --all '%' '%25' -- $PWD)
end
# The C mark carries the submitted line, truncated and with the bytes that
# would break OSC framing or the daemon's percent-decode escaped (% ESC BEL
# CR NL) — the Windows agent-detection input (see core::cli_agent). fish
# command substitution splits output on newlines, so a multi-line command
# arrives as a list; the final `string join` re-joins it with the escaped
# newline. `%` must be escaped first (the other escapes introduce `%`).
function __tty7_preexec --on-event fish_preexec
set -g __tty7_cmd_active 1
__tty7_osc "133;C"
set -l cmd (string sub -l 512 -- $argv[1] | string replace -a '%' '%25' | string replace -a \e '%1B' | string replace -a \a '%07' | string replace -a \r '%0D' | string join '%0A')
__tty7_osc "133;C;$cmd"
end
# Runs on the fish_prompt *event*, which fires before fish calls the
@@ -270,9 +291,18 @@ if [[ $- == *i* ]] && [[ -z "$TTY7_SHELL_INTEGRATION" ]]; then
return $ret
}
# The C mark carries the submitted line ($1, from bash-preexec), truncated
# and escaped the same way as the zsh path — the Windows agent-detection
# input (git-bash; see core::cli_agent).
__tty7_preexec() {
__tty7_cmd_active=1
__tty7_osc "133;C"
local cmd=${1:0:512}
cmd=${cmd//\%/%25}
cmd=${cmd//$'\e'/%1B}
cmd=${cmd//$'\a'/%07}
cmd=${cmd//$'\r'/%0D}
cmd=${cmd//$'\n'/%0A}
__tty7_osc "133;C;$cmd"
}
if [[ -z "${bash_preexec_imported:-}" ]]; then
@@ -464,7 +494,13 @@ fi
/// sniffer keys `at_prompt` off (see `daemon::pane::handle_osc133`).
/// - **`PSConsoleHostReadLine`** is PSReadLine's line reader — the closest
/// thing PowerShell has to a preexec. After it returns the submitted line,
/// before the command runs, we emit `133;C` (command output begins).
/// before the command runs, we emit `133;C;<command>` (command output
/// begins), carrying the submitted line percent-encoded as a tty7
/// extension. That capture is the Windows coding-agent detection input:
/// ConPTY has no foreground process group for the daemon's process-table
/// poll to read an `argv` from, so — like Warp — the daemon learns what
/// runs from the line the shell itself reported (see
/// `core::cli_agent::CLIAgent::detect_from_command_with`).
///
/// `$?` must be captured as the very first statement of `prompt` (an
/// assignment sets `$?` to true, clobbering it), and is restored before the
@@ -543,7 +579,25 @@ if (-not $env:TTY7_SHELL_INTEGRATION) {
$line = & $global:__Tty7OrigReadLine
if (-not [string]::IsNullOrWhiteSpace($line)) {
$global:__Tty7CmdActive = $true
Write-Host -NoNewline "$($global:__Tty7Esc)]133;C$($global:__Tty7Bel)"
# Carry the submitted line on the C mark (tty7 extension): the daemon
# detects coding agents from it on Windows, where ConPTY exposes no
# foreground process group to read an argv from. Truncated (detection
# only reads the front) and percent-encoded so the payload can't carry
# a raw `;`, ESC or BEL into the OSC framing. The cut can split a
# surrogate pair, and a lone high surrogate makes EscapeDataString
# throw on .NET Framework (PS 5.1) — inside this wrapper that would
# swallow the submitted line, so drop it and keep the whole mark
# best-effort: a plain `C` still flips the prompt state.
$cmd = if ($line.Length -gt 512) { $line.Substring(0, 512) } else { $line }
if ([char]::IsHighSurrogate($cmd[$cmd.Length - 1])) {
$cmd = $cmd.Substring(0, $cmd.Length - 1)
}
try {
$cmd = [Uri]::EscapeDataString($cmd)
Write-Host -NoNewline "$($global:__Tty7Esc)]133;C;$cmd$($global:__Tty7Bel)"
} catch {
Write-Host -NoNewline "$($global:__Tty7Esc)]133;C$($global:__Tty7Bel)"
}
}
$line
}