Skip to content

Commit 86bc7a7

Browse files
committed
feat(windows): prefer pwsh over powershell.exe for Shell tool
Environment.detect() resolves pwsh via PATH, then default install under Program Files\PowerShell\7, then inbox System32 powershell.exe, then PATH powershell. shell_name remains Windows PowerShell so Shell and background workers keep using -command. Add tests for pwsh-from-PATH and pwsh-from-install-dir; mock shutil.which in Windows environment tests. Document in CHANGELOG (en/zh).
1 parent 8adf1bb commit 86bc7a7

File tree

5 files changed

+101
-11
lines changed

5 files changed

+101
-11
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ Only write entries that are worth mentioning to users.
1919
- Shell: Exclude empty current session from `/sessions` picker — completely empty sessions (no conversation history and no custom title) are no longer shown in the session list; sessions with a custom title are still displayed
2020
- Shell: Fix slash command completion Enter key behavior — accepting a completion now submits in a single Enter press; auto-submit is limited to slash command completions only; file mention completions (`@`) accept without submitting so the user can continue editing; re-completion after accepting is suppressed to prevent stale completion state
2121
- Shell: Add directory scope toggle to `/sessions` picker — press `Ctrl+A` to switch between showing sessions for the current working directory only or across all known directories; uses a new full-screen session picker UI with header scope indicator and footer hint bar
22+
- Tool: On Windows, the Shell tool now prefers PowerShell 7+ (`pwsh`) when available, and falls back to inbox Windows PowerShell (`powershell.exe`)
2223
- Shell: Add `/btw` side question command — ask a quick question during streaming without interrupting the main conversation; uses the same system prompt and tool definitions for prompt cache alignment; responses display in a scrollable modal panel with streaming support
2324
- Shell: Redesign bottom dynamic area — split the monolithic `visualize.py` (1865 lines) into a modular package (`visualize/`) with dedicated modules for input routing, interactive prompts, approval/question panels, and btw modal; unify input semantics with `classify_input()` for consistent command routing
2425
- Shell: Add queue and steer dual-channel input during streaming — Enter queues messages for delivery after the current turn; Ctrl+S injects messages immediately into the running turn's context; queued messages display in the prompt area with count indicator and can be recalled with ↑

docs/en/release-notes/changelog.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ This page documents the changes in each Kimi Code CLI release.
1212
- Shell: Exclude empty current session from `/sessions` picker — completely empty sessions (no conversation history and no custom title) are no longer shown in the session list; sessions with a custom title are still displayed
1313
- Shell: Fix slash command completion Enter key behavior — accepting a completion now submits in a single Enter press; auto-submit is limited to slash command completions only; file mention completions (`@`) accept without submitting so the user can continue editing; re-completion after accepting is suppressed to prevent stale completion state
1414
- Shell: Add directory scope toggle to `/sessions` picker — press `Ctrl+A` to switch between showing sessions for the current working directory only or across all known directories; uses a new full-screen session picker UI with header scope indicator and footer hint bar
15+
- Tool: On Windows, the Shell tool now prefers PowerShell 7+ (`pwsh`) when available, and falls back to inbox Windows PowerShell (`powershell.exe`)
1516
- Shell: Add `/btw` side question command — ask a quick question during streaming without interrupting the main conversation; uses the same system prompt and tool definitions for prompt cache alignment; responses display in a scrollable modal panel with streaming support
1617
- Shell: Redesign bottom dynamic area — split the monolithic `visualize.py` (1865 lines) into a modular package (`visualize/`) with dedicated modules for input routing, interactive prompts, approval/question panels, and btw modal; unify input semantics with `classify_input()` for consistent command routing
1718
- Shell: Add queue and steer dual-channel input during streaming — Enter queues messages for delivery after the current turn; Ctrl+S injects messages immediately into the running turn's context; queued messages display in the prompt area with count indicator and can be recalled with ↑

