Skip to content

feat: add session context export/import slash commands with shell utilities and tests#1317

Merged
RealKai42 merged 21 commits intomainfrom
kaiyi/export-import-cmd
Mar 3, 2026
Merged

feat: add session context export/import slash commands with shell utilities and tests#1317
RealKai42 merged 21 commits intomainfrom
kaiyi/export-import-cmd

Conversation

@RealKai42
Copy link
Copy Markdown
Collaborator

@RealKai42 RealKai42 commented Mar 3, 2026

This PR introduces session context export/import support via slash commands.

What changed

  • Added /export and /import command handling in soul/shell slash command flow.
  • Added shell-side export/import helper module.
  • Implemented export utilities (including path-related helpers).
  • Extended path utility coverage for export/import scenarios.
  • Added comprehensive tests:
    • UI/conversation tests for export/import behavior
    • Utility tests for path handling
    • E2E wire protocol updates

Scope

  • 8 files changed, ~1.5k insertions.
  • Focused on session context management and test coverage.

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

- Implemented the /export command to allow users to export the current session context to a markdown file.
- Implemented the /import command to enable users to import context from a file or session ID.
- Added utility functions for building export markdown and validating importable file types.
- Created tests for the new commands and utility functions to ensure functionality and correctness.
- Updated path utility to sanitize CLI paths for better handling of user input.
Copilot AI review requested due to automatic review settings March 3, 2026 06:10
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 session context export/import support to Kimi CLI via new /export and /import slash commands, including markdown export formatting utilities, CLI path sanitization, and new tests to validate export/import serialization and wire protocol command discovery.

Changes:

  • Register /export and /import in both soul-level and shell-level slash command flows.
  • Add kimi_cli.utils.export for export markdown generation and import transcript stringification + importable-file filtering.
  • Add sanitize_cli_path() and expand path/export-related test coverage and wire protocol expectations.

Reviewed changes

Copilot reviewed 8 out of 8 changed files in this pull request and generated 10 comments.

Show a summary per file
File Description
tests_e2e/test_wire_protocol.py Updates initialize handshake expectations to include export/import slash commands.
tests/utils/test_utils_path.py Adds unit tests for sanitize_cli_path() behavior (quotes/whitespace).
tests/ui_and_conv/test_export_import.py Adds comprehensive unit tests for export/import helper functions (formatting, grouping, filtering).
src/kimi_cli/utils/path.py Introduces sanitize_cli_path() for normalizing quoted CLI path args.
src/kimi_cli/utils/export.py New export/import utility module (markdown export + import transcript + extension allowlist).
src/kimi_cli/ui/shell/slash.py Exposes ensure_kimi_soul() and imports new export_import module to register commands.
src/kimi_cli/ui/shell/export_import.py Implements shell-side /export + /import commands (file IO, session lookup, wire markers).
src/kimi_cli/soul/slash.py Adds soul-level /export + /import commands and applies sanitize_cli_path() to /add-dir.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +369 to +371
"""File extensions accepted by ``/import``. Only text-based formats are
supported — importing binary files (images, PDFs, archives, …) is rejected
with a friendly message."""
Copy link

Copilot AI Mar 3, 2026

Choose a reason for hiding this comment

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

Same issue as above: the triple-quoted text after _IMPORTABLE_EXTENSIONS is a standalone string literal (no-op). Please change it to a comment so it doesn’t look like a misplaced docstring / trigger “pointless statement” lint rules.

Suggested change
"""File extensions accepted by ``/import``. Only text-based formats are
supportedimporting binary files (images, PDFs, archives, …) is rejected
with a friendly message."""
# File extensions accepted by ``/import``. Only text-based formats are
# supported — importing binary files (images, PDFs, archives, …) is rejected
# with a friendly message.

Copilot uses AI. Check for mistakes.

target_path = Path(target).expanduser()

if target_path.exists() and target_path.is_file():
Copy link

Copilot AI Mar 3, 2026

Choose a reason for hiding this comment

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

/import treats an existing directory path as a session-id lookup (because it only checks exists() and is_file()), which leads to a confusing “not a valid file path or session ID” error for directories. Add an explicit if target_path.exists() and target_path.is_dir(): branch with a clearer message (e.g., “path is a directory; please provide a file”).

