Compare commits

...
Author SHA1 Message Date
陈大猫 25074f58f0 fix(mcp): recognize Powerlevel10k ❯ idle prompts
Powerlevel10k paints idle prompts like `~ ❯` / `alice@host ~/src ❯`.
The POSIX idle whitelist never matched Unicode ❯, so MCP terminal_execute
stayed SHELL_NOT_READY while bash `$` worked. Accept common p10k ❯ lines
and still reject pending input and bare ❯.

Fixes #4075
2026-10-31 16:07:35 +08:00
2 changed files with 77 additions and 3 deletions

No files matched your search

+13 -3
View File
@@ -99,6 +99,15 @@ const OH_MY_ZSH_YS_HOST_LINE_PATTERN = /^#\s+[^\s@]+\s+@\s+[^\s]+(?:\s+[^\n\r]+)
// within the path so partially entered commands do not count as idle prompts.
const ALPINE_PROMPT_PATTERN = /^[A-Za-z0-9_][A-Za-z0-9_.-]*:(?:~(?:\/[^#$\n\r]*)?|\/[^#$\n\r]*)[#$]$/;
// Powerlevel10k / similar Unicode prompt-ending ❯ (U+276F). Common idle forms:
// `~ ❯`, `~/path ❯`, `/tmp ❯`, short-dir `proj ❯`, optional `user@host `
// prefix (`alice@host ~ ❯`). Require a non-empty prefix and whitespace before
// ❯ so pending input (`~ ❯ echo READY`, `❯ vim`) cannot match. Bare `❯` alone
// is intentionally rejected — too ambiguous without a path/host prefix
// (same rationale as bare `$`/`#`). Do not attempt two-line bare-❯ here.
const POWERLEVEL10K_PROMPT_PATTERN =
/^(?:[^\s@]+@[^\s:]+\s+)?[^\s❯][^❯\r\n]*\s+❯$/;
function isDefaultPowerShellPromptLine(line) {
return POWERSHELL_PROMPT_PATTERN.test(String(line || ""));
}
@@ -117,7 +126,8 @@ function isDefaultPosixPromptLine(line) {
POSIX_PROMPT_PATTERN.test(withoutEnvPrefix) ||
PAREN_POSIX_PROMPT_PATTERN.test(withoutEnvPrefix) ||
ALPINE_PROMPT_PATTERN.test(withoutEnvPrefix) ||
ZSH_PROMPT_PATTERN.test(withoutEnvPrefix)
ZSH_PROMPT_PATTERN.test(withoutEnvPrefix) ||
POWERLEVEL10K_PROMPT_PATTERN.test(withoutEnvPrefix)
);
}
@@ -234,8 +244,8 @@ function trackSessionIdlePrompt(session, chunk) {
// still ends with it. The cached prompt is updated only when
// extractTrailingIdlePrompt recognizes a known shape (PowerShell, cmd.exe,
// `user@host[:path][#$]`, two-line classic `user@host[:path]` or Oh My Zsh
// `ys` (`# user @ host ...`) + `$`/`#`, RHEL `[user@host path][#$]`, or
// Alpine `host:path[#$]`);
// `ys` (`# user @ host ...`) + `$`/`#`, RHEL `[user@host path][#$]`,
// Alpine `host:path[#$]`, or Powerlevel10k-style `... ❯`);
// a remote shell switch into another shell, an oh-my-posh / starship / custom
// PS1, or any unrecognized prompt would
// otherwise leave a stale value behind, which `resolveEffectiveShellKind`
+64
View File
@@ -261,6 +261,70 @@ test("extracts an Alpine idle prompt while preserving its trailing whitespace",
assert.equal(extractTrailingIdlePrompt("localhost:~# \r\n"), "");
});
test("isDefaultPosixPromptLine matches Powerlevel10k ❯ idle prompts", () => {
for (const prompt of [
"~ ❯",
"~ ❯ ",
"~/proj ❯",
"~/git/Netcatty ❯ ",
"/tmp ❯",
"proj ❯",
"alice@host ~ ❯",
"alice@host ~/src ❯ ",
"(venv) ~ ❯ ",
"(venv) alice@host ~ ❯",
]) {
assert.equal(isDefaultPosixPromptLine(prompt), true, prompt);
}
});
test("Powerlevel10k ❯ recognition rejects pending input and bare ❯", () => {
for (const line of [
"❯",
"❯ ",
"❯ vim",
"~ ❯ echo READY",
"~ ❯echo",
"alice@host ~ ❯ ls",
"proj ❯ vim",
"(venv) ~ ❯ echo hi",
// Must not treat a lone marker as idle (same rationale as bare $/).
]) {
assert.equal(isDefaultPosixPromptLine(line), false, line);
assert.equal(extractTrailingIdlePrompt(`welcome\r\n${line}`), "", line);
}
});
test("extractTrailingIdlePrompt recognizes Powerlevel10k ❯ and preserves trailing space", () => {
assert.equal(extractTrailingIdlePrompt("welcome\r\n~ ❯"), "~ ❯");
assert.equal(extractTrailingIdlePrompt("welcome\r\n~ ❯ "), "~ ❯ ");
assert.equal(
extractTrailingIdlePrompt("Last login...\r\nalice@host ~/src ❯ "),
"alice@host ~/src ❯ ",
);
assert.equal(extractTrailingIdlePrompt("welcome\r\nproj ❯"), "proj ❯");
assert.equal(extractTrailingIdlePrompt("(venv) ~ ❯ "), "(venv) ~ ❯ ");
assert.equal(extractTrailingIdlePrompt("welcome\r\n~ ❯ echo READY"), "");
assert.equal(extractTrailingIdlePrompt("welcome\r\n❯ "), "");
assert.equal(extractTrailingIdlePrompt("welcome\r\n~ ❯\r\n"), "");
});
test("trackSessionIdlePrompt and getFreshIdlePrompt accept a Powerlevel10k ❯ prompt", () => {
const session = {};
trackSessionIdlePrompt(session, "Last login...\r\n~ ❯ ");
assert.equal(session.lastIdlePrompt, "~ ❯ ");
assert.equal(getFreshIdlePrompt(session), "~ ❯ ");
assert.equal(getVisibleIdlePrompt(session), "~ ❯ ");
trackSessionIdlePrompt(session, "printf busy");
assert.equal(getFreshIdlePrompt(session), "");
assert.equal(getVisibleIdlePrompt(session), "");
trackSessionIdlePrompt(session, "\r\nbusy\r\nalice@host ~/src ❯ ");
assert.equal(getFreshIdlePrompt(session), "alice@host ~/src ❯ ");
assert.equal(getVisibleIdlePrompt(session), "alice@host ~/src ❯ ");
});
test("extracts a trailing RHEL-family bracketed idle prompt", () => {
// Oracle Linux / RHEL / CentOS / Fedora default PS1 is `[\u@\h \W]\$ `.
// Trailing space must round-trip, same as the PowerShell prompt capture.