diff --git a/CHANGELOG.md b/CHANGELOG.md index 7ee5f0aae..9086e12b4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -115,9 +115,6 @@ Only write entries that are worth mentioning to users. - Shell: Enhance `Ctrl-V` clipboard paste to support video files in addition to images — video file paths are inserted as text, and a crash when clipboard data is `None` is fixed - Core: Pass session ID as `user_id` metadata to Anthropic API - Web: Preserve slash commands on WebSocket reconnect and add automatic retry logic for session initialization -- CLI: Add `--sessions` option to interactively select a session to resume -- CLI: Add `--list-sessions` option to list all sessions for the working directory -- Core: Add custom `shorten` function for better CJK text support in session titles and exports ## 1.17.0 (2026-03-03) diff --git a/docs/en/reference/kimi-command.md b/docs/en/reference/kimi-command.md index 02d22f911..3432f3759 100644 --- a/docs/en/reference/kimi-command.md +++ b/docs/en/reference/kimi-command.md @@ -56,10 +56,8 @@ The working directory determines the root directory for file operations. Relativ |--------|-------|-------------| | `--continue` | `-C` | Continue the previous session in the current working directory | | `--session ID` | `-S` | Resume session with specified ID, creates new session if not exists | -| `--sessions` | | Interactively select a session from the current working directory | -| `--list-sessions` | | List all sessions in the current working directory and exit | -`--continue`, `--session`, `--sessions`, and `--list-sessions` are mutually exclusive. +`--continue` and `--session` are mutually exclusive. ## Input and commands diff --git a/docs/en/release-notes/changelog.md b/docs/en/release-notes/changelog.md index 6866e558c..fb3fd4a05 100644 --- a/docs/en/release-notes/changelog.md +++ b/docs/en/release-notes/changelog.md @@ -108,9 +108,6 @@ This page documents the changes in each Kimi Code CLI release. - Shell: Enhance `Ctrl-V` clipboard paste to support video files in addition to images — video file paths are inserted as text, and a crash when clipboard data is `None` is fixed - Core: Pass session ID as `user_id` metadata to Anthropic API - Web: Preserve slash commands on WebSocket reconnect and add automatic retry logic for session initialization -- CLI: Add `--sessions` option to interactively select a session to resume -- CLI: Add `--list-sessions` option to list all sessions for the working directory -- Core: Add custom `shorten` function for better CJK text support in session titles and exports ## 1.17.0 (2026-03-03) diff --git a/docs/zh/reference/kimi-command.md b/docs/zh/reference/kimi-command.md index b4612257c..ac43207ae 100644 --- a/docs/zh/reference/kimi-command.md +++ b/docs/zh/reference/kimi-command.md @@ -56,10 +56,8 @@ kimi [OPTIONS] COMMAND [ARGS] |------|------|------| | `--continue` | `-C` | 继续当前工作目录的上一个会话 | | `--session ID` | `-S` | 恢复指定 ID 的会话,若不存在则创建新会话 | -| `--sessions` | | 交互式选择当前工作目录的会话 | -| `--list-sessions` | | 列出当前工作目录的所有会话并退出 | -`--continue`、`--session`、`--sessions` 和 `--list-sessions` 互斥。 +`--continue` 和 `--session` 互斥。 ## 输入与命令 diff --git a/docs/zh/release-notes/changelog.md b/docs/zh/release-notes/changelog.md index fc9ce8619..4d30d461f 100644 --- a/docs/zh/release-notes/changelog.md +++ b/docs/zh/release-notes/changelog.md @@ -108,9 +108,6 @@ - Shell:增强 `Ctrl-V` 剪贴板粘贴功能,支持粘贴视频文件——视频文件路径以文本形式插入输入框,同时修复剪贴板数据为 `None` 时的崩溃问题 - Core:将会话 ID 作为 `user_id` 元数据传递给 Anthropic API - Web:修复 WebSocket 重连时斜杠命令丢失的问题,并为会话初始化添加自动重试逻辑 -- CLI:新增 `--sessions` 选项,支持交互式选择要恢复的会话 -- CLI:新增 `--list-sessions` 选项,支持列出工作目录下的所有会话 -- Core:新增自定义 `shorten` 函数,优化中日韩文本在会话标题和导出内容中的截断显示效果 ## 1.17.0 (2026-03-03) diff --git a/src/kimi_cli/cli/__init__.py b/src/kimi_cli/cli/__init__.py index 14c6ec1c4..fc93eb70d 100644 --- a/src/kimi_cli/cli/__init__.py +++ b/src/kimi_cli/cli/__init__.py @@ -39,12 +39,6 @@ def __init__(self, session_id: str | None = None): OutputFormat = Literal["text", "stream-json"] -def _strip_session_id_suffix(title: str, session_id: str) -> str: - """Remove the trailing `` (session_id)`` that `Session.refresh` appends.""" - suffix = f" ({session_id})" - return title.rsplit(suffix, 1)[0] if title.endswith(suffix) else title - - def _version_callback(value: bool) -> None: if value: from kimi_cli.constant import get_version @@ -125,20 +119,6 @@ def kimi( help="Continue the previous session for the working directory. Default: no.", ), ] = False, - sessions: Annotated[ - bool, - typer.Option( - "--pick-session", - help="Interactively select a session to resume for the working directory.", - ), - ] = False, - list_sessions: Annotated[ - bool, - typer.Option( - "--list-sessions", - help="List all sessions for the working directory and exit.", - ), - ] = False, config_string: Annotated[ str | None, typer.Option( @@ -406,8 +386,6 @@ def _emit_fatal_error(message: str) -> None: { "--continue": continue_, "--session": session_id is not None, - "--pick-session": sessions, - "--list-sessions": list_sessions, }, { "--config": config_string is not None, @@ -457,11 +435,6 @@ def _emit_fatal_error(message: str) -> None: "Final-message-only output is only supported for print UI", param_hint="--final-message-only", ) - if sessions and ui != "shell": - raise typer.BadParameter( - "--pick-session is only supported for shell UI", - param_hint="--pick-session", - ) config: Config | Path | None = None if config_string is not None: @@ -500,32 +473,6 @@ def _emit_fatal_error(message: str) -> None: work_dir = KaosPath.unsafe_from_local_path(local_work_dir) if local_work_dir else KaosPath.cwd() - if list_sessions: - from rich.console import Console - from rich.table import Table - - from kimi_cli.utils.datetime import format_relative_time - - async def _list(): - return await Session.list(work_dir) - - all_sessions = asyncio.run(_list()) - console = Console() - if not all_sessions: - console.print("[yellow]No sessions found for the working directory.[/yellow]") - raise typer.Exit(0) - - table = Table(show_header=True, show_edge=False) - table.add_column("ID") - table.add_column("Title") - table.add_column("Updated") - for s in all_sessions: - name = _strip_session_id_suffix(s.title, s.id) - table.add_row(s.id, name, format_relative_time(s.updated_at)) - - console.print(table) - raise typer.Exit(0) - async def _run(session_id: str | None) -> tuple[Session, bool]: """ Create/load session and run the CLI instance. @@ -693,43 +640,6 @@ async def _reload_loop(session_id: str | None) -> bool: await _post_run(last_session, succeeded) return False - if sessions: - from prompt_toolkit.shortcuts.choice_input import ChoiceInput - from rich.console import Console - - from kimi_cli.utils.datetime import format_relative_time - - async def _pick_session() -> str: - all_sessions = await Session.list(work_dir) - if not all_sessions: - Console().print("[yellow]No sessions found for the working directory.[/yellow]") - raise typer.Exit(0) - - choices: list[tuple[str, str]] = [] - for s in all_sessions: - time_str = format_relative_time(s.updated_at) - short_id = s.id[:8] - name = _strip_session_id_suffix(s.title, s.id) - label = f"{name} ({short_id}), {time_str}" - choices.append((s.id, label)) - - try: - selection = await ChoiceInput( - message="Select a session to resume" - " (↑↓ navigate, Enter select, Ctrl+C cancel):", - options=choices, - default=choices[0][0], - ).prompt_async() - except (EOFError, KeyboardInterrupt): - raise typer.Exit(0) from None - - if not selection: - raise typer.Exit(0) - - return selection - - session_id = asyncio.run(_pick_session()) - try: switch_to_web = asyncio.run(_reload_loop(session_id)) except (typer.BadParameter, typer.Exit): diff --git a/src/kimi_cli/session.py b/src/kimi_cli/session.py index 5aa8e038f..e1f50df0f 100644 --- a/src/kimi_cli/session.py +++ b/src/kimi_cli/session.py @@ -7,6 +7,7 @@ import uuid from dataclasses import dataclass from pathlib import Path +from textwrap import shorten from kaos.path import KaosPath from kosong.message import Message @@ -14,7 +15,6 @@ from kimi_cli.metadata import WorkDirMeta, load_metadata, save_metadata from kimi_cli.session_state import SessionState, load_session_state, save_session_state from kimi_cli.utils.logging import logger -from kimi_cli.utils.string import shorten from kimi_cli.wire.file import WireFile from kimi_cli.wire.types import TurnBegin diff --git a/src/kimi_cli/utils/export.py b/src/kimi_cli/utils/export.py index 50b5c3306..933a7c7b4 100644 --- a/src/kimi_cli/utils/export.py +++ b/src/kimi_cli/utils/export.py @@ -4,6 +4,7 @@ from collections.abc import Sequence from datetime import datetime from pathlib import Path +from textwrap import shorten from typing import TYPE_CHECKING, cast import aiofiles @@ -13,7 +14,6 @@ from kimi_cli.soul.message import is_system_reminder_message, system from kimi_cli.utils.message import message_stringify from kimi_cli.utils.path import sanitize_cli_path -from kimi_cli.utils.string import shorten from kimi_cli.wire.types import ( AudioURLPart, ContentPart, @@ -66,12 +66,12 @@ def _extract_tool_call_hint(args_json: str) -> str: for key in _HINT_KEYS: val = args.get(key) if isinstance(val, str) and val.strip(): - return shorten(val, width=60) + return shorten(val, width=60, placeholder="…") # Fallback: first short string value for val in args.values(): if isinstance(val, str) and 0 < len(val) <= 80: - return shorten(val, width=60) + return shorten(val, width=60, placeholder="…") return "" diff --git a/src/kimi_cli/utils/string.py b/src/kimi_cli/utils/string.py index e6fff6f7a..bd4379bba 100644 --- a/src/kimi_cli/utils/string.py +++ b/src/kimi_cli/utils/string.py @@ -7,25 +7,6 @@ _NEWLINE_RE = re.compile(r"[\r\n]+") -def shorten(text: str, *, width: int, placeholder: str = "…") -> str: - """Shorten text to at most *width* characters. - - Normalises whitespace, then truncates — preferring a word boundary - when one exists near the cut point, but falling back to a hard cut - so that CJK text without spaces won't collapse to just the placeholder. - """ - text = " ".join(text.split()) - if len(text) <= width: - return text - cut = width - len(placeholder) - if cut <= 0: - return text[:width] - space = text.rfind(" ", 0, cut + 1) - if space > 0: - cut = space - return text[:cut].rstrip() + placeholder - - def shorten_middle(text: str, width: int, remove_newline: bool = True) -> str: """Shorten the text by inserting ellipsis in the middle.""" if len(text) <= width: diff --git a/src/kimi_cli/web/api/sessions.py b/src/kimi_cli/web/api/sessions.py index bf12a638e..36cd42507 100644 --- a/src/kimi_cli/web/api/sessions.py +++ b/src/kimi_cli/web/api/sessions.py @@ -951,11 +951,12 @@ async def generate_session_title( if not user_message: return GenerateTitleResponse(title="Untitled") - from kimi_cli.utils.string import shorten + # Fallback title from user message (used if AI generation fails) + from textwrap import shorten user_text = user_message.strip() user_text = " ".join(user_text.split()) - fallback_title = shorten(user_text, width=50) or "Untitled" + fallback_title = shorten(user_text, width=50, placeholder="...") or "Untitled" # If AI generation failed too many times, use fallback and mark as generated if metadata.title_generate_attempts >= 3: @@ -1017,7 +1018,7 @@ async def generate_session_title( title = generated_title ai_generated = True elif generated_title: - title = shorten(generated_title, width=50) + title = shorten(generated_title, width=50, placeholder="...") ai_generated = True except Exception as e: diff --git a/src/kimi_cli/web/store/sessions.py b/src/kimi_cli/web/store/sessions.py index b3a20b34b..357c749f6 100644 --- a/src/kimi_cli/web/store/sessions.py +++ b/src/kimi_cli/web/store/sessions.py @@ -129,11 +129,10 @@ def _derive_title_from_wire(session_dir: Path) -> str: try: import json + from textwrap import shorten from kosong.message import Message - from kimi_cli.utils.string import shorten - with open(wire_file, encoding="utf-8") as f: for line in f: line = line.strip() diff --git a/tests/e2e/test_cli_error_output.py b/tests/e2e/test_cli_error_output.py index e8e78108e..857400f32 100644 --- a/tests/e2e/test_cli_error_output.py +++ b/tests/e2e/test_cli_error_output.py @@ -111,61 +111,6 @@ def test_invalid_config_toml_is_reported(tmp_path: Path) -> None: ) -def test_sessions_and_continue_conflict_is_reported(tmp_path: Path) -> None: - share_dir = tmp_path / "share" - result = _run_kimi(["--pick-session", "--continue"], share_dir=share_dir) - assert result.returncode == snapshot(2) - assert result.stdout == snapshot("") - assert _normalize_cli_error_output(result.stderr) == snapshot( - """\ -Usage: python -m kimi_cli.cli [OPTIONS] COMMAND [ARGS]... -Try 'python -m kimi_cli.cli -h' for help. -Error: -Invalid value for --continue: Cannot combine --continue, --pick-session. -""" - ) - - -def test_list_sessions_and_session_conflict_is_reported(tmp_path: Path) -> None: - share_dir = tmp_path / "share" - result = _run_kimi(["--list-sessions", "--session", "abc"], share_dir=share_dir) - assert result.returncode == snapshot(2) - assert result.stdout == snapshot("") - assert _normalize_cli_error_output(result.stderr) == snapshot( - """\ -Usage: python -m kimi_cli.cli [OPTIONS] COMMAND [ARGS]... -Try 'python -m kimi_cli.cli -h' for help. -Error: -Invalid value for --session: Cannot combine --session, --list-sessions. -""" - ) - - -def test_sessions_with_print_mode_is_reported(tmp_path: Path) -> None: - share_dir = tmp_path / "share" - result = _run_kimi(["--pick-session", "--print", "--prompt", "hi"], share_dir=share_dir) - assert result.returncode == snapshot(2) - assert result.stdout == snapshot("") - assert _normalize_cli_error_output(result.stderr) == snapshot( - """\ -Usage: python -m kimi_cli.cli [OPTIONS] COMMAND [ARGS]... -Try 'python -m kimi_cli.cli -h' for help. -Error: -Invalid value for --pick-session: --pick-session is only supported for shell UI -""" - ) - - -def test_list_sessions_with_no_sessions(tmp_path: Path) -> None: - share_dir = tmp_path / "share" - work_dir = tmp_path / "work" - work_dir.mkdir(parents=True, exist_ok=True) - result = _run_kimi(["--list-sessions", "--work-dir", str(work_dir)], share_dir=share_dir) - assert result.returncode == snapshot(0) - assert "No sessions found" in result.stdout - assert result.stderr == snapshot("") - - def test_continue_without_previous_session_is_reported(tmp_path: Path) -> None: share_dir = tmp_path / "share" work_dir = tmp_path / "work"