Suggested change
if target_path.exists() and target_path.is_file():
if target_path.exists() and target_path.is_dir():
console.print(
"[red]The specified path is a directory; please provide a file to import.[/red]"
)
return
elif target_path.exists() and target_path.is_file():

Copilot uses AI. Check for mistakes.
Comment on lines +90 to +120
@registry.command(name="import")
async def import_context(app: Shell, args: str):
"""Import context from a file or session ID"""
soul = ensure_kimi_soul(app)
if soul is None:
return

target = sanitize_cli_path(args)
if not target:
console.print("[yellow]Usage: /import <file_path or session_id>[/yellow]")
return

target_path = Path(target).expanduser()

if target_path.exists() and target_path.is_file():
# Check file extension
if not is_importable_file(target_path.name):
console.print(
f"[red]Unsupported file type '{target_path.suffix}'. "
"/import only supports text-based files "
"(e.g. .md, .txt, .json, .py, .log, …).[/red]"
)
return

# Import from file
try:
async with aiofiles.open(target_path, encoding="utf-8") as f:
content = await f.read()
except UnicodeDecodeError:
console.print(
f"[red]Cannot import '{target_path.name}': "
Copy link

Copilot AI Mar 3, 2026

Choose a reason for hiding this comment

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

The /export and /import implementations here largely duplicate the logic in src/kimi_cli/soul/slash.py (path resolution, import-from-file/session, message construction). To reduce drift over time, consider extracting shared helpers (e.g., “resolve_import_source”, “build_import_message”, “resolve_export_path”) into kimi_cli.utils.export and reusing them from both entry points.

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

from kimi_cli.utils.path import is_within_directory, list_directory
from kimi_cli.utils.path import is_within_directory, list_directory, sanitize_cli_path
Copy link

Copilot AI Mar 3, 2026

Choose a reason for hiding this comment

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

Inside add_dir, sanitize_cli_path is imported again even though it’s already imported at module scope and used elsewhere in this file. Removing it from the inner import list would avoid redundant imports and keep the import list focused on the local-only dependencies.

Suggested change
from kimi_cli.utils.path import is_within_directory, list_directory, sanitize_cli_path
from kimi_cli.utils.path import is_within_directory, list_directory

Copilot uses AI. Check for mistakes.
Comment on lines +216 to +223
target_path = Path(target).expanduser()

if target_path.exists() and target_path.is_file():
# Check file extension
from kimi_cli.utils.export import is_importable_file

if not is_importable_file(target_path.name):
wire_send(
Copy link

Copilot AI Mar 3, 2026

Choose a reason for hiding this comment

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

Same as the shell handler: an existing directory path falls through to the session-id lookup branch because only exists() and is_file() is checked. Add an explicit is_dir() branch to return a clearer error for directory inputs.

Copilot uses AI. Check for mistakes.
Comment on lines +278 to +290
# Build and append import message
import_text = f'<imported_context source="{source_desc}">\n{content}\n</imported_context>'
message = Message(
role="user",
content=[
system(
f"The user has imported context from {source_desc}. "
"This is a prior conversation history that may be relevant "
"to the current session. "
"Please review this context and use it to inform your responses."
),
TextPart(text=import_text),
],
Copy link

Copilot AI Mar 3, 2026

Choose a reason for hiding this comment

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

source_desc is interpolated into an XML-like attribute value (source="..."). If the value contains " or & (e.g., unusual file names), the tag becomes malformed and may confuse the model. Escape/encode the attribute value or avoid attributes and include the source as plain text inside the block.

Copilot uses AI. Check for mistakes.
Comment on lines +26 to +27
_HINT_KEYS = ("path", "file_path", "command", "query", "url", "name", "pattern")
"""Common tool-call argument keys whose values make good one-line hints."""
Copy link

Copilot AI Mar 3, 2026

Choose a reason for hiding this comment

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

The triple-quoted string on the line after _HINT_KEYS = ... is a standalone string literal (a no-op statement), not an actual docstring, and will be flagged by some linters as a pointless statement. Convert it to a # comment or incorporate the explanation into a module docstring/comment block.

Suggested change
_HINT_KEYS = ("path", "file_path", "command", "query", "url", "name", "pattern")
"""Common tool-call argument keys whose values make good one-line hints."""
# Common tool-call argument keys whose values make good one-line hints.
_HINT_KEYS = ("path", "file_path", "command", "query", "url", "name", "pattern")

Copilot uses AI. Check for mistakes.


def _group_into_turns(history: Sequence[Message]) -> list[list[Message]]:
"""Group messages into logical turns, each starting at a real user message."""
Copy link

Copilot AI Mar 3, 2026

Choose a reason for hiding this comment

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

_group_into_turns docstring says each turn starts at a real user message, but the implementation intentionally groups leading system/developer messages into their own initial turn (see behavior in this loop). Please update the docstring to match the actual grouping semantics so future changes don’t “fix” this accidentally.

Suggested change
"""Group messages into logical turns, each starting at a real user message."""
"""Group messages into logical turns.
Any leading non-user messages (e.g. system/developer) before the first
real user message are grouped into an initial turn. Each subsequent turn
starts at a user message and includes all following messages up to (but
not including) the next user message or the end of the history. Internal
checkpoint messages are skipped entirely.
"""

Copilot uses AI. Check for mistakes.
Comment on lines +159 to +161
# Build the import message
import_text = f'<imported_context source="{source_desc}">\n{content}\n</imported_context>'

Copy link

Copilot AI Mar 3, 2026

Choose a reason for hiding this comment

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

source_desc is interpolated into an XML-like attribute value (source="..."). If the file name or session id contains a double quote (or &), this will produce malformed markup and potentially confuse downstream consumers. Consider escaping attribute values (e.g., replace "/&) or avoid attributes entirely (put the source on its own line inside the tag).

Copilot uses AI. Check for mistakes.
Comment on lines +32 to +40
@registry.command
async def export(app: Shell, args: str):
"""Export current session context to a markdown file"""
soul = ensure_kimi_soul(app)
if soul is None:
return

context = soul.context
history = list(context.history) # snapshot to avoid concurrent mutation
Copy link

Copilot AI Mar 3, 2026

Choose a reason for hiding this comment

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

This new module adds significant user-facing behavior (file IO, path resolution, and context mutation) for /export and /import, but there don’t appear to be tests covering the shell-command behavior end-to-end (e.g., /export creates the expected file in a temp dir; /import from a file/session appends the expected Message and writes the TurnBegin/TurnEnd markers). Adding a small set of shell-level command tests would help prevent regressions.

Copilot uses AI. Check for mistakes.
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 6 additional findings.

Open in Devin Review

devin-ai-integration[bot]

This comment was marked as resolved.

devin-ai-integration[bot]

This comment was marked as resolved.

@RealKai42 RealKai42 merged commit 8ecc294 into main Mar 3, 2026
14 checks passed
@RealKai42 RealKai42 deleted the kaiyi/export-import-cmd branch March 3, 2026 14:56
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 1 new potential issue.

View 16 additional findings in Devin Review.

Open in Devin Review

Comment on lines +235 to +236
source_desc, content_len = result
wire_send(TextPart(text=f"Imported context from {source_desc} ({content_len} chars)."))
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.

🟡 Soul-level /import handler missing wire_file writes, breaking session replay

The shell-level /import handler in export_import.py:97-101 explicitly writes TurnBegin and TurnEnd to soul.wire_file so the import appears in session replay. However, the soul-level /import handler in soul/slash.py:205-243 does not write to wire_file. When /import is invoked through the wire protocol (non-shell UIs), the import action will not be recorded in the session's wire file and won't appear during session replay.

Root Cause

The shell handler at src/kimi_cli/ui/shell/export_import.py:97-101 writes wire markers:

await soul.wire_file.append_message(
    TurnBegin(user_input=f"[Imported context from {source_desc}]")
)
await soul.wire_file.append_message(TurnEnd())

But the soul-level handler at src/kimi_cli/soul/slash.py:205-243 only uses wire_send() (which sends events over the live wire protocol) and never writes to wire_file (which persists events for session replay). KimiSoul.run() at src/kimi_cli/soul/kimisoul.py:241 does call wire_send(TurnBegin(...)) but this only sends events to the wire transport — it does not write to the wire_file for replay.

Impact: When using /import in wire mode (or any non-shell UI), the import won't be visible if the session is later replayed or resumed.

Open in Devin Review

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

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.

2 participants