Skip to content
Draft
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: 1 addition & 1 deletion docs/en/configuration/data-locations.md
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,7 @@ To clean only specific data:
| --- | --- |
| Reset configuration | Delete `~/.kimi/config.toml` |
| Clear all sessions | Delete `~/.kimi/sessions/` directory |
| Clear sessions for specific working directory | Use `/sessions` in shell mode to view and delete |
| Clear sessions for specific working directory | Use `/sessions` to view, then `/delete` (or `/remove`) in shell mode |
| Clear plan files | Delete `~/.kimi/plans/` directory, or use `/plan clear` in plan mode |
| Clear input history | Delete `~/.kimi/user-history/` directory |
| Clear logs | Delete `~/.kimi/logs/` directory |
Expand Down
16 changes: 16 additions & 0 deletions docs/en/reference/slash-commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,22 @@ Alias: `/resume`

Use arrow keys to select a session, press `Enter` to confirm switch, press `Ctrl-C` to cancel. Press `Ctrl-A` to toggle between showing sessions for the current directory only or across all directories.

### `/delete`

Delete a session from the current working directory.

Alias: `/remove`

Usage:

- `/delete <session_id>`: Delete the specified session in the current working directory
- `/delete`: Open the session picker and choose a session to delete

Behavior constraints:
- Cannot delete the current active session
- Cross-directory deletion is rejected in this command; switch to that project and run `/delete` there
- A confirmation prompt is required before deletion

### `/title`

View or set the current session title. The configured title is shown in the `/sessions` list, making it easier to identify and find sessions.
Expand Down
3 changes: 1 addition & 2 deletions docs/zh/configuration/data-locations.md
Original file line number Diff line number Diff line change
Expand Up @@ -149,10 +149,9 @@ Plan 模式的方案文件存储在 `~/.kimi/plans/` 目录下。每个 Plan 会
| --- | --- |
| 重置配置 | 删除 `~/.kimi/config.toml` |
| 清理所有会话 | 删除 `~/.kimi/sessions/` 目录 |
| 清理特定工作目录的会话 | 在 Shell 模式下使用 `/sessions` 查看并删除 |
| 清理特定工作目录的会话 | 在 Shell 模式下使用 `/sessions` 查看,再用 `/delete`(或 `/remove`)删除 |
| 清理 Plan 方案文件 | 删除 `~/.kimi/plans/` 目录,或在 Plan 模式下使用 `/plan clear` |
| 清理输入历史 | 删除 `~/.kimi/user-history/` 目录 |
| 清理日志 | 删除 `~/.kimi/logs/` 目录 |
| 清理 MCP 配置 | 删除 `~/.kimi/mcp.json` 或使用 `kimi mcp remove` |
| 清理登录凭据 | 删除 `~/.kimi/credentials/` 目录或使用 `/logout` |

16 changes: 16 additions & 0 deletions docs/zh/reference/slash-commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,22 @@

使用方向键选择会话,按 `Enter` 确认切换,按 `Ctrl-C` 取消。按 `Ctrl-A` 可在 「仅当前目录」 和 「所有目录」 之间切换会话范围。

### `/delete`

删除当前工作目录中的会话。

别名:`/remove`

用法:

- `/delete <session_id>`:删除当前工作目录中指定的会话
- `/delete`:打开会话选择器,选择一个会话删除

行为约束:
- 不能删除当前正在使用的会话
- 此命令不支持跨目录删除;请切换到目标目录后再执行 `/delete`
- 删除前会进行确认

### `/title`

查看或设置当前会话的标题。设置的标题会显示在 `/sessions` 列表中,方便识别和查找会话。
Expand Down
130 changes: 130 additions & 0 deletions src/kimi_cli/ui/shell/slash.py
Original file line number Diff line number Diff line change
@@ -1,26 +1,31 @@
from __future__ import annotations

import asyncio
import shlex
from collections.abc import Awaitable, Callable
from typing import TYPE_CHECKING, Any, cast

from prompt_toolkit.shortcuts.choice_input import ChoiceInput
from rich.prompt import Confirm

from kimi_cli import logger
from kimi_cli.auth.platforms import get_platform_name_for_provider, refresh_managed_models
from kimi_cli.cli import Reload, SwitchToVis, SwitchToWeb
from kimi_cli.config import load_config, save_config
from kimi_cli.exception import ConfigError
from kimi_cli.metadata import load_metadata, save_metadata
from kimi_cli.session import Session
from kimi_cli.soul.kimisoul import KimiSoul
from kimi_cli.ui.shell.console import console
from kimi_cli.ui.shell.mcp_status import render_mcp_console
from kimi_cli.ui.shell.session_picker import SessionPickerApp
from kimi_cli.ui.shell.task_browser import TaskBrowserApp
from kimi_cli.utils.changelog import CHANGELOG
from kimi_cli.utils.slashcmd import SlashCommand, SlashCommandRegistry

