kimi-cli 0.35__py3-none-any.whl → 0.52__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.
- kimi_cli/CHANGELOG.md +165 -0
- kimi_cli/__init__.py +0 -374
- kimi_cli/agents/{koder → default}/agent.yaml +1 -1
- kimi_cli/agents/{koder → default}/system.md +1 -1
- kimi_cli/agentspec.py +115 -0
- kimi_cli/app.py +208 -0
- kimi_cli/cli.py +321 -0
- kimi_cli/config.py +33 -16
- kimi_cli/constant.py +4 -0
- kimi_cli/exception.py +16 -0
- kimi_cli/llm.py +144 -3
- kimi_cli/metadata.py +6 -69
- kimi_cli/prompts/__init__.py +4 -0
- kimi_cli/session.py +103 -0
- kimi_cli/soul/__init__.py +130 -9
- kimi_cli/soul/agent.py +159 -0
- kimi_cli/soul/approval.py +5 -6
- kimi_cli/soul/compaction.py +106 -0
- kimi_cli/soul/context.py +1 -1
- kimi_cli/soul/kimisoul.py +180 -80
- kimi_cli/soul/message.py +6 -6
- kimi_cli/soul/runtime.py +96 -0
- kimi_cli/soul/toolset.py +3 -2
- kimi_cli/tools/__init__.py +35 -31
- kimi_cli/tools/bash/__init__.py +25 -9
- kimi_cli/tools/bash/cmd.md +31 -0
- kimi_cli/tools/dmail/__init__.py +5 -4
- kimi_cli/tools/file/__init__.py +8 -0
- kimi_cli/tools/file/glob.md +1 -1
- kimi_cli/tools/file/glob.py +4 -4
- kimi_cli/tools/file/grep.py +36 -19
- kimi_cli/tools/file/patch.py +52 -10
- kimi_cli/tools/file/read.py +6 -5
- kimi_cli/tools/file/replace.py +16 -4
- kimi_cli/tools/file/write.py +16 -4
- kimi_cli/tools/mcp.py +7 -4
- kimi_cli/tools/task/__init__.py +60 -41
- kimi_cli/tools/task/task.md +1 -1
- kimi_cli/tools/todo/__init__.py +4 -2
- kimi_cli/tools/utils.py +1 -1
- kimi_cli/tools/web/fetch.py +2 -1
- kimi_cli/tools/web/search.py +13 -12
- kimi_cli/ui/__init__.py +0 -68
- kimi_cli/ui/acp/__init__.py +67 -38
- kimi_cli/ui/print/__init__.py +46 -69
- kimi_cli/ui/shell/__init__.py +145 -154
- kimi_cli/ui/shell/console.py +27 -1
- kimi_cli/ui/shell/debug.py +187 -0
- kimi_cli/ui/shell/keyboard.py +183 -0
- kimi_cli/ui/shell/metacmd.py +34 -81
- kimi_cli/ui/shell/prompt.py +245 -28
- kimi_cli/ui/shell/replay.py +104 -0
- kimi_cli/ui/shell/setup.py +19 -19
- kimi_cli/ui/shell/update.py +11 -5
- kimi_cli/ui/shell/visualize.py +576 -0
- kimi_cli/ui/wire/README.md +109 -0
- kimi_cli/ui/wire/__init__.py +340 -0
- kimi_cli/ui/wire/jsonrpc.py +48 -0
- kimi_cli/utils/__init__.py +0 -0
- kimi_cli/utils/aiohttp.py +10 -0
- kimi_cli/utils/changelog.py +6 -2
- kimi_cli/utils/clipboard.py +10 -0
- kimi_cli/utils/message.py +15 -1
- kimi_cli/utils/rich/__init__.py +33 -0
- kimi_cli/utils/rich/markdown.py +959 -0
- kimi_cli/utils/rich/markdown_sample.md +108 -0
- kimi_cli/utils/rich/markdown_sample_short.md +2 -0
- kimi_cli/utils/signals.py +41 -0
- kimi_cli/utils/string.py +8 -0
- kimi_cli/utils/term.py +114 -0
- kimi_cli/wire/__init__.py +73 -0
- kimi_cli/wire/message.py +191 -0
- kimi_cli-0.52.dist-info/METADATA +186 -0
- kimi_cli-0.52.dist-info/RECORD +99 -0
- kimi_cli-0.52.dist-info/entry_points.txt +3 -0
- kimi_cli/agent.py +0 -261
- kimi_cli/agents/koder/README.md +0 -3
- kimi_cli/prompts/metacmds/__init__.py +0 -4
- kimi_cli/soul/wire.py +0 -101
- kimi_cli/ui/shell/liveview.py +0 -158
- kimi_cli/utils/provider.py +0 -64
- kimi_cli-0.35.dist-info/METADATA +0 -24
- kimi_cli-0.35.dist-info/RECORD +0 -76
- kimi_cli-0.35.dist-info/entry_points.txt +0 -3
- /kimi_cli/agents/{koder → default}/sub.yaml +0 -0
- /kimi_cli/prompts/{metacmds/compact.md → compact.md} +0 -0
- /kimi_cli/prompts/{metacmds/init.md → init.md} +0 -0
- {kimi_cli-0.35.dist-info → kimi_cli-0.52.dist-info}/WHEEL +0 -0
kimi_cli/ui/shell/__init__.py
CHANGED
|
@@ -1,60 +1,56 @@
|
|
|
1
1
|
import asyncio
|
|
2
|
-
import signal
|
|
3
2
|
from collections.abc import Awaitable, Coroutine
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from enum import Enum
|
|
4
5
|
from typing import Any
|
|
5
6
|
|
|
6
|
-
from kosong.base.message import ContentPart, TextPart, ToolCall, ToolCallPart
|
|
7
7
|
from kosong.chat_provider import APIStatusError, ChatProviderError
|
|
8
|
-
from kosong.
|
|
8
|
+
from kosong.message import ContentPart
|
|
9
9
|
from rich.console import Group, RenderableType
|
|
10
10
|
from rich.panel import Panel
|
|
11
11
|
from rich.table import Table
|
|
12
12
|
from rich.text import Text
|
|
13
13
|
|
|
14
|
-
from kimi_cli.soul import LLMNotSet, MaxStepsReached, Soul
|
|
14
|
+
from kimi_cli.soul import LLMNotSet, LLMNotSupported, MaxStepsReached, RunCancelled, Soul, run_soul
|
|
15
15
|
from kimi_cli.soul.kimisoul import KimiSoul
|
|
16
|
-
from kimi_cli.soul.wire import (
|
|
17
|
-
ApprovalRequest,
|
|
18
|
-
ApprovalResponse,
|
|
19
|
-
StatusUpdate,
|
|
20
|
-
StepBegin,
|
|
21
|
-
StepInterrupted,
|
|
22
|
-
Wire,
|
|
23
|
-
)
|
|
24
|
-
from kimi_cli.ui import RunCancelled, run_soul
|
|
25
16
|
from kimi_cli.ui.shell.console import console
|
|
26
|
-
from kimi_cli.ui.shell.liveview import StepLiveView
|
|
27
17
|
from kimi_cli.ui.shell.metacmd import get_meta_command
|
|
28
18
|
from kimi_cli.ui.shell.prompt import CustomPromptSession, PromptMode, toast
|
|
29
|
-
from kimi_cli.ui.shell.
|
|
19
|
+
from kimi_cli.ui.shell.replay import replay_recent_history
|
|
20
|
+
from kimi_cli.ui.shell.update import LATEST_VERSION_FILE, UpdateResult, do_update, semver_tuple
|
|
21
|
+
from kimi_cli.ui.shell.visualize import visualize
|
|
30
22
|
from kimi_cli.utils.logging import logger
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
class Reload(Exception):
|
|
34
|
-
"""Reload configuration."""
|
|
35
|
-
|
|
36
|
-
pass
|
|
23
|
+
from kimi_cli.utils.signals import install_sigint_handler
|
|
24
|
+
from kimi_cli.utils.term import ensure_new_line
|
|
37
25
|
|
|
38
26
|
|
|
39
27
|
class ShellApp:
|
|
40
|
-
def __init__(self, soul: Soul, welcome_info:
|
|
28
|
+
def __init__(self, soul: Soul, welcome_info: list["WelcomeInfoItem"] | None = None):
|
|
41
29
|
self.soul = soul
|
|
42
|
-
self.
|
|
30
|
+
self._welcome_info = list(welcome_info or [])
|
|
43
31
|
self._background_tasks: set[asyncio.Task[Any]] = set()
|
|
44
32
|
|
|
45
33
|
async def run(self, command: str | None = None) -> bool:
|
|
46
34
|
if command is not None:
|
|
47
35
|
# run single command and exit
|
|
48
36
|
logger.info("Running agent with command: {command}", command=command)
|
|
49
|
-
return await self.
|
|
37
|
+
return await self._run_soul_command(command)
|
|
38
|
+
|
|
39
|
+
self._start_background_task(self._auto_update())
|
|
50
40
|
|
|
51
|
-
self.
|
|
41
|
+
_print_welcome_info(self.soul.name or "Kimi CLI", self._welcome_info)
|
|
52
42
|
|
|
53
|
-
|
|
43
|
+
if isinstance(self.soul, KimiSoul):
|
|
44
|
+
await replay_recent_history(self.soul.context.history)
|
|
54
45
|
|
|
55
|
-
with CustomPromptSession(
|
|
46
|
+
with CustomPromptSession(
|
|
47
|
+
status_provider=lambda: self.soul.status,
|
|
48
|
+
model_capabilities=self.soul.model_capabilities or set(),
|
|
49
|
+
initial_thinking=isinstance(self.soul, KimiSoul) and self.soul.thinking,
|
|
50
|
+
) as prompt_session:
|
|
56
51
|
while True:
|
|
57
52
|
try:
|
|
53
|
+
ensure_new_line()
|
|
58
54
|
user_input = await prompt_session.prompt()
|
|
59
55
|
except KeyboardInterrupt:
|
|
60
56
|
logger.debug("Exiting by KeyboardInterrupt")
|
|
@@ -79,14 +75,17 @@ class ShellApp:
|
|
|
79
75
|
await self._run_shell_command(user_input.command)
|
|
80
76
|
continue
|
|
81
77
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
await self._run_meta_command(command[1:])
|
|
78
|
+
if user_input.command.startswith("/"):
|
|
79
|
+
logger.debug("Running meta command: {command}", command=user_input.command)
|
|
80
|
+
await self._run_meta_command(user_input.command[1:])
|
|
86
81
|
continue
|
|
87
82
|
|
|
88
|
-
logger.info(
|
|
89
|
-
|
|
83
|
+
logger.info(
|
|
84
|
+
"Running agent command: {command} with thinking {thinking}",
|
|
85
|
+
command=user_input.content,
|
|
86
|
+
thinking="on" if user_input.thinking else "off",
|
|
87
|
+
)
|
|
88
|
+
await self._run_soul_command(user_input.content, user_input.thinking)
|
|
90
89
|
|
|
91
90
|
return True
|
|
92
91
|
|
|
@@ -96,26 +95,71 @@ class ShellApp:
|
|
|
96
95
|
return
|
|
97
96
|
|
|
98
97
|
logger.info("Running shell command: {cmd}", cmd=command)
|
|
98
|
+
|
|
99
|
+
proc: asyncio.subprocess.Process | None = None
|
|
100
|
+
|
|
101
|
+
def _handler():
|
|
102
|
+
logger.debug("SIGINT received.")
|
|
103
|
+
if proc:
|
|
104
|
+
proc.terminate()
|
|
105
|
+
|
|
99
106
|
loop = asyncio.get_running_loop()
|
|
107
|
+
remove_sigint = install_sigint_handler(loop, _handler)
|
|
100
108
|
try:
|
|
101
109
|
# TODO: For the sake of simplicity, we now use `create_subprocess_shell`.
|
|
102
110
|
# Later we should consider making this behave like a real shell.
|
|
103
111
|
proc = await asyncio.create_subprocess_shell(command)
|
|
104
|
-
|
|
105
|
-
def _handler():
|
|
106
|
-
logger.debug("SIGINT received.")
|
|
107
|
-
proc.terminate()
|
|
108
|
-
|
|
109
|
-
loop.add_signal_handler(signal.SIGINT, _handler)
|
|
110
|
-
|
|
111
112
|
await proc.wait()
|
|
112
113
|
except Exception as e:
|
|
113
114
|
logger.exception("Failed to run shell command:")
|
|
114
115
|
console.print(f"[red]Failed to run shell command: {e}[/red]")
|
|
115
116
|
finally:
|
|
116
|
-
|
|
117
|
+
remove_sigint()
|
|
118
|
+
|
|
119
|
+
async def _run_meta_command(self, command_str: str):
|
|
120
|
+
from kimi_cli.cli import Reload
|
|
121
|
+
|
|
122
|
+
parts = command_str.split(" ")
|
|
123
|
+
command_name = parts[0]
|
|
124
|
+
command_args = parts[1:]
|
|
125
|
+
command = get_meta_command(command_name)
|
|
126
|
+
if command is None:
|
|
127
|
+
console.print(f"Meta command /{command_name} not found")
|
|
128
|
+
return
|
|
129
|
+
if command.kimi_soul_only and not isinstance(self.soul, KimiSoul):
|
|
130
|
+
console.print(f"Meta command /{command_name} not supported")
|
|
131
|
+
return
|
|
132
|
+
logger.debug(
|
|
133
|
+
"Running meta command: {command_name} with args: {command_args}",
|
|
134
|
+
command_name=command_name,
|
|
135
|
+
command_args=command_args,
|
|
136
|
+
)
|
|
137
|
+
try:
|
|
138
|
+
ret = command.func(self, command_args)
|
|
139
|
+
if isinstance(ret, Awaitable):
|
|
140
|
+
await ret
|
|
141
|
+
except LLMNotSet:
|
|
142
|
+
logger.error("LLM not set")
|
|
143
|
+
console.print("[red]LLM not set, send /setup to configure[/red]")
|
|
144
|
+
except ChatProviderError as e:
|
|
145
|
+
logger.exception("LLM provider error:")
|
|
146
|
+
console.print(f"[red]LLM provider error: {e}[/red]")
|
|
147
|
+
except asyncio.CancelledError:
|
|
148
|
+
logger.info("Interrupted by user")
|
|
149
|
+
console.print("[red]Interrupted by user[/red]")
|
|
150
|
+
except Reload:
|
|
151
|
+
# just propagate
|
|
152
|
+
raise
|
|
153
|
+
except BaseException as e:
|
|
154
|
+
logger.exception("Unknown error:")
|
|
155
|
+
console.print(f"[red]Unknown error: {e}[/red]")
|
|
156
|
+
raise # re-raise unknown error
|
|
117
157
|
|
|
118
|
-
async def
|
|
158
|
+
async def _run_soul_command(
|
|
159
|
+
self,
|
|
160
|
+
user_input: str | list[ContentPart],
|
|
161
|
+
thinking: bool | None = None,
|
|
162
|
+
) -> bool:
|
|
119
163
|
"""
|
|
120
164
|
Run the soul and handle any known exceptions.
|
|
121
165
|
|
|
@@ -129,14 +173,35 @@ class ShellApp:
|
|
|
129
173
|
cancel_event.set()
|
|
130
174
|
|
|
131
175
|
loop = asyncio.get_running_loop()
|
|
132
|
-
loop
|
|
176
|
+
remove_sigint = install_sigint_handler(loop, _handler)
|
|
133
177
|
|
|
134
178
|
try:
|
|
135
|
-
|
|
179
|
+
if isinstance(self.soul, KimiSoul) and thinking is not None:
|
|
180
|
+
self.soul.set_thinking(thinking)
|
|
181
|
+
|
|
182
|
+
# Use lambda to pass cancel_event via closure
|
|
183
|
+
await run_soul(
|
|
184
|
+
self.soul,
|
|
185
|
+
user_input,
|
|
186
|
+
lambda wire: visualize(
|
|
187
|
+
wire,
|
|
188
|
+
initial_status=self.soul.status,
|
|
189
|
+
cancel_event=cancel_event,
|
|
190
|
+
),
|
|
191
|
+
cancel_event,
|
|
192
|
+
)
|
|
136
193
|
return True
|
|
137
194
|
except LLMNotSet:
|
|
138
195
|
logger.error("LLM not set")
|
|
139
196
|
console.print("[red]LLM not set, send /setup to configure[/red]")
|
|
197
|
+
except LLMNotSupported as e:
|
|
198
|
+
# actually unsupported input/mode should already be blocked by prompt session
|
|
199
|
+
logger.error(
|
|
200
|
+
"LLM model '{model_name}' does not support required capabilities: {capabilities}",
|
|
201
|
+
model_name=e.llm.model_name,
|
|
202
|
+
capabilities=", ".join(e.capabilities),
|
|
203
|
+
)
|
|
204
|
+
console.print(f"[red]{e}[/red]")
|
|
140
205
|
except ChatProviderError as e:
|
|
141
206
|
logger.exception("LLM provider error:")
|
|
142
207
|
if isinstance(e, APIStatusError) and e.status_code == 401:
|
|
@@ -153,31 +218,29 @@ class ShellApp:
|
|
|
153
218
|
except RunCancelled:
|
|
154
219
|
logger.info("Cancelled by user")
|
|
155
220
|
console.print("[red]Interrupted by user[/red]")
|
|
156
|
-
except Reload:
|
|
157
|
-
# just propagate
|
|
158
|
-
raise
|
|
159
221
|
except BaseException as e:
|
|
160
222
|
logger.exception("Unknown error:")
|
|
161
223
|
console.print(f"[red]Unknown error: {e}[/red]")
|
|
162
224
|
raise # re-raise unknown error
|
|
163
225
|
finally:
|
|
164
|
-
|
|
226
|
+
remove_sigint()
|
|
165
227
|
return False
|
|
166
228
|
|
|
167
|
-
def
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
async def _auto_update_background(self) -> None:
|
|
171
|
-
toast("checking for updates...", duration=2.0)
|
|
229
|
+
async def _auto_update(self) -> None:
|
|
230
|
+
toast("checking for updates...", topic="update", duration=2.0)
|
|
172
231
|
result = await do_update(print=False, check_only=True)
|
|
173
232
|
if result == UpdateResult.UPDATE_AVAILABLE:
|
|
174
233
|
while True:
|
|
175
|
-
toast(
|
|
234
|
+
toast(
|
|
235
|
+
"new version found, run `uv tool upgrade kimi-cli` to upgrade",
|
|
236
|
+
topic="update",
|
|
237
|
+
duration=30.0,
|
|
238
|
+
)
|
|
176
239
|
await asyncio.sleep(60.0)
|
|
177
240
|
elif result == UpdateResult.UPDATED:
|
|
178
|
-
toast("auto updated, restart to use the new version", duration=5.0)
|
|
241
|
+
toast("auto updated, restart to use the new version", topic="update", duration=5.0)
|
|
179
242
|
|
|
180
|
-
def
|
|
243
|
+
def _start_background_task(self, coro: Coroutine[Any, Any, Any]) -> asyncio.Task[Any]:
|
|
181
244
|
task = asyncio.create_task(coro)
|
|
182
245
|
self._background_tasks.add(task)
|
|
183
246
|
|
|
@@ -193,93 +256,6 @@ class ShellApp:
|
|
|
193
256
|
task.add_done_callback(_cleanup)
|
|
194
257
|
return task
|
|
195
258
|
|
|
196
|
-
async def _visualize(self, wire: Wire):
|
|
197
|
-
"""
|
|
198
|
-
A loop to consume agent events and visualize the agent behavior.
|
|
199
|
-
This loop never raise any exception except asyncio.CancelledError.
|
|
200
|
-
"""
|
|
201
|
-
try:
|
|
202
|
-
# expect a StepBegin
|
|
203
|
-
assert isinstance(await wire.receive(), StepBegin)
|
|
204
|
-
|
|
205
|
-
while True:
|
|
206
|
-
# spin the moon at the beginning of each step
|
|
207
|
-
with console.status("", spinner="moon"):
|
|
208
|
-
msg = await wire.receive()
|
|
209
|
-
|
|
210
|
-
with StepLiveView(self.soul.status) as step:
|
|
211
|
-
# visualization loop for one step
|
|
212
|
-
while True:
|
|
213
|
-
match msg:
|
|
214
|
-
case TextPart(text=text):
|
|
215
|
-
step.append_text(text)
|
|
216
|
-
case ContentPart():
|
|
217
|
-
# TODO: support more content parts
|
|
218
|
-
step.append_text(f"[{msg.__class__.__name__}]")
|
|
219
|
-
case ToolCall():
|
|
220
|
-
step.append_tool_call(msg)
|
|
221
|
-
case ToolCallPart():
|
|
222
|
-
step.append_tool_call_part(msg)
|
|
223
|
-
case ToolResult():
|
|
224
|
-
step.append_tool_result(msg)
|
|
225
|
-
case ApprovalRequest():
|
|
226
|
-
msg.resolve(ApprovalResponse.APPROVE)
|
|
227
|
-
# TODO(approval): handle approval request
|
|
228
|
-
case StatusUpdate(status=status):
|
|
229
|
-
step.update_status(status)
|
|
230
|
-
case _:
|
|
231
|
-
break # break the step loop
|
|
232
|
-
msg = await wire.receive()
|
|
233
|
-
|
|
234
|
-
# cleanup the step live view
|
|
235
|
-
if isinstance(msg, StepInterrupted):
|
|
236
|
-
step.interrupt()
|
|
237
|
-
else:
|
|
238
|
-
step.finish()
|
|
239
|
-
|
|
240
|
-
if isinstance(msg, StepInterrupted):
|
|
241
|
-
# for StepInterrupted, the visualization loop should end immediately
|
|
242
|
-
break
|
|
243
|
-
|
|
244
|
-
assert isinstance(msg, StepBegin), "expect a StepBegin"
|
|
245
|
-
# start a new step
|
|
246
|
-
except asyncio.QueueShutDown:
|
|
247
|
-
logger.debug("Visualization loop shutting down")
|
|
248
|
-
|
|
249
|
-
async def _run_meta_command(self, command_str: str):
|
|
250
|
-
parts = command_str.split(" ")
|
|
251
|
-
command_name = parts[0]
|
|
252
|
-
command_args = parts[1:]
|
|
253
|
-
command = get_meta_command(command_name)
|
|
254
|
-
if command is None:
|
|
255
|
-
console.print(f"Meta command /{command_name} not found")
|
|
256
|
-
return
|
|
257
|
-
if command.kimi_soul_only and not isinstance(self.soul, KimiSoul):
|
|
258
|
-
console.print(f"Meta command /{command_name} not supported")
|
|
259
|
-
return
|
|
260
|
-
logger.debug(
|
|
261
|
-
"Running meta command: {command_name} with args: {command_args}",
|
|
262
|
-
command_name=command_name,
|
|
263
|
-
command_args=command_args,
|
|
264
|
-
)
|
|
265
|
-
try:
|
|
266
|
-
ret = command.func(self, command_args)
|
|
267
|
-
if isinstance(ret, Awaitable):
|
|
268
|
-
await ret
|
|
269
|
-
except LLMNotSet:
|
|
270
|
-
logger.error("LLM not set")
|
|
271
|
-
console.print("[red]LLM not set, send /setup to configure[/red]")
|
|
272
|
-
except ChatProviderError as e:
|
|
273
|
-
logger.exception("LLM provider error:")
|
|
274
|
-
console.print(f"[red]LLM provider error: {e}[/red]")
|
|
275
|
-
except Reload:
|
|
276
|
-
# just propagate
|
|
277
|
-
raise
|
|
278
|
-
except BaseException as e:
|
|
279
|
-
logger.exception("Unknown error:")
|
|
280
|
-
console.print(f"[red]Unknown error: {e}[/red]")
|
|
281
|
-
raise # re-raise unknown error
|
|
282
|
-
|
|
283
259
|
|
|
284
260
|
_KIMI_BLUE = "dodger_blue1"
|
|
285
261
|
_LOGO = f"""\
|
|
@@ -290,7 +266,19 @@ _LOGO = f"""\
|
|
|
290
266
|
"""
|
|
291
267
|
|
|
292
268
|
|
|
293
|
-
|
|
269
|
+
@dataclass(slots=True)
|
|
270
|
+
class WelcomeInfoItem:
|
|
271
|
+
class Level(Enum):
|
|
272
|
+
INFO = "grey50"
|
|
273
|
+
WARN = "yellow"
|
|
274
|
+
ERROR = "red"
|
|
275
|
+
|
|
276
|
+
name: str
|
|
277
|
+
value: str
|
|
278
|
+
level: Level = Level.INFO
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
def _print_welcome_info(name: str, info_items: list[WelcomeInfoItem]) -> None:
|
|
294
282
|
head = Text.from_markup(f"[bold]Welcome to {name}![/bold]")
|
|
295
283
|
help_text = Text.from_markup("[grey50]Send /help for help information.[/grey50]")
|
|
296
284
|
|
|
@@ -304,17 +292,20 @@ def _print_welcome_info(name: str, model: str, info_items: dict[str, str]) -> No
|
|
|
304
292
|
rows: list[RenderableType] = [table]
|
|
305
293
|
|
|
306
294
|
rows.append(Text("")) # Empty line
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
if
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
295
|
+
for item in info_items:
|
|
296
|
+
rows.append(Text(f"{item.name}: {item.value}", style=item.level.value))
|
|
297
|
+
|
|
298
|
+
if LATEST_VERSION_FILE.exists():
|
|
299
|
+
from kimi_cli.constant import VERSION as current_version
|
|
300
|
+
|
|
301
|
+
latest_version = LATEST_VERSION_FILE.read_text(encoding="utf-8").strip()
|
|
302
|
+
if semver_tuple(latest_version) > semver_tuple(current_version):
|
|
303
|
+
rows.append(
|
|
304
|
+
Text.from_markup(
|
|
305
|
+
f"\n[yellow]New version available: {latest_version}. "
|
|
306
|
+
"Please run `uv tool upgrade kimi-cli` to upgrade.[/yellow]"
|
|
307
|
+
)
|
|
316
308
|
)
|
|
317
|
-
)
|
|
318
309
|
|
|
319
310
|
console.print(
|
|
320
311
|
Panel(
|
kimi_cli/ui/shell/console.py
CHANGED
|
@@ -1,3 +1,29 @@
|
|
|
1
1
|
from rich.console import Console
|
|
2
|
+
from rich.theme import Theme
|
|
2
3
|
|
|
3
|
-
|
|
4
|
+
_NEUTRAL_MARKDOWN_THEME = Theme(
|
|
5
|
+
{
|
|
6
|
+
"markdown.paragraph": "none",
|
|
7
|
+
"markdown.block_quote": "none",
|
|
8
|
+
"markdown.hr": "none",
|
|
9
|
+
"markdown.item": "none",
|
|
10
|
+
"markdown.item.bullet": "none",
|
|
11
|
+
"markdown.item.number": "none",
|
|
12
|
+
"markdown.link": "none",
|
|
13
|
+
"markdown.link_url": "none",
|
|
14
|
+
"markdown.h1": "none",
|
|
15
|
+
"markdown.h1.border": "none",
|
|
16
|
+
"markdown.h2": "none",
|
|
17
|
+
"markdown.h3": "none",
|
|
18
|
+
"markdown.h4": "none",
|
|
19
|
+
"markdown.h5": "none",
|
|
20
|
+
"markdown.h6": "none",
|
|
21
|
+
"markdown.em": "none",
|
|
22
|
+
"markdown.strong": "none",
|
|
23
|
+
"markdown.s": "none",
|
|
24
|
+
"status.spinner": "none",
|
|
25
|
+
},
|
|
26
|
+
inherit=True,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
console = Console(highlight=False, theme=_NEUTRAL_MARKDOWN_THEME)
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from typing import TYPE_CHECKING
|
|
3
|
+
|
|
4
|
+
from kosong.message import (
|
|
5
|
+
AudioURLPart,
|
|
6
|
+
ContentPart,
|
|
7
|
+
ImageURLPart,
|
|
8
|
+
Message,
|
|
9
|
+
TextPart,
|
|
10
|
+
ThinkPart,
|
|
11
|
+
ToolCall,
|
|
12
|
+
)
|
|
13
|
+
from rich.console import Group
|
|
14
|
+
from rich.panel import Panel
|
|
15
|
+
from rich.rule import Rule
|
|
16
|
+
from rich.syntax import Syntax
|
|
17
|
+
from rich.text import Text
|
|
18
|
+
|
|
19
|
+
from kimi_cli.soul.kimisoul import KimiSoul
|
|
20
|
+
from kimi_cli.ui.shell.console import console
|
|
21
|
+
from kimi_cli.ui.shell.metacmd import meta_command
|
|
22
|
+
|
|
23
|
+
if TYPE_CHECKING:
|
|
24
|
+
from kimi_cli.ui.shell import ShellApp
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _format_content_part(part: ContentPart) -> Text | Panel | Group:
|
|
28
|
+
"""Format a single content part."""
|
|
29
|
+
match part:
|
|
30
|
+
case TextPart(text=text):
|
|
31
|
+
# Check if it looks like a system tag
|
|
32
|
+
if text.strip().startswith("<system>") and text.strip().endswith("</system>"):
|
|
33
|
+
return Panel(
|
|
34
|
+
text.strip()[8:-9].strip(),
|
|
35
|
+
title="[dim]system[/dim]",
|
|
36
|
+
border_style="dim yellow",
|
|
37
|
+
padding=(0, 1),
|
|
38
|
+
)
|
|
39
|
+
return Text(text, style="white")
|
|
40
|
+
|
|
41
|
+
case ThinkPart(think=think):
|
|
42
|
+
return Panel(
|
|
43
|
+
think,
|
|
44
|
+
title="[dim]thinking[/dim]",
|
|
45
|
+
border_style="dim cyan",
|
|
46
|
+
padding=(0, 1),
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
case ImageURLPart(image_url=img):
|
|
50
|
+
url_display = img.url[:80] + "..." if len(img.url) > 80 else img.url
|
|
51
|
+
id_text = f" (id: {img.id})" if img.id else ""
|
|
52
|
+
return Text(f"[Image{id_text}] {url_display}", style="blue")
|
|
53
|
+
|
|
54
|
+
case AudioURLPart(audio_url=audio):
|
|
55
|
+
url_display = audio.url[:80] + "..." if len(audio.url) > 80 else audio.url
|
|
56
|
+
id_text = f" (id: {audio.id})" if audio.id else ""
|
|
57
|
+
return Text(f"[Audio{id_text}] {url_display}", style="blue")
|
|
58
|
+
|
|
59
|
+
case _:
|
|
60
|
+
return Text(f"[Unknown content type: {type(part).__name__}]", style="red")
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _format_tool_call(tool_call: ToolCall) -> Panel:
|
|
64
|
+
"""Format a tool call."""
|
|
65
|
+
args = tool_call.function.arguments or "{}"
|
|
66
|
+
try:
|
|
67
|
+
args_formatted = json.dumps(json.loads(args), indent=2)
|
|
68
|
+
args_syntax = Syntax(args_formatted, "json", theme="monokai", padding=(0, 1))
|
|
69
|
+
except json.JSONDecodeError:
|
|
70
|
+
args_syntax = Text(args, style="red")
|
|
71
|
+
|
|
72
|
+
content = Group(
|
|
73
|
+
Text(f"Function: {tool_call.function.name}", style="bold cyan"),
|
|
74
|
+
Text(f"Call ID: {tool_call.id}", style="dim"),
|
|
75
|
+
Text("Arguments:", style="bold"),
|
|
76
|
+
args_syntax,
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
return Panel(
|
|
80
|
+
content,
|
|
81
|
+
title="[bold yellow]Tool Call[/bold yellow]",
|
|
82
|
+
border_style="yellow",
|
|
83
|
+
padding=(0, 1),
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _format_message(msg: Message, index: int) -> Panel:
|
|
88
|
+
"""Format a single message."""
|
|
89
|
+
# Role styling
|
|
90
|
+
role_colors = {
|
|
91
|
+
"system": "magenta",
|
|
92
|
+
"developer": "magenta",
|
|
93
|
+
"user": "green",
|
|
94
|
+
"assistant": "blue",
|
|
95
|
+
"tool": "yellow",
|
|
96
|
+
}
|
|
97
|
+
role_color = role_colors.get(msg.role, "white")
|
|
98
|
+
role_text = f"[bold {role_color}]{msg.role.upper()}[/bold {role_color}]"
|
|
99
|
+
|
|
100
|
+
# Add name if present
|
|
101
|
+
if msg.name:
|
|
102
|
+
role_text += f" [dim]({msg.name})[/dim]"
|
|
103
|
+
|
|
104
|
+
# Add tool call ID for tool messages
|
|
105
|
+
if msg.tool_call_id:
|
|
106
|
+
role_text += f" [dim]→ {msg.tool_call_id}[/dim]"
|
|
107
|
+
|
|
108
|
+
# Format content
|
|
109
|
+
content_items: list = []
|
|
110
|
+
|
|
111
|
+
if isinstance(msg.content, str):
|
|
112
|
+
content_items.append(Text(msg.content, style="white"))
|
|
113
|
+
else:
|
|
114
|
+
for part in msg.content:
|
|
115
|
+
formatted = _format_content_part(part)
|
|
116
|
+
content_items.append(formatted)
|
|
117
|
+
|
|
118
|
+
# Add tool calls if present
|
|
119
|
+
if msg.tool_calls:
|
|
120
|
+
if content_items:
|
|
121
|
+
content_items.append(Text()) # Empty line
|
|
122
|
+
for tool_call in msg.tool_calls:
|
|
123
|
+
content_items.append(_format_tool_call(tool_call))
|
|
124
|
+
|
|
125
|
+
# Combine all content
|
|
126
|
+
if not content_items:
|
|
127
|
+
content_items.append(Text("[empty message]", style="dim italic"))
|
|
128
|
+
|
|
129
|
+
group = Group(*content_items)
|
|
130
|
+
|
|
131
|
+
# Create panel
|
|
132
|
+
title = f"#{index + 1} {role_text}"
|
|
133
|
+
if msg.partial:
|
|
134
|
+
title += " [dim italic](partial)[/dim italic]"
|
|
135
|
+
|
|
136
|
+
return Panel(
|
|
137
|
+
group,
|
|
138
|
+
title=title,
|
|
139
|
+
border_style=role_color,
|
|
140
|
+
padding=(0, 1),
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
@meta_command(kimi_soul_only=True)
|
|
145
|
+
def debug(app: "ShellApp", args: list[str]):
|
|
146
|
+
"""Debug the context"""
|
|
147
|
+
assert isinstance(app.soul, KimiSoul)
|
|
148
|
+
|
|
149
|
+
context = app.soul._context
|
|
150
|
+
history = context.history
|
|
151
|
+
|
|
152
|
+
if not history:
|
|
153
|
+
console.print(
|
|
154
|
+
Panel(
|
|
155
|
+
"Context is empty - no messages yet",
|
|
156
|
+
border_style="yellow",
|
|
157
|
+
padding=(1, 2),
|
|
158
|
+
)
|
|
159
|
+
)
|
|
160
|
+
return
|
|
161
|
+
|
|
162
|
+
# Build the debug output
|
|
163
|
+
output_items = [
|
|
164
|
+
Panel(
|
|
165
|
+
Group(
|
|
166
|
+
Text(f"Total messages: {len(history)}", style="bold"),
|
|
167
|
+
Text(f"Token count: {context.token_count:,}", style="bold"),
|
|
168
|
+
Text(f"Checkpoints: {context.n_checkpoints}", style="bold"),
|
|
169
|
+
Text(f"Trajectory: {context._file_backend}", style="dim"),
|
|
170
|
+
),
|
|
171
|
+
title="[bold]Context Info[/bold]",
|
|
172
|
+
border_style="cyan",
|
|
173
|
+
padding=(0, 1),
|
|
174
|
+
),
|
|
175
|
+
Rule(style="dim"),
|
|
176
|
+
]
|
|
177
|
+
|
|
178
|
+
# Add all messages
|
|
179
|
+
for idx, msg in enumerate(history):
|
|
180
|
+
output_items.append(_format_message(msg, idx))
|
|
181
|
+
|
|
182
|
+
# Display using rich pager
|
|
183
|
+
display_group = Group(*output_items)
|
|
184
|
+
|
|
185
|
+
# Use pager to display
|
|
186
|
+
with console.pager(styles=True):
|
|
187
|
+
console.print(display_group)
|