kimi-cli 0.44__py3-none-any.whl → 0.78__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of kimi-cli might be problematic. Click here for more details.

Files changed (137) hide show
  1. kimi_cli/CHANGELOG.md +349 -40
  2. kimi_cli/__init__.py +6 -0
  3. kimi_cli/acp/AGENTS.md +91 -0
  4. kimi_cli/acp/__init__.py +13 -0
  5. kimi_cli/acp/convert.py +111 -0
  6. kimi_cli/acp/kaos.py +270 -0
  7. kimi_cli/acp/mcp.py +46 -0
  8. kimi_cli/acp/server.py +335 -0
  9. kimi_cli/acp/session.py +445 -0
  10. kimi_cli/acp/tools.py +158 -0
  11. kimi_cli/acp/types.py +13 -0
  12. kimi_cli/agents/default/agent.yaml +4 -4
  13. kimi_cli/agents/default/sub.yaml +2 -1
  14. kimi_cli/agents/default/system.md +79 -21
  15. kimi_cli/agents/okabe/agent.yaml +17 -0
  16. kimi_cli/agentspec.py +53 -25
  17. kimi_cli/app.py +180 -52
  18. kimi_cli/cli/__init__.py +595 -0
  19. kimi_cli/cli/__main__.py +8 -0
  20. kimi_cli/cli/info.py +63 -0
  21. kimi_cli/cli/mcp.py +349 -0
  22. kimi_cli/config.py +153 -17
  23. kimi_cli/constant.py +3 -0
  24. kimi_cli/exception.py +23 -2
  25. kimi_cli/flow/__init__.py +117 -0
  26. kimi_cli/flow/d2.py +376 -0
  27. kimi_cli/flow/mermaid.py +218 -0
  28. kimi_cli/llm.py +129 -23
  29. kimi_cli/metadata.py +32 -7
  30. kimi_cli/platforms.py +262 -0
  31. kimi_cli/prompts/__init__.py +2 -0
  32. kimi_cli/prompts/compact.md +4 -5
  33. kimi_cli/session.py +223 -31
  34. kimi_cli/share.py +2 -0
  35. kimi_cli/skill.py +145 -0
  36. kimi_cli/skills/kimi-cli-help/SKILL.md +55 -0
  37. kimi_cli/skills/skill-creator/SKILL.md +351 -0
  38. kimi_cli/soul/__init__.py +51 -20
  39. kimi_cli/soul/agent.py +213 -85
  40. kimi_cli/soul/approval.py +86 -17
  41. kimi_cli/soul/compaction.py +64 -53
  42. kimi_cli/soul/context.py +38 -5
  43. kimi_cli/soul/denwarenji.py +2 -0
  44. kimi_cli/soul/kimisoul.py +442 -60
  45. kimi_cli/soul/message.py +54 -54
  46. kimi_cli/soul/slash.py +72 -0
  47. kimi_cli/soul/toolset.py +387 -6
  48. kimi_cli/toad.py +74 -0
  49. kimi_cli/tools/AGENTS.md +5 -0
  50. kimi_cli/tools/__init__.py +42 -34
  51. kimi_cli/tools/display.py +25 -0
  52. kimi_cli/tools/dmail/__init__.py +10 -10
  53. kimi_cli/tools/dmail/dmail.md +11 -9
  54. kimi_cli/tools/file/__init__.py +1 -3
  55. kimi_cli/tools/file/glob.py +20 -23
  56. kimi_cli/tools/file/grep.md +1 -1
  57. kimi_cli/tools/file/{grep.py → grep_local.py} +51 -23
  58. kimi_cli/tools/file/read.md +24 -6
  59. kimi_cli/tools/file/read.py +134 -50
  60. kimi_cli/tools/file/replace.md +1 -1
  61. kimi_cli/tools/file/replace.py +36 -29
  62. kimi_cli/tools/file/utils.py +282 -0
  63. kimi_cli/tools/file/write.py +43 -22
  64. kimi_cli/tools/multiagent/__init__.py +7 -0
  65. kimi_cli/tools/multiagent/create.md +11 -0
  66. kimi_cli/tools/multiagent/create.py +50 -0
  67. kimi_cli/tools/{task/__init__.py → multiagent/task.py} +48 -53
  68. kimi_cli/tools/shell/__init__.py +120 -0
  69. kimi_cli/tools/{bash → shell}/bash.md +1 -2
  70. kimi_cli/tools/shell/powershell.md +25 -0
  71. kimi_cli/tools/test.py +4 -4
  72. kimi_cli/tools/think/__init__.py +2 -2
  73. kimi_cli/tools/todo/__init__.py +14 -8
  74. kimi_cli/tools/utils.py +64 -24
  75. kimi_cli/tools/web/fetch.py +68 -13
  76. kimi_cli/tools/web/search.py +10 -12
  77. kimi_cli/ui/acp/__init__.py +65 -412
  78. kimi_cli/ui/print/__init__.py +37 -49
  79. kimi_cli/ui/print/visualize.py +179 -0
  80. kimi_cli/ui/shell/__init__.py +141 -84
  81. kimi_cli/ui/shell/console.py +2 -0
  82. kimi_cli/ui/shell/debug.py +28 -23
  83. kimi_cli/ui/shell/keyboard.py +5 -1
  84. kimi_cli/ui/shell/prompt.py +220 -194
  85. kimi_cli/ui/shell/replay.py +111 -46
  86. kimi_cli/ui/shell/setup.py +89 -82
  87. kimi_cli/ui/shell/slash.py +422 -0
  88. kimi_cli/ui/shell/update.py +4 -2
  89. kimi_cli/ui/shell/usage.py +271 -0
  90. kimi_cli/ui/shell/visualize.py +574 -72
  91. kimi_cli/ui/wire/__init__.py +267 -0
  92. kimi_cli/ui/wire/jsonrpc.py +142 -0
  93. kimi_cli/ui/wire/protocol.py +1 -0
  94. kimi_cli/utils/__init__.py +0 -0
  95. kimi_cli/utils/aiohttp.py +2 -0
  96. kimi_cli/utils/aioqueue.py +72 -0
  97. kimi_cli/utils/broadcast.py +37 -0
  98. kimi_cli/utils/changelog.py +12 -7
  99. kimi_cli/utils/clipboard.py +12 -0
  100. kimi_cli/utils/datetime.py +37 -0
  101. kimi_cli/utils/environment.py +58 -0
  102. kimi_cli/utils/envvar.py +12 -0
  103. kimi_cli/utils/frontmatter.py +44 -0
  104. kimi_cli/utils/logging.py +7 -6
  105. kimi_cli/utils/message.py +9 -14
  106. kimi_cli/utils/path.py +99 -9
  107. kimi_cli/utils/pyinstaller.py +6 -0
  108. kimi_cli/utils/rich/__init__.py +33 -0
  109. kimi_cli/utils/rich/columns.py +99 -0
  110. kimi_cli/utils/rich/markdown.py +961 -0
  111. kimi_cli/utils/rich/markdown_sample.md +108 -0
  112. kimi_cli/utils/rich/markdown_sample_short.md +2 -0
  113. kimi_cli/utils/signals.py +2 -0
  114. kimi_cli/utils/slashcmd.py +124 -0
  115. kimi_cli/utils/string.py +2 -0
  116. kimi_cli/utils/term.py +168 -0
  117. kimi_cli/utils/typing.py +20 -0
  118. kimi_cli/wire/__init__.py +98 -29
  119. kimi_cli/wire/serde.py +45 -0
  120. kimi_cli/wire/types.py +299 -0
  121. kimi_cli-0.78.dist-info/METADATA +200 -0
  122. kimi_cli-0.78.dist-info/RECORD +135 -0
  123. kimi_cli-0.78.dist-info/entry_points.txt +4 -0
  124. kimi_cli/cli.py +0 -250
  125. kimi_cli/soul/runtime.py +0 -96
  126. kimi_cli/tools/bash/__init__.py +0 -99
  127. kimi_cli/tools/file/patch.md +0 -8
  128. kimi_cli/tools/file/patch.py +0 -143
  129. kimi_cli/tools/mcp.py +0 -85
  130. kimi_cli/ui/shell/liveview.py +0 -386
  131. kimi_cli/ui/shell/metacmd.py +0 -262
  132. kimi_cli/wire/message.py +0 -91
  133. kimi_cli-0.44.dist-info/METADATA +0 -188
  134. kimi_cli-0.44.dist-info/RECORD +0 -89
  135. kimi_cli-0.44.dist-info/entry_points.txt +0 -3
  136. /kimi_cli/tools/{task → multiagent}/task.md +0 -0
  137. {kimi_cli-0.44.dist-info → kimi_cli-0.78.dist-info}/WHEEL +0 -0
