Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ Only write entries that are worth mentioning to users.

## Unreleased

- Shell: Fix crash when working directory is removed during session — instead of flooding the terminal with OSError tracebacks, the shell now exits cleanly with a crash report showing the session ID and working directory path, allowing users to restart in a valid directory

## 1.27.0 (2026-03-28)

- Shell: Add `/feedback` command — submit feedback directly from the CLI session; the command falls back to opening GitHub Issues on network errors or timeouts
Expand Down
2 changes: 2 additions & 0 deletions docs/en/release-notes/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ This page documents the changes in each Kimi Code CLI release.

## Unreleased

- Shell: Fix crash when working directory is removed during session — instead of flooding the terminal with OSError tracebacks, the shell now exits cleanly with a crash report showing the session ID and working directory path, allowing users to restart in a valid directory

## 1.27.0 (2026-03-28)

- Shell: Add `/feedback` command — submit feedback directly from the CLI session; the command falls back to opening GitHub Issues on network errors or timeouts
Expand Down
2 changes: 2 additions & 0 deletions docs/zh/release-notes/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

## 未发布

- Shell:修复会话期间工作目录被移除时崩溃的问题——不再向终端输出大量 OSError 堆栈跟踪,而是以崩溃报告形式优雅退出,显示会话 ID 和工作目录路径,用户可在有效目录下重启继续

## 1.27.0 (2026-03-28)

- Shell:新增 `/feedback` 命令——可直接在 CLI 会话中提交反馈,网络错误或超时时自动回退到打开 GitHub Issues 页面
Expand Down
42 changes: 42 additions & 0 deletions src/kimi_cli/ui/shell/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
from kimi_cli.ui.shell.mcp_status import render_mcp_prompt
from kimi_cli.ui.shell.prompt import (
CustomPromptSession,
CwdLostError,
PromptMode,
UserInput,
toast,
Expand Down Expand Up @@ -160,6 +161,37 @@ def available_slash_commands(self) -> dict[str, SlashCommand[Any]]:
"""Get all available slash commands, including shell-level and soul-level commands."""
return self._available_slash_commands

def _print_cwd_lost_crash(self) -> None:
"""Print a crash report when the working directory is no longer accessible."""
runtime = self.soul.runtime if isinstance(self.soul, KimiSoul) else None
session_id = runtime.session.id if runtime else "unknown"
work_dir = str(runtime.session.work_dir) if runtime else "unknown"

info = Table.grid(padding=(0, 1))
info.add_row("Session:", session_id)
info.add_row("Working directory:", work_dir)

panel = Panel(
Group(
Text(
"The working directory is no longer accessible "
"(external drive unplugged, directory deleted, or filesystem unmounted).",
),
Text(""),
info,
Text(""),
Text(
"Your conversation history has been saved. "
"Restart kimi in a valid directory to continue.",
style="dim",
),
),
title="[bold red]Session crashed[/bold red]",
border_style="red",
)
console.print()
console.print(panel)

@staticmethod
def _should_exit_input(user_input: UserInput) -> bool:
return user_input.command.strip() in {"exit", "quit", "/exit", "/quit"}
Expand Down Expand Up @@ -240,6 +272,11 @@ async def _route_prompt_events(
resume_prompt.clear()
await idle_events.put(_PromptEvent(kind="eof"))
return
except CwdLostError:
logger.error("Working directory no longer exists")
resume_prompt.clear()
await idle_events.put(_PromptEvent(kind="cwd_lost"))
return
except Exception:
logger.exception("Prompt router crashed")
resume_prompt.clear()
Expand Down Expand Up @@ -422,6 +459,11 @@ async def _invalidate_after_mcp_loading() -> None:
console.print("Bye!")
break

if event.kind == "cwd_lost":
self._print_cwd_lost_crash()
shell_ok = False
break

if event.kind == "error":
shell_ok = False
break
Expand Down
14 changes: 13 additions & 1 deletion src/kimi_cli/ui/shell/prompt.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,10 @@
PROMPT_SYMBOL_PLAN = "📋"


class CwdLostError(OSError):
"""Raised when the working directory no longer exists (e.g. external drive unplugged)."""


class SlashCommandCompleter(Completer):
"""
A completer that:
Expand Down Expand Up @@ -2042,7 +2046,15 @@ def _render_bottom_toolbar(self) -> FormattedText:

# CWD (truncated from left) + git branch with status badge
# Degrade gracefully on narrow terminals: full → cwd-only → truncated cwd → skip
cwd = _truncate_left(_shorten_cwd(str(KaosPath.cwd())), _MAX_CWD_COLS)
try:
cwd = _truncate_left(_shorten_cwd(str(KaosPath.cwd())), _MAX_CWD_COLS)
except OSError:
# CWD no longer exists (e.g. external drive unplugged). Ask
# prompt_toolkit to exit; the raised exception will propagate out
# of prompt_async() into the Shell's event router which prints a
# crash report with session info and exits cleanly.
app.exit(exception=CwdLostError())
return FormattedText([])
branch = _get_git_branch()
if branch:
dirty, ahead, behind = _get_git_status()
Expand Down
21 changes: 20 additions & 1 deletion tests/ui_and_conv/test_shell_prompt_router.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@

import kimi_cli.ui.shell as shell_module
from kimi_cli.soul import Soul
from kimi_cli.ui.shell.prompt import PromptMode, UserInput
from kimi_cli.ui.shell.prompt import CwdLostError, PromptMode, UserInput
from kimi_cli.wire.types import TextPart


Expand Down Expand Up @@ -245,3 +245,22 @@ def running_prompt_accepts_submission(self) -> bool:
event = idle_events.get_nowait()
assert event.kind == "eof"
assert shell._exit_after_run is False


@pytest.mark.asyncio
async def test_route_prompt_events_cwd_lost_posts_cwd_lost_event(
_patched_prompt_router,
) -> None:
"""When prompt_next raises CwdLostError the router should post a 'cwd_lost'
event and stop, so the main loop can print a crash report and exit."""
shell = shell_module.Shell(cast(Soul, _make_fake_soul()))
prompt_session = _FakePromptSession([(False, CwdLostError())])
idle_events: asyncio.Queue[shell_module._PromptEvent] = asyncio.Queue()
resume_prompt = asyncio.Event()
resume_prompt.set()

await shell._route_prompt_events(cast(Any, prompt_session), idle_events, resume_prompt)

event = idle_events.get_nowait()
assert event.kind == "cwd_lost"
assert not resume_prompt.is_set()
Loading