Compare commits

...
Author SHA1 Message Date
Cursor Agentand陈大猫 9b648ad580 fix(mcp): allow ] inside RHEL prompt \W segment
The final ] before $ or # stays the delimiter, so a basename
like My [Project] still clears the idle-prompt readiness gate.

Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
2026-10-24 17:47:30 +00:00
Cursor Agentand陈大猫 b151aa3aba fix(mcp): accept whitespace in RHEL prompt \W segment
Bracketed bash \W can contain spaces, so a prompt like
[user@host My Project]$ never became an idle prompt and
terminal_execute waited out the readiness timeout.

Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
2026-10-24 17:43:51 +00:00
Cursor Agentand陈大猫 6a2646dd90 fix(mcp): recognize RHEL-family bash prompts as idle
Oracle/RHEL/CentOS/Fedora default PS1 `[user@host path]$` never matched
the narrow POSIX prompt pattern, so MCP terminal_execute waited out the
shell-ready budget and sent no command.

Fixes #3914

Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
2026-10-24 17:34:55 +00:00
2 changed files with 73 additions and 5 deletions

No files matched your search

+8 -5
View File
@@ -61,10 +61,13 @@ const POWERSHELL_PROMPT_PATTERN = /^PS(?:\s+\S.*)?>$/;
// PowerShell's `PS C:\...>` (handled by POWERSHELL_PROMPT_PATTERN first).
const CMD_PROMPT_PATTERN = /^[A-Za-z]:(?:\\[^<>"|]*)?>$/;
// Classic `user@host:...$` / `user@host:...#` login prompt (bash/zsh in WSL,
// remote Linux, etc.). Intentionally narrow so custom / fish prompts do not
// flip a Windows DefaultShell soft hint.
const POSIX_PROMPT_PATTERN = /^[^\s@]+@[^\s:]+(?::[^\n\r]*)?[#$]$/;
// Default login prompts, kept narrow so custom / fish / starship prompts do
// not flip a Windows DefaultShell soft hint:
// - bash/zsh `user@host[:path][#$]` (colon before the path; `root@host ~#` stays rejected)
// - RHEL-family `[\u@\h \W]\$` -> `[user@host path][#$]`, or `[user@host][#$]`
// with no path. `\W` may contain whitespace or `]` (`[user@host My [Project]]$`);
// the final `]` before `$`/`#` is the delimiter.
const POSIX_PROMPT_PATTERN = /^(?:[^\s@]+@[^\s:]+(?::[^\n\r]*)?|\[[^\s@\]]+@[^\s\]:]+(?:\s+[^\n\r]+)?\])[#$]$/;
function isDefaultPowerShellPromptLine(line) {
return POWERSHELL_PROMPT_PATTERN.test(String(line || ""));
@@ -149,7 +152,7 @@ function trackSessionIdlePrompt(session, chunk) {
// Return `session.lastIdlePrompt` only if the PTY's recent rolling tail
// still ends with it. The cached prompt is updated only when
// extractTrailingIdlePrompt recognizes a known shape (PowerShell, cmd.exe,
// or `user@host[:path][#$]`); a remote shell switch into another shell, an
// `user@host[:path][#$]`, or RHEL `[user@host path][#$]`); 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`
// would then keep using to coerce future commands into a PowerShell
+65
View File
@@ -133,14 +133,50 @@ test("isDefaultCmdPromptLine matches drive-letter cmd prompts only", () => {
test("isDefaultPosixPromptLine matches classic user@host prompts", () => {
assert.equal(isDefaultPosixPromptLine("alice@host:~$"), true);
assert.equal(isDefaultPosixPromptLine("user@host:~$"), true);
assert.equal(isDefaultPosixPromptLine("alice@wsl:/mnt/c$"), true);
assert.equal(isDefaultPosixPromptLine("root@box:/#"), true);
assert.equal(isDefaultPosixPromptLine("[opc@instance-20260905-1821 ~]$"), true);
assert.equal(isDefaultPosixPromptLine("[opc@instance-20260905-1821 ~]$ "), true);
assert.equal(isDefaultPosixPromptLine("[root@host ~]#"), true);
assert.equal(isDefaultPosixPromptLine("[user@host]$"), true);
assert.equal(isDefaultPosixPromptLine("[user@host My Project]$"), true);
assert.equal(isDefaultPosixPromptLine("[user@host My Project]$ "), true);
assert.equal(isDefaultPosixPromptLine("[user@host My [Project]]$"), true);
assert.equal(isDefaultPosixPromptLine("[user@host My [Project]]$ "), true);
assert.equal(isDefaultPosixPromptLine("root@host ~#"), false);
assert.equal(isDefaultPosixPromptLine("PS C:\\Users\\alice>"), false);
assert.equal(isDefaultPosixPromptLine("C:\\Users\\alice>"), false);
assert.equal(isDefaultPosixPromptLine(""), false);
});
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.
assert.equal(
extractTrailingIdlePrompt("[opc@instance-20260905-1821 ~]$ "),
"[opc@instance-20260905-1821 ~]$ ",
);
assert.equal(
extractTrailingIdlePrompt("Last login: Fri Oct 24\r\n[root@host ~]#"),
"[root@host ~]#",
);
assert.equal(
extractTrailingIdlePrompt("welcome\r\nuser@host:~$ "),
"user@host:~$ ",
);
assert.equal(
extractTrailingIdlePrompt("welcome\r\n[user@host My Project]$ "),
"[user@host My Project]$ ",
);
assert.equal(
extractTrailingIdlePrompt("welcome\r\n[user@host My [Project]]$ "),
"[user@host My [Project]]$ ",
);
// Unbracketed space form is still not a recognized default prompt.
assert.equal(extractTrailingIdlePrompt("welcome\r\nroot@host ~#"), "");
});
test("isPlausibleCliVersionOutput rejects stack traces and file URLs", () => {
assert.equal(isPlausibleCliVersionOutput("2.1.123 (Claude Code)"), true);
assert.equal(isPlausibleCliVersionOutput("codex-cli 0.125.0"), true);
@@ -674,6 +710,35 @@ test("getFreshIdlePrompt returns empty string when the session has no cached pro
);
});
test("trackSessionIdlePrompt and getFreshIdlePrompt accept an Oracle/RHEL bracketed bash prompt", () => {
const session = {};
trackSessionIdlePrompt(session, "Last login...\r\n[opc@instance-20260905-1821 ~]$ ");
assert.equal(session.lastIdlePrompt, "[opc@instance-20260905-1821 ~]$ ");
assert.equal(getFreshIdlePrompt(session), "[opc@instance-20260905-1821 ~]$ ");
const rootSession = {};
trackSessionIdlePrompt(rootSession, "[root@host ~]#");
assert.equal(getFreshIdlePrompt(rootSession), "[root@host ~]#");
const classic = {};
trackSessionIdlePrompt(classic, "user@host:~$ ");
assert.equal(getFreshIdlePrompt(classic), "user@host:~$ ");
const unbracketed = {};
assert.equal(trackSessionIdlePrompt(unbracketed, "root@host ~#"), "");
assert.equal(getFreshIdlePrompt(unbracketed), "");
const spacedPath = {};
trackSessionIdlePrompt(spacedPath, "Last login...\r\n[user@host My Project]$ ");
assert.equal(spacedPath.lastIdlePrompt, "[user@host My Project]$ ");
assert.equal(getFreshIdlePrompt(spacedPath), "[user@host My Project]$ ");
const bracketedPath = {};
trackSessionIdlePrompt(bracketedPath, "Last login...\r\n[user@host My [Project]]$ ");
assert.equal(bracketedPath.lastIdlePrompt, "[user@host My [Project]]$ ");
assert.equal(getFreshIdlePrompt(bracketedPath), "[user@host My [Project]]$ ");
});
test("getFreshIdlePrompt and trackSessionIdlePrompt round-trip through a real PTY-like flow", () => {
// (1) Remote PowerShell prompt arrives — lastIdlePrompt is captured.
const session = {};