@@ -1,104 +1,169 @@
1
+ from __future__ import annotations
2
+
1
3
  import asyncio
2
4
  import contextlib
3
5
  import getpass
6
+ from collections import deque
4
7
  from collections.abc import Sequence
5
8
  from dataclasses import dataclass
9
+ from pathlib import Path
6
10
 
7
- from kosong.base.message import Message, TextPart
11
+ import aiofiles
12
+ from kosong.message import Message
8
13
  from kosong.tooling import ToolError, ToolOk
9
14
 
10
- from kimi_cli.soul import StatusSnapshot
11
15
  from kimi_cli.ui.shell.console import console
12
16
  from kimi_cli.ui.shell.prompt import PROMPT_SYMBOL
13
17
  from kimi_cli.ui.shell.visualize import visualize
14
- from kimi_cli.utils.message import message_extract_text, message_stringify
18
+ from kimi_cli.utils.aioqueue import QueueShutDown
19
+ from kimi_cli.utils.logging import logger
20
+ from kimi_cli.utils.message import message_stringify
15
21
  from kimi_cli.wire import Wire
16
- from kimi_cli.wire.message import ContentPart, StepBegin, ToolCall, ToolResult
17
-
18
- MAX_REPLAY_RUNS = 5
22
+ from kimi_cli.wire.serde import WireMessageRecord
23
+ from kimi_cli.wire.types import (
24
+ Event,
25
+ StatusUpdate,
26
+ StepBegin,
27
+ TextPart,
28
+ ToolResult,
29
+ TurnBegin,
30
+ is_event,
31
+ )
19
32
 