if TYPE_CHECKING:
from kimi_cli.ui.shell import Shell
from kaos.path import KaosPath

type ShellSlashCmdFunc = Callable[[Shell, str], None | Awaitable[None]]
"""
Expand All @@ -34,6 +39,24 @@
registry = SlashCommandRegistry[ShellSlashCmdFunc]()
shell_mode_registry = SlashCommandRegistry[ShellSlashCmdFunc]()

_DELETE_USAGE = "[yellow]Usage: /delete [session_id][/yellow]"
_DELETE_CROSS_WORK_DIR = (
"[yellow]Cannot delete sessions from a different working directory in this command. "
"Switch to that project and run /delete there.[/yellow]"
)
_DELETE_INVALID_SESSION_ID = "[red]Invalid session id.[/red]"
_DELETE_NOT_FOUND = "[yellow]Session not found in current working directory: {session_id}[/yellow]"
_DELETE_CURRENT_FORBIDDEN = (
"[yellow]Cannot delete the current session. Switch to another session first.[/yellow]"
)
_DELETE_CANCELLED = "[yellow]Deletion cancelled.[/yellow]"
_DELETE_FAILED = "[red]Failed to delete session {session_id}: {error}[/red]"
_DELETE_METADATA_WARNING = (
"[yellow]Session deleted, but failed to update metadata. "
"Please run /sessions to verify state.[/yellow]"
)
_DELETE_SUCCESS = "[green]Deleted session {session_id}.[/green]"


def ensure_kimi_soul(app: Shell) -> KimiSoul | None:
if not isinstance(app.soul, KimiSoul):
Expand Down Expand Up @@ -571,6 +594,113 @@ async def list_sessions(app: Shell, args: str):
raise Reload(session_id=selection)


def _validate_session_id_compat(raw: str) -> str | None:
session_id = raw.strip()
if not session_id:
return None
if "/" in session_id or "\\" in session_id:
return None
if ".." in session_id or "\x00" in session_id:
return None
return session_id


def _confirm_delete(session_id: str, work_dir: KaosPath) -> bool:
return Confirm.ask(f'Delete session "{session_id}" in "{work_dir}"?', default=False)


def _clear_last_session_id_if_matches(work_dir: KaosPath, session_id: str) -> None:
metadata = load_metadata()
work_dir_meta = metadata.get_work_dir_meta(work_dir)
if work_dir_meta is None:
return
if work_dir_meta.last_session_id != session_id:
return
work_dir_meta.last_session_id = None
save_metadata(metadata)


async def _delete_session_by_id(app: Shell, target_session_id: str) -> None:
soul = ensure_kimi_soul(app)
if soul is None:
return

current_session = soul.runtime.session
work_dir = current_session.work_dir
session_id = _validate_session_id_compat(target_session_id)
if session_id is None:
console.print(_DELETE_INVALID_SESSION_ID)
return

target = await Session.find(work_dir, session_id)
if target is None:
console.print(_DELETE_NOT_FOUND.format(session_id=session_id))
return

if target.id == current_session.id:
console.print(_DELETE_CURRENT_FORBIDDEN)
return

if not _confirm_delete(target.id, target.work_dir):
console.print(_DELETE_CANCELLED)
return

try:
await target.delete()
except Exception as exc:
console.print(_DELETE_FAILED.format(session_id=target.id, error=exc))
return

try:
_clear_last_session_id_if_matches(work_dir, target.id)
except Exception as exc:
logger.exception(
"Failed to clear last_session_id after deleting session {session_id}: {error}",
session_id=target.id,
error=exc,
)
console.print(_DELETE_METADATA_WARNING)

console.print(_DELETE_SUCCESS.format(session_id=target.id))


@registry.command(name="delete", aliases=["remove"])
async def delete(app: Shell, args: str):
"""Delete a session in current working directory"""
soul = ensure_kimi_soul(app)
if soul is None:
return

current_session = soul.runtime.session
raw_args = args.strip()

if raw_args:
try:
parts = shlex.split(raw_args)
except ValueError:
console.print(_DELETE_USAGE)
return
if len(parts) != 1:
console.print(_DELETE_USAGE)
return
await _delete_session_by_id(app, parts[0])
return

result = await SessionPickerApp(
work_dir=current_session.work_dir,
current_session=current_session,
).run()
if result is None:
return

selection, selected_work_dir = result
if selected_work_dir != current_session.work_dir:
console.print(_DELETE_CROSS_WORK_DIR)
return

await _delete_session_by_id(app, selection)


@registry.command(name="task")
@shell_mode_registry.command(name="task")
async def task(app: Shell, args: str):
Expand Down
Loading
Loading