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.
Files changed (32) hide show
  1. klaude_code/cli/main.py +42 -22
  2. klaude_code/cli/runtime.py +41 -2
  3. klaude_code/{version.py → cli/self_update.py} +110 -2
  4. klaude_code/command/export_online_cmd.py +8 -3
  5. klaude_code/command/registry.py +67 -22
  6. klaude_code/command/thinking_cmd.py +4 -3
  7. klaude_code/core/executor.py +21 -1
  8. klaude_code/core/prompts/prompt-sub-agent-explore.md +14 -2
  9. klaude_code/core/task.py +9 -7
  10. klaude_code/core/tool/file/read_tool.md +1 -1
  11. klaude_code/core/tool/file/read_tool.py +3 -2
  12. klaude_code/core/tool/memory/skill_loader.py +12 -10
  13. klaude_code/core/tool/tool_registry.py +1 -1
  14. klaude_code/llm/anthropic/client.py +25 -9
  15. klaude_code/llm/openai_compatible/client.py +5 -2
  16. klaude_code/llm/openrouter/client.py +7 -3
  17. klaude_code/llm/responses/client.py +6 -1
  18. klaude_code/session/session.py +33 -13
  19. klaude_code/session/templates/export_session.html +43 -41
  20. klaude_code/ui/modes/repl/completers.py +212 -71
  21. klaude_code/ui/modes/repl/input_prompt_toolkit.py +1 -1
  22. klaude_code/ui/modes/repl/renderer.py +2 -2
  23. klaude_code/ui/renderers/common.py +54 -0
  24. klaude_code/ui/renderers/developer.py +2 -3
  25. klaude_code/ui/renderers/errors.py +1 -1
  26. klaude_code/ui/renderers/metadata.py +10 -1
  27. klaude_code/ui/renderers/tools.py +3 -4
  28. klaude_code/ui/utils/common.py +0 -18
  29. {klaude_code-1.2.18.dist-info → klaude_code-1.2.19.dist-info}/METADATA +17 -2
  30. {klaude_code-1.2.18.dist-info → klaude_code-1.2.19.dist-info}/RECORD +32 -32
  31. {klaude_code-1.2.18.dist-info → klaude_code-1.2.19.dist-info}/WHEEL +0 -0
  32. {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 = 10.0,
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
- # Cache for ignored paths (gitignored files)
179
- self._last_ignored_paths: set[str] = set()
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
- return self._filter_and_format(self._last_results, cwd, key_norm, self._last_ignored_paths)
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, self._last_ignored_paths)
287
+ return self._filter_and_format(self._last_results, cwd, key_norm)
257
288
 
258
- # Prefer fd; otherwise fallback to rg --files
289
+ # Prefer git index (fast in large repos), then fd, then rg --files.
259
290
  results: list[str] = []
260
- ignored_paths: set[str] = set()
261
- if self._has_cmd("fd"):
262
- # Use fd to search anywhere in full path (files and directories), case-insensitive
263
- results, ignored_paths = self._run_fd_search(cwd, key_norm)
264
- elif self._has_cmd("rg"):
265
- # Use rg to search only in current directory
266
- if self._rg_file_list is None or now - self._rg_file_list_time > max(self._cache_ttl, 30.0):
267
- cmd = ["rg", "--files", "--no-ignore", "--hidden"]
268
- r = self._run_cmd(cmd, cwd=cwd) # Search from current directory
269
- if r.ok:
270
- self._rg_file_list = r.lines
271
- self._rg_file_list_time = now
272
- else:
273
- self._rg_file_list = []
274
- self._rg_file_list_time = now
275
- # Filter by keyword
276
- all_files = self._rg_file_list or []
277
- kn = key_norm
278
- results = [p for p in all_files if kn in p.lower()]
279
- # For rg fallback, we don't distinguish ignored files (no priority sorting)
280
- else:
281
- return []
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._last_ignored_paths = ignored_paths
289
- return self._filter_and_format(results, cwd, key_norm, ignored_paths)
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. Non-gitignored files first (is_ignored: 0 or 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
- ignored_paths = ignored_paths or set()
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
- # Use path directly since it's already relative to current directory
311
- rel_to_cwd = p.lstrip("./")
312
- base = os.path.basename(p).lower()
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], set[str]]:
375
- """Run fd search and return (all_results, ignored_paths).
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
- First runs fd without --no-ignore to get tracked files,
378
- then runs with --no-ignore to get all files including gitignored ones.
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
- pattern = self._escape_regex(keyword_norm)
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(self._max_results * 3),
456
+ str(max_results),
394
457
  "--exclude",
395
458
  ".git",
396
459
  "--exclude",
397
460
  ".venv",
398
461
  "--exclude",
399
462
  "node_modules",
400
- pattern,
463
+ keyword_norm,
401
464
  ".",
402
465
  ]
403
466
 
404
- # First run: get tracked (non-ignored) files
405
- r_tracked = self._run_cmd(base_cmd, cwd=cwd)
406
- tracked_paths: set[str] = set(p.lstrip("./") for p in r_tracked.lines) if r_tracked.ok else set()
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
- # Second run: get all files including ignored ones
409
- cmd_all = base_cmd.copy()
410
- cmd_all.insert(2, "--no-ignore") # Insert after --color=never
411
- r_all = self._run_cmd(cmd_all, cwd=cwd)
412
- all_paths = r_all.lines if r_all.ok else []
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
- # Calculate which paths are gitignored (in all but not in tracked)
415
- ignored_paths = set(p.lstrip("./") for p in all_paths) - tracked_paths
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
- return all_paths, ignored_paths
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
- def _escape_regex(self, s: str) -> str:
420
- # Escape for fd (regex by default). Keep '/' as is for path boundaries.
421
- return re.escape(s).replace("/", "/")
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=1.5,
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([("#ansiblue", toolbar_text)])
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
- self.console.render_str(truncate_display(event.error_message)),
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(Text(truncate_display(content), style=style), level=2)
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.stylize(ThemeKey.ERROR)
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(Text(truncate_display(result), style=style), level=2)
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 = Text(truncate_display(e.result))
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
@@ -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.18
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.