klaude-code 1.2.22__py3-none-any.whl → 1.2.24__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.
- klaude_code/command/prompt-jj-describe.md +32 -0
- klaude_code/command/status_cmd.py +1 -1
- klaude_code/{const/__init__.py → const.py} +11 -2
- klaude_code/core/executor.py +1 -1
- klaude_code/core/manager/sub_agent_manager.py +1 -1
- klaude_code/core/reminders.py +51 -0
- klaude_code/core/task.py +37 -18
- klaude_code/core/tool/__init__.py +1 -4
- klaude_code/core/tool/file/read_tool.py +23 -1
- klaude_code/core/tool/file/write_tool.py +7 -3
- klaude_code/core/tool/skill/__init__.py +0 -0
- klaude_code/core/tool/{memory → skill}/skill_tool.py +16 -39
- klaude_code/llm/openai_compatible/client.py +29 -102
- klaude_code/llm/openai_compatible/stream.py +272 -0
- klaude_code/llm/openrouter/client.py +29 -109
- klaude_code/llm/openrouter/{reasoning_handler.py → reasoning.py} +24 -2
- klaude_code/protocol/model.py +15 -2
- klaude_code/session/export.py +1 -1
- klaude_code/session/store.py +4 -2
- klaude_code/skill/__init__.py +27 -0
- klaude_code/skill/assets/deslop/SKILL.md +17 -0
- klaude_code/skill/assets/dev-docs/SKILL.md +108 -0
- klaude_code/skill/assets/handoff/SKILL.md +39 -0
- klaude_code/skill/assets/jj-workspace/SKILL.md +20 -0
- klaude_code/skill/assets/skill-creator/SKILL.md +139 -0
- klaude_code/{core/tool/memory/skill_loader.py → skill/loader.py} +60 -24
- klaude_code/skill/manager.py +70 -0
- klaude_code/skill/system_skills.py +192 -0
- klaude_code/ui/core/stage_manager.py +0 -3
- klaude_code/ui/modes/repl/completers.py +103 -3
- klaude_code/ui/modes/repl/event_handler.py +101 -49
- klaude_code/ui/modes/repl/input_prompt_toolkit.py +55 -6
- klaude_code/ui/modes/repl/renderer.py +24 -17
- klaude_code/ui/renderers/assistant.py +7 -2
- klaude_code/ui/renderers/developer.py +12 -0
- klaude_code/ui/renderers/diffs.py +1 -1
- klaude_code/ui/renderers/metadata.py +6 -8
- klaude_code/ui/renderers/sub_agent.py +28 -5
- klaude_code/ui/renderers/thinking.py +16 -10
- klaude_code/ui/renderers/tools.py +83 -34
- klaude_code/ui/renderers/user_input.py +32 -2
- klaude_code/ui/rich/markdown.py +40 -20
- klaude_code/ui/rich/status.py +15 -19
- klaude_code/ui/rich/theme.py +70 -17
- {klaude_code-1.2.22.dist-info → klaude_code-1.2.24.dist-info}/METADATA +18 -13
- {klaude_code-1.2.22.dist-info → klaude_code-1.2.24.dist-info}/RECORD +49 -45
- klaude_code/command/prompt-deslop.md +0 -14
- klaude_code/command/prompt-dev-docs-update.md +0 -56
- klaude_code/command/prompt-dev-docs.md +0 -46
- klaude_code/command/prompt-handoff.md +0 -33
- klaude_code/command/prompt-jj-workspace.md +0 -18
- klaude_code/core/tool/memory/__init__.py +0 -5
- klaude_code/llm/openai_compatible/stream_processor.py +0 -83
- /klaude_code/core/tool/{memory → skill}/skill_tool.md +0 -0
- {klaude_code-1.2.22.dist-info → klaude_code-1.2.24.dist-info}/WHEEL +0 -0
- {klaude_code-1.2.22.dist-info → klaude_code-1.2.24.dist-info}/entry_points.txt +0 -0
|
@@ -2,8 +2,10 @@ import json
|
|
|
2
2
|
from pathlib import Path
|
|
3
3
|
from typing import Any, cast
|
|
4
4
|
|
|
5
|
+
from rich import box
|
|
5
6
|
from rich.console import Group, RenderableType
|
|
6
7
|
from rich.padding import Padding
|
|
8
|
+
from rich.panel import Panel
|
|
7
9
|
from rich.text import Text
|
|
8
10
|
|
|
9
11
|
from klaude_code import const
|
|
@@ -11,8 +13,27 @@ from klaude_code.protocol import events, model, tools
|
|
|
11
13
|
from klaude_code.protocol.sub_agent import is_sub_agent_tool as _is_sub_agent_tool
|
|
12
14
|
from klaude_code.ui.renderers import diffs as r_diffs
|
|
13
15
|
from klaude_code.ui.renderers.common import create_grid, truncate_display
|
|
16
|
+
from klaude_code.ui.rich.markdown import NoInsetMarkdown
|
|
14
17
|
from klaude_code.ui.rich.theme import ThemeKey
|
|
15
18
|
|
|
19
|
+
# Tool markers (Unicode symbols for UI display)
|
|
20
|
+
MARK_GENERIC = "⚒"
|
|
21
|
+
MARK_BASH = "→"
|
|
22
|
+
MARK_PLAN = "▪"
|
|
23
|
+
MARK_READ = "←"
|
|
24
|
+
MARK_EDIT = "±"
|
|
25
|
+
MARK_WRITE = "+"
|
|
26
|
+
MARK_MERMAID = "⧉"
|
|
27
|
+
MARK_WEB_FETCH = "←"
|
|
28
|
+
MARK_WEB_SEARCH = ""
|
|
29
|
+
MARK_DONE = "✔"
|
|
30
|
+
MARK_SKILL = "✪"
|
|
31
|
+
|
|
32
|
+
# Todo status markers
|
|
33
|
+
MARK_TODO_PENDING = "▢"
|
|
34
|
+
MARK_TODO_IN_PROGRESS = "◉"
|
|
35
|
+
MARK_TODO_COMPLETED = "✔"
|
|
36
|
+
|
|
16
37
|
|
|
17
38
|
def is_sub_agent_tool(tool_name: str) -> bool:
|
|
18
39
|
return _is_sub_agent_tool(tool_name)
|
|
@@ -30,7 +51,7 @@ def render_path(path: str, style: str, is_directory: bool = False) -> Text:
|
|
|
30
51
|
return Text(path, style=style)
|
|
31
52
|
|
|
32
53
|
|
|
33
|
-
def render_generic_tool_call(tool_name: str, arguments: str, markup: str =
|
|
54
|
+
def render_generic_tool_call(tool_name: str, arguments: str, markup: str = MARK_GENERIC) -> RenderableType:
|
|
34
55
|
grid = create_grid()
|
|
35
56
|
|
|
36
57
|
tool_name_column = Text.assemble((markup, ThemeKey.TOOL_MARK), " ", (tool_name, ThemeKey.TOOL_NAME))
|
|
@@ -60,7 +81,7 @@ def render_generic_tool_call(tool_name: str, arguments: str, markup: str = "•"
|
|
|
60
81
|
|
|
61
82
|
def render_bash_tool_call(arguments: str) -> RenderableType:
|
|
62
83
|
grid = create_grid()
|
|
63
|
-
tool_name_column = Text.assemble((
|
|
84
|
+
tool_name_column = Text.assemble((MARK_BASH, ThemeKey.TOOL_MARK), " ", ("Bash", ThemeKey.TOOL_NAME))
|
|
64
85
|
|
|
65
86
|
try:
|
|
66
87
|
payload_raw: Any = json.loads(arguments) if arguments else {}
|
|
@@ -103,7 +124,7 @@ def render_bash_tool_call(arguments: str) -> RenderableType:
|
|
|
103
124
|
|
|
104
125
|
def render_update_plan_tool_call(arguments: str) -> RenderableType:
|
|
105
126
|
grid = create_grid()
|
|
106
|
-
tool_name_column = Text.assemble((
|
|
127
|
+
tool_name_column = Text.assemble((MARK_PLAN, ThemeKey.TOOL_MARK), " ", ("Update Plan", ThemeKey.TOOL_NAME))
|
|
107
128
|
explanation_column = Text("")
|
|
108
129
|
|
|
109
130
|
if arguments:
|
|
@@ -160,13 +181,13 @@ def render_read_tool_call(arguments: str) -> RenderableType:
|
|
|
160
181
|
style=ThemeKey.INVALID_TOOL_CALL_ARGS,
|
|
161
182
|
)
|
|
162
183
|
)
|
|
163
|
-
grid.add_row(Text(
|
|
184
|
+
grid.add_row(Text(MARK_READ, ThemeKey.TOOL_MARK), render_result)
|
|
164
185
|
return grid
|
|
165
186
|
|
|
166
187
|
|
|
167
188
|
def render_edit_tool_call(arguments: str) -> RenderableType:
|
|
168
189
|
grid = create_grid()
|
|
169
|
-
tool_name_column = Text.assemble((
|
|
190
|
+
tool_name_column = Text.assemble((MARK_EDIT, ThemeKey.TOOL_MARK), " ", ("Edit", ThemeKey.TOOL_NAME))
|
|
170
191
|
try:
|
|
171
192
|
json_dict = json.loads(arguments)
|
|
172
193
|
file_path = json_dict.get("file_path")
|
|
@@ -185,10 +206,10 @@ def render_write_tool_call(arguments: str) -> RenderableType:
|
|
|
185
206
|
try:
|
|
186
207
|
json_dict = json.loads(arguments)
|
|
187
208
|
file_path = json_dict.get("file_path")
|
|
188
|
-
tool_name_column = Text.assemble((
|
|
209
|
+
tool_name_column = Text.assemble((MARK_WRITE, ThemeKey.TOOL_MARK), " ", ("Write", ThemeKey.TOOL_NAME))
|
|
189
210
|
arguments_column = render_path(file_path, ThemeKey.TOOL_PARAM_FILE_PATH)
|
|
190
211
|
except json.JSONDecodeError:
|
|
191
|
-
tool_name_column = Text.assemble((
|
|
212
|
+
tool_name_column = Text.assemble((MARK_WRITE, ThemeKey.TOOL_MARK), " ", ("Write", ThemeKey.TOOL_NAME))
|
|
192
213
|
arguments_column = Text(
|
|
193
214
|
arguments.strip()[: const.INVALID_TOOL_CALL_MAX_LENGTH],
|
|
194
215
|
style=ThemeKey.INVALID_TOOL_CALL_ARGS,
|
|
@@ -199,7 +220,7 @@ def render_write_tool_call(arguments: str) -> RenderableType:
|
|
|
199
220
|
|
|
200
221
|
def render_apply_patch_tool_call(arguments: str) -> RenderableType:
|
|
201
222
|
grid = create_grid()
|
|
202
|
-
tool_name_column = Text.assemble((
|
|
223
|
+
tool_name_column = Text.assemble((MARK_EDIT, ThemeKey.TOOL_MARK), " ", ("Apply Patch", ThemeKey.TOOL_NAME))
|
|
203
224
|
|
|
204
225
|
try:
|
|
205
226
|
payload = json.loads(arguments)
|
|
@@ -215,9 +236,27 @@ def render_apply_patch_tool_call(arguments: str) -> RenderableType:
|
|
|
215
236
|
arguments_column = Text("", ThemeKey.TOOL_PARAM)
|
|
216
237
|
|
|
217
238
|
if isinstance(patch_content, str):
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
239
|
+
update_count = 0
|
|
240
|
+
add_count = 0
|
|
241
|
+
delete_count = 0
|
|
242
|
+
for line in patch_content.splitlines():
|
|
243
|
+
if line.startswith("*** Update File:"):
|
|
244
|
+
update_count += 1
|
|
245
|
+
elif line.startswith("*** Add File:"):
|
|
246
|
+
add_count += 1
|
|
247
|
+
elif line.startswith("*** Delete File:"):
|
|
248
|
+
delete_count += 1
|
|
249
|
+
|
|
250
|
+
parts: list[str] = []
|
|
251
|
+
if update_count > 0:
|
|
252
|
+
parts.append(f"Update File × {update_count}" if update_count > 1 else "Update File")
|
|
253
|
+
if add_count > 0:
|
|
254
|
+
parts.append(f"Add File × {add_count}" if add_count > 1 else "Add File")
|
|
255
|
+
if delete_count > 0:
|
|
256
|
+
parts.append(f"Delete File × {delete_count}" if delete_count > 1 else "Delete File")
|
|
257
|
+
|
|
258
|
+
if parts:
|
|
259
|
+
arguments_column = Text(", ".join(parts), ThemeKey.TOOL_PARAM)
|
|
221
260
|
else:
|
|
222
261
|
arguments_column = Text(
|
|
223
262
|
str(patch_content)[: const.INVALID_TOOL_CALL_MAX_LENGTH],
|
|
@@ -229,34 +268,24 @@ def render_apply_patch_tool_call(arguments: str) -> RenderableType:
|
|
|
229
268
|
|
|
230
269
|
|
|
231
270
|
def render_todo(tr: events.ToolResultEvent) -> RenderableType:
|
|
232
|
-
|
|
233
|
-
return Text.assemble(
|
|
234
|
-
(" ✘", ThemeKey.ERROR_BOLD),
|
|
235
|
-
" ",
|
|
236
|
-
Text("(no content)" if tr.ui_extra is None else "(invalid ui_extra)", style=ThemeKey.ERROR),
|
|
237
|
-
)
|
|
238
|
-
|
|
271
|
+
assert isinstance(tr.ui_extra, model.TodoListUIExtra)
|
|
239
272
|
ui_extra = tr.ui_extra.todo_list
|
|
240
273
|
todo_grid = create_grid()
|
|
241
274
|
for todo in ui_extra.todos:
|
|
242
275
|
is_new_completed = todo.content in ui_extra.new_completed
|
|
243
276
|
match todo.status:
|
|
244
277
|
case "pending":
|
|
245
|
-
mark =
|
|
278
|
+
mark = MARK_TODO_PENDING
|
|
246
279
|
mark_style = ThemeKey.TODO_PENDING_MARK
|
|
247
280
|
text_style = ThemeKey.TODO_PENDING
|
|
248
281
|
case "in_progress":
|
|
249
|
-
mark =
|
|
282
|
+
mark = MARK_TODO_IN_PROGRESS
|
|
250
283
|
mark_style = ThemeKey.TODO_IN_PROGRESS_MARK
|
|
251
284
|
text_style = ThemeKey.TODO_IN_PROGRESS
|
|
252
285
|
case "completed":
|
|
253
|
-
mark =
|
|
286
|
+
mark = MARK_TODO_COMPLETED
|
|
254
287
|
mark_style = ThemeKey.TODO_NEW_COMPLETED_MARK if is_new_completed else ThemeKey.TODO_COMPLETED_MARK
|
|
255
288
|
text_style = ThemeKey.TODO_NEW_COMPLETED if is_new_completed else ThemeKey.TODO_COMPLETED
|
|
256
|
-
case _:
|
|
257
|
-
mark = "?"
|
|
258
|
-
mark_style = ThemeKey.TODO_PENDING_MARK
|
|
259
|
-
text_style = ThemeKey.TODO_PENDING
|
|
260
289
|
text = Text(todo.content)
|
|
261
290
|
text.stylize(text_style)
|
|
262
291
|
todo_grid.add_row(Text(mark, style=mark_style), text)
|
|
@@ -280,7 +309,7 @@ def _extract_mermaid_link(
|
|
|
280
309
|
|
|
281
310
|
def render_mermaid_tool_call(arguments: str) -> RenderableType:
|
|
282
311
|
grid = create_grid()
|
|
283
|
-
tool_name_column = Text.assemble((
|
|
312
|
+
tool_name_column = Text.assemble((MARK_MERMAID, ThemeKey.TOOL_MARK), " ", ("Mermaid", ThemeKey.TOOL_NAME))
|
|
284
313
|
summary = Text("", ThemeKey.TOOL_PARAM)
|
|
285
314
|
|
|
286
315
|
try:
|
|
@@ -320,7 +349,7 @@ def _truncate_url(url: str, max_length: int = 400) -> str:
|
|
|
320
349
|
|
|
321
350
|
def render_web_fetch_tool_call(arguments: str) -> RenderableType:
|
|
322
351
|
grid = create_grid()
|
|
323
|
-
tool_name_column = Text.assemble((
|
|
352
|
+
tool_name_column = Text.assemble((MARK_WEB_FETCH, ThemeKey.TOOL_MARK), " ", ("Fetch", ThemeKey.TOOL_NAME))
|
|
324
353
|
|
|
325
354
|
try:
|
|
326
355
|
payload: dict[str, str] = json.loads(arguments)
|
|
@@ -341,7 +370,7 @@ def render_web_fetch_tool_call(arguments: str) -> RenderableType:
|
|
|
341
370
|
|
|
342
371
|
def render_web_search_tool_call(arguments: str) -> RenderableType:
|
|
343
372
|
grid = create_grid()
|
|
344
|
-
tool_name_column = Text.assemble((
|
|
373
|
+
tool_name_column = Text.assemble((MARK_WEB_SEARCH, ThemeKey.TOOL_MARK), " ", ("Web Search", ThemeKey.TOOL_NAME))
|
|
345
374
|
|
|
346
375
|
try:
|
|
347
376
|
payload: dict[str, Any] = json.loads(arguments)
|
|
@@ -418,7 +447,7 @@ def get_truncation_info(tr: events.ToolResultEvent) -> model.TruncationUIExtra |
|
|
|
418
447
|
|
|
419
448
|
def render_report_back_tool_call() -> RenderableType:
|
|
420
449
|
grid = create_grid()
|
|
421
|
-
tool_name_column = Text.assemble((
|
|
450
|
+
tool_name_column = Text.assemble((MARK_DONE, ThemeKey.TOOL_MARK), " ", ("Report Back", ThemeKey.TOOL_NAME))
|
|
422
451
|
grid.add_row(tool_name_column, "")
|
|
423
452
|
return grid
|
|
424
453
|
|
|
@@ -474,19 +503,18 @@ def render_tool_call(e: events.ToolCallEvent) -> RenderableType | None:
|
|
|
474
503
|
return render_edit_tool_call(e.arguments)
|
|
475
504
|
case tools.WRITE:
|
|
476
505
|
return render_write_tool_call(e.arguments)
|
|
477
|
-
|
|
478
506
|
case tools.BASH:
|
|
479
507
|
return render_bash_tool_call(e.arguments)
|
|
480
508
|
case tools.APPLY_PATCH:
|
|
481
509
|
return render_apply_patch_tool_call(e.arguments)
|
|
482
510
|
case tools.TODO_WRITE:
|
|
483
|
-
return render_generic_tool_call("Update Todos", "",
|
|
511
|
+
return render_generic_tool_call("Update Todos", "", MARK_PLAN)
|
|
484
512
|
case tools.UPDATE_PLAN:
|
|
485
513
|
return render_update_plan_tool_call(e.arguments)
|
|
486
514
|
case tools.MERMAID:
|
|
487
515
|
return render_mermaid_tool_call(e.arguments)
|
|
488
516
|
case tools.SKILL:
|
|
489
|
-
return render_generic_tool_call(e.tool_name, e.arguments,
|
|
517
|
+
return render_generic_tool_call(e.tool_name, e.arguments, MARK_SKILL)
|
|
490
518
|
case tools.REPORT_BACK:
|
|
491
519
|
return render_report_back_tool_call()
|
|
492
520
|
case tools.WEB_FETCH:
|
|
@@ -503,7 +531,23 @@ def _extract_diff(ui_extra: model.ToolResultUIExtra | None) -> model.DiffUIExtra
|
|
|
503
531
|
return None
|
|
504
532
|
|
|
505
533
|
|
|
506
|
-
def
|
|
534
|
+
def _extract_markdown_doc(ui_extra: model.ToolResultUIExtra | None) -> model.MarkdownDocUIExtra | None:
|
|
535
|
+
if isinstance(ui_extra, model.MarkdownDocUIExtra):
|
|
536
|
+
return ui_extra
|
|
537
|
+
return None
|
|
538
|
+
|
|
539
|
+
|
|
540
|
+
def render_markdown_doc(md_ui: model.MarkdownDocUIExtra, *, code_theme: str) -> RenderableType:
|
|
541
|
+
"""Render markdown document content in a panel."""
|
|
542
|
+
return Panel.fit(
|
|
543
|
+
NoInsetMarkdown(md_ui.content, code_theme=code_theme),
|
|
544
|
+
box=box.SIMPLE,
|
|
545
|
+
border_style=ThemeKey.LINES,
|
|
546
|
+
style=ThemeKey.WRITE_MARKDOWN_PANEL,
|
|
547
|
+
)
|
|
548
|
+
|
|
549
|
+
|
|
550
|
+
def render_tool_result(e: events.ToolResultEvent, *, code_theme: str = "monokai") -> RenderableType | None:
|
|
507
551
|
"""Unified entry point for rendering tool results.
|
|
508
552
|
|
|
509
553
|
Returns a Rich Renderable or None if the tool result should not be rendered.
|
|
@@ -524,11 +568,16 @@ def render_tool_result(e: events.ToolResultEvent) -> RenderableType | None:
|
|
|
524
568
|
return Group(render_truncation_info(truncation_info), render_generic_tool_result(e.result))
|
|
525
569
|
|
|
526
570
|
diff_ui = _extract_diff(e.ui_extra)
|
|
571
|
+
md_ui = _extract_markdown_doc(e.ui_extra)
|
|
527
572
|
|
|
528
573
|
match e.tool_name:
|
|
529
574
|
case tools.READ:
|
|
530
575
|
return None
|
|
531
|
-
case tools.EDIT
|
|
576
|
+
case tools.EDIT:
|
|
577
|
+
return Padding.indent(r_diffs.render_structured_diff(diff_ui) if diff_ui else Text(""), level=2)
|
|
578
|
+
case tools.WRITE:
|
|
579
|
+
if md_ui:
|
|
580
|
+
return Padding.indent(render_markdown_doc(md_ui, code_theme=code_theme), level=2)
|
|
532
581
|
return Padding.indent(r_diffs.render_structured_diff(diff_ui) if diff_ui else Text(""), level=2)
|
|
533
582
|
case tools.APPLY_PATCH:
|
|
534
583
|
if diff_ui:
|
|
@@ -4,6 +4,7 @@ from rich.console import Group, RenderableType
|
|
|
4
4
|
from rich.text import Text
|
|
5
5
|
|
|
6
6
|
from klaude_code.command import is_slash_command_name
|
|
7
|
+
from klaude_code.skill import get_available_skills
|
|
7
8
|
from klaude_code.ui.renderers.common import create_grid
|
|
8
9
|
from klaude_code.ui.rich.theme import ThemeKey
|
|
9
10
|
|
|
@@ -12,6 +13,11 @@ from klaude_code.ui.rich.theme import ThemeKey
|
|
|
12
13
|
# patterns such as foo@bar.com as file references.
|
|
13
14
|
AT_FILE_RENDER_PATTERN = re.compile(r'(?<!\S)@("([^"]+)"|\S+)')
|
|
14
15
|
|
|
16
|
+
# Match $skill or ¥skill pattern at the beginning of the first line
|
|
17
|
+
SKILL_RENDER_PATTERN = re.compile(r"^[$¥](\S+)")
|
|
18
|
+
|
|
19
|
+
USER_MESSAGE_MARK = "❯ "
|
|
20
|
+
|
|
15
21
|
|
|
16
22
|
def render_at_pattern(
|
|
17
23
|
text: str,
|
|
@@ -38,15 +44,24 @@ def render_at_pattern(
|
|
|
38
44
|
return result
|
|
39
45
|
|
|
40
46
|
|
|
47
|
+
def _is_valid_skill_name(name: str) -> bool:
|
|
48
|
+
"""Check if a skill name is valid (exists in loaded skills)."""
|
|
49
|
+
short = name.split(":")[-1] if ":" in name else name
|
|
50
|
+
available_skills = get_available_skills()
|
|
51
|
+
return any(skill_name in (name, short) for skill_name, _, _ in available_skills)
|
|
52
|
+
|
|
53
|
+
|
|
41
54
|
def render_user_input(content: str) -> RenderableType:
|
|
42
55
|
"""Render a user message as a group of quoted lines with styles.
|
|
43
56
|
|
|
44
57
|
- Highlights slash command on the first line if recognized
|
|
58
|
+
- Highlights $skill pattern on the first line if recognized
|
|
45
59
|
- Highlights @file patterns in all lines
|
|
46
60
|
"""
|
|
47
61
|
lines = content.strip().split("\n")
|
|
48
62
|
renderables: list[RenderableType] = []
|
|
49
63
|
has_command = False
|
|
64
|
+
command_style: str | None = None
|
|
50
65
|
for i, line in enumerate(lines):
|
|
51
66
|
line_text = render_at_pattern(line)
|
|
52
67
|
|
|
@@ -54,6 +69,7 @@ def render_user_input(content: str) -> RenderableType:
|
|
|
54
69
|
splits = line.split(" ", maxsplit=1)
|
|
55
70
|
if is_slash_command_name(splits[0][1:]):
|
|
56
71
|
has_command = True
|
|
72
|
+
command_style = ThemeKey.USER_INPUT_SLASH_COMMAND
|
|
57
73
|
line_text = Text.assemble(
|
|
58
74
|
(f"{splits[0]}", ThemeKey.USER_INPUT_SLASH_COMMAND),
|
|
59
75
|
" ",
|
|
@@ -62,13 +78,27 @@ def render_user_input(content: str) -> RenderableType:
|
|
|
62
78
|
renderables.append(line_text)
|
|
63
79
|
continue
|
|
64
80
|
|
|
81
|
+
if i == 0 and (line.startswith("$") or line.startswith("¥")):
|
|
82
|
+
m = SKILL_RENDER_PATTERN.match(line)
|
|
83
|
+
if m and _is_valid_skill_name(m.group(1)):
|
|
84
|
+
has_command = True
|
|
85
|
+
command_style = ThemeKey.USER_INPUT_SKILL
|
|
86
|
+
skill_token = m.group(0) # e.g. "$skill-name"
|
|
87
|
+
rest = line[len(skill_token) :]
|
|
88
|
+
line_text = Text.assemble(
|
|
89
|
+
(skill_token, ThemeKey.USER_INPUT_SKILL),
|
|
90
|
+
render_at_pattern(rest) if rest else Text(""),
|
|
91
|
+
)
|
|
92
|
+
renderables.append(line_text)
|
|
93
|
+
continue
|
|
94
|
+
|
|
65
95
|
renderables.append(line_text)
|
|
66
96
|
grid = create_grid()
|
|
67
97
|
grid.padding = (0, 0)
|
|
68
98
|
mark = (
|
|
69
|
-
Text(
|
|
99
|
+
Text(USER_MESSAGE_MARK, style=ThemeKey.USER_INPUT_PROMPT)
|
|
70
100
|
if not has_command
|
|
71
|
-
else Text(" ", style=ThemeKey.USER_INPUT_SLASH_COMMAND)
|
|
101
|
+
else Text(" ", style=command_style or ThemeKey.USER_INPUT_SLASH_COMMAND)
|
|
72
102
|
)
|
|
73
103
|
grid.add_row(mark, Group(*renderables))
|
|
74
104
|
return grid
|
klaude_code/ui/rich/markdown.py
CHANGED
|
@@ -9,10 +9,10 @@ from typing import Any, ClassVar
|
|
|
9
9
|
|
|
10
10
|
from rich.console import Console, ConsoleOptions, Group, RenderableType, RenderResult
|
|
11
11
|
from rich.live import Live
|
|
12
|
-
from rich.markdown import CodeBlock, Heading,
|
|
12
|
+
from rich.markdown import CodeBlock, Heading, Markdown, MarkdownElement
|
|
13
13
|
from rich.rule import Rule
|
|
14
14
|
from rich.spinner import Spinner
|
|
15
|
-
from rich.style import Style
|
|
15
|
+
from rich.style import Style, StyleType
|
|
16
16
|
from rich.syntax import Syntax
|
|
17
17
|
from rich.text import Text
|
|
18
18
|
from rich.theme import Theme
|
|
@@ -45,12 +45,12 @@ class ThinkingCodeBlock(CodeBlock):
|
|
|
45
45
|
yield CodePanel(text, border_style="markdown.code.border")
|
|
46
46
|
|
|
47
47
|
|
|
48
|
-
class
|
|
48
|
+
class Divider(MarkdownElement):
|
|
49
49
|
"""A horizontal rule with an extra blank line below."""
|
|
50
50
|
|
|
51
51
|
def __rich_console__(self, console: Console, options: ConsoleOptions) -> RenderResult:
|
|
52
|
-
|
|
53
|
-
yield
|
|
52
|
+
style = console.get_style("markdown.hr", default="none")
|
|
53
|
+
yield Rule(style=style, characters="-")
|
|
54
54
|
|
|
55
55
|
|
|
56
56
|
class LeftHeading(Heading):
|
|
@@ -64,7 +64,7 @@ class LeftHeading(Heading):
|
|
|
64
64
|
yield h1_text
|
|
65
65
|
elif self.tag == "h2":
|
|
66
66
|
text.stylize(Style(bold=True, underline=False))
|
|
67
|
-
yield Rule(title=text, characters="
|
|
67
|
+
yield Rule(title=text, characters="·", style="markdown.h2.border", align="left")
|
|
68
68
|
else:
|
|
69
69
|
yield text
|
|
70
70
|
|
|
@@ -77,7 +77,7 @@ class NoInsetMarkdown(Markdown):
|
|
|
77
77
|
"fence": NoInsetCodeBlock,
|
|
78
78
|
"code_block": NoInsetCodeBlock,
|
|
79
79
|
"heading_open": LeftHeading,
|
|
80
|
-
"hr":
|
|
80
|
+
"hr": Divider,
|
|
81
81
|
}
|
|
82
82
|
|
|
83
83
|
|
|
@@ -89,7 +89,7 @@ class ThinkingMarkdown(Markdown):
|
|
|
89
89
|
"fence": ThinkingCodeBlock,
|
|
90
90
|
"code_block": ThinkingCodeBlock,
|
|
91
91
|
"heading_open": LeftHeading,
|
|
92
|
-
"hr":
|
|
92
|
+
"hr": Divider,
|
|
93
93
|
}
|
|
94
94
|
|
|
95
95
|
|
|
@@ -108,7 +108,9 @@ class MarkdownStream:
|
|
|
108
108
|
console: Console | None = None,
|
|
109
109
|
spinner: Spinner | None = None,
|
|
110
110
|
mark: str | None = None,
|
|
111
|
-
|
|
111
|
+
mark_style: StyleType | None = None,
|
|
112
|
+
left_margin: int = 0,
|
|
113
|
+
right_margin: int = const.MARKDOWN_RIGHT_MARGIN,
|
|
112
114
|
markdown_class: Callable[..., Markdown] | None = None,
|
|
113
115
|
) -> None:
|
|
114
116
|
"""Initialize the markdown stream.
|
|
@@ -117,8 +119,10 @@ class MarkdownStream:
|
|
|
117
119
|
mdargs (dict, optional): Additional arguments to pass to rich Markdown renderer
|
|
118
120
|
theme (Theme, optional): Theme for rendering markdown
|
|
119
121
|
console (Console, optional): External console to use for rendering
|
|
120
|
-
mark (str | None, optional): Marker shown before the first non-empty line when
|
|
121
|
-
|
|
122
|
+
mark (str | None, optional): Marker shown before the first non-empty line when left_margin >= 2
|
|
123
|
+
mark_style (StyleType | None, optional): Style to apply to the mark
|
|
124
|
+
left_margin (int, optional): Number of columns to reserve on the left side
|
|
125
|
+
right_margin (int, optional): Number of columns to reserve on the right side
|
|
122
126
|
markdown_class: Markdown class to use for rendering (defaults to NoInsetMarkdown)
|
|
123
127
|
"""
|
|
124
128
|
self.printed: list[str] = [] # Stores lines that have already been printed
|
|
@@ -140,7 +144,11 @@ class MarkdownStream:
|
|
|
140
144
|
self.console = console
|
|
141
145
|
self.spinner: Spinner | None = spinner
|
|
142
146
|
self.mark: str | None = mark
|
|
143
|
-
self.
|
|
147
|
+
self.mark_style: StyleType | None = mark_style
|
|
148
|
+
|
|
149
|
+
self.left_margin: int = max(left_margin, 0)
|
|
150
|
+
|
|
151
|
+
self.right_margin: int = max(right_margin, 0)
|
|
144
152
|
self.markdown_class: Callable[..., Markdown] = markdown_class or NoInsetMarkdown
|
|
145
153
|
|
|
146
154
|
@property
|
|
@@ -160,15 +168,15 @@ class MarkdownStream:
|
|
|
160
168
|
# Render the markdown to a string buffer
|
|
161
169
|
string_io = io.StringIO()
|
|
162
170
|
|
|
163
|
-
# Determine console width and adjust for left
|
|
164
|
-
# the rendered content plus
|
|
171
|
+
# Determine console width and adjust for left margin so that
|
|
172
|
+
# the rendered content plus margins does not exceed the available width.
|
|
165
173
|
if self.console is not None:
|
|
166
174
|
base_width = self.console.options.max_width
|
|
167
175
|
else:
|
|
168
176
|
probe_console = Console(theme=self.theme)
|
|
169
177
|
base_width = probe_console.options.max_width
|
|
170
178
|
|
|
171
|
-
effective_width = max(base_width - self.
|
|
179
|
+
effective_width = max(base_width - self.left_margin - self.right_margin, 1)
|
|
172
180
|
|
|
173
181
|
# Use external console for consistent theming, or create temporary one
|
|
174
182
|
temp_console = Console(
|
|
@@ -182,19 +190,31 @@ class MarkdownStream:
|
|
|
182
190
|
temp_console.print(markdown)
|
|
183
191
|
output = string_io.getvalue()
|
|
184
192
|
|
|
185
|
-
# Split rendered output into lines, strip trailing spaces, and apply left
|
|
193
|
+
# Split rendered output into lines, strip trailing spaces, and apply left margin.
|
|
186
194
|
lines = output.splitlines(keepends=True)
|
|
187
|
-
indent_prefix = " " * self.
|
|
195
|
+
indent_prefix = " " * self.left_margin if self.left_margin > 0 else ""
|
|
188
196
|
processed_lines: list[str] = []
|
|
189
197
|
mark_applied = False
|
|
190
|
-
use_mark = bool(self.mark) and self.
|
|
198
|
+
use_mark = bool(self.mark) and self.left_margin >= 2
|
|
199
|
+
|
|
200
|
+
# Pre-render styled mark if needed
|
|
201
|
+
styled_mark: str | None = None
|
|
202
|
+
if use_mark and self.mark:
|
|
203
|
+
if self.mark_style:
|
|
204
|
+
mark_text = Text(self.mark, style=self.mark_style)
|
|
205
|
+
mark_buffer = io.StringIO()
|
|
206
|
+
mark_console = Console(file=mark_buffer, force_terminal=True, theme=self.theme)
|
|
207
|
+
mark_console.print(mark_text, end="")
|
|
208
|
+
styled_mark = mark_buffer.getvalue()
|
|
209
|
+
else:
|
|
210
|
+
styled_mark = self.mark
|
|
191
211
|
|
|
192
212
|
for line in lines:
|
|
193
213
|
stripped = line.rstrip()
|
|
194
214
|
|
|
195
|
-
# Apply mark to the first non-empty line only when
|
|
215
|
+
# Apply mark to the first non-empty line only when left_margin is at least 2.
|
|
196
216
|
if use_mark and not mark_applied and stripped:
|
|
197
|
-
stripped = f"{
|
|
217
|
+
stripped = f"{styled_mark} {stripped}"
|
|
198
218
|
mark_applied = True
|
|
199
219
|
elif indent_prefix:
|
|
200
220
|
stripped = indent_prefix + stripped
|
klaude_code/ui/rich/status.py
CHANGED
|
@@ -22,18 +22,7 @@ BREATHING_SPINNER_NAME = "dots"
|
|
|
22
22
|
|
|
23
23
|
# Alternating glyphs for the breathing spinner - switches at each "transparent" point
|
|
24
24
|
_BREATHING_SPINNER_GLYPHS_BASE = [
|
|
25
|
-
"
|
|
26
|
-
"✶",
|
|
27
|
-
"✲",
|
|
28
|
-
"◆",
|
|
29
|
-
"❖",
|
|
30
|
-
"✧",
|
|
31
|
-
"❋",
|
|
32
|
-
"✸",
|
|
33
|
-
"✻",
|
|
34
|
-
"◇",
|
|
35
|
-
"✴",
|
|
36
|
-
"✷",
|
|
25
|
+
"◉",
|
|
37
26
|
]
|
|
38
27
|
|
|
39
28
|
# Shuffle glyphs on module load for variety across sessions
|
|
@@ -114,7 +103,6 @@ def _shimmer_style(console: Console, base_style: Style, intensity: float) -> Sty
|
|
|
114
103
|
|
|
115
104
|
base_r, base_g, base_b = base_triplet
|
|
116
105
|
bg_r, bg_g, bg_b = bg_triplet
|
|
117
|
-
|
|
118
106
|
r = int(bg_r * alpha + base_r * (1.0 - alpha))
|
|
119
107
|
g = int(bg_g * alpha + base_g * (1.0 - alpha))
|
|
120
108
|
b = int(bg_b * alpha + base_b * (1.0 - alpha))
|
|
@@ -181,9 +169,19 @@ class ShimmerStatusText:
|
|
|
181
169
|
Supports optional right-aligned text that stays fixed at the right edge.
|
|
182
170
|
"""
|
|
183
171
|
|
|
184
|
-
def __init__(
|
|
185
|
-
self
|
|
186
|
-
|
|
172
|
+
def __init__(
|
|
173
|
+
self,
|
|
174
|
+
main_text: str | Text,
|
|
175
|
+
right_text: Text | None = None,
|
|
176
|
+
main_style: ThemeKey = ThemeKey.STATUS_TEXT,
|
|
177
|
+
) -> None:
|
|
178
|
+
if isinstance(main_text, Text):
|
|
179
|
+
text = main_text.copy()
|
|
180
|
+
if not text.style:
|
|
181
|
+
text.style = str(main_style)
|
|
182
|
+
self._main_text = text
|
|
183
|
+
else:
|
|
184
|
+
self._main_text = Text(main_text, style=main_style)
|
|
187
185
|
self._hint_text = Text(const.STATUS_HINT_TEXT)
|
|
188
186
|
self._hint_style = ThemeKey.STATUS_HINT
|
|
189
187
|
self._right_text = right_text
|
|
@@ -205,13 +203,11 @@ class ShimmerStatusText:
|
|
|
205
203
|
def _render_left_text(self, console: Console) -> Text:
|
|
206
204
|
"""Render the left part with shimmer effect on main text only."""
|
|
207
205
|
result = Text()
|
|
208
|
-
main_style = console.get_style(str(self._main_style))
|
|
209
206
|
hint_style = console.get_style(str(self._hint_style))
|
|
210
207
|
|
|
211
208
|
# Apply shimmer only to main text
|
|
212
209
|
for index, (ch, intensity) in enumerate(_shimmer_profile(self._main_text.plain)):
|
|
213
|
-
|
|
214
|
-
base_style = main_style + char_style
|
|
210
|
+
base_style = self._main_text.get_style_at_offset(console, index)
|
|
215
211
|
style = _shimmer_style(console, base_style, intensity)
|
|
216
212
|
result.append(ch, style=style)
|
|
217
213
|
|