kash-shell 0.3.24__py3-none-any.whl → 0.3.26__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 (42) hide show
  1. kash/commands/help/assistant_commands.py +4 -3
  2. kash/config/colors.py +5 -3
  3. kash/config/text_styles.py +1 -0
  4. kash/config/unified_live.py +251 -0
  5. kash/docs/markdown/assistant_instructions_template.md +3 -3
  6. kash/docs/markdown/topics/a1_what_is_kash.md +22 -20
  7. kash/docs/markdown/topics/a2_installation.md +10 -10
  8. kash/docs/markdown/topics/a3_getting_started.md +8 -8
  9. kash/docs/markdown/topics/a4_elements.md +3 -3
  10. kash/docs/markdown/topics/a5_tips_for_use_with_other_tools.md +12 -12
  11. kash/docs/markdown/topics/b0_philosophy_of_kash.md +17 -17
  12. kash/docs/markdown/topics/b1_kash_overview.md +7 -7
  13. kash/docs/markdown/topics/b2_workspace_and_file_formats.md +1 -1
  14. kash/docs/markdown/topics/b3_modern_shell_tool_recommendations.md +1 -1
  15. kash/docs/markdown/topics/b4_faq.md +7 -7
  16. kash/docs/markdown/welcome.md +1 -1
  17. kash/embeddings/embeddings.py +112 -43
  18. kash/embeddings/text_similarity.py +4 -7
  19. kash/exec/shell_callable_action.py +4 -3
  20. kash/help/help_embeddings.py +8 -2
  21. kash/llm_utils/llm_features.py +1 -1
  22. kash/llm_utils/llms.py +5 -7
  23. kash/model/graph_model.py +2 -0
  24. kash/model/items_model.py +3 -3
  25. kash/model/params_model.py +1 -1
  26. kash/shell/output/shell_output.py +2 -2
  27. kash/utils/file_utils/csv_utils.py +105 -0
  28. kash/utils/rich_custom/multitask_status.py +19 -5
  29. kash/utils/text_handling/doc_normalization.py +2 -0
  30. kash/web_gen/templates/base_styles.css.jinja +356 -24
  31. kash/web_gen/templates/base_webpage.html.jinja +11 -0
  32. kash/web_gen/templates/components/toc_styles.css.jinja +15 -3
  33. kash/web_gen/templates/components/tooltip_styles.css.jinja +1 -0
  34. kash/web_gen/templates/content_styles.css.jinja +23 -9
  35. kash/web_gen/templates/item_view.html.jinja +12 -4
  36. kash/web_gen/templates/simple_webpage.html.jinja +2 -2
  37. kash/xonsh_custom/custom_shell.py +7 -4
  38. {kash_shell-0.3.24.dist-info → kash_shell-0.3.26.dist-info}/METADATA +58 -55
  39. {kash_shell-0.3.24.dist-info → kash_shell-0.3.26.dist-info}/RECORD +42 -40
  40. {kash_shell-0.3.24.dist-info → kash_shell-0.3.26.dist-info}/WHEEL +0 -0
  41. {kash_shell-0.3.24.dist-info → kash_shell-0.3.26.dist-info}/entry_points.txt +0 -0
  42. {kash_shell-0.3.24.dist-info → kash_shell-0.3.26.dist-info}/licenses/LICENSE +0 -0
@@ -209,7 +209,7 @@ A list of parameter declarations, possibly with default values.
209
209
  DEFAULT_CAREFUL_LLM = LLM.o3
210
210
  DEFAULT_STRUCTURED_LLM = LLM.gpt_4o
211
211
  DEFAULT_STANDARD_LLM = LLM.claude_4_sonnet
212
- DEFAULT_FAST_LLM = LLM.o1_mini
212
+ DEFAULT_FAST_LLM = LLM.gpt_4o
213
213
 
214
214
 
215
215
  # Parameters set globally such as in the workspace.
@@ -86,8 +86,8 @@ def multitask_status(
86
86
  ) -> MultiTaskStatus | nullcontext:
87
87
  """
88
88
  Create a `MultiTaskStatus` context manager for displaying multiple task progress
89
- using the global shell console. If disabled, returns a null context, so it's convenient
90
- to disable status display.
89
+ using the global shell console with live display conflict prevention. If disabled,
90
+ returns a null context, so it's convenient to disable status display.
91
91
  """
