Skip to content

feat: add --sessions/--list-sessions CLI options & fix CJK shorten#1376

Merged
n-WN merged 6 commits intoMoonshotAI:mainfrom
DRunkPiano114:feat/session-argument
Mar 27, 2026
Merged

feat: add --sessions/--list-sessions CLI options & fix CJK shorten#1376
n-WN merged 6 commits intoMoonshotAI:mainfrom
DRunkPiano114:feat/session-argument

Conversation

@DRunkPiano114
Copy link
Copy Markdown
Contributor

@DRunkPiano114 DRunkPiano114 commented Mar 9, 2026

Related Issue

Resolves #1366

Description

  • Add --sessions / --list-sessions CLI options for interactive session selection and listing, see the result below.
    Screenshot 2026-03-10 at 02 07 24

  • 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

  • I have read the CONTRIBUTING document.
  • I have linked the related issue, if any.
  • I have added tests that prove my fix is effective or that my feature works.
  • I have run make gen-changelog to update the changelog.
  • I have run make gen-docs to update the user documentation.

Open with Devin

Copilot AI review requested due to automatic review settings March 9, 2026 15:48
Copy link
Copy Markdown
Contributor

@devin-ai-integration devin-ai-integration bot left a comment

Choose a reason for hiding this comment

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

✅ Devin Review: No Issues Found

Devin Review analyzed this PR and found no potential bugs to report.

View in Devin Review to see 5 additional findings.

Open in Devin Review

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

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.

Comment on lines +10 to +24
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
Copy link

Copilot AI Mar 9, 2026

Choose a reason for hiding this comment

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

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.

Copilot uses AI. Check for mistakes.
Comment on lines +11 to +23
"""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
Copy link

Copilot AI Mar 9, 2026

Choose a reason for hiding this comment

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

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.

Copilot uses AI. Check for mistakes.
Comment on lines +515 to +517
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))
Copy link

Copilot AI Mar 9, 2026

Choose a reason for hiding this comment

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

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.

Copilot uses AI. Check for mistakes.
Comment on lines +681 to +684
# 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}"
Copy link

Copilot AI Mar 9, 2026

Choose a reason for hiding this comment

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

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.

Copilot uses AI. Check for mistakes.
@n-WN
Copy link
Copy Markdown
Collaborator

n-WN commented Mar 19, 2026

这个想法看起来不错 @RealKai42


This idea looks good @RealKai42

Copy link
Copy Markdown
Collaborator

@n-WN n-WN left a comment

Choose a reason for hiding this comment

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

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 space

Since 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.title

Also, 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 title

4. 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=0

Add a guard:

cut = width - len(placeholder)
if cut <= 0:
    return text[:width]

Minor suggestions (non-blocking)

  • Naming: --sessions is a bare noun plural — the project convention leans toward action verbs (--continue, --session <id>). Consider --pick-session or --select-session for consistency.
  • Docstring: shorten() says "always hard-truncates" but the implementation still tries word-boundary breaking via rfind(" "). Either update the docstring or remove the rfind logic.

Overall the approach is good — just needs the bugs fixed and a rebase. Looking forward to the updated version!

@DRunkPiano114
Copy link
Copy Markdown
Contributor Author

@n-WN Thanks for the detailed review! All issues you mentioned have been addressed:

  1. Rebase — Done, conflict in export.py resolved (kept _is_internal_user_message + placeholder="…" from main).
  2. Revert width=50 — Reverted. 1a8201b9
  3. Suffix stripping bug — Fixed the missing leading space in endswith, and extracted _strip_session_id_suffix() helper to deduplicate. 715ed260
  4. shorten() boundary case — Added if cut <= 0: return text[:width] guard. 1a8201b9
  5. Naming — Renamed --sessions to --pick-session, updated tests and docs. Also added a breaking-changes entry with migration guide. acf8cc62
  6. Docstring — Updated shorten() docstring to accurately describe the word-boundary fallback behavior. 1a8201b9

@DRunkPiano114 DRunkPiano114 requested a review from n-WN March 25, 2026 05:27
…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
@n-WN n-WN force-pushed the feat/session-argument branch from ca5b087 to 5767b5b Compare March 25, 2026 11:54
Copy link
Copy Markdown
Collaborator

@n-WN n-WN left a comment

Choose a reason for hiding this comment

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

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!

Copy link
Copy Markdown
Contributor

@devin-ai-integration devin-ai-integration bot left a comment

Choose a reason for hiding this comment

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

Devin Review found 3 new potential issues.

View 6 additional findings in Devin Review.

Open in Devin Review

Comment on lines +59 to +62
| `--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.
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.

🟡 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.

Suggested change
| `--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.
Open in Devin Review

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

Comment on lines +59 to +62
| `--sessions` | | 交互式选择当前工作目录的会话 |
| `--list-sessions` | | 列出当前工作目录的所有会话并退出 |

`--continue` 和 `--session` 互斥。
`--continue`、`--session`、`--sessions` 和 `--list-sessions` 互斥。
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.

🟡 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).

Suggested change
| `--sessions` | | 交互式选择当前工作目录的会话 |
| `--list-sessions` | | 列出当前工作目录的所有会话并退出 |
`--continue``--session` 互斥。
`--continue``--session``--sessions``--list-sessions` 互斥。
| `--pick-session` | | 交互式选择当前工作目录的会话 |
| `--list-sessions` | | 列出当前工作目录的所有会话并退出 |
`--continue``--session``--pick-session``--list-sessions` 互斥。
Open in Devin Review

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
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.

🟡 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).

Suggested change
- CLI: Add `--sessions` option to interactively select a session to resume
- CLI: Add `--pick-session` option to interactively select a session to resume
Open in Devin Review

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

@n-WN n-WN merged commit 8654a09 into MoonshotAI:main Mar 27, 2026
14 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add arguments for cli to select the history session

3 participants