20
- type _ReplayEvent = StepBegin | ToolCall | ContentPart | ToolResult
33
+ MAX_REPLAY_TURNS = 5
21
34
 
22
35
 
23
36
  @dataclass(slots=True)
24
- class _ReplayRun:
37
+ class _ReplayTurn:
25
38
  user_message: Message
26
- events: list[_ReplayEvent]
39
+ events: list[Event]
27
40
  n_steps: int = 0
28
41
 
29
42
 
30
- async def replay_recent_history(history: Sequence[Message]) -> None:
43
+ async def replay_recent_history(
44
+ history: Sequence[Message],
45
+ *,
46
+ wire_file: Path | None = None,
47
+ ) -> None:
31
48
  """
32
- Replay the most recent user-initiated runs from the provided message history.
49
+ Replay the most recent user-initiated turns from the provided message history or wire file.
33
50
  """
34
- start_idx = _find_replay_start(history)
35
- if start_idx is None:
51
+ turns = await _build_replay_turns_from_wire(wire_file)
52
+ if not turns:
53
+ start_idx = _find_replay_start(history)
54
+ if start_idx is None:
55
+ return
56
+ turns = _build_replay_turns_from_history(history[start_idx:])
57
+ if not turns:
36
58
  return
37
59
 
38
- runs = _build_replay_runs(history[start_idx:])
39
- if not runs:
40
- return
41
-
42
- for run in runs:
60
+ for turn in turns:
43
61
  wire = Wire()