92
92
  if not enabled:
93
93
  return nullcontext()
@@ -0,0 +1,105 @@
1
+ from __future__ import annotations
2
+
3
+ import csv
4
+ from pathlib import Path
5
+ from typing import NamedTuple
6
+
7
+
8
+ class CsvMetadata(NamedTuple):
9
+ """
10
+ Result of CSV analysis, containing skip rows, metadata, and dialect.
11
+ """
12
+
13
+ skip_rows: int
14
+ metadata: dict[str, str]
15
+ dialect: type[csv.Dialect]
16
+
17
+
18
+ def sniff_csv_metadata(
19
+ file_path: Path,
20
+ *,
21
+ max_scan_lines: int = 500,
22
+ threshold_ratio: float = 0.8,
23
+ min_columns: int = 3,
24
+ sample_size: int = 32768,
25
+ ) -> CsvMetadata:
26
+ """
27
+ Detect CSV metadata and where the table data starts by finding the first row that looks
28
+ like proper headers.
29
+
30
+ This function handles various CSV formats:
31
+ - Normal CSV files: returns skip_rows=0 (no rows to skip)
32
+ - Files with metadata: detects the first row with multiple columns that looks like headers
33
+ - Survey exports: handles key-value metadata followed by proper CSV structure
34
+
35
+ Args:
36
+ file_path: Path to the CSV file to analyze
37
+ max_scan_lines: Maximum number of lines to scan before giving up
38
+ threshold_ratio: Minimum ratio of max columns a row must have to be considered headers
39
+ min_columns: Minimum number of columns required to be considered headers
40
+ sample_size: Number of bytes to read for dialect detection
41
+
42
+ Returns:
43
+ CsvMetadata with skip_rows, metadata dict, and detected dialect
44
+ """
45
+ # Read sample for dialect detection
46
+ sample_text = file_path.read_text(encoding="utf-8", errors="replace")[:sample_size]
47
+
48
+ # Detect CSV dialect
49
+ try:
50
+ dialect = csv.Sniffer().sniff(sample_text)
51
+ except csv.Error:
52
+ # Fall back to default dialect if detection fails
53
+ dialect = csv.excel
54
+
55
+ # Analyze file structure
56
+ with open(file_path, encoding="utf-8", errors="replace") as file:
57
+ reader = csv.reader(file, dialect=dialect)
58
+
59
+ max_columns = 0
60
+ header_candidates = []
61
+ metadata = {}
62
+
63
+ for line_num, row in enumerate(reader):
64
+ # Stop scanning if we've looked at too many lines
65
+ if line_num >= max_scan_lines:
66
+ break
67
+
68
+ # Skip completely empty rows
69
+ non_empty_cells = [cell.strip() for cell in row if cell.strip()]
70
+ if not non_empty_cells:
71
+ continue
72
+
73
+ column_count = len(non_empty_cells)
74
+
75
+ # Track the maximum number of columns seen
76
+ if column_count > max_columns:
77
+ max_columns = column_count
78
+
79
+ # Collect potential key-value metadata (exactly 2 columns)
80
+ # Only collect metadata before we find any header candidates with min_columns
81
+ if column_count == 2 and not any(hc[1] >= min_columns for hc in header_candidates):
82
+ key, value = non_empty_cells[0], non_empty_cells[1]
83
+ # Simple heuristic: if it looks like a key-value pair, store it
84
+ if not key.isdigit() and not value.replace(".", "").replace(",", "").isdigit():
85
+ metadata[key] = value
86
+
87
+ # Consider this a potential header if it has minimum required columns
88
+ if column_count >= min_columns:
89
+ header_candidates.append((line_num, column_count, row))
90
+
91
+ # If no multi-column rows found, assume it's a normal CSV starting at line 0
92
+ if not header_candidates:
93
+ return CsvMetadata(skip_rows=0, metadata=metadata, dialect=dialect)
94
+
95
+ # Look for the first row that has close to the maximum number of columns
96
+ # This helps distinguish metadata (usually fewer columns) from real headers (many columns)
97
+ threshold = max(min_columns, max_columns * threshold_ratio)
98
+
99
+ for line_num, column_count, _row in header_candidates:
100
+ if column_count >= threshold:
101
+ return CsvMetadata(skip_rows=line_num, metadata=metadata, dialect=dialect)
102
+
103
+ # If no clear header found but we have candidates, return the first multi-column row
104
+ first_candidate_line = header_candidates[0][0]
105
+ return CsvMetadata(skip_rows=first_candidate_line, metadata=metadata, dialect=dialect)
@@ -4,7 +4,7 @@ import asyncio
4
4
  from contextlib import AbstractAsyncContextManager
