klaude-code 1.2.18__py3-none-any.whl → 1.2.19__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/main.py +42 -22
- klaude_code/cli/runtime.py +41 -2
- klaude_code/{version.py → cli/self_update.py} +110 -2
- klaude_code/command/export_online_cmd.py +8 -3
- klaude_code/command/registry.py +67 -22
- klaude_code/command/thinking_cmd.py +4 -3
- klaude_code/core/executor.py +21 -1
- klaude_code/core/prompts/prompt-sub-agent-explore.md +14 -2
- klaude_code/core/task.py +9 -7
- klaude_code/core/tool/file/read_tool.md +1 -1
- klaude_code/core/tool/file/read_tool.py +3 -2
- klaude_code/core/tool/memory/skill_loader.py +12 -10
- klaude_code/core/tool/tool_registry.py +1 -1
- klaude_code/llm/anthropic/client.py +25 -9
- klaude_code/llm/openai_compatible/client.py +5 -2
- klaude_code/llm/openrouter/client.py +7 -3
- klaude_code/llm/responses/client.py +6 -1
- klaude_code/session/session.py +33 -13
- klaude_code/session/templates/export_session.html +43 -41
- klaude_code/ui/modes/repl/completers.py +212 -71
- klaude_code/ui/modes/repl/input_prompt_toolkit.py +1 -1
- klaude_code/ui/modes/repl/renderer.py +2 -2
- klaude_code/ui/renderers/common.py +54 -0
- klaude_code/ui/renderers/developer.py +2 -3
- klaude_code/ui/renderers/errors.py +1 -1
- klaude_code/ui/renderers/metadata.py +10 -1
- klaude_code/ui/renderers/tools.py +3 -4
- klaude_code/ui/utils/common.py +0 -18
- {klaude_code-1.2.18.dist-info → klaude_code-1.2.19.dist-info}/METADATA +17 -2
- {klaude_code-1.2.18.dist-info → klaude_code-1.2.19.dist-info}/RECORD +32 -32
- {klaude_code-1.2.18.dist-info → klaude_code-1.2.19.dist-info}/WHEEL +0 -0
- {klaude_code-1.2.18.dist-info → klaude_code-1.2.19.dist-info}/entry_points.txt +0 -0
|
@@ -158,7 +158,7 @@ class _AtFilesCompleter(Completer):
|
|
|
158
158
|
def __init__(
|
|
159
159
|
self,
|
|
160
160
|
debounce_sec: float = 0.25,
|
|
161
|
-
cache_ttl_sec: float =
|
|
161
|
+
cache_ttl_sec: float = 60.0,
|
|
162
162
|
max_results: int = 20,
|
|
163
163
|
):
|
|
164
164
|
self._debounce_sec = debounce_sec
|
|
@@ -170,13 +170,26 @@ class _AtFilesCompleter(Completer):
|
|
|
170
170
|
self._last_query_key: str | None = None
|
|
171
171
|
self._last_results: list[str] = []
|
|
172
172
|
self._last_results_time: float = 0.0
|
|
173
|
+
self._last_results_truncated: bool = False
|
|
173
174
|
|
|
174
175
|
# rg --files cache (used when fd is unavailable)
|
|
175
176
|
self._rg_file_list: list[str] | None = None
|
|
176
177
|
self._rg_file_list_time: float = 0.0
|
|
178
|
+
self._rg_file_list_cwd: Path | None = None
|
|
177
179
|
|
|
178
|
-
#
|
|
179
|
-
self.
|
|
180
|
+
# git ls-files cache (preferred when inside a git repo)
|
|
181
|
+
self._git_repo_root: Path | None = None
|
|
182
|
+
self._git_repo_root_time: float = 0.0
|
|
183
|
+
self._git_repo_root_cwd: Path | None = None
|
|
184
|
+
|
|
185
|
+
self._git_file_list: list[str] | None = None
|
|
186
|
+
self._git_file_list_lower: list[str] | None = None
|
|
187
|
+
self._git_file_list_time: float = 0.0
|
|
188
|
+
self._git_file_list_cwd: Path | None = None
|
|
189
|
+
|
|
190
|
+
# Command timeout is intentionally higher than a keypress cadence.
|
|
191
|
+
# We rely on caching/narrowing to avoid calling fd repeatedly.
|
|
192
|
+
self._cmd_timeout_sec: float = 3.0
|
|
180
193
|
|
|
181
194
|
# ---- prompt_toolkit API ----
|
|
182
195
|
def get_completions(self, document: Document, complete_event) -> Iterable[Completion]: # type: ignore[override]
|
|
@@ -234,6 +247,8 @@ class _AtFilesCompleter(Completer):
|
|
|
234
247
|
key_norm = keyword.lower()
|
|
235
248
|
query_key = f"{cwd.resolve()}::search::{key_norm}"
|
|
236
249
|
|
|
250
|
+
max_scan_results = self._max_results * 3
|
|
251
|
+
|
|
237
252
|
# Debounce: if called too soon again, filter last results
|
|
238
253
|
if self._last_results and self._last_query_key is not None:
|
|
239
254
|
prev = self._last_query_key
|
|
@@ -247,85 +262,121 @@ class _AtFilesCompleter(Completer):
|
|
|
247
262
|
and len(cur_kw) >= len(prev_kw)
|
|
248
263
|
and cur_kw.startswith(prev_kw)
|
|
249
264
|
)
|
|
265
|
+
|
|
266
|
+
# If the previous result set was not truncated, it is a complete
|
|
267
|
+
# superset for any narrower prefix. Reuse it even if the user
|
|
268
|
+
# pauses between keystrokes.
|
|
269
|
+
if (
|
|
270
|
+
is_narrowing
|
|
271
|
+
and not self._last_results_truncated
|
|
272
|
+
and now - self._last_results_time < self._cache_ttl
|
|
273
|
+
):
|
|
274
|
+
return self._filter_and_format(self._last_results, cwd, key_norm)
|
|
275
|
+
|
|
250
276
|
if is_narrowing and (now - self._last_cmd_time) < self._debounce_sec:
|
|
251
|
-
# For narrowing, fast-filter previous results to avoid expensive calls
|
|
252
|
-
|
|
277
|
+
# For rapid narrowing, fast-filter previous results to avoid expensive calls
|
|
278
|
+
# If the previous result set was truncated (e.g., for a 1-char query),
|
|
279
|
+
# filtering it can legitimately produce an empty set even when matches
|
|
280
|
+
# exist elsewhere. Fall back to a real search in that case.
|
|
281
|
+
filtered = self._filter_and_format(self._last_results, cwd, key_norm)
|
|
282
|
+
if filtered:
|
|
283
|
+
return filtered
|
|
253
284
|
|
|
254
285
|
# Cache TTL: reuse cached results for same query within TTL
|
|
255
286
|
if self._last_results and self._last_query_key == query_key and now - self._last_results_time < self._cache_ttl:
|
|
256
|
-
return self._filter_and_format(self._last_results, cwd, key_norm
|
|
287
|
+
return self._filter_and_format(self._last_results, cwd, key_norm)
|
|
257
288
|
|
|
258
|
-
# Prefer
|
|
289
|
+
# Prefer git index (fast in large repos), then fd, then rg --files.
|
|
259
290
|
results: list[str] = []
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
291
|
+
truncated = False
|
|
292
|
+
|
|
293
|
+
# For very short keywords, prefer fd's early-exit behavior.
|
|
294
|
+
# For keywords >= 2 chars, using git's file list is typically faster
|
|
295
|
+
# than scanning the filesystem repeatedly.
|
|
296
|
+
if len(key_norm) >= 2:
|
|
297
|
+
results, truncated = self._git_paths_for_keyword(cwd, key_norm, max_results=max_scan_results)
|
|
298
|
+
|
|
299
|
+
if not results:
|
|
300
|
+
if self._has_cmd("fd"):
|
|
301
|
+
# Use fd to search anywhere in full path (files and directories), case-insensitive
|
|
302
|
+
results, truncated = self._run_fd_search(cwd, key_norm, max_results=max_scan_results)
|
|
303
|
+
elif self._has_cmd("rg"):
|
|
304
|
+
# Use rg to search only in current directory
|
|
305
|
+
rg_cache_ttl = max(self._cache_ttl, 30.0)
|
|
306
|
+
if (
|
|
307
|
+
self._rg_file_list is None
|
|
308
|
+
or self._rg_file_list_cwd != cwd
|
|
309
|
+
or now - self._rg_file_list_time > rg_cache_ttl
|
|
310
|
+
):
|
|
311
|
+
cmd = [
|
|
312
|
+
"rg",
|
|
313
|
+
"--files",
|
|
314
|
+
"--no-ignore",
|
|
315
|
+
"--hidden",
|
|
316
|
+
"--glob",
|
|
317
|
+
"!**/.git/**",
|
|
318
|
+
"--glob",
|
|
319
|
+
"!**/.venv/**",
|
|
320
|
+
"--glob",
|
|
321
|
+
"!**/node_modules/**",
|
|
322
|
+
]
|
|
323
|
+
r = self._run_cmd(cmd, cwd=cwd, timeout_sec=self._cmd_timeout_sec) # Search from current directory
|
|
324
|
+
if r.ok:
|
|
325
|
+
self._rg_file_list = r.lines
|
|
326
|
+
self._rg_file_list_time = now
|
|
327
|
+
self._rg_file_list_cwd = cwd
|
|
328
|
+
else:
|
|
329
|
+
self._rg_file_list = []
|
|
330
|
+
self._rg_file_list_time = now
|
|
331
|
+
self._rg_file_list_cwd = cwd
|
|
332
|
+
# Filter by keyword
|
|
333
|
+
all_files = self._rg_file_list or []
|
|
334
|
+
kn = key_norm
|
|
335
|
+
results = [p for p in all_files if kn in p.lower()]
|
|
336
|
+
# For rg fallback, we don't distinguish ignored files (no priority sorting)
|
|
337
|
+
else:
|
|
338
|
+
return []
|
|
282
339
|
|
|
283
340
|
# Update caches
|
|
284
341
|
self._last_cmd_time = now
|
|
285
342
|
self._last_query_key = query_key
|
|
286
343
|
self._last_results = results
|
|
287
344
|
self._last_results_time = now
|
|
288
|
-
self.
|
|
289
|
-
return self._filter_and_format(results, cwd, key_norm
|
|
345
|
+
self._last_results_truncated = truncated
|
|
346
|
+
return self._filter_and_format(results, cwd, key_norm)
|
|
290
347
|
|
|
291
348
|
def _filter_and_format(
|
|
292
349
|
self,
|
|
293
350
|
paths_from_root: list[str],
|
|
294
351
|
cwd: Path,
|
|
295
352
|
keyword_norm: str,
|
|
296
|
-
ignored_paths: set[str] | None = None,
|
|
297
353
|
) -> list[str]:
|
|
298
354
|
# Filter to keyword (case-insensitive) and rank by:
|
|
299
|
-
# 1.
|
|
300
|
-
# 2. Basename hit first, then path hit position, then length
|
|
355
|
+
# 1. Basename hit first, then path hit position, then length
|
|
301
356
|
# Since both fd and rg now search from current directory, all paths are relative to cwd
|
|
302
357
|
kn = keyword_norm
|
|
303
|
-
|
|
304
|
-
out: list[tuple[str, tuple[int, int, int, int, int]]] = []
|
|
358
|
+
out: list[tuple[str, tuple[int, int, int, int]]] = []
|
|
305
359
|
for p in paths_from_root:
|
|
306
360
|
pl = p.lower()
|
|
307
361
|
if kn not in pl:
|
|
308
362
|
continue
|
|
309
363
|
|
|
310
|
-
#
|
|
311
|
-
|
|
312
|
-
|
|
364
|
+
# Most tools return paths relative to cwd. Some include a leading
|
|
365
|
+
# './' prefix; strip that exact prefix only.
|
|
366
|
+
#
|
|
367
|
+
# Do not use lstrip('./') here: it would also remove the leading '.'
|
|
368
|
+
# from dotfiles/directories like '.claude/'.
|
|
369
|
+
rel_to_cwd = p.removeprefix("./").removeprefix(".\\")
|
|
370
|
+
base = os.path.basename(rel_to_cwd.rstrip("/")).lower()
|
|
313
371
|
base_pos = base.find(kn)
|
|
314
372
|
path_pos = pl.find(kn)
|
|
315
|
-
# Check if this path is in the ignored set (gitignored files)
|
|
316
|
-
is_ignored = 1 if rel_to_cwd in ignored_paths else 0
|
|
317
373
|
score = (
|
|
318
|
-
is_ignored,
|
|
319
374
|
0 if base_pos != -1 else 1,
|
|
320
375
|
base_pos if base_pos != -1 else 10_000,
|
|
321
376
|
path_pos,
|
|
322
377
|
len(p),
|
|
323
378
|
)
|
|
324
379
|
|
|
325
|
-
# Append trailing slash for directories
|
|
326
|
-
full_path = cwd / rel_to_cwd
|
|
327
|
-
if full_path.is_dir() and not rel_to_cwd.endswith("/"):
|
|
328
|
-
rel_to_cwd = rel_to_cwd + "/"
|
|
329
380
|
out.append((rel_to_cwd, score))
|
|
330
381
|
# Sort by score
|
|
331
382
|
out.sort(key=lambda x: x[1])
|
|
@@ -336,6 +387,19 @@ class _AtFilesCompleter(Completer):
|
|
|
336
387
|
if s not in seen:
|
|
337
388
|
seen.add(s)
|
|
338
389
|
uniq.append(s)
|
|
390
|
+
|
|
391
|
+
# Append trailing slash for directories, but avoid excessive stats.
|
|
392
|
+
# For large candidate lists, only stat the most relevant prefixes.
|
|
393
|
+
stat_limit = min(len(uniq), max(self._max_results * 3, 60))
|
|
394
|
+
for idx in range(stat_limit):
|
|
395
|
+
s = uniq[idx]
|
|
396
|
+
if s.endswith("/"):
|
|
397
|
+
continue
|
|
398
|
+
try:
|
|
399
|
+
if (cwd / s).is_dir():
|
|
400
|
+
uniq[idx] = f"{s}/"
|
|
401
|
+
except Exception:
|
|
402
|
+
continue
|
|
339
403
|
return uniq
|
|
340
404
|
|
|
341
405
|
def _format_completion_text(self, suggestion: str, *, is_quoted: bool) -> str:
|
|
@@ -371,15 +435,13 @@ class _AtFilesCompleter(Completer):
|
|
|
371
435
|
return None, None
|
|
372
436
|
|
|
373
437
|
# ---- Utilities ----
|
|
374
|
-
def _run_fd_search(self, cwd: Path, keyword_norm: str) -> tuple[list[str],
|
|
375
|
-
"""Run fd search and return (
|
|
438
|
+
def _run_fd_search(self, cwd: Path, keyword_norm: str, *, max_results: int) -> tuple[list[str], bool]:
|
|
439
|
+
"""Run fd search and return (results, truncated).
|
|
376
440
|
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
Returns the combined results and a set of paths that are gitignored.
|
|
441
|
+
Note: This is called in the prompt_toolkit completion path, so avoid
|
|
442
|
+
doing extra background scans here.
|
|
380
443
|
"""
|
|
381
|
-
|
|
382
|
-
base_cmd = [
|
|
444
|
+
cmd = [
|
|
383
445
|
"fd",
|
|
384
446
|
"--color=never",
|
|
385
447
|
"--type",
|
|
@@ -389,36 +451,115 @@ class _AtFilesCompleter(Completer):
|
|
|
389
451
|
"--hidden",
|
|
390
452
|
"--full-path",
|
|
391
453
|
"-i",
|
|
454
|
+
"-F",
|
|
392
455
|
"--max-results",
|
|
393
|
-
str(
|
|
456
|
+
str(max_results),
|
|
394
457
|
"--exclude",
|
|
395
458
|
".git",
|
|
396
459
|
"--exclude",
|
|
397
460
|
".venv",
|
|
398
461
|
"--exclude",
|
|
399
462
|
"node_modules",
|
|
400
|
-
|
|
463
|
+
keyword_norm,
|
|
401
464
|
".",
|
|
402
465
|
]
|
|
403
466
|
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
467
|
+
r = self._run_cmd(cmd, cwd=cwd, timeout_sec=self._cmd_timeout_sec)
|
|
468
|
+
lines = r.lines if r.ok else []
|
|
469
|
+
return lines, (len(lines) >= max_results)
|
|
470
|
+
|
|
471
|
+
def _git_paths_for_keyword(self, cwd: Path, keyword_norm: str, *, max_results: int) -> tuple[list[str], bool]:
|
|
472
|
+
"""Get path suggestions from the git index (fast for large repos).
|
|
473
|
+
|
|
474
|
+
Returns (candidates, truncated). "truncated" is True when we
|
|
475
|
+
intentionally stop early to keep per-keystroke costs bounded.
|
|
476
|
+
"""
|
|
477
|
+
repo_root = self._get_git_repo_root(cwd)
|
|
478
|
+
if repo_root is None:
|
|
479
|
+
return [], False
|
|
480
|
+
|
|
481
|
+
now = time.monotonic()
|
|
482
|
+
git_cache_ttl = max(self._cache_ttl, 30.0)
|
|
483
|
+
if (
|
|
484
|
+
self._git_file_list is None
|
|
485
|
+
or self._git_file_list_cwd != cwd
|
|
486
|
+
or now - self._git_file_list_time > git_cache_ttl
|
|
487
|
+
):
|
|
488
|
+
cmd = ["git", "ls-files", "-co", "--exclude-standard"]
|
|
489
|
+
r = self._run_cmd(cmd, cwd=repo_root, timeout_sec=self._cmd_timeout_sec)
|
|
490
|
+
if not r.ok:
|
|
491
|
+
self._git_file_list = []
|
|
492
|
+
self._git_file_list_lower = []
|
|
493
|
+
self._git_file_list_time = now
|
|
494
|
+
self._git_file_list_cwd = cwd
|
|
495
|
+
else:
|
|
496
|
+
cwd_resolved = cwd.resolve()
|
|
497
|
+
root_resolved = repo_root.resolve()
|
|
498
|
+
files: list[str] = []
|
|
499
|
+
files_lower: list[str] = []
|
|
500
|
+
for rel in r.lines:
|
|
501
|
+
abs_path = root_resolved / rel
|
|
502
|
+
try:
|
|
503
|
+
rel_to_cwd = abs_path.relative_to(cwd_resolved)
|
|
504
|
+
except ValueError:
|
|
505
|
+
continue
|
|
506
|
+
rel_posix = rel_to_cwd.as_posix()
|
|
507
|
+
files.append(rel_posix)
|
|
508
|
+
files_lower.append(rel_posix.lower())
|
|
509
|
+
self._git_file_list = files
|
|
510
|
+
self._git_file_list_lower = files_lower
|
|
511
|
+
self._git_file_list_time = now
|
|
512
|
+
self._git_file_list_cwd = cwd
|
|
513
|
+
|
|
514
|
+
all_files = self._git_file_list or []
|
|
515
|
+
all_files_lower = self._git_file_list_lower or []
|
|
516
|
+
kn = keyword_norm
|
|
407
517
|
|
|
408
|
-
#
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
518
|
+
# Bound per-keystroke work: stop scanning once enough matches are found.
|
|
519
|
+
matching_files: list[str] = []
|
|
520
|
+
scan_truncated = False
|
|
521
|
+
for p, pl in zip(all_files, all_files_lower, strict=False):
|
|
522
|
+
if kn in pl:
|
|
523
|
+
matching_files.append(p)
|
|
524
|
+
if len(matching_files) >= max_results:
|
|
525
|
+
scan_truncated = True
|
|
526
|
+
break
|
|
527
|
+
|
|
528
|
+
# Also include parent directories of matching files so users can
|
|
529
|
+
# complete into a folder, similar to fd's directory results.
|
|
530
|
+
dir_candidates: set[str] = set()
|
|
531
|
+
for p in matching_files[: max_results * 3]:
|
|
532
|
+
parent = os.path.dirname(p)
|
|
533
|
+
while parent and parent != ".":
|
|
534
|
+
dir_candidates.add(f"{parent}/")
|
|
535
|
+
parent = os.path.dirname(parent)
|
|
536
|
+
|
|
537
|
+
dir_list = sorted(dir_candidates)
|
|
538
|
+
dir_truncated = False
|
|
539
|
+
if len(dir_list) > max_results:
|
|
540
|
+
dir_list = dir_list[:max_results]
|
|
541
|
+
dir_truncated = True
|
|
542
|
+
|
|
543
|
+
candidates = matching_files + dir_list
|
|
544
|
+
truncated = scan_truncated or dir_truncated
|
|
545
|
+
return candidates, truncated
|
|
546
|
+
|
|
547
|
+
def _get_git_repo_root(self, cwd: Path) -> Path | None:
|
|
548
|
+
if not self._has_cmd("git"):
|
|
549
|
+
return None
|
|
413
550
|
|
|
414
|
-
|
|
415
|
-
|
|
551
|
+
now = time.monotonic()
|
|
552
|
+
ttl = max(self._cache_ttl, 30.0)
|
|
553
|
+
if self._git_repo_root_cwd == cwd and now - self._git_repo_root_time < ttl:
|
|
554
|
+
return self._git_repo_root
|
|
416
555
|
|
|
417
|
-
|
|
556
|
+
r = self._run_cmd(["git", "rev-parse", "--show-toplevel"], cwd=cwd, timeout_sec=0.5)
|
|
557
|
+
root = Path(r.lines[0]) if r.ok and r.lines else None
|
|
418
558
|
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
559
|
+
self._git_repo_root = root
|
|
560
|
+
self._git_repo_root_time = now
|
|
561
|
+
self._git_repo_root_cwd = cwd
|
|
562
|
+
return root
|
|
422
563
|
|
|
423
564
|
def _has_cmd(self, name: str) -> bool:
|
|
424
565
|
return shutil.which(name) is not None
|
|
@@ -444,7 +585,7 @@ class _AtFilesCompleter(Completer):
|
|
|
444
585
|
return []
|
|
445
586
|
return items[: min(self._max_results, 100)]
|
|
446
587
|
|
|
447
|
-
def _run_cmd(self, cmd: list[str], cwd: Path | None = None) -> _CmdResult:
|
|
588
|
+
def _run_cmd(self, cmd: list[str], cwd: Path | None = None, *, timeout_sec: float) -> _CmdResult:
|
|
448
589
|
cmd_str = " ".join(cmd)
|
|
449
590
|
start = time.monotonic()
|
|
450
591
|
try:
|
|
@@ -454,7 +595,7 @@ class _AtFilesCompleter(Completer):
|
|
|
454
595
|
stdout=subprocess.PIPE,
|
|
455
596
|
stderr=subprocess.DEVNULL,
|
|
456
597
|
text=True,
|
|
457
|
-
timeout=
|
|
598
|
+
timeout=timeout_sec,
|
|
458
599
|
)
|
|
459
600
|
elapsed_ms = (time.monotonic() - start) * 1000
|
|
460
601
|
if p.returncode == 0:
|
|
@@ -144,7 +144,7 @@ class PromptToolkitInput(InputProviderABC):
|
|
|
144
144
|
|
|
145
145
|
# Build result with style
|
|
146
146
|
toolbar_text = left_text + padding + right_text
|
|
147
|
-
return FormattedText([("#
|
|
147
|
+
return FormattedText([("#2c7eac", toolbar_text)])
|
|
148
148
|
|
|
149
149
|
async def start(self) -> None:
|
|
150
150
|
pass
|
|
@@ -22,11 +22,11 @@ from klaude_code.ui.renderers import sub_agent as r_sub_agent
|
|
|
22
22
|
from klaude_code.ui.renderers import thinking as r_thinking
|
|
23
23
|
from klaude_code.ui.renderers import tools as r_tools
|
|
24
24
|
from klaude_code.ui.renderers import user_input as r_user_input
|
|
25
|
+
from klaude_code.ui.renderers.common import truncate_display
|
|
25
26
|
from klaude_code.ui.rich import status as r_status
|
|
26
27
|
from klaude_code.ui.rich.quote import Quote
|
|
27
28
|
from klaude_code.ui.rich.status import ShimmerStatusText
|
|
28
29
|
from klaude_code.ui.rich.theme import ThemeKey, get_theme
|
|
29
|
-
from klaude_code.ui.utils.common import truncate_display
|
|
30
30
|
|
|
31
31
|
|
|
32
32
|
@dataclass
|
|
@@ -244,7 +244,7 @@ class REPLRenderer:
|
|
|
244
244
|
def display_error(self, event: events.ErrorEvent) -> None:
|
|
245
245
|
self.print(
|
|
246
246
|
r_errors.render_error(
|
|
247
|
-
|
|
247
|
+
truncate_display(event.error_message),
|
|
248
248
|
indent=0,
|
|
249
249
|
)
|
|
250
250
|
)
|
|
@@ -1,4 +1,9 @@
|
|
|
1
|
+
from rich.style import Style
|
|
1
2
|
from rich.table import Table
|
|
3
|
+
from rich.text import Text
|
|
4
|
+
|
|
5
|
+
from klaude_code import const
|
|
6
|
+
from klaude_code.ui.rich.theme import ThemeKey
|
|
2
7
|
|
|
3
8
|
|
|
4
9
|
def create_grid() -> Table:
|
|
@@ -6,3 +11,52 @@ def create_grid() -> Table:
|
|
|
6
11
|
grid.add_column(no_wrap=True)
|
|
7
12
|
grid.add_column(overflow="fold")
|
|
8
13
|
return grid
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def truncate_display(
|
|
17
|
+
text: str,
|
|
18
|
+
max_lines: int = const.TRUNCATE_DISPLAY_MAX_LINES,
|
|
19
|
+
max_line_length: int = const.TRUNCATE_DISPLAY_MAX_LINE_LENGTH,
|
|
20
|
+
*,
|
|
21
|
+
base_style: str | Style | None = None,
|
|
22
|
+
) -> Text:
|
|
23
|
+
"""Truncate long text for terminal display.
|
|
24
|
+
|
|
25
|
+
Applies `ThemeKey.TOOL_RESULT_TRUNCATED` style to truncation indicators.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
if max_lines <= 0:
|
|
29
|
+
truncated_lines = text.split("\n")
|
|
30
|
+
remaining = max(0, len(truncated_lines))
|
|
31
|
+
return Text(f"… (more {remaining} lines)", style=ThemeKey.TOOL_RESULT_TRUNCATED)
|
|
32
|
+
|
|
33
|
+
lines = text.split("\n")
|
|
34
|
+
extra_lines = 0
|
|
35
|
+
if len(lines) > max_lines:
|
|
36
|
+
extra_lines = len(lines) - max_lines
|
|
37
|
+
lines = lines[:max_lines]
|
|
38
|
+
|
|
39
|
+
out = Text()
|
|
40
|
+
if base_style is not None:
|
|
41
|
+
out.style = base_style
|
|
42
|
+
|
|
43
|
+
for idx, line in enumerate(lines):
|
|
44
|
+
if len(line) > max_line_length:
|
|
45
|
+
extra_chars = len(line) - max_line_length
|
|
46
|
+
out.append(line[:max_line_length])
|
|
47
|
+
out.append_text(
|
|
48
|
+
Text(
|
|
49
|
+
f" … (more {extra_chars} characters in this line)",
|
|
50
|
+
style=ThemeKey.TOOL_RESULT_TRUNCATED,
|
|
51
|
+
)
|
|
52
|
+
)
|
|
53
|
+
else:
|
|
54
|
+
out.append(line)
|
|
55
|
+
|
|
56
|
+
if idx != len(lines) - 1 or extra_lines > 0:
|
|
57
|
+
out.append("\n")
|
|
58
|
+
|
|
59
|
+
if extra_lines > 0:
|
|
60
|
+
out.append_text(Text(f"… (more {extra_lines} lines)", style=ThemeKey.TOOL_RESULT_TRUNCATED))
|
|
61
|
+
|
|
62
|
+
return out
|
|
@@ -5,11 +5,10 @@ from rich.text import Text
|
|
|
5
5
|
|
|
6
6
|
from klaude_code.protocol import commands, events, model
|
|
7
7
|
from klaude_code.ui.renderers import diffs as r_diffs
|
|
8
|
-
from klaude_code.ui.renderers.common import create_grid
|
|
8
|
+
from klaude_code.ui.renderers.common import create_grid, truncate_display
|
|
9
9
|
from klaude_code.ui.renderers.tools import render_path
|
|
10
10
|
from klaude_code.ui.rich.markdown import NoInsetMarkdown
|
|
11
11
|
from klaude_code.ui.rich.theme import ThemeKey
|
|
12
|
-
from klaude_code.ui.utils.common import truncate_display
|
|
13
12
|
|
|
14
13
|
|
|
15
14
|
def need_render_developer_message(e: events.DeveloperMessageEvent) -> bool:
|
|
@@ -117,7 +116,7 @@ def render_command_output(e: events.DeveloperMessageEvent) -> RenderableType:
|
|
|
117
116
|
case _:
|
|
118
117
|
content = e.item.content or "(no content)"
|
|
119
118
|
style = ThemeKey.TOOL_RESULT if not e.item.command_output.is_error else ThemeKey.ERROR
|
|
120
|
-
return Padding.indent(
|
|
119
|
+
return Padding.indent(truncate_display(content, base_style=style), level=2)
|
|
121
120
|
|
|
122
121
|
|
|
123
122
|
def _format_tokens(tokens: int) -> str:
|
|
@@ -11,6 +11,6 @@ def render_error(error_msg: Text, indent: int = 2) -> RenderableType:
|
|
|
11
11
|
Shows a two-column grid with an error mark and truncated message.
|
|
12
12
|
"""
|
|
13
13
|
grid = create_grid()
|
|
14
|
-
error_msg.
|
|
14
|
+
error_msg.style = ThemeKey.ERROR
|
|
15
15
|
grid.add_row(Text(" " * indent + "✘", style=ThemeKey.ERROR_BOLD), error_msg)
|
|
16
16
|
return grid
|
|
@@ -108,7 +108,16 @@ def _render_task_metadata_block(
|
|
|
108
108
|
parts.append(
|
|
109
109
|
Text.assemble(
|
|
110
110
|
(f"{metadata.usage.throughput_tps:.1f} ", ThemeKey.METADATA),
|
|
111
|
-
("tps", ThemeKey.METADATA_DIM),
|
|
111
|
+
("avg-tps", ThemeKey.METADATA_DIM),
|
|
112
|
+
)
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
# First token latency
|
|
116
|
+
if metadata.usage.first_token_latency_ms is not None:
|
|
117
|
+
parts.append(
|
|
118
|
+
Text.assemble(
|
|
119
|
+
(f"{metadata.usage.first_token_latency_ms:.0f}", ThemeKey.METADATA),
|
|
120
|
+
("ms avg-ftl", ThemeKey.METADATA_DIM),
|
|
112
121
|
)
|
|
113
122
|
)
|
|
114
123
|
|
|
@@ -10,9 +10,8 @@ from klaude_code import const
|
|
|
10
10
|
from klaude_code.protocol import events, model, tools
|
|
11
11
|
from klaude_code.protocol.sub_agent import is_sub_agent_tool as _is_sub_agent_tool
|
|
12
12
|
from klaude_code.ui.renderers import diffs as r_diffs
|
|
13
|
-
from klaude_code.ui.renderers.common import create_grid
|
|
13
|
+
from klaude_code.ui.renderers.common import create_grid, truncate_display
|
|
14
14
|
from klaude_code.ui.rich.theme import ThemeKey
|
|
15
|
-
from klaude_code.ui.utils.common import truncate_display
|
|
16
15
|
|
|
17
16
|
|
|
18
17
|
def is_sub_agent_tool(tool_name: str) -> bool:
|
|
@@ -290,7 +289,7 @@ def render_todo(tr: events.ToolResultEvent) -> RenderableType:
|
|
|
290
289
|
def render_generic_tool_result(result: str, *, is_error: bool = False) -> RenderableType:
|
|
291
290
|
"""Render a generic tool result as indented, truncated text."""
|
|
292
291
|
style = ThemeKey.ERROR if is_error else ThemeKey.TOOL_RESULT
|
|
293
|
-
return Padding.indent(
|
|
292
|
+
return Padding.indent(truncate_display(result, base_style=style), level=2)
|
|
294
293
|
|
|
295
294
|
|
|
296
295
|
def _extract_mermaid_link(
|
|
@@ -597,7 +596,7 @@ def render_tool_result(e: events.ToolResultEvent) -> RenderableType | None:
|
|
|
597
596
|
|
|
598
597
|
# Handle error case
|
|
599
598
|
if e.status == "error" and e.ui_extra is None:
|
|
600
|
-
error_msg =
|
|
599
|
+
error_msg = truncate_display(e.result)
|
|
601
600
|
return r_errors.render_error(error_msg)
|
|
602
601
|
|
|
603
602
|
# Show truncation info if output was truncated and saved to file
|
klaude_code/ui/utils/common.py
CHANGED
|
@@ -2,8 +2,6 @@ import re
|
|
|
2
2
|
import subprocess
|
|
3
3
|
from pathlib import Path
|
|
4
4
|
|
|
5
|
-
from klaude_code import const
|
|
6
|
-
|
|
7
5
|
LEADING_NEWLINES_REGEX = re.compile(r"^\n{2,}")
|
|
8
6
|
|
|
9
7
|
|
|
@@ -90,19 +88,3 @@ def show_path_with_tilde(path: Path | None = None):
|
|
|
90
88
|
return f"~/{relative_path}"
|
|
91
89
|
except ValueError:
|
|
92
90
|
return str(path)
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
def truncate_display(
|
|
96
|
-
text: str,
|
|
97
|
-
max_lines: int = const.TRUNCATE_DISPLAY_MAX_LINES,
|
|
98
|
-
max_line_length: int = const.TRUNCATE_DISPLAY_MAX_LINE_LENGTH,
|
|
99
|
-
) -> str:
|
|
100
|
-
lines = text.split("\n")
|
|
101
|
-
if len(lines) > max_lines:
|
|
102
|
-
lines = [*lines[:max_lines], "… (more " + str(len(lines) - max_lines) + " lines)"]
|
|
103
|
-
for i, line in enumerate(lines):
|
|
104
|
-
if len(line) > max_line_length:
|
|
105
|
-
lines[i] = (
|
|
106
|
-
line[:max_line_length] + "… (more " + str(len(line) - max_line_length) + " characters in this line)"
|
|
107
|
-
)
|
|
108
|
-
return "\n".join(lines)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: klaude-code
|
|
3
|
-
Version: 1.2.
|
|
3
|
+
Version: 1.2.19
|
|
4
4
|
Summary: Add your description here
|
|
5
5
|
Requires-Dist: anthropic>=0.66.0
|
|
6
6
|
Requires-Dist: ddgs>=9.9.3
|
|
@@ -41,6 +41,21 @@ To update:
|
|
|
41
41
|
uv tool upgrade klaude-code
|
|
42
42
|
```
|
|
43
43
|
|
|
44
|
+
Or use the built-in alias command:
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
klaude update
|
|
48
|
+
klaude upgrade
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
To show version:
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
klaude --version
|
|
55
|
+
klaude -v
|
|
56
|
+
klaude version
|
|
57
|
+
```
|
|
58
|
+
|
|
44
59
|
## Usage
|
|
45
60
|
|
|
46
61
|
### Interactive Mode
|
|
@@ -50,7 +65,7 @@ klaude [--model <name>] [--select-model]
|
|
|
50
65
|
```
|
|
51
66
|
|
|
52
67
|
**Options:**
|
|
53
|
-
- `--version`/`-V`: Show version and exit.
|
|
68
|
+
- `--version`/`-V`/`-v`: Show version and exit.
|
|
54
69
|
- `--model`/`-m`: Preferred model name (exact match picks immediately; otherwise opens the interactive selector filtered by this value).
|
|
55
70
|
- `--select-model`/`-s`: Open the interactive model selector at startup (shows all models unless `--model` is also provided).
|
|
56
71
|
- `--continue`/`-c`: Resume the most recent session.
|