feat: add --sessions/--list-sessions CLI options & fix CJK shorten#1376
feat: add --sessions/--list-sessions CLI options & fix CJK shorten#1376n-WN merged 6 commits intoMoonshotAI:mainfrom
Conversation
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.
src/kimi_cli/utils/string.py
Outdated
| """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.
src/kimi_cli/cli/__init__.py
Outdated
| # 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.
|
这个想法看起来不错 @RealKai42 This idea looks good @RealKai42 |
n-WN
left a comment
There was a problem hiding this comment.
Hey @DRunkPiano114, thanks for working on this — the feature itself is solid and the interactive session picker works well in practice. I tested it locally in a real terminal (tmux) and the ChoiceInput navigation, rendering, and cancel flow all behave correctly.
However, there are a few issues that need to be addressed before merging:
1. Rebase required
The PR is 40 commits behind main. There's a merge conflict in src/kimi_cli/utils/export.py, and the session format has changed since 1.18 (new _system_prompt role), so session restore can't be tested on the current branch.
2. Revert width=50 → 30 in session.py
# session.py line 81
- width=50,
+ width=30,This change affects all session title storage globally, not just the display in --sessions. The width 50 is used consistently across the codebase (web/api/sessions.py, export.py). 30 characters is especially short for CJK text (~15 Chinese characters). If narrower display is needed for the picker, truncate at the display layer, not the storage layer.
3. Fix suffix stripping inconsistency (bug)
The endswith check and rsplit use different strings:
# Current (in both --list-sessions and --sessions):
name = s.title.rsplit(f" ({s.id})", 1)[0] if s.title.endswith(f"({s.id})") else s.title
# ^ has space ^ missing spaceSince session.refresh() always produces f"{title} ({self.id})" (with a leading space before the parenthesis), the endswith check should match the same string:
suffix = f" ({s.id})"
name = s.title.rsplit(suffix, 1)[0] if s.title.endswith(suffix) else s.titleAlso, this logic is duplicated in both --list-sessions and --sessions blocks. Consider extracting a small helper:
def _strip_session_id_suffix(title: str, session_id: str) -> str:
suffix = f" ({session_id})"
return title.rsplit(suffix, 1)[0] if title.endswith(suffix) else title4. Fix shorten() boundary case (bug)
When width <= len(placeholder), cut becomes 0 or negative, and the result can exceed width:
>>> shorten("Hello", width=0, placeholder="…")
'Hell…' # 4 chars, exceeds width=0Add a guard:
cut = width - len(placeholder)
if cut <= 0:
return text[:width]Minor suggestions (non-blocking)
- Naming:
--sessionsis a bare noun plural — the project convention leans toward action verbs (--continue,--session <id>). Consider--pick-sessionor--select-sessionfor consistency. - Docstring:
shorten()says "always hard-truncates" but the implementation still tries word-boundary breaking viarfind(" "). Either update the docstring or remove therfindlogic.
Overall the approach is good — just needs the bugs fixed and a rebase. Looking forward to the updated version!
60a46a3 to
ca5b087
Compare
|
@n-WN Thanks for the detailed review! All issues you mentioned have been addressed:
|
…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.
…ry case - Revert width=30 back to width=50 in session.py to avoid globally shortening session titles (30 chars is too short for CJK text) - Add guard for width <= len(placeholder) in shorten() to prevent result exceeding the requested width - Update shorten() docstring to accurately describe the word-boundary fallback behavior
- Fix endswith check missing leading space in suffix string, causing the strip logic to never match - Extract _strip_session_id_suffix() helper to deduplicate logic between --list-sessions and --pick-session
Rename to follow the project's action-verb naming convention (--continue, --session <id>) and update e2e test snapshots
ca5b087 to
5767b5b
Compare
n-WN
left a comment
There was a problem hiding this comment.
All four issues from my previous review have been addressed. I also rebased the branch onto the latest main (the only conflict was in the auto-generated changelog files, which were skipped — make gen-changelog should be re-run before merge).
LGTM, thanks @DRunkPiano114!
| | `--sessions` | | Interactively select a session from the current working directory | | ||
| | `--list-sessions` | | List all sessions in the current working directory and exit | | ||
|
|
||
| `--continue` and `--session` are mutually exclusive. | ||
| `--continue`, `--session`, `--sessions`, and `--list-sessions` are mutually exclusive. |
There was a problem hiding this comment.
🟡 Documentation references --sessions but the actual CLI flag is --pick-session
The English reference docs document the flag as --sessions (line 59) and list it as mutually exclusive under that name (line 62), but the actual Typer option registered in the code is --pick-session (src/kimi_cli/cli/__init__.py:131). A user following the documentation would type kimi --sessions and receive an unrecognized option error. The tests confirm the real flag is --pick-session (e.g., tests/e2e/test_cli_error_output.py:116). The same mismatch appears in the Chinese docs (docs/zh/reference/kimi-command.md:59,62), the English changelog (docs/en/release-notes/changelog.md:99), the Chinese changelog (docs/zh/release-notes/changelog.md:99), and CHANGELOG.md:106.
| | `--sessions` | | Interactively select a session from the current working directory | | |
| | `--list-sessions` | | List all sessions in the current working directory and exit | | |
| `--continue` and `--session` are mutually exclusive. | |
| `--continue`, `--session`, `--sessions`, and `--list-sessions` are mutually exclusive. | |
| | `--pick-session` | | Interactively select a session from the current working directory | | |
| | `--list-sessions` | | List all sessions in the current working directory and exit | | |
| `--continue`, `--session`, `--pick-session`, and `--list-sessions` are mutually exclusive. |
Was this helpful? React with 👍 or 👎 to provide feedback.
| | `--sessions` | | 交互式选择当前工作目录的会话 | | ||
| | `--list-sessions` | | 列出当前工作目录的所有会话并退出 | | ||
|
|
||
| `--continue` 和 `--session` 互斥。 | ||
| `--continue`、`--session`、`--sessions` 和 `--list-sessions` 互斥。 |
There was a problem hiding this comment.
🟡 Chinese docs reference --sessions but actual CLI flag is --pick-session
Same documentation/code mismatch as the English docs: the Chinese reference docs use --sessions (line 59) and the mutual-exclusion note (line 62), but the implemented flag is --pick-session (src/kimi_cli/cli/__init__.py:131).
| | `--sessions` | | 交互式选择当前工作目录的会话 | | |
| | `--list-sessions` | | 列出当前工作目录的所有会话并退出 | | |
| `--continue` 和 `--session` 互斥。 | |
| `--continue`、`--session`、`--sessions` 和 `--list-sessions` 互斥。 | |
| | `--pick-session` | | 交互式选择当前工作目录的会话 | | |
| | `--list-sessions` | | 列出当前工作目录的所有会话并退出 | | |
| `--continue`、`--session`、`--pick-session` 和 `--list-sessions` 互斥。 |
Was this helpful? React with 👍 or 👎 to provide feedback.
| - 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 |
There was a problem hiding this comment.
🟡 CHANGELOG references --sessions but actual CLI flag is --pick-session
The CHANGELOG entry at line 106 says --sessions but the actual Typer option is --pick-session (src/kimi_cli/cli/__init__.py:131). The same incorrect name propagates to both the English and Chinese changelog docs (docs/en/release-notes/changelog.md:99, docs/zh/release-notes/changelog.md:99).
| - CLI: Add `--sessions` option to interactively select a session to resume | |
| - CLI: Add `--pick-session` option to interactively select a session to resume |
Was this helpful? React with 👍 or 👎 to provide feedback.
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.