44
- console.print(f"{getpass.getuser()}{PROMPT_SYMBOL} {message_stringify(run.user_message)}")
62
+ console.print(f"{getpass.getuser()}{PROMPT_SYMBOL} {message_stringify(turn.user_message)}")
45
63
  ui_task = asyncio.create_task(
46
- visualize(wire.ui_side, initial_status=StatusSnapshot(context_usage=0.0))
64
+ visualize(wire.ui_side(merge=False), initial_status=StatusUpdate())
47
65
  )
48
- for event in run.events:
66
+ for event in turn.events:
49
67
  wire.soul_side.send(event)
50
68
  await asyncio.sleep(0) # yield to UI loop
51
69
  wire.shutdown()
52
- with contextlib.suppress(asyncio.QueueShutDown):
70
+ with contextlib.suppress(QueueShutDown):
53
71
  await ui_task
54
72
 
55
73
 
74
+ async def _build_replay_turns_from_wire(wire_file: Path | None) -> list[_ReplayTurn]:
75
+ if wire_file is None or not wire_file.exists():
76
+ return []
77
+
78
+ size = wire_file.stat().st_size
79
+ if size > 20 * 1024 * 1024:
80
+ logger.info(
81
+ "Wire file too large for replay, skipping: {file} ({size} bytes)",
82
+ file=wire_file,
83
+ size=size,
84
+ )
85
+ return []
86
+
87
+ turns: deque[_ReplayTurn] = deque(maxlen=MAX_REPLAY_TURNS)
88
+ try:
89
+ async with aiofiles.open(wire_file, encoding="utf-8") as f:
90
+ async for line in f:
91
+ line = line.strip()
92
+ if not line:
93
+ continue
94
+ try:
95
+ record = WireMessageRecord.model_validate_json(line)
96
+ wire_msg = record.to_wire_message()
97
+ except ValueError:
98
+ continue
99
+
100
+ if isinstance(wire_msg, TurnBegin):
101
+ turns.append(
102
+ _ReplayTurn(
103
+ user_message=Message(role="user", content=wire_msg.user_input),
104
+ events=[],
105
+ )
106
+ )
107
+ continue
108
+
109
+ if not is_event(wire_msg) or not turns:
110
+ continue
111
+
112
+ current_turn = turns[-1]
113
+ if isinstance(wire_msg, StepBegin):
114
+ current_turn.n_steps = wire_msg.n
115
+ current_turn.events.append(wire_msg)
116
+ except Exception:
117
+ logger.exception("Failed to build replay turns from wire file {file}:", file=wire_file)
118
+ return []
119
+ return list(turns)
120
+
121
+
56
122
  def _is_user_message(message: Message) -> bool:
57
123
  # FIXME: should consider non-text tool call results which are sent as user messages
58
124
  if message.role != "user":
59
125
  return False
60
- return not message_extract_text(message).startswith("<system>CHECKPOINT")
126
+ return not message.extract_text().startswith("<system>CHECKPOINT")
61
127
 
62
128
 
63
129
  def _find_replay_start(history: Sequence[Message]) -> int | None:
64
130
  indices = [idx for idx, message in enumerate(history) if _is_user_message(message)]
65
131
  if not indices:
66
132
  return None
67
- # only replay last MAX_REPLAY_RUNS messages
68
- return indices[max(0, len(indices) - MAX_REPLAY_RUNS)]
133
+ # only replay last MAX_REPLAY_TURNS messages
134
+ return indices[max(0, len(indices) - MAX_REPLAY_TURNS)]
69
135
 
70
136
 
71
- def _build_replay_runs(history: Sequence[Message]) -> list[_ReplayRun]:
72
- runs: list[_ReplayRun] = []
73
- current_run: _ReplayRun | None = None
137
+ def _build_replay_turns_from_history(history: Sequence[Message]) -> list[_ReplayTurn]:
138
+ turns: list[_ReplayTurn] = []
139
+ current_turn: _ReplayTurn | None = None
74
140
  for message in history:
75
141
  if _is_user_message(message):
76
- # start a new run
77
- if current_run is not None:
78
- runs.append(current_run)
79
- current_run = _ReplayRun(user_message=message, events=[])
142
+ # start a new turn
143
+ if current_turn is not None:
144
+ turns.append(current_turn)
145
+ current_turn = _ReplayTurn(user_message=message, events=[])
80
146
  elif message.role == "assistant":
81
- if current_run is None:
147
+ if current_turn is None:
82
148
  continue
83
- current_run.n_steps += 1
84
- current_run.events.append(StepBegin(n=current_run.n_steps))
85
- if isinstance(message.content, str):
86
- current_run.events.append(TextPart(text=message.content))
87
- else:
88
- current_run.events.extend(message.content)
89
- current_run.events.extend(message.tool_calls or [])
149
+ current_turn.n_steps += 1
150
+ current_turn.events.append(StepBegin(n=current_turn.n_steps))
151
+ current_turn.events.extend(message.content)
152
+ current_turn.events.extend(message.tool_calls or [])
90
153
  elif message.role == "tool":
91
- if current_run is None:
154
+ if current_turn is None:
92
155
  continue
93
156
  assert message.tool_call_id is not None
94
- if isinstance(message.content, list) and any(
157
+ if any(
95
158
  isinstance(part, TextPart) and part.text.startswith("<system>ERROR")
96
159
  for part in message.content
97
160
  ):
98
161
  result = ToolError(message="", output="", brief="")
99
162
  else:
100
163
  result = ToolOk(output=message.content)
101
- current_run.events.append(ToolResult(tool_call_id=message.tool_call_id, result=result))
102
- if current_run is not None:
103
- runs.append(current_run)
104
- return runs
164
+ current_turn.events.append(
165
+ ToolResult(tool_call_id=message.tool_call_id, return_value=result)
166
+ )
167
+ if current_turn is not None:
168
+ turns.append(current_turn)
169
+ return turns
@@ -1,52 +1,39 @@
1
+ from __future__ import annotations
2
+
1
3
  import asyncio
2
4
  from typing import TYPE_CHECKING, NamedTuple
3
5
 
4
- import aiohttp
6
+ from loguru import logger
5
7
  from prompt_toolkit import PromptSession
6
8
  from prompt_toolkit.shortcuts.choice_input import ChoiceInput
7
9
  from pydantic import SecretStr
8
10
 
9
- from kimi_cli.config import LLMModel, LLMProvider, MoonshotSearchConfig, load_config, save_config
11
+ from kimi_cli.config import (
12
+ LLMModel,
13
+ LLMProvider,
14
+ MoonshotFetchConfig,
15
+ MoonshotSearchConfig,
16
+ load_config,
17
+ save_config,
18
+ )
19
+ from kimi_cli.platforms import (
20
+ PLATFORMS,
21
+ ModelInfo,
22
+ Platform,
23
+ get_platform_by_name,
24
+ list_models,
25
+ managed_model_key,
26
+ managed_provider_key,
27
+ )
10
28
  from kimi_cli.ui.shell.console import console
11
- from kimi_cli.ui.shell.metacmd import meta_command
12
- from kimi_cli.utils.aiohttp import new_client_session
29
+ from kimi_cli.ui.shell.slash import registry
13
30
 
14
31
  if TYPE_CHECKING:
15
- from kimi_cli.ui.shell import ShellApp
16
-
17
-
18
- class _Platform(NamedTuple):
19
- id: str
20
- name: str
21
- base_url: str
22
- search_url: str | None = None
23
- allowed_models: list[str] | None = None
24
-
25
-
26
- _PLATFORMS = [
27
- _Platform(
28
- id="kimi-for-coding",
29
- name="Kimi For Coding",
30
- base_url="https://api.kimi.com/coding/v1",
31
- search_url="https://api.kimi.com/coding/v1/search",
32
- ),
33
- _Platform(
34
- id="moonshot-cn",
35
- name="Moonshot AI 开放平台 (moonshot.cn)",
36
- base_url="https://api.moonshot.cn/v1",
37
- allowed_models=["kimi-k2-turbo-preview", "kimi-k2-0905-preview", "kimi-k2-0711-preview"],
38
- ),
39
- _Platform(
40
- id="moonshot-ai",
41
- name="Moonshot AI Open Platform (moonshot.ai)",
42
- base_url="https://api.moonshot.ai/v1",
43
- allowed_models=["kimi-k2-turbo-preview", "kimi-k2-0905-preview", "kimi-k2-0711-preview"],
44
- ),
45
- ]
46
-
47
-
48
- @meta_command
49
- async def setup(app: "ShellApp", args: list[str]):
32
+ from kimi_cli.ui.shell import Shell
33
+
34
+
35
+ @registry.command
36
+ async def setup(app: Shell, args: str):
50
37
  """Setup Kimi CLI"""
51
38
  result = await _setup()
52
39
  if not result:
@@ -54,17 +41,26 @@ async def setup(app: "ShellApp", args: list[str]):
54
41
  return
55
42
 
56
43
  config = load_config()
57
- config.providers[result.platform.id] = LLMProvider(
44
+ provider_key = managed_provider_key(result.platform.id)
45
+ model_key = managed_model_key(result.platform.id, result.selected_model.id)
46
+ config.providers[provider_key] = LLMProvider(
58
47
  type="kimi",
59
48
  base_url=result.platform.base_url,
60
49
  api_key=result.api_key,
61
50
  )
62
- config.models[result.model_id] = LLMModel(
63
- provider=result.platform.id,
64
- model=result.model_id,
65
- max_context_size=result.max_context_size,
66
- )
67
- config.default_model = result.model_id
51
+ for key, model in list(config.models.items()):
52
+ if model.provider == provider_key:
53
+ del config.models[key]
54
+ for model_info in result.models:
55
+ capabilities = model_info.capabilities or None
56
+ config.models[managed_model_key(result.platform.id, model_info.id)] = LLMModel(
57
+ provider=provider_key,
58
+ model=model_info.id,
59
+ max_context_size=model_info.context_length,
60
+ capabilities=capabilities,
61
+ )
62
+ config.default_model = model_key
63
+ config.default_thinking = result.thinking
68
64
 
69
65
  if result.platform.search_url:
70
66
  config.services.moonshot_search = MoonshotSearchConfig(
@@ -72,6 +68,12 @@ async def setup(app: "ShellApp", args: list[str]):
72
68
  api_key=result.api_key,
73
69
  )
74
70
 
71
+ if result.platform.fetch_url:
72
+ config.services.moonshot_fetch = MoonshotFetchConfig(
73
+ base_url=result.platform.fetch_url,
74
+ api_key=result.api_key,
75
+ )
76
+
75
77
  save_config(config)
76
78
  console.print("[green]✓[/green] Kimi CLI has been setup! Reloading...")
77
79
  await asyncio.sleep(1)
@@ -83,23 +85,27 @@ async def setup(app: "ShellApp", args: list[str]):
83
85
 
84
86
 
85
87
  class _SetupResult(NamedTuple):
86
- platform: _Platform
88
+ platform: Platform
87
89
  api_key: SecretStr
88
- model_id: str
89
- max_context_size: int
90
+ selected_model: ModelInfo
91
+ models: list[ModelInfo]
92
+ thinking: bool
90
93
 
91
94
 
92
95
  async def _setup() -> _SetupResult | None:
93
96
  # select the API platform
94
97
  platform_name = await _prompt_choice(
95
- header="Select the API platform",
96
- choices=[platform.name for platform in _PLATFORMS],
98
+ header="Select a platform (↑↓ navigate, Enter select, Ctrl+C cancel):",
99
+ choices=[platform.name for platform in PLATFORMS],
97
100
  )
98
101
  if not platform_name:
99
102
  console.print("[red]No platform selected[/red]")
100
103
  return None
101
104
 
102
- platform = next(platform for platform in _PLATFORMS if platform.name == platform_name)
105
+ platform = get_platform_by_name(platform_name)
106
+ if platform is None:
107
+ console.print("[red]Unknown platform[/red]")
108
+ return None
103
109
 
104
110
  # enter the API key
105
111
  api_key = await _prompt_text("Enter your API key", is_password=True)
@@ -107,51 +113,52 @@ async def _setup() -> _SetupResult | None:
107
113
  return None
108
114
 
109
115
  # list models
110
- models_url = f"{platform.base_url}/models"
111
116
  try:
112
- async with (
113
- new_client_session() as session,
114
- session.get(
115
- models_url,
116
- headers={
117
- "Authorization": f"Bearer {api_key}",
118
- },
119
- raise_for_status=True,
120
- ) as response,
121
- ):
122
- resp_json = await response.json()
123
- except aiohttp.ClientError as e:
117
+ models = await list_models(platform, api_key)
118
+ except Exception as e:
119
+ logger.error("Failed to get models: {error}", error=e)
124
120
  console.print(f"[red]Failed to get models: {e}[/red]")
125
121
  return None
126
122
 
127
- model_dict = {model["id"]: model for model in resp_json["data"]}
128
-
129
123
  # select the model
130
- if platform.allowed_models is None:
131
- model_ids = [model["id"] for model in resp_json["data"]]
132
- else:
133
- id_set = set(model["id"] for model in resp_json["data"])
134
- model_ids = [model_id for model_id in platform.allowed_models if model_id in id_set]
135
-
136
- if not model_ids:
124
+ if not models:
137
125
  console.print("[red]No models available for the selected platform[/red]")
138
126
  return None
139
127
 
128
+ model_map = {model.id: model for model in models}
140
129
  model_id = await _prompt_choice(
141
- header="Select the model",
142
- choices=model_ids,
130
+ header="Select a model (↑↓ navigate, Enter select, Ctrl+C cancel):",
131
+ choices=list(model_map),
143
132
  )
144
133
  if not model_id:
145
134
  console.print("[red]No model selected[/red]")
146
135
  return None
147
136
 
148
- model = model_dict[model_id]
137
+ selected_model = model_map[model_id]
138
+
139
+ # Determine thinking mode based on model capabilities
140
+ capabilities = selected_model.capabilities
141
+ thinking: bool
142
+
143
+ if "always_thinking" in capabilities:
144
+ thinking = True
145
+ elif "thinking" in capabilities:
146
+ thinking_selection = await _prompt_choice(
147
+ header="Enable thinking mode? (↑↓ navigate, Enter select, Ctrl+C cancel):",
148
+ choices=["off", "on"],
149
+ )
150
+ if not thinking_selection:
151
+ return None
152
+ thinking = thinking_selection == "on"
153
+ else:
154
+ thinking = False
149
155
 
150
156
  return _SetupResult(
151
157
  platform=platform,
152
158
  api_key=SecretStr(api_key),
153
- model_id=model_id,
154
- max_context_size=model["context_length"],
159
+ selected_model=selected_model,
160
+ models=models,
161
+ thinking=thinking,
155
162
  )
156
163
 
157
164
 
@@ -170,7 +177,7 @@ async def _prompt_choice(*, header: str, choices: list[str]) -> str | None:
170
177
 
171
178
 
172
179
  async def _prompt_text(prompt: str, *, is_password: bool = False) -> str | None:
173
- session = PromptSession()
180
+ session = PromptSession[str]()
174
181
  try:
175
182
  return str(
176
183
  await session.prompt_async(
@@ -182,8 +189,8 @@ async def _prompt_text(prompt: str, *, is_password: bool = False) -> str | None:
182
189
  return None
183
190
 
184
191
 
185
- @meta_command
186
- def reload(app: "ShellApp", args: list[str]):
192
+ @registry.command
193
+ def reload(app: Shell, args: str):
187
194
  """Reload configuration"""
188
195
  from kimi_cli.cli import Reload
189
196