5
5
  from dataclasses import dataclass
6
6
  from types import TracebackType
7
- from typing import TYPE_CHECKING, TypeVar
7
+ from typing import TYPE_CHECKING, Any, TypeVar
8
8
 
9
9
  from strif import abbrev_str, single_line
10
10
  from typing_extensions import override
@@ -17,6 +17,7 @@ from rich.progress import BarColumn, Progress, ProgressColumn, Task, TaskID
17
17
  from rich.spinner import Spinner
18
18
  from rich.text import Text
19
19
 
20
+ from kash.config.unified_live import get_unified_live
20
21
  from kash.utils.api_utils.progress_protocol import (
21
22
  EMOJI_FAILURE,
22
23
  EMOJI_RETRY,
@@ -229,7 +230,7 @@ class TruncatedLabelColumn(ProgressColumn):
229
230
  def __init__(self, console_width: int):
230
231
  super().__init__()
231
232
  # Reserve half the console width for labels/status messages
232
- self.max_label_width = console_width // 2
233
+ self.max_label_width: int = console_width // 2
233
234
 
234
235
  @override
235
236
  def render(self, task: Task) -> Text:
@@ -298,6 +299,9 @@ class MultiTaskStatus(AbstractAsyncContextManager):
298
299
  self._next_id: int = 1
299
300
  self._rich_task_ids: dict[int, TaskID] = {} # Map our IDs to Rich Progress IDs
300
301
 
302
+ # Unified live integration
303
+ self._unified_live: Any | None = None # Reference to the global unified live
304
+
301
305
  # Calculate spinner width for consistent spacing
302
306
  self._spinner_width = _get_spinner_width(SPINNER_NAME)
303
307
 
@@ -367,7 +371,13 @@ class MultiTaskStatus(AbstractAsyncContextManager):
367
371
  @override
368
372
  async def __aenter__(self) -> MultiTaskStatus:
369
373
  """Start the live display."""
370
- self._progress.__enter__()
374
+ # Try to integrate with unified live display
375
+
376
+ # Always integrate with unified live display (auto-initialized)
377
+ unified_live = get_unified_live()
378
+ self._unified_live = unified_live
379
+ # Register our progress display with the unified live
380
+ unified_live.set_multitask_display(self._progress)
371
381
  return self
372
382
 
373
383
  @override
@@ -378,9 +388,13 @@ class MultiTaskStatus(AbstractAsyncContextManager):
378
388
  exc_tb: TracebackType | None,
379
389
  ) -> None:
380
390
  """Stop the live display and show automatic summary if enabled."""
381
- self._progress.__exit__(exc_type, exc_val, exc_tb)
391
+ # Always clean up unified live integration
392
+ if self._unified_live is not None:
393
+ # Remove our display from the unified live
394
+ self._unified_live.set_multitask_display(None)
395
+ self._unified_live = None
382
396
 
383
- # Show automatic summary if enabled
397
+ # Show automatic summary if enabled (always print to console now)
384
398
  if self.auto_summary:
385
399
  summary = self.get_summary()
386
400
  self.console.print(summary)
@@ -21,6 +21,7 @@ def normalize_formatting(
21
21
  format: Format | None,
22
22
  support_ansi: bool = True,
23
23
  cleanups: bool = True,
24
+ smartquotes: bool = True,
24
25
  ) -> str:
25
26
  """
26
27
  Normalize formatting. Currently only normalizes Markdown and leaves plaintext
@@ -35,6 +36,7 @@ def normalize_formatting(
35
36
  text,
36
37
  line_wrapper=line_wrap_by_sentence(len_fn=len_fn, is_markdown=True),
37
38
  cleanups=cleanups,
39
+ smartquotes=smartquotes,
38
40
  )
39
41
  elif format == Format.plaintext:
40
42
  # Consider plaintext a raw format and don't normalize.