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.
Files changed (88) hide show
  1. kimi_cli/CHANGELOG.md +165 -0
  2. kimi_cli/__init__.py +0 -374
  3. kimi_cli/agents/{koder → default}/agent.yaml +1 -1
  4. kimi_cli/agents/{koder → default}/system.md +1 -1
  5. kimi_cli/agentspec.py +115 -0
  6. kimi_cli/app.py +208 -0
  7. kimi_cli/cli.py +321 -0
  8. kimi_cli/config.py +33 -16
  9. kimi_cli/constant.py +4 -0
  10. kimi_cli/exception.py +16 -0
  11. kimi_cli/llm.py +144 -3
  12. kimi_cli/metadata.py +6 -69
  13. kimi_cli/prompts/__init__.py +4 -0
  14. kimi_cli/session.py +103 -0
  15. kimi_cli/soul/__init__.py +130 -9
  16. kimi_cli/soul/agent.py +159 -0
  17. kimi_cli/soul/approval.py +5 -6
  18. kimi_cli/soul/compaction.py +106 -0
  19. kimi_cli/soul/context.py +1 -1
  20. kimi_cli/soul/kimisoul.py +180 -80
  21. kimi_cli/soul/message.py +6 -6
  22. kimi_cli/soul/runtime.py +96 -0
  23. kimi_cli/soul/toolset.py +3 -2
  24. kimi_cli/tools/__init__.py +35 -31
  25. kimi_cli/tools/bash/__init__.py +25 -9
  26. kimi_cli/tools/bash/cmd.md +31 -0
  27. kimi_cli/tools/dmail/__init__.py +5 -4
  28. kimi_cli/tools/file/__init__.py +8 -0
  29. kimi_cli/tools/file/glob.md +1 -1
  30. kimi_cli/tools/file/glob.py +4 -4
  31. kimi_cli/tools/file/grep.py +36 -19
  32. kimi_cli/tools/file/patch.py +52 -10
  33. kimi_cli/tools/file/read.py +6 -5
  34. kimi_cli/tools/file/replace.py +16 -4
  35. kimi_cli/tools/file/write.py +16 -4
  36. kimi_cli/tools/mcp.py +7 -4
  37. kimi_cli/tools/task/__init__.py +60 -41
  38. kimi_cli/tools/task/task.md +1 -1
  39. kimi_cli/tools/todo/__init__.py +4 -2
  40. kimi_cli/tools/utils.py +1 -1
  41. kimi_cli/tools/web/fetch.py +2 -1
  42. kimi_cli/tools/web/search.py +13 -12
  43. kimi_cli/ui/__init__.py +0 -68
  44. kimi_cli/ui/acp/__init__.py +67 -38
  45. kimi_cli/ui/print/__init__.py +46 -69
  46. kimi_cli/ui/shell/__init__.py +145 -154
  47. kimi_cli/ui/shell/console.py +27 -1
  48. kimi_cli/ui/shell/debug.py +187 -0
  49. kimi_cli/ui/shell/keyboard.py +183 -0
  50. kimi_cli/ui/shell/metacmd.py +34 -81
  51. kimi_cli/ui/shell/prompt.py +245 -28
  52. kimi_cli/ui/shell/replay.py +104 -0
  53. kimi_cli/ui/shell/setup.py +19 -19
  54. kimi_cli/ui/shell/update.py +11 -5
  55. kimi_cli/ui/shell/visualize.py +576 -0
  56. kimi_cli/ui/wire/README.md +109 -0
  57. kimi_cli/ui/wire/__init__.py +340 -0
  58. kimi_cli/ui/wire/jsonrpc.py +48 -0
  59. kimi_cli/utils/__init__.py +0 -0
  60. kimi_cli/utils/aiohttp.py +10 -0
  61. kimi_cli/utils/changelog.py +6 -2
  62. kimi_cli/utils/clipboard.py +10 -0
  63. kimi_cli/utils/message.py +15 -1
  64. kimi_cli/utils/rich/__init__.py +33 -0
  65. kimi_cli/utils/rich/markdown.py +959 -0
  66. kimi_cli/utils/rich/markdown_sample.md +108 -0
  67. kimi_cli/utils/rich/markdown_sample_short.md +2 -0
  68. kimi_cli/utils/signals.py +41 -0
  69. kimi_cli/utils/string.py +8 -0
  70. kimi_cli/utils/term.py +114 -0
  71. kimi_cli/wire/__init__.py +73 -0
  72. kimi_cli/wire/message.py +191 -0
  73. kimi_cli-0.52.dist-info/METADATA +186 -0
  74. kimi_cli-0.52.dist-info/RECORD +99 -0
  75. kimi_cli-0.52.dist-info/entry_points.txt +3 -0
  76. kimi_cli/agent.py +0 -261
  77. kimi_cli/agents/koder/README.md +0 -3
  78. kimi_cli/prompts/metacmds/__init__.py +0 -4
  79. kimi_cli/soul/wire.py +0 -101
  80. kimi_cli/ui/shell/liveview.py +0 -158
  81. kimi_cli/utils/provider.py +0 -64
  82. kimi_cli-0.35.dist-info/METADATA +0 -24
  83. kimi_cli-0.35.dist-info/RECORD +0 -76
  84. kimi_cli-0.35.dist-info/entry_points.txt +0 -3
  85. /kimi_cli/agents/{koder → default}/sub.yaml +0 -0
  86. /kimi_cli/prompts/{metacmds/compact.md → compact.md} +0 -0
  87. /kimi_cli/prompts/{metacmds/init.md → init.md} +0 -0
  88. {kimi_cli-0.35.dist-info → kimi_cli-0.52.dist-info}/WHEEL +0 -0
@@ -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.tooling import ToolResult
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.update import UpdateResult, do_update
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: dict[str, str] | None = None):
28
+ def __init__(self, soul: Soul, welcome_info: list["WelcomeInfoItem"] | None = None):
41
29
  self.soul = soul
42
- self.welcome_info = welcome_info or {}
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._run(command)
37
+ return await self._run_soul_command(command)
38
+
39
+ self._start_background_task(self._auto_update())
50
40
 
51
- self._start_auto_update_task()
41
+ _print_welcome_info(self.soul.name or "Kimi CLI", self._welcome_info)
52
42
 
53
- _print_welcome_info(self.soul.name or "Kimi CLI", self.soul.model, self.welcome_info)
43
+ if isinstance(self.soul, KimiSoul):
44
+ await replay_recent_history(self.soul.context.history)
54
45
 
55
- with CustomPromptSession(lambda: self.soul.status) as prompt_session:
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
- command = user_input.command
83
- if command.startswith("/"):
84
- logger.debug("Running meta command: {command}", command=command)
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("Running agent command: {command}", command=command)
89
- await self._run(command)
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
- loop.remove_signal_handler(signal.SIGINT)
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 _run(self, command: str) -> bool:
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.add_signal_handler(signal.SIGINT, _handler)
176
+ remove_sigint = install_sigint_handler(loop, _handler)
133
177
 
134
178
  try:
135
- await run_soul(self.soul, command, self._visualize, cancel_event)
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
- loop.remove_signal_handler(signal.SIGINT)
226
+ remove_sigint()
165
227
  return False
166
228
 
167
- def _start_auto_update_task(self) -> None:
168
- self._add_background_task(self._auto_update_background())
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("new version found, run `uv tool upgrade ikimi` to upgrade", duration=30.0)
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 _add_background_task(self, coro: Coroutine[Any, Any, Any]) -> asyncio.Task[Any]:
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
- def _print_welcome_info(name: str, model: str, info_items: dict[str, str]) -> None:
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
- rows.extend(
308
- Text.from_markup(f"[grey50]{key}: {value}[/grey50]") for key, value in info_items.items()
309
- )
310
- if model:
311
- rows.append(Text.from_markup(f"[grey50]Model: {model}[/grey50]"))
312
- else:
313
- rows.append(
314
- Text.from_markup(
315
- "[grey50]Model:[/grey50] [yellow]not set, send /setup to configure[/yellow]"
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(
@@ -1,3 +1,29 @@
1
1
  from rich.console import Console
2
+ from rich.theme import Theme
2
3
 
3
- console = Console(highlight=False)
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)