Skip to content
Merged
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
3 changes: 0 additions & 3 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
4 changes: 1 addition & 3 deletions docs/en/reference/kimi-command.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
3 changes: 0 additions & 3 deletions docs/en/release-notes/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
4 changes: 1 addition & 3 deletions docs/zh/reference/kimi-command.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,10 +56,8 @@ kimi [OPTIONS] COMMAND [ARGS]
|------|------|------|
| `--continue` | `-C` | 继续当前工作目录的上一个会话 |
| `--session ID` | `-S` | 恢复指定 ID 的会话,若不存在则创建新会话 |
| `--sessions` | | 交互式选择当前工作目录的会话 |
| `--list-sessions` | | 列出当前工作目录的所有会话并退出 |

`--continue`、`--session`、`--sessions` 和 `--list-sessions` 互斥。
`--continue` 和 `--session` 互斥。

## 输入与命令

Expand Down
3 changes: 0 additions & 3 deletions docs/zh/release-notes/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
90 changes: 0 additions & 90 deletions src/kimi_cli/cli/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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):
Expand Down
2 changes: 1 addition & 1 deletion src/kimi_cli/session.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,14 @@
import uuid
from dataclasses import dataclass
from pathlib import Path
from textwrap import shorten
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Replacing custom shorten with textwrap.shorten breaks CJK text truncation in session titles

The custom shorten function (removed from src/kimi_cli/utils/string.py) was specifically designed to fall back to a hard character cut for CJK text without word boundaries (spaces). Python's textwrap.shorten only breaks at whitespace, so when CJK text exceeds the width and contains no ASCII spaces, it returns only the placeholder instead of truncated content.

Demonstration of the regression
from textwrap import shorten
text = '请帮我分析一下这段代码中存在的所有潜在安全漏洞并且给出详细的修复方案和最佳实践建议我需要确保生产环境的安全性'  # 54 chars
shorten(text, width=50)        # => '[...]'  (just the placeholder!)
shorten(text, width=50, placeholder='...')  # => '...'

The old custom function would return the first 49 characters + "…" placeholder.

At src/kimi_cli/session.py:101-104, shorten(text, width=50) uses the default ' [...]' placeholder (6 chars). For a CJK user prompt like "请帮我分析这段代码的安全问题并给出修复方案和最佳实践建议确保上线安全" (>50 chars, no spaces), the session title becomes ' [...] (session-id)' — conveying zero information about the session content. The same regression affects src/kimi_cli/web/store/sessions.py:149, src/kimi_cli/web/api/sessions.py:959, src/kimi_cli/web/api/sessions.py:1021, src/kimi_cli/utils/export.py:69, and src/kimi_cli/utils/export.py:234.

Prompt for agents
The custom `shorten` function was removed from `src/kimi_cli/utils/string.py` and all call sites were migrated to `textwrap.shorten`. However, `textwrap.shorten` does not handle CJK text correctly — it only breaks at whitespace, so for CJK text without ASCII spaces, it returns just the placeholder instead of truncated content.

Restore the custom `shorten` function in `src/kimi_cli/utils/string.py` and revert the import changes in these files:

1. `src/kimi_cli/session.py:10` — change `from textwrap import shorten` back to `from kimi_cli.utils.string import shorten`
2. `src/kimi_cli/utils/export.py:7` — change `from textwrap import shorten` back to `from kimi_cli.utils.string import shorten`
3. `src/kimi_cli/web/store/sessions.py:132` — change `from textwrap import shorten` back to `from kimi_cli.utils.string import shorten`
4. `src/kimi_cli/web/api/sessions.py:955` — change `from textwrap import shorten` back to `from kimi_cli.utils.string import shorten`

Also restore the CHANGELOG entries about the custom `shorten` function in CHANGELOG.md, docs/en/release-notes/changelog.md, and docs/zh/release-notes/changelog.md.
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.


Comment on lines 9 to 11
Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now that Session.refresh() uses textwrap.shorten, note that textwrap.shorten(..., width=50) defaults to the placeholder " [...]" and may return only the placeholder for long single-token inputs (common with CJK text). That can yield titles like " [...] (<id>)" and differs from other truncation call sites that use an ellipsis placeholder explicitly; consider passing an explicit placeholder (and a hard-cut fallback if you want non-empty content for no-space strings).

Copilot uses AI. Check for mistakes.
from kaos.path import KaosPath
from kosong.message import Message

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

Expand Down
6 changes: 3 additions & 3 deletions src/kimi_cli/utils/export.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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,
Expand Down Expand Up @@ -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="…")
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep key argument prefixes in exported tool-call hints

This now relies on textwrap.shorten, which truncates on word boundaries only; when the argument is a long no-space string (e.g., CJK text, long tokenized commands/queries), the hint degrades to just . That removes the identifying context from exported tool-call headings, making transcript exports much less diagnosable for these inputs.

Useful? React with 👍 / 👎.


# 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 ""

Expand Down
19 changes: 0 additions & 19 deletions src/kimi_cli/utils/string.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
7 changes: 4 additions & 3 deletions src/kimi_cli/web/api/sessions.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Preserve non-space text when truncating fallback titles

Using textwrap.shorten here regresses title generation for inputs without whitespace (common in Chinese/Japanese): for long strings it returns only the placeholder ("...") instead of a readable prefix. In generate_session_title, that placeholder can be persisted as the final title after retry exhaustion, causing many sessions to collapse to identical names and become hard to distinguish in the session list.

Useful? React with 👍 / 👎.

Comment on lines +954 to +959
Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The truncation placeholder is set to "..." here, while other truncation in the codebase (e.g., export helpers) uses the single-character ellipsis "…". Consider standardizing on one placeholder (possibly via a shared constant) to keep UI/export output consistent and avoid snapshot churn.

Copilot uses AI. Check for mistakes.

# If AI generation failed too many times, use fallback and mark as generated
if metadata.title_generate_attempts >= 3:
Expand Down Expand Up @@ -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:
Expand Down
3 changes: 1 addition & 2 deletions src/kimi_cli/web/store/sessions.py
Original file line number Diff line number Diff line change
Expand Up @@ -129,11 +129,10 @@ def _derive_title_from_wire(session_dir: Path) -> str:

try:
import json
from textwrap import shorten

Comment on lines 131 to 133
Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Swapping to textwrap.shorten changes truncation semantics: default placeholder becomes " [...]", and for long single-token inputs (e.g., CJK text without spaces) it can return only the placeholder. Since this function later uses shorten(text, width=300), consider passing an explicit placeholder (and adding a hard-cut fallback if you need to avoid placeholder-only titles).

Copilot uses AI. Check for mistakes.
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()
Expand Down
55 changes: 0 additions & 55 deletions tests/e2e/test_cli_error_output.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
Loading