docs/zh/release-notes/changelog.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
- Shell:从 `/sessions` 选择器中排除空的当前会话——完全为空的会话(既无对话记录也无自定义标题)不再显示在会话列表中;有自定义标题的会话仍然正常显示
1313
- Shell:修复斜杠命令补全 Enter 键行为——接受补全后现在通过一次 Enter 即可提交命令;自动提交仅限于斜杠命令补全,文件引用(`@`)补全接受后不提交以便继续编辑;接受补全时抑制重新补全,防止过时的补全状态
1414
- Shell:为 `/sessions` 会话选择器新增目录范围切换功能——按 `Ctrl+A` 可在"仅当前工作目录"和"所有已知目录"之间切换会话列表;采用全屏会话选择器 UI,顶部显示当前范围,底部显示快捷键提示
15+
- Tool:Windows 上 Shell 工具在可用时优先使用 PowerShell 7+(`pwsh`),否则回退至系统自带的 Windows PowerShell(`powershell.exe`
1516
- Shell:新增 `/btw` 侧问命令——在 streaming 期间提出快速问题,不打断主对话;使用相同的系统提示词和工具定义以对齐 Prompt 缓存;响应在可滚动的模态面板中显示,支持流式输出
1617
- Shell:重新设计底部动态区——将单体 `visualize.py`(1865 行)拆分为模块化包(`visualize/`),包含输入路由、交互式提示、审批/提问面板和 btw 模态面板等独立模块;通过 `classify_input()` 统一输入语义,实现一致的命令路由
1718
- Shell:新增 streaming 期间的排队和 steer 双通道输入——Enter 将消息排队,在当前轮次结束后发送;Ctrl+S 将消息立即注入到正在运行的轮次上下文中;排队消息在提示区域显示计数指示器,可通过 ↑ 键召回编辑

src/kimi_cli/utils/environment.py

Lines changed: 42 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,53 @@
22

33
import os
44
import platform
5+
import shutil
56
from dataclasses import dataclass
67
from typing import Literal
78

89
from kaos.path import KaosPath
910

1011

12+
def _windows_shell_candidates() -> list[KaosPath]:
13+
"""PowerShell executables to probe, in order.
14+
15+
Prefer PowerShell 7+ (`pwsh`) when present, then fall back to Windows PowerShell 5.1
16+
(`powershell.exe`), matching common developer installs while remaining usable on systems
17+
that only ship the inbox shell.
18+
"""
19+
candidates: list[KaosPath] = []
20+
seen: set[str] = set()
21+
22+
def add(path: str) -> None:
23+
normalized = os.path.normcase(os.path.normpath(path))
24+
if normalized not in seen:
25+
seen.add(normalized)
26+
candidates.append(KaosPath(path))
27+
28+
pwsh = shutil.which("pwsh")
29+
if pwsh:
30+
add(pwsh)
31+
32+
program_files = os.environ.get("ProgramW6432") or os.environ.get(
33+
"ProgramFiles", r"C:\Program Files"
34+
)
35+
add(os.path.join(program_files, "PowerShell", "7", "pwsh.exe"))
36+
37+
system_root = os.environ.get("SYSTEMROOT", r"C:\Windows")
38+
add(
39+
os.path.join(
40+
system_root, "System32", "WindowsPowerShell", "v1.0", "powershell.exe"
41+
)
42+
)
43+
44+
powershell = shutil.which("powershell")
45+
if powershell:
46+
add(powershell)
47+
48+
add("powershell.exe")
49+
return candidates
50+
51+
1152
@dataclass(slots=True, frozen=True, kw_only=True)
1253
class Environment:
1354
os_kind: Literal["Windows", "Linux", "macOS"] | str
@@ -33,16 +74,8 @@ async def detect() -> Environment:
3374

3475
if os_kind == "Windows":
3576
shell_name = "Windows PowerShell"
36-
system_root = os.environ.get("SYSTEMROOT", r"C:\Windows")
37-
possible_paths = [
38-
KaosPath(
39-
os.path.join(
40-
system_root, "System32", "WindowsPowerShell", "v1.0", "powershell.exe"
41-
)
42-
),
43-
]
4477
fallback_path = KaosPath("powershell.exe")
45-
for path in possible_paths:
78+
for path in _windows_shell_candidates():
4679
if await path.is_file():
4780
shell_path = path
4881
break

tests/utils/test_utils_environment.py

Lines changed: 56 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,13 +32,15 @@ async def test_environment_detection_windows(monkeypatch):
3232
monkeypatch.setattr(platform, "machine", lambda: "AMD64")
3333
monkeypatch.setattr(platform, "version", lambda: "10.0.19044")
3434
monkeypatch.setenv("SYSTEMROOT", r"C:\Windows")
35+
monkeypatch.delenv("ProgramW6432", raising=False)
36+
monkeypatch.setattr("kimi_cli.utils.environment.shutil.which", lambda *_a, **_k: None)
3537

3638
expected = os.path.join(
3739
r"C:\Windows", "System32", "WindowsPowerShell", "v1.0", "powershell.exe"
3840
)
3941

4042
async def _mock_is_file(self: KaosPath) -> bool:
41-
return str(self) == expected
43+
return os.path.normcase(str(self)) == os.path.normcase(expected)
4244

4345
monkeypatch.setattr(KaosPath, "is_file", _mock_is_file)
4446

@@ -49,7 +51,58 @@ async def _mock_is_file(self: KaosPath) -> bool:
4951
assert env.os_arch == "AMD64"
5052
assert env.os_version == "10.0.19044"
5153
assert env.shell_name == "Windows PowerShell"
52-
assert str(env.shell_path) == expected
54+
assert os.path.normcase(str(env.shell_path)) == os.path.normcase(expected)
55+
56+
57+
@pytest.mark.skipif(platform.system() == "Windows", reason="Skipping test on Windows")
58+
async def test_environment_detection_windows_prefers_pwsh_from_path(monkeypatch):
59+
monkeypatch.setattr(platform, "system", lambda: "Windows")
60+
monkeypatch.setattr(platform, "machine", lambda: "AMD64")
61+
monkeypatch.setattr(platform, "version", lambda: "10.0.19044")
62+
monkeypatch.setenv("SYSTEMROOT", r"C:\Windows")
63+
expected = os.path.join(r"C:\Program Files", "PowerShell", "7", "pwsh.exe")
64+
65+
def _which(cmd: str, path: str | None = None) -> str | None:
66+
if cmd == "pwsh":
67+
return expected
68+
return None
69+
70+
monkeypatch.setattr("kimi_cli.utils.environment.shutil.which", _which)
71+
72+
async def _mock_is_file(self: KaosPath) -> bool:
73+
return os.path.normcase(str(self)) == os.path.normcase(expected)
74+
75+
monkeypatch.setattr(KaosPath, "is_file", _mock_is_file)
76+
77+
from kimi_cli.utils.environment import Environment
78+
79+
env = await Environment.detect()
80+
assert env.shell_name == "Windows PowerShell"
81+
assert os.path.normcase(str(env.shell_path)) == os.path.normcase(expected)
82+
83+
84+
@pytest.mark.skipif(platform.system() == "Windows", reason="Skipping test on Windows")
85+
async def test_environment_detection_windows_prefers_pwsh_from_program_files(monkeypatch):
86+
monkeypatch.setattr(platform, "system", lambda: "Windows")
87+
monkeypatch.setattr(platform, "machine", lambda: "AMD64")
88+
monkeypatch.setattr(platform, "version", lambda: "10.0.19044")
89+
monkeypatch.setenv("SYSTEMROOT", r"C:\Windows")
90+
monkeypatch.delenv("ProgramW6432", raising=False)
91+
monkeypatch.setenv("ProgramFiles", r"C:\Program Files")
92+
monkeypatch.setattr("kimi_cli.utils.environment.shutil.which", lambda *_a, **_k: None)
93+
94+
expected = os.path.join(r"C:\Program Files", "PowerShell", "7", "pwsh.exe")
95+
96+
async def _mock_is_file(self: KaosPath) -> bool:
97+
return os.path.normcase(str(self)) == os.path.normcase(expected)
98+
99+
monkeypatch.setattr(KaosPath, "is_file", _mock_is_file)
100+
101+
from kimi_cli.utils.environment import Environment
102+
103+
env = await Environment.detect()
104+
assert env.shell_name == "Windows PowerShell"
105+
assert os.path.normcase(str(env.shell_path)) == os.path.normcase(expected)
53106

54107

55108
@pytest.mark.skipif(platform.system() == "Windows", reason="Skipping test on Windows")
@@ -58,6 +111,7 @@ async def test_environment_detection_windows_fallback(monkeypatch):
58111
monkeypatch.setattr(platform, "machine", lambda: "AMD64")
59112
monkeypatch.setattr(platform, "version", lambda: "10.0.19044")
60113
monkeypatch.setenv("SYSTEMROOT", r"C:\Windows")
114+
monkeypatch.setattr("kimi_cli.utils.environment.shutil.which", lambda *_a, **_k: None)
61115

62116
async def _mock_is_file(self: KaosPath) -> bool:
63117
return False

0 commit comments

Comments
 (0)