feat: add --sessions/--list-sessions CLI options & fix CJK shorten#1376
feat: add --sessions/--list-sessions CLI options & fix CJK shorten#1376DRunkPiano114 wants to merge 3 commits intoMoonshotAI:mainfrom
Conversation
…oonshotAI#1366) Replace textwrap.shorten with a custom implementation that hard-truncates instead of word-boundary breaking, preventing CJK text without spaces from collapsing to just the placeholder.
There was a problem hiding this comment.
Pull request overview
Adds CLI affordances for working with historical sessions (interactive selection and listing) and replaces textwrap.shorten usages with a custom truncation helper to avoid CJK-without-spaces collapsing to just the placeholder.
Changes:
- Add
--sessions(interactive picker) and--list-sessions(print and exit) to the main CLI entrypoint, with conflict/validation and e2e coverage. - Introduce
kimi_cli.utils.string.shorten()and migrate session-title/export/title-generation callsites to use it. - Update English/Chinese docs and changelogs to reflect the new options and truncation behavior.
Reviewed changes
Copilot reviewed 12 out of 12 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/e2e/test_cli_error_output.py | Adds e2e assertions for new flag conflicts, shell-only validation, and empty-session listing output. |
| src/kimi_cli/cli/init.py | Implements --sessions picker and --list-sessions output path; adds mutual-exclusion checks. |
| src/kimi_cli/utils/string.py | Adds custom shorten() helper intended to hard-truncate and be CJK-safe. |
| src/kimi_cli/session.py | Switches session title truncation to the new shorten() and adjusts truncation width. |
| src/kimi_cli/utils/export.py | Uses custom shorten() for export overview/hints truncation. |
| src/kimi_cli/web/api/sessions.py | Uses custom shorten() for session title fallback/generated title truncation. |
| src/kimi_cli/web/store/sessions.py | Uses custom shorten() when deriving titles from wire logs. |
| docs/en/reference/kimi-command.md | Documents new CLI flags and updated mutual-exclusion rule. |
| docs/zh/reference/kimi-command.md | Documents new CLI flags and updated mutual-exclusion rule (ZH). |
| docs/en/release-notes/changelog.md | Notes new CLI flags and CJK truncation improvement. |
| docs/zh/release-notes/changelog.md | Notes new CLI flags and CJK truncation improvement (ZH). |
| CHANGELOG.md | Adds release notes entries for the new flags and truncation change. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
You can also share your feedback on Copilot code review. Take the survey.
| def shorten(text: str, *, width: int, placeholder: str = "…") -> str: | ||
| """Shorten text to at most width characters. | ||
|
|
||
| This always hard-truncates instead of | ||
| trying word-boundary breaking, so 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) | ||
| space = text.rfind(" ", 0, cut + 1) | ||
| if space > 0: | ||
| cut = space | ||
| return text[:cut].rstrip() + placeholder |
There was a problem hiding this comment.
shorten() claims to return at most width characters, but when width <= len(placeholder) (or if a longer placeholder is passed), cut becomes 0/negative and the returned string can exceed width (e.g., slicing with a negative index + placeholder). Consider guarding this case explicitly (e.g., return placeholder[:width] or raise) to preserve the function contract.
| """Shorten text to at most width characters. | ||
|
|
||
| This always hard-truncates instead of | ||
| trying word-boundary breaking, so 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) | ||
| space = text.rfind(" ", 0, cut + 1) | ||
| if space > 0: | ||
| cut = space |
There was a problem hiding this comment.
The docstring says this function “always hard-truncates instead of trying word-boundary breaking”, but the implementation still prefers truncating at the last space before cut (rfind(" ")). Either update the docstring to match the behavior, or remove the word-boundary logic if you want truly hard truncation.
| for s in all_sessions: | ||
| name = s.title.rsplit(f" ({s.id})", 1)[0] if s.title.endswith(f"({s.id})") else s.title | ||
| table.add_row(s.id, name, format_relative_time(s.updated_at)) |
There was a problem hiding this comment.
The suffix check and split use different strings: endswith(f"({s.id})") vs rsplit(f" ({s.id})", 1). Using the same suffix value for both (e.g., suffix = f" ({s.id})" then endswith(suffix)) avoids subtle mismatches if the title format ever changes.
| # s.title is "{content} ({full_id})" – strip the id suffix for display | ||
| suffix = f" ({s.id})" | ||
| name = s.title.rsplit(suffix, 1)[0] if s.title.endswith(f"({s.id})") else s.title | ||
| label = f"{name} ({short_id}), {time_str}" |
There was a problem hiding this comment.
Same as the --list-sessions path: endswith(f"({s.id})") is less strict than the suffix = f" ({s.id})" used for rsplit. Reusing suffix in the endswith check makes the stripping logic consistent and easier to maintain.
Related Issue
Resolves #1366
Description
Add

--sessions/--list-sessionsCLI options for interactive session selection and listing, see the result below.Replace textwrap.shorten with a custom implementation that hard-truncates instead of word-boundary breaking, preventing CJK text without spaces from collapsing to just the placeholder.
Checklist
make gen-changelogto update the changelog.make gen-docsto update the user documentation.