klaude-code 1.2.21__py3-none-any.whl → 1.2.23__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/cli/debug.py +8 -10
- klaude_code/command/__init__.py +0 -3
- klaude_code/command/status_cmd.py +1 -1
- klaude_code/const/__init__.py +10 -7
- klaude_code/core/manager/sub_agent_manager.py +1 -1
- klaude_code/core/prompt.py +5 -2
- klaude_code/core/prompts/prompt-codex-gpt-5-2-codex.md +117 -0
- klaude_code/core/prompts/{prompt-codex-gpt-5-1.md → prompt-codex.md} +9 -42
- klaude_code/core/reminders.py +87 -2
- klaude_code/core/task.py +37 -18
- klaude_code/core/tool/__init__.py +1 -9
- klaude_code/core/tool/file/_utils.py +6 -0
- klaude_code/core/tool/file/apply_patch_tool.py +30 -72
- klaude_code/core/tool/file/diff_builder.py +151 -0
- klaude_code/core/tool/file/edit_tool.py +35 -18
- klaude_code/core/tool/file/read_tool.py +45 -86
- klaude_code/core/tool/file/write_tool.py +40 -30
- klaude_code/core/tool/shell/bash_tool.py +147 -0
- klaude_code/core/tool/skill/__init__.py +0 -0
- klaude_code/core/tool/{memory → skill}/skill_tool.py +16 -39
- klaude_code/protocol/commands.py +0 -1
- klaude_code/protocol/model.py +31 -11
- klaude_code/protocol/tools.py +1 -2
- klaude_code/session/export.py +76 -21
- klaude_code/session/store.py +4 -2
- klaude_code/session/templates/export_session.html +28 -0
- 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/modes/repl/completers.py +103 -3
- klaude_code/ui/modes/repl/event_handler.py +7 -3
- klaude_code/ui/modes/repl/input_prompt_toolkit.py +42 -3
- klaude_code/ui/renderers/assistant.py +7 -2
- klaude_code/ui/renderers/common.py +26 -11
- klaude_code/ui/renderers/developer.py +12 -5
- klaude_code/ui/renderers/diffs.py +85 -1
- klaude_code/ui/renderers/metadata.py +4 -2
- klaude_code/ui/renderers/thinking.py +1 -1
- klaude_code/ui/renderers/tools.py +75 -129
- klaude_code/ui/renderers/user_input.py +32 -2
- klaude_code/ui/rich/markdown.py +27 -12
- klaude_code/ui/rich/status.py +9 -24
- klaude_code/ui/rich/theme.py +17 -5
- {klaude_code-1.2.21.dist-info → klaude_code-1.2.23.dist-info}/METADATA +19 -13
- {klaude_code-1.2.21.dist-info → klaude_code-1.2.23.dist-info}/RECORD +54 -54
- klaude_code/command/diff_cmd.py +0 -136
- 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/file/multi_edit_tool.md +0 -42
- klaude_code/core/tool/file/multi_edit_tool.py +0 -175
- klaude_code/core/tool/memory/__init__.py +0 -5
- klaude_code/core/tool/memory/memory_tool.md +0 -20
- klaude_code/core/tool/memory/memory_tool.py +0 -456
- /klaude_code/core/tool/{memory → skill}/skill_tool.md +0 -0
- {klaude_code-1.2.21.dist-info → klaude_code-1.2.23.dist-info}/WHEEL +0 -0
- {klaude_code-1.2.21.dist-info → klaude_code-1.2.23.dist-info}/entry_points.txt +0 -0
|
@@ -13,6 +13,24 @@ from klaude_code.ui.renderers import diffs as r_diffs
|
|
|
13
13
|
from klaude_code.ui.renderers.common import create_grid, truncate_display
|
|
14
14
|
from klaude_code.ui.rich.theme import ThemeKey
|
|
15
15
|
|
|
16
|
+
# Tool markers (Unicode symbols for UI display)
|
|
17
|
+
MARK_GENERIC = "⚒"
|
|
18
|
+
MARK_BASH = "→"
|
|
19
|
+
MARK_PLAN = "▪"
|
|
20
|
+
MARK_READ = "←"
|
|
21
|
+
MARK_EDIT = "±"
|
|
22
|
+
MARK_WRITE = "+"
|
|
23
|
+
MARK_MERMAID = "⧉"
|
|
24
|
+
MARK_WEB_FETCH = "←"
|
|
25
|
+
MARK_WEB_SEARCH = ""
|
|
26
|
+
MARK_DONE = "✔"
|
|
27
|
+
MARK_SKILL = "✪"
|
|
28
|
+
|
|
29
|
+
# Todo status markers
|
|
30
|
+
MARK_TODO_PENDING = "▢"
|
|
31
|
+
MARK_TODO_IN_PROGRESS = "◉"
|
|
32
|
+
MARK_TODO_COMPLETED = "✔"
|
|
33
|
+
|
|
16
34
|
|
|
17
35
|
def is_sub_agent_tool(tool_name: str) -> bool:
|
|
18
36
|
return _is_sub_agent_tool(tool_name)
|
|
@@ -30,7 +48,7 @@ def render_path(path: str, style: str, is_directory: bool = False) -> Text:
|
|
|
30
48
|
return Text(path, style=style)
|
|
31
49
|
|
|
32
50
|
|
|
33
|
-
def render_generic_tool_call(tool_name: str, arguments: str, markup: str =
|
|
51
|
+
def render_generic_tool_call(tool_name: str, arguments: str, markup: str = MARK_GENERIC) -> RenderableType:
|
|
34
52
|
grid = create_grid()
|
|
35
53
|
|
|
36
54
|
tool_name_column = Text.assemble((markup, ThemeKey.TOOL_MARK), " ", (tool_name, ThemeKey.TOOL_NAME))
|
|
@@ -60,7 +78,7 @@ def render_generic_tool_call(tool_name: str, arguments: str, markup: str = "•"
|
|
|
60
78
|
|
|
61
79
|
def render_bash_tool_call(arguments: str) -> RenderableType:
|
|
62
80
|
grid = create_grid()
|
|
63
|
-
tool_name_column = Text.assemble((
|
|
81
|
+
tool_name_column = Text.assemble((MARK_BASH, ThemeKey.TOOL_MARK), " ", ("Bash", ThemeKey.TOOL_NAME))
|
|
64
82
|
|
|
65
83
|
try:
|
|
66
84
|
payload_raw: Any = json.loads(arguments) if arguments else {}
|
|
@@ -103,7 +121,7 @@ def render_bash_tool_call(arguments: str) -> RenderableType:
|
|
|
103
121
|
|
|
104
122
|
def render_update_plan_tool_call(arguments: str) -> RenderableType:
|
|
105
123
|
grid = create_grid()
|
|
106
|
-
tool_name_column = Text.assemble((
|
|
124
|
+
tool_name_column = Text.assemble((MARK_PLAN, ThemeKey.TOOL_MARK), " ", ("Update Plan", ThemeKey.TOOL_NAME))
|
|
107
125
|
explanation_column = Text("")
|
|
108
126
|
|
|
109
127
|
if arguments:
|
|
@@ -160,13 +178,13 @@ def render_read_tool_call(arguments: str) -> RenderableType:
|
|
|
160
178
|
style=ThemeKey.INVALID_TOOL_CALL_ARGS,
|
|
161
179
|
)
|
|
162
180
|
)
|
|
163
|
-
grid.add_row(Text(
|
|
181
|
+
grid.add_row(Text(MARK_READ, ThemeKey.TOOL_MARK), render_result)
|
|
164
182
|
return grid
|
|
165
183
|
|
|
166
184
|
|
|
167
185
|
def render_edit_tool_call(arguments: str) -> RenderableType:
|
|
168
186
|
grid = create_grid()
|
|
169
|
-
tool_name_column = Text.assemble((
|
|
187
|
+
tool_name_column = Text.assemble((MARK_EDIT, ThemeKey.TOOL_MARK), " ", ("Edit", ThemeKey.TOOL_NAME))
|
|
170
188
|
try:
|
|
171
189
|
json_dict = json.loads(arguments)
|
|
172
190
|
file_path = json_dict.get("file_path")
|
|
@@ -185,32 +203,10 @@ def render_write_tool_call(arguments: str) -> RenderableType:
|
|
|
185
203
|
try:
|
|
186
204
|
json_dict = json.loads(arguments)
|
|
187
205
|
file_path = json_dict.get("file_path")
|
|
188
|
-
tool_name_column = Text.assemble((
|
|
206
|
+
tool_name_column = Text.assemble((MARK_WRITE, ThemeKey.TOOL_MARK), " ", ("Write", ThemeKey.TOOL_NAME))
|
|
189
207
|
arguments_column = render_path(file_path, ThemeKey.TOOL_PARAM_FILE_PATH)
|
|
190
208
|
except json.JSONDecodeError:
|
|
191
|
-
tool_name_column = Text.assemble((
|
|
192
|
-
arguments_column = Text(
|
|
193
|
-
arguments.strip()[: const.INVALID_TOOL_CALL_MAX_LENGTH],
|
|
194
|
-
style=ThemeKey.INVALID_TOOL_CALL_ARGS,
|
|
195
|
-
)
|
|
196
|
-
grid.add_row(tool_name_column, arguments_column)
|
|
197
|
-
return grid
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
def render_multi_edit_tool_call(arguments: str) -> RenderableType:
|
|
201
|
-
grid = create_grid()
|
|
202
|
-
tool_name_column = Text.assemble(("→", ThemeKey.TOOL_MARK), " ", ("MultiEdit", ThemeKey.TOOL_NAME))
|
|
203
|
-
try:
|
|
204
|
-
json_dict = json.loads(arguments)
|
|
205
|
-
file_path = json_dict.get("file_path")
|
|
206
|
-
edits = json_dict.get("edits", [])
|
|
207
|
-
arguments_column = Text.assemble(
|
|
208
|
-
render_path(file_path, ThemeKey.TOOL_PARAM_FILE_PATH),
|
|
209
|
-
Text(" - "),
|
|
210
|
-
Text(f"{len(edits)}", ThemeKey.TOOL_PARAM_BOLD),
|
|
211
|
-
Text(" updates", ThemeKey.TOOL_PARAM_FILE_PATH),
|
|
212
|
-
)
|
|
213
|
-
except json.JSONDecodeError:
|
|
209
|
+
tool_name_column = Text.assemble((MARK_WRITE, ThemeKey.TOOL_MARK), " ", ("Write", ThemeKey.TOOL_NAME))
|
|
214
210
|
arguments_column = Text(
|
|
215
211
|
arguments.strip()[: const.INVALID_TOOL_CALL_MAX_LENGTH],
|
|
216
212
|
style=ThemeKey.INVALID_TOOL_CALL_ARGS,
|
|
@@ -221,7 +217,7 @@ def render_multi_edit_tool_call(arguments: str) -> RenderableType:
|
|
|
221
217
|
|
|
222
218
|
def render_apply_patch_tool_call(arguments: str) -> RenderableType:
|
|
223
219
|
grid = create_grid()
|
|
224
|
-
tool_name_column = Text.assemble((
|
|
220
|
+
tool_name_column = Text.assemble((MARK_EDIT, ThemeKey.TOOL_MARK), " ", ("Apply Patch", ThemeKey.TOOL_NAME))
|
|
225
221
|
|
|
226
222
|
try:
|
|
227
223
|
payload = json.loads(arguments)
|
|
@@ -237,9 +233,27 @@ def render_apply_patch_tool_call(arguments: str) -> RenderableType:
|
|
|
237
233
|
arguments_column = Text("", ThemeKey.TOOL_PARAM)
|
|
238
234
|
|
|
239
235
|
if isinstance(patch_content, str):
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
236
|
+
update_count = 0
|
|
237
|
+
add_count = 0
|
|
238
|
+
delete_count = 0
|
|
239
|
+
for line in patch_content.splitlines():
|
|
240
|
+
if line.startswith("*** Update File:"):
|
|
241
|
+
update_count += 1
|
|
242
|
+
elif line.startswith("*** Add File:"):
|
|
243
|
+
add_count += 1
|
|
244
|
+
elif line.startswith("*** Delete File:"):
|
|
245
|
+
delete_count += 1
|
|
246
|
+
|
|
247
|
+
parts: list[str] = []
|
|
248
|
+
if update_count > 0:
|
|
249
|
+
parts.append(f"Update File × {update_count}" if update_count > 1 else "Update File")
|
|
250
|
+
if add_count > 0:
|
|
251
|
+
parts.append(f"Add File × {add_count}" if add_count > 1 else "Add File")
|
|
252
|
+
if delete_count > 0:
|
|
253
|
+
parts.append(f"Delete File × {delete_count}" if delete_count > 1 else "Delete File")
|
|
254
|
+
|
|
255
|
+
if parts:
|
|
256
|
+
arguments_column = Text(", ".join(parts), ThemeKey.TOOL_PARAM)
|
|
243
257
|
else:
|
|
244
258
|
arguments_column = Text(
|
|
245
259
|
str(patch_content)[: const.INVALID_TOOL_CALL_MAX_LENGTH],
|
|
@@ -251,34 +265,24 @@ def render_apply_patch_tool_call(arguments: str) -> RenderableType:
|
|
|
251
265
|
|
|
252
266
|
|
|
253
267
|
def render_todo(tr: events.ToolResultEvent) -> RenderableType:
|
|
254
|
-
|
|
255
|
-
return Text.assemble(
|
|
256
|
-
(" ✘", ThemeKey.ERROR_BOLD),
|
|
257
|
-
" ",
|
|
258
|
-
Text("(no content)" if tr.ui_extra is None else "(invalid ui_extra)", style=ThemeKey.ERROR),
|
|
259
|
-
)
|
|
260
|
-
|
|
268
|
+
assert isinstance(tr.ui_extra, model.TodoListUIExtra)
|
|
261
269
|
ui_extra = tr.ui_extra.todo_list
|
|
262
270
|
todo_grid = create_grid()
|
|
263
271
|
for todo in ui_extra.todos:
|
|
264
272
|
is_new_completed = todo.content in ui_extra.new_completed
|
|
265
273
|
match todo.status:
|
|
266
274
|
case "pending":
|
|
267
|
-
mark =
|
|
275
|
+
mark = MARK_TODO_PENDING
|
|
268
276
|
mark_style = ThemeKey.TODO_PENDING_MARK
|
|
269
277
|
text_style = ThemeKey.TODO_PENDING
|
|
270
278
|
case "in_progress":
|
|
271
|
-
mark =
|
|
279
|
+
mark = MARK_TODO_IN_PROGRESS
|
|
272
280
|
mark_style = ThemeKey.TODO_IN_PROGRESS_MARK
|
|
273
281
|
text_style = ThemeKey.TODO_IN_PROGRESS
|
|
274
282
|
case "completed":
|
|
275
|
-
mark =
|
|
283
|
+
mark = MARK_TODO_COMPLETED
|
|
276
284
|
mark_style = ThemeKey.TODO_NEW_COMPLETED_MARK if is_new_completed else ThemeKey.TODO_COMPLETED_MARK
|
|
277
285
|
text_style = ThemeKey.TODO_NEW_COMPLETED if is_new_completed else ThemeKey.TODO_COMPLETED
|
|
278
|
-
case _:
|
|
279
|
-
mark = "?"
|
|
280
|
-
mark_style = ThemeKey.TODO_PENDING_MARK
|
|
281
|
-
text_style = ThemeKey.TODO_PENDING
|
|
282
286
|
text = Text(todo.content)
|
|
283
287
|
text.stylize(text_style)
|
|
284
288
|
todo_grid.add_row(Text(mark, style=mark_style), text)
|
|
@@ -300,63 +304,9 @@ def _extract_mermaid_link(
|
|
|
300
304
|
return None
|
|
301
305
|
|
|
302
306
|
|
|
303
|
-
def render_memory_tool_call(arguments: str) -> RenderableType:
|
|
304
|
-
grid = create_grid()
|
|
305
|
-
command_display_names: dict[str, str] = {
|
|
306
|
-
"view": "View",
|
|
307
|
-
"create": "Create",
|
|
308
|
-
"str_replace": "Replace",
|
|
309
|
-
"insert": "Insert",
|
|
310
|
-
"delete": "Delete",
|
|
311
|
-
"rename": "Rename",
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
try:
|
|
315
|
-
payload: dict[str, str] = json.loads(arguments)
|
|
316
|
-
except json.JSONDecodeError:
|
|
317
|
-
tool_name_column = Text.assemble(("★", ThemeKey.TOOL_MARK), " ", ("Memory", ThemeKey.TOOL_NAME))
|
|
318
|
-
summary = Text(
|
|
319
|
-
arguments.strip()[: const.INVALID_TOOL_CALL_MAX_LENGTH],
|
|
320
|
-
style=ThemeKey.INVALID_TOOL_CALL_ARGS,
|
|
321
|
-
)
|
|
322
|
-
grid.add_row(tool_name_column, summary)
|
|
323
|
-
return grid
|
|
324
|
-
|
|
325
|
-
command = payload.get("command", "")
|
|
326
|
-
display_name = command_display_names.get(command, command.title())
|
|
327
|
-
tool_name_column = Text.assemble(("★", ThemeKey.TOOL_MARK), " ", (f"{display_name} Memory", ThemeKey.TOOL_NAME))
|
|
328
|
-
|
|
329
|
-
summary = Text("", ThemeKey.TOOL_PARAM)
|
|
330
|
-
path = payload.get("path")
|
|
331
|
-
old_path = payload.get("old_path")
|
|
332
|
-
new_path = payload.get("new_path")
|
|
333
|
-
|
|
334
|
-
if command == "rename" and old_path and new_path:
|
|
335
|
-
summary = Text.assemble(
|
|
336
|
-
Text(old_path, ThemeKey.TOOL_PARAM_FILE_PATH),
|
|
337
|
-
Text(" -> ", ThemeKey.TOOL_PARAM),
|
|
338
|
-
Text(new_path, ThemeKey.TOOL_PARAM_FILE_PATH),
|
|
339
|
-
)
|
|
340
|
-
elif command == "insert" and path:
|
|
341
|
-
insert_line = payload.get("insert_line")
|
|
342
|
-
summary = Text(path, ThemeKey.TOOL_PARAM_FILE_PATH)
|
|
343
|
-
if insert_line is not None:
|
|
344
|
-
summary.append(f" line {insert_line}", ThemeKey.TOOL_PARAM)
|
|
345
|
-
elif command == "view" and path:
|
|
346
|
-
view_range = payload.get("view_range")
|
|
347
|
-
summary = Text(path, ThemeKey.TOOL_PARAM_FILE_PATH)
|
|
348
|
-
if view_range and isinstance(view_range, list) and len(view_range) >= 2:
|
|
349
|
-
summary.append(f" {view_range[0]}:{view_range[1]}", ThemeKey.TOOL_PARAM)
|
|
350
|
-
elif path:
|
|
351
|
-
summary = Text(path, ThemeKey.TOOL_PARAM_FILE_PATH)
|
|
352
|
-
|
|
353
|
-
grid.add_row(tool_name_column, summary)
|
|
354
|
-
return grid
|
|
355
|
-
|
|
356
|
-
|
|
357
307
|
def render_mermaid_tool_call(arguments: str) -> RenderableType:
|
|
358
308
|
grid = create_grid()
|
|
359
|
-
tool_name_column = Text.assemble((
|
|
309
|
+
tool_name_column = Text.assemble((MARK_MERMAID, ThemeKey.TOOL_MARK), " ", ("Mermaid", ThemeKey.TOOL_NAME))
|
|
360
310
|
summary = Text("", ThemeKey.TOOL_PARAM)
|
|
361
311
|
|
|
362
312
|
try:
|
|
@@ -396,7 +346,7 @@ def _truncate_url(url: str, max_length: int = 400) -> str:
|
|
|
396
346
|
|
|
397
347
|
def render_web_fetch_tool_call(arguments: str) -> RenderableType:
|
|
398
348
|
grid = create_grid()
|
|
399
|
-
tool_name_column = Text.assemble((
|
|
349
|
+
tool_name_column = Text.assemble((MARK_WEB_FETCH, ThemeKey.TOOL_MARK), " ", ("Fetch", ThemeKey.TOOL_NAME))
|
|
400
350
|
|
|
401
351
|
try:
|
|
402
352
|
payload: dict[str, str] = json.loads(arguments)
|
|
@@ -417,7 +367,7 @@ def render_web_fetch_tool_call(arguments: str) -> RenderableType:
|
|
|
417
367
|
|
|
418
368
|
def render_web_search_tool_call(arguments: str) -> RenderableType:
|
|
419
369
|
grid = create_grid()
|
|
420
|
-
tool_name_column = Text.assemble((
|
|
370
|
+
tool_name_column = Text.assemble((MARK_WEB_SEARCH, ThemeKey.TOOL_MARK), " ", ("Web Search", ThemeKey.TOOL_NAME))
|
|
421
371
|
|
|
422
372
|
try:
|
|
423
373
|
payload: dict[str, Any] = json.loads(arguments)
|
|
@@ -494,7 +444,7 @@ def get_truncation_info(tr: events.ToolResultEvent) -> model.TruncationUIExtra |
|
|
|
494
444
|
|
|
495
445
|
def render_report_back_tool_call() -> RenderableType:
|
|
496
446
|
grid = create_grid()
|
|
497
|
-
tool_name_column = Text.assemble((
|
|
447
|
+
tool_name_column = Text.assemble((MARK_DONE, ThemeKey.TOOL_MARK), " ", ("Report Back", ThemeKey.TOOL_NAME))
|
|
498
448
|
grid.add_row(tool_name_column, "")
|
|
499
449
|
return grid
|
|
500
450
|
|
|
@@ -504,14 +454,12 @@ _TOOL_ACTIVE_FORM: dict[str, str] = {
|
|
|
504
454
|
tools.BASH: "Bashing",
|
|
505
455
|
tools.APPLY_PATCH: "Patching",
|
|
506
456
|
tools.EDIT: "Editing",
|
|
507
|
-
tools.MULTI_EDIT: "Editing",
|
|
508
457
|
tools.READ: "Reading",
|
|
509
458
|
tools.WRITE: "Writing",
|
|
510
459
|
tools.TODO_WRITE: "Planning",
|
|
511
460
|
tools.UPDATE_PLAN: "Planning",
|
|
512
461
|
tools.SKILL: "Skilling",
|
|
513
462
|
tools.MERMAID: "Diagramming",
|
|
514
|
-
tools.MEMORY: "Memorizing",
|
|
515
463
|
tools.WEB_FETCH: "Fetching Web",
|
|
516
464
|
tools.WEB_SEARCH: "Searching Web",
|
|
517
465
|
tools.REPORT_BACK: "Reporting",
|
|
@@ -552,22 +500,18 @@ def render_tool_call(e: events.ToolCallEvent) -> RenderableType | None:
|
|
|
552
500
|
return render_edit_tool_call(e.arguments)
|
|
553
501
|
case tools.WRITE:
|
|
554
502
|
return render_write_tool_call(e.arguments)
|
|
555
|
-
case tools.MULTI_EDIT:
|
|
556
|
-
return render_multi_edit_tool_call(e.arguments)
|
|
557
503
|
case tools.BASH:
|
|
558
504
|
return render_bash_tool_call(e.arguments)
|
|
559
505
|
case tools.APPLY_PATCH:
|
|
560
506
|
return render_apply_patch_tool_call(e.arguments)
|
|
561
507
|
case tools.TODO_WRITE:
|
|
562
|
-
return render_generic_tool_call("Update Todos", "",
|
|
508
|
+
return render_generic_tool_call("Update Todos", "", MARK_PLAN)
|
|
563
509
|
case tools.UPDATE_PLAN:
|
|
564
510
|
return render_update_plan_tool_call(e.arguments)
|
|
565
511
|
case tools.MERMAID:
|
|
566
512
|
return render_mermaid_tool_call(e.arguments)
|
|
567
|
-
case tools.MEMORY:
|
|
568
|
-
return render_memory_tool_call(e.arguments)
|
|
569
513
|
case tools.SKILL:
|
|
570
|
-
return render_generic_tool_call(e.tool_name, e.arguments,
|
|
514
|
+
return render_generic_tool_call(e.tool_name, e.arguments, MARK_SKILL)
|
|
571
515
|
case tools.REPORT_BACK:
|
|
572
516
|
return render_report_back_tool_call()
|
|
573
517
|
case tools.WEB_FETCH:
|
|
@@ -578,9 +522,9 @@ def render_tool_call(e: events.ToolCallEvent) -> RenderableType | None:
|
|
|
578
522
|
return render_generic_tool_call(e.tool_name, e.arguments)
|
|
579
523
|
|
|
580
524
|
|
|
581
|
-
def
|
|
582
|
-
if isinstance(ui_extra, model.
|
|
583
|
-
return ui_extra
|
|
525
|
+
def _extract_diff(ui_extra: model.ToolResultUIExtra | None) -> model.DiffUIExtra | None:
|
|
526
|
+
if isinstance(ui_extra, model.DiffUIExtra):
|
|
527
|
+
return ui_extra
|
|
584
528
|
return None
|
|
585
529
|
|
|
586
530
|
|
|
@@ -604,28 +548,30 @@ def render_tool_result(e: events.ToolResultEvent) -> RenderableType | None:
|
|
|
604
548
|
if truncation_info:
|
|
605
549
|
return Group(render_truncation_info(truncation_info), render_generic_tool_result(e.result))
|
|
606
550
|
|
|
607
|
-
|
|
551
|
+
diff_ui = _extract_diff(e.ui_extra)
|
|
608
552
|
|
|
609
553
|
match e.tool_name:
|
|
610
554
|
case tools.READ:
|
|
611
555
|
return None
|
|
612
|
-
case tools.EDIT | tools.
|
|
613
|
-
return Padding.indent(r_diffs.
|
|
614
|
-
case tools.
|
|
615
|
-
if
|
|
616
|
-
return Padding.indent(r_diffs.
|
|
617
|
-
|
|
618
|
-
return render_generic_tool_result(
|
|
619
|
-
return
|
|
556
|
+
case tools.EDIT | tools.WRITE:
|
|
557
|
+
return Padding.indent(r_diffs.render_structured_diff(diff_ui) if diff_ui else Text(""), level=2)
|
|
558
|
+
case tools.APPLY_PATCH:
|
|
559
|
+
if diff_ui:
|
|
560
|
+
return Padding.indent(r_diffs.render_structured_diff(diff_ui, show_file_name=True), level=2)
|
|
561
|
+
if len(e.result.strip()) == 0:
|
|
562
|
+
return render_generic_tool_result("(no content)")
|
|
563
|
+
return render_generic_tool_result(e.result)
|
|
620
564
|
case tools.TODO_WRITE | tools.UPDATE_PLAN:
|
|
621
565
|
return render_todo(e)
|
|
622
566
|
case tools.MERMAID:
|
|
623
567
|
return render_mermaid_tool_result(e)
|
|
624
|
-
case
|
|
625
|
-
if e.
|
|
568
|
+
case tools.BASH:
|
|
569
|
+
if e.result.startswith("diff --git"):
|
|
626
570
|
return r_diffs.render_diff_panel(e.result, show_file_name=True)
|
|
627
|
-
if e.
|
|
628
|
-
return
|
|
571
|
+
if len(e.result.strip()) == 0:
|
|
572
|
+
return render_generic_tool_result("(no content)")
|
|
573
|
+
return render_generic_tool_result(e.result)
|
|
574
|
+
case _:
|
|
629
575
|
if len(e.result.strip()) == 0:
|
|
630
576
|
return render_generic_tool_result("(no content)")
|
|
631
577
|
return render_generic_tool_result(e.result)
|
|
@@ -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,7 +9,7 @@ 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, Markdown
|
|
12
|
+
from rich.markdown import CodeBlock, Heading, Markdown, MarkdownElement
|
|
13
13
|
from rich.rule import Rule
|
|
14
14
|
from rich.spinner import Spinner
|
|
15
15
|
from rich.style import Style
|
|
@@ -45,6 +45,14 @@ class ThinkingCodeBlock(CodeBlock):
|
|
|
45
45
|
yield CodePanel(text, border_style="markdown.code.border")
|
|
46
46
|
|
|
47
47
|
|
|
48
|
+
class Divider(MarkdownElement):
|
|
49
|
+
"""A horizontal rule with an extra blank line below."""
|
|
50
|
+
|
|
51
|
+
def __rich_console__(self, console: Console, options: ConsoleOptions) -> RenderResult:
|
|
52
|
+
style = console.get_style("markdown.hr", default="none")
|
|
53
|
+
yield Rule(style=style, characters="-")
|
|
54
|
+
|
|
55
|
+
|
|
48
56
|
class LeftHeading(Heading):
|
|
49
57
|
"""A heading class that renders left-justified."""
|
|
50
58
|
|
|
@@ -69,6 +77,7 @@ class NoInsetMarkdown(Markdown):
|
|
|
69
77
|
"fence": NoInsetCodeBlock,
|
|
70
78
|
"code_block": NoInsetCodeBlock,
|
|
71
79
|
"heading_open": LeftHeading,
|
|
80
|
+
"hr": Divider,
|
|
72
81
|
}
|
|
73
82
|
|
|
74
83
|
|
|
@@ -80,6 +89,7 @@ class ThinkingMarkdown(Markdown):
|
|
|
80
89
|
"fence": ThinkingCodeBlock,
|
|
81
90
|
"code_block": ThinkingCodeBlock,
|
|
82
91
|
"heading_open": LeftHeading,
|
|
92
|
+
"hr": Divider,
|
|
83
93
|
}
|
|
84
94
|
|
|
85
95
|
|
|
@@ -98,7 +108,8 @@ class MarkdownStream:
|
|
|
98
108
|
console: Console | None = None,
|
|
99
109
|
spinner: Spinner | None = None,
|
|
100
110
|
mark: str | None = None,
|
|
101
|
-
|
|
111
|
+
left_margin: int = 0,
|
|
112
|
+
right_margin: int = const.MARKDOWN_RIGHT_MARGIN,
|
|
102
113
|
markdown_class: Callable[..., Markdown] | None = None,
|
|
103
114
|
) -> None:
|
|
104
115
|
"""Initialize the markdown stream.
|
|
@@ -107,8 +118,9 @@ class MarkdownStream:
|
|
|
107
118
|
mdargs (dict, optional): Additional arguments to pass to rich Markdown renderer
|
|
108
119
|
theme (Theme, optional): Theme for rendering markdown
|
|
109
120
|
console (Console, optional): External console to use for rendering
|
|
110
|
-
mark (str | None, optional): Marker shown before the first non-empty line when
|
|
111
|
-
|
|
121
|
+
mark (str | None, optional): Marker shown before the first non-empty line when left_margin >= 2
|
|
122
|
+
left_margin (int, optional): Number of columns to reserve on the left side
|
|
123
|
+
right_margin (int, optional): Number of columns to reserve on the right side
|
|
112
124
|
markdown_class: Markdown class to use for rendering (defaults to NoInsetMarkdown)
|
|
113
125
|
"""
|
|
114
126
|
self.printed: list[str] = [] # Stores lines that have already been printed
|
|
@@ -130,7 +142,10 @@ class MarkdownStream:
|
|
|
130
142
|
self.console = console
|
|
131
143
|
self.spinner: Spinner | None = spinner
|
|
132
144
|
self.mark: str | None = mark
|
|
133
|
-
|
|
145
|
+
|
|
146
|
+
self.left_margin: int = max(left_margin, 0)
|
|
147
|
+
|
|
148
|
+
self.right_margin: int = max(right_margin, 0)
|
|
134
149
|
self.markdown_class: Callable[..., Markdown] = markdown_class or NoInsetMarkdown
|
|
135
150
|
|
|
136
151
|
@property
|
|
@@ -150,15 +165,15 @@ class MarkdownStream:
|
|
|
150
165
|
# Render the markdown to a string buffer
|
|
151
166
|
string_io = io.StringIO()
|
|
152
167
|
|
|
153
|
-
# Determine console width and adjust for left
|
|
154
|
-
# the rendered content plus
|
|
168
|
+
# Determine console width and adjust for left margin so that
|
|
169
|
+
# the rendered content plus margins does not exceed the available width.
|
|
155
170
|
if self.console is not None:
|
|
156
171
|
base_width = self.console.options.max_width
|
|
157
172
|
else:
|
|
158
173
|
probe_console = Console(theme=self.theme)
|
|
159
174
|
base_width = probe_console.options.max_width
|
|
160
175
|
|
|
161
|
-
effective_width = max(base_width - self.
|
|
176
|
+
effective_width = max(base_width - self.left_margin - self.right_margin, 1)
|
|
162
177
|
|
|
163
178
|
# Use external console for consistent theming, or create temporary one
|
|
164
179
|
temp_console = Console(
|
|
@@ -172,17 +187,17 @@ class MarkdownStream:
|
|
|
172
187
|
temp_console.print(markdown)
|
|
173
188
|
output = string_io.getvalue()
|
|
174
189
|
|
|
175
|
-
# Split rendered output into lines, strip trailing spaces, and apply left
|
|
190
|
+
# Split rendered output into lines, strip trailing spaces, and apply left margin.
|
|
176
191
|
lines = output.splitlines(keepends=True)
|
|
177
|
-
indent_prefix = " " * self.
|
|
192
|
+
indent_prefix = " " * self.left_margin if self.left_margin > 0 else ""
|
|
178
193
|
processed_lines: list[str] = []
|
|
179
194
|
mark_applied = False
|
|
180
|
-
use_mark = bool(self.mark) and self.
|
|
195
|
+
use_mark = bool(self.mark) and self.left_margin >= 2
|
|
181
196
|
|
|
182
197
|
for line in lines:
|
|
183
198
|
stripped = line.rstrip()
|
|
184
199
|
|
|
185
|
-
# Apply mark to the first non-empty line only when
|
|
200
|
+
# Apply mark to the first non-empty line only when left_margin is at least 2.
|
|
186
201
|
if use_mark and not mark_applied and stripped:
|
|
187
202
|
stripped = f"{self.mark} {stripped}"
|
|
188
203
|
mark_applied = True
|
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))
|
|
@@ -203,24 +191,21 @@ class ShimmerStatusText:
|
|
|
203
191
|
yield table
|
|
204
192
|
|
|
205
193
|
def _render_left_text(self, console: Console) -> Text:
|
|
206
|
-
"""Render the left part with shimmer effect."""
|
|
194
|
+
"""Render the left part with shimmer effect on main text only."""
|
|
207
195
|
result = Text()
|
|
208
196
|
main_style = console.get_style(str(self._main_style))
|
|
209
197
|
hint_style = console.get_style(str(self._hint_style))
|
|
210
198
|
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
if index < split_index:
|
|
216
|
-
# Get style from main_text, merge with main_style
|
|
217
|
-
char_style = self._main_text.get_style_at_offset(console, index)
|
|
218
|
-
base_style = main_style + char_style
|
|
219
|
-
else:
|
|
220
|
-
base_style = hint_style
|
|
199
|
+
# Apply shimmer only to main text
|
|
200
|
+
for index, (ch, intensity) in enumerate(_shimmer_profile(self._main_text.plain)):
|
|
201
|
+
char_style = self._main_text.get_style_at_offset(console, index)
|
|
202
|
+
base_style = main_style + char_style
|
|
221
203
|
style = _shimmer_style(console, base_style, intensity)
|
|
222
204
|
result.append(ch, style=style)
|
|
223
205
|
|
|
206
|
+
# Append hint text without shimmer
|
|
207
|
+
result.append(self._hint_text.plain, style=hint_style)
|
|
208
|
+
|
|
224
209
|
return result
|
|
225
210
|
|
|
226
211
|
|