-
Notifications
You must be signed in to change notification settings - Fork 1.1k
feat(shell): guide users to upgrade to the new Kimi Code #2432
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 3 commits
d9a4b28
0aa8b6a
c274678
3043435
26d34de
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,95 @@ | ||||||
| from __future__ import annotations | ||||||
|
|
||||||
| import contextlib | ||||||
| from datetime import date | ||||||
| from pathlib import Path | ||||||
|
|
||||||
| from rich.console import Console | ||||||
| from rich.text import Text | ||||||
|
|
||||||
| from kimi_cli.share import get_share_dir | ||||||
|
|
||||||
| _INSTALL_SH = "curl -fsSL https://code.kimi.com/kimi-code/install.sh | bash" | ||||||
| _INSTALL_PS = "irm https://code.kimi.com/kimi-code/install.ps1 | iex" | ||||||
|
|
||||||
|
|
||||||
| def install_command(platform: str) -> str: | ||||||
| """Return the Kimi Code install command for the given sys.platform value.""" | ||||||
| if platform == "win32": | ||||||
| return _INSTALL_PS | ||||||
| return _INSTALL_SH | ||||||
|
|
||||||
|
|
||||||
| def kimi_code_installed(home: Path | None = None) -> bool: | ||||||
| """True if the standalone Kimi Code is installed (its data dir ~/.kimi-code exists).""" | ||||||
| home = home or Path.home() | ||||||
| return (home / ".kimi-code").is_dir() | ||||||
|
|
||||||
|
|
||||||
| def exit_nudge_marker(share_dir: Path) -> Path: | ||||||
| """Path of the throttle marker recording the last day the exit nudge was shown.""" | ||||||
| return share_dir / ".migration-nudge" | ||||||
|
|
||||||
|
|
||||||
| def should_show_exit_nudge(marker: Path, today: str) -> bool: | ||||||
| """Return True at most once per calendar day; record `today` when returning True. | ||||||
|
|
||||||
| `today` is an ISO date string (e.g. "2026-06-05"), injected for testability. | ||||||
| """ | ||||||
| try: | ||||||
| last = marker.read_text(encoding="utf-8").strip() | ||||||
| except OSError: | ||||||
| last = "" | ||||||
| if last == today: | ||||||
| return False | ||||||
| with contextlib.suppress(OSError): | ||||||
| marker.write_text(today, encoding="utf-8") | ||||||
| return True | ||||||
|
|
||||||
|
|
||||||
| def welcome_card_text() -> Text: | ||||||
| """Welcome-screen card nudging users to upgrade (shown when Kimi Code is NOT installed).""" | ||||||
| return Text.assemble( | ||||||
| "The new Kimi Code is here — rebuilt to be faster and more powerful.\n", | ||||||
| "Run ", | ||||||
| ("/upgrade", "bold"), | ||||||
| "; your config & sessions carry over.", | ||||||
| ) | ||||||
|
|
||||||
|
|
||||||
| def already_installed_text() -> Text: | ||||||
| """Welcome-screen note shown when Kimi Code IS already installed on this machine.""" | ||||||
| return Text.assemble( | ||||||
| "The new Kimi Code is already installed. Start it in a fresh terminal with ", | ||||||
| ("kimi", "bold"), | ||||||
| " (verify: ", | ||||||
| ("which kimi", "cyan"), | ||||||
| " → ~/.kimi-code).", | ||||||
|
Comment on lines
+81
to
+85
|
||||||
| ) | ||||||
|
|
||||||
|
|
||||||
| def exit_nudge_text() -> Text: | ||||||
| """Throttled tip printed on graceful exit.""" | ||||||
| return Text.assemble( | ||||||
| ("Tip: ", "yellow"), | ||||||
| "The new Kimi Code is rebuilt to be faster and more powerful.\n", | ||||||
| "Install: ", | ||||||
| (_INSTALL_SH, "cyan"), | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡
Suggested change
Was this helpful? React with 👍 or 👎 to provide feedback. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When the CLI exits on Windows and Kimi Code is not installed, Useful? React with 👍 / 👎. |
||||||
| (" (or run /upgrade next time)", "grey50"), | ||||||
|
Comment on lines
+92
to
+96
|
||||||
| ) | ||||||
|
|
||||||
|
|
||||||
| def print_migration_goodbye( | ||||||
| console: Console, *, home: Path | None = None, today: str | None = None | ||||||
| ) -> None: | ||||||
| """Print the farewell ("Bye!") plus, at most once per day, the migration tip. | ||||||
|
|
||||||
| Skipped entirely (only "Bye!") if Kimi Code is already installed. | ||||||
| `home`/`today` are injectable for testing; in production they default to the real values. | ||||||
| """ | ||||||
| console.print("Bye!") | ||||||
| if kimi_code_installed(home): | ||||||
| return | ||||||
| today = today or date.today().isoformat() | ||||||
| if should_show_exit_nudge(exit_nudge_marker(get_share_dir()), today): | ||||||
| console.print(exit_nudge_text()) | ||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,6 +1,7 @@ | ||
| from __future__ import annotations | ||
|
|
||
| import asyncio | ||
| import sys | ||
| from collections.abc import Awaitable, Callable, Iterable | ||
| from typing import TYPE_CHECKING, Any, cast | ||
|
|
||
|
|
@@ -718,6 +719,43 @@ def vis(app: Shell, args: str): | |
| raise SwitchToVis(session_id=session_id) | ||
|
|
||
|
|
||
| @registry.command | ||
| async def upgrade(app: Shell, args: str): | ||
|
Comment on lines
+722
to
+723
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When the prompt is toggled to shell mode with Ctrl-X, Useful? React with 👍 / 👎. |
||
| """Install Kimi Code — the faster successor (migrates your config & sessions)""" | ||
| from kimi_cli.telemetry import track | ||
| from kimi_cli.ui.shell.migration_nudge import install_command | ||
|
|
||
| track("upgrade_invoked") | ||
|
|
||
| cmd = install_command(sys.platform) | ||
| console.print( | ||
| "[bold]This will install the new Kimi Code by running:[/bold]\n" | ||
| f" [cyan]{cmd}[/cyan]\n" | ||
| "Your existing config & sessions will be migrated automatically." | ||
| ) | ||
| try: | ||
| choice = await ChoiceInput( | ||
| message="Proceed with installation? (↑↓ navigate, Enter select, Ctrl+C cancel):", | ||
| options=[("yes", "Yes, install now"), ("no", "No, just show me the command")], | ||
| default="yes", | ||
| ).prompt_async() | ||
| except (EOFError, KeyboardInterrupt): | ||
| console.print("[grey50]Upgrade cancelled.[/grey50]") | ||
| return | ||
|
|
||
| if choice != "yes": | ||
| console.print(f"No problem. To install later, run:\n [cyan]{cmd}[/cyan]") | ||
| return | ||
|
|
||
| await app._run_shell_command(cmd) # pyright: ignore[reportPrivateUsage] | ||
| console.print( | ||
| "\n[green]The new Kimi Code is installed ✓[/green] " | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
If the installer exits non-zero (for example due to no network, a 404 from the install URL, or a shell error), Useful? React with 👍 / 👎. |
||
| "Your config & sessions were migrated automatically.\n" | ||
|
Comment on lines
+756
to
+758
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
After a successful installer run, this message says config and sessions were migrated, but the Kimi Code migration flow does not run during installation: the first Useful? React with 👍 / 👎. |
||
| "Open a [bold]new terminal[/bold] and run [bold]kimi[/bold] to start it.\n" | ||
| "[grey50](Verify with `which kimi` — it should point inside ~/.kimi-code.)[/grey50]" | ||
| ) | ||
|
Comment on lines
+757
to
+762
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔴 After Prompt for agentsWas this helpful? React with 👍 or 👎 to provide feedback. |
||
|
|
||
|
|
||
| @registry.command | ||
| async def mcp(app: Shell, args: str): | ||
| """Show MCP servers and tools""" | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,35 @@ | ||
| from __future__ import annotations | ||
|
|
||
| from pathlib import Path | ||
| from unittest.mock import Mock | ||
|
|
||
| from kimi_cli.ui.shell.migration_nudge import print_migration_goodbye | ||
|
|
||
|
|
||
| def _printed(console: Mock) -> str: | ||
| return " ".join(str(c.args[0]) for c in console.print.call_args_list if c.args) | ||
|
|
||
|
|
||
| def test_goodbye_shows_nudge_when_not_installed(tmp_path: Path, monkeypatch): | ||
| monkeypatch.setattr("kimi_cli.ui.shell.migration_nudge.get_share_dir", lambda: tmp_path) | ||
| console = Mock() | ||
| print_migration_goodbye(console, home=tmp_path, today="2026-06-05") | ||
| # "Bye!" plus the migration tip = 2 prints | ||
| assert console.print.call_count == 2 | ||
| assert "Bye!" in _printed(console) | ||
|
|
||
| # same day -> throttled, only "Bye!" | ||
| console.print.reset_mock() | ||
| print_migration_goodbye(console, home=tmp_path, today="2026-06-05") | ||
| assert console.print.call_count == 1 | ||
| assert "Bye!" in _printed(console) | ||
|
|
||
|
|
||
| def test_goodbye_skips_nudge_when_installed(tmp_path: Path, monkeypatch): | ||
| monkeypatch.setattr("kimi_cli.ui.shell.migration_nudge.get_share_dir", lambda: tmp_path) | ||
| (tmp_path / ".kimi-code").mkdir() | ||
| console = Mock() | ||
| print_migration_goodbye(console, home=tmp_path, today="2026-06-05") | ||
| # installed -> no nudge, only "Bye!" | ||
| assert console.print.call_count == 1 | ||
| assert "Bye!" in _printed(console) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,28 @@ | ||
| from __future__ import annotations | ||
|
|
||
| from pathlib import Path | ||
|
|
||
| from kimi_cli.ui.shell import migration_nudge as mn | ||
|
|
||
|
|
||
| def test_install_command_per_platform(): | ||
| assert mn.install_command("darwin") == ( | ||
| "curl -fsSL https://code.kimi.com/kimi-code/install.sh | bash" | ||
| ) | ||
| assert mn.install_command("linux") == ( | ||
| "curl -fsSL https://code.kimi.com/kimi-code/install.sh | bash" | ||
| ) | ||
| assert mn.install_command("win32") == ("irm https://code.kimi.com/kimi-code/install.ps1 | iex") | ||
|
|
||
|
|
||
| def test_kimi_code_installed_detects_dir(tmp_path: Path): | ||
| assert mn.kimi_code_installed(tmp_path) is False | ||
| (tmp_path / ".kimi-code").mkdir() | ||
| assert mn.kimi_code_installed(tmp_path) is True | ||
|
|
||
|
|
||
| def test_exit_nudge_throttled_once_per_day(tmp_path: Path): | ||
| marker = mn.exit_nudge_marker(tmp_path) | ||
| assert mn.should_show_exit_nudge(marker, "2026-06-05") is True | ||
| assert mn.should_show_exit_nudge(marker, "2026-06-05") is False | ||
| assert mn.should_show_exit_nudge(marker, "2026-06-06") is True |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,20 @@ | ||
| from __future__ import annotations | ||
|
|
||
| from pathlib import Path | ||
|
|
||
| from kimi_cli.ui.shell.migration_nudge import ( | ||
| already_installed_text, | ||
| kimi_code_installed, | ||
| welcome_card_text, | ||
| ) | ||
|
|
||
|
|
||
| def test_card_when_not_installed(tmp_path: Path): | ||
| assert kimi_code_installed(tmp_path) is False | ||
| assert "/upgrade" in welcome_card_text().plain | ||
|
|
||
|
|
||
| def test_note_when_installed(tmp_path: Path): | ||
| (tmp_path / ".kimi-code").mkdir() | ||
| assert kimi_code_installed(tmp_path) is True | ||
| assert "already installed" in already_installed_text().plain |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When
sys.platform == "win32",/upgradepasses this bare PowerShell pipeline to_run_shell_command, which runs it throughasyncio.create_subprocess_shellusing the platform default shell (normallycmd.exe), whereirmandiexare not commands. Windows users who accept the prompt therefore get a shell failure instead of an install; return a command that explicitly launches PowerShell or teach the caller to use PowerShell for this platform.Useful? React with 👍 / 👎.