kash-shell 0.3.20__py3-none-any.whl → 0.3.22__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 (40) hide show
  1. kash/actions/core/markdownify_html.py +11 -0
  2. kash/actions/core/tabbed_webpage_generate.py +2 -2
  3. kash/commands/help/assistant_commands.py +2 -4
  4. kash/commands/help/logo.py +12 -17
  5. kash/commands/help/welcome.py +5 -4
  6. kash/config/colors.py +8 -6
  7. kash/config/text_styles.py +2 -0
  8. kash/docs/markdown/topics/b1_kash_overview.md +34 -45
  9. kash/docs/markdown/warning.md +3 -3
  10. kash/docs/markdown/welcome.md +2 -1
  11. kash/exec/action_decorators.py +20 -5
  12. kash/exec/fetch_url_items.py +6 -4
  13. kash/exec/llm_transforms.py +1 -1
  14. kash/exec/preconditions.py +7 -2
  15. kash/exec/shell_callable_action.py +1 -1
  16. kash/llm_utils/llm_completion.py +1 -1
  17. kash/model/actions_model.py +6 -0
  18. kash/model/items_model.py +14 -11
  19. kash/shell/output/shell_output.py +20 -1
  20. kash/utils/api_utils/api_retries.py +305 -0
  21. kash/utils/api_utils/cache_requests_limited.py +84 -0
  22. kash/utils/api_utils/gather_limited.py +987 -0
  23. kash/utils/api_utils/progress_protocol.py +299 -0
  24. kash/utils/common/function_inspect.py +66 -1
  25. kash/utils/common/testing.py +10 -7
  26. kash/utils/rich_custom/multitask_status.py +631 -0
  27. kash/utils/text_handling/escape_html_tags.py +16 -11
  28. kash/utils/text_handling/markdown_render.py +1 -0
  29. kash/utils/text_handling/markdown_utils.py +158 -1
  30. kash/web_gen/tabbed_webpage.py +2 -2
  31. kash/web_gen/templates/base_styles.css.jinja +26 -20
  32. kash/web_gen/templates/components/toc_styles.css.jinja +1 -1
  33. kash/web_gen/templates/components/tooltip_scripts.js.jinja +171 -19
  34. kash/web_gen/templates/components/tooltip_styles.css.jinja +23 -8
  35. kash/xonsh_custom/load_into_xonsh.py +0 -3
  36. {kash_shell-0.3.20.dist-info → kash_shell-0.3.22.dist-info}/METADATA +3 -1
  37. {kash_shell-0.3.20.dist-info → kash_shell-0.3.22.dist-info}/RECORD +40 -35
  38. {kash_shell-0.3.20.dist-info → kash_shell-0.3.22.dist-info}/WHEEL +0 -0
  39. {kash_shell-0.3.20.dist-info → kash_shell-0.3.22.dist-info}/entry_points.txt +0 -0
  40. {kash_shell-0.3.20.dist-info → kash_shell-0.3.22.dist-info}/licenses/LICENSE +0 -0
@@ -1,8 +1,13 @@
1
1
  import re
2
2
  from collections.abc import Set
3
3
 
4
- HTML_IN_MD_TAGS = frozenset(["div", "span", "sup", "sub", "br", "details", "summary"])
5
- """These are tags that have reasonable usage in Markdown so typically would be preserved."""
4
+ HTML_IN_MD_TAGS = frozenset(
5
+ ["div", "span", "i", "b", "em", "sup", "sub", "br", "details", "summary"]
6
+ )
7
+ """
8
+ These are tags that have reasonable usage in Markdown so typically would be preserved.
9
+ Note we want `<i>` because it's used for icons like `<i data-feather="list"></i>`.
10
+ """
6
11
 
7
12
  ALLOWED_BARE_PROTOS = frozenset(["http://", "https://", "file://"])
8
13
 
@@ -68,7 +73,7 @@ def escape_html_tags(
68
73
  def test_escape_html_tags():
69
74
  """Tests the escape_html_tags function with various cases."""
70
75
 
71
- # 1. Basic Whitelist Check (Default)
76
+ # Basic Whitelist Check (Default)
72
77
  assert escape_html_tags("<div>Test</div>") == "<div>Test</div>"
73
78
  assert escape_html_tags("<span>Test</span>") == "<span>Test</span>"
74
79
  assert escape_html_tags("<br>") == "<br>"
@@ -77,21 +82,21 @@ def test_escape_html_tags():
77
82
  == "<details><summary>Sum</summary>Det</details>"
78
83
  )
79
84
 
80
- # 2. Basic Escape Check
85
+ # Basic Escape Check
81
86
  assert escape_html_tags("<p>Test</p>") == "&lt;p>Test&lt;/p>"
82
87
  assert escape_html_tags("<script>alert('x');</script>") == "&lt;script>alert('x');&lt;/script>"
83
88
  assert escape_html_tags("<img>") == "&lt;img>"
84
89
 
85
- # 3. Case Insensitivity
90
+ # Case Insensitivity
86
91
  assert escape_html_tags("<DiV>Case</DiV>") == "<DiV>Case</DiV>" # Whitelisted
87
92
  assert escape_html_tags("<P>Test</P>") == "&lt;P>Test&lt;/P>" # Escaped
88
93
 
89
- # 4. Self-closing tags
94
+ # Self-closing tags
90
95
  assert escape_html_tags("<br/>") == "<br/>" # Whitelisted
91
96
  assert escape_html_tags("<br />") == "<br />" # Whitelisted
92
97
  assert escape_html_tags("<img/>") == "&lt;img/>" # Escaped
93
98
 
94
- # 5. Tags with Attributes
99
+ # Tags with Attributes
95
100
  assert (
96
101
  escape_html_tags('<div class="foo">Test</div>') == '<div class="foo">Test</div>'
97
102
  ) # Whitelisted
@@ -102,7 +107,7 @@ def test_escape_html_tags():
102
107
  assert escape_html_tags('<p class="foo">Test</p>') == '&lt;p class="foo">Test&lt;/p>' # Escaped
103
108
  assert escape_html_tags('<img src="a.jpg"/>') == '&lt;img src="a.jpg"/>' # Escaped
104
109
 
105
- # 6. Markdown URL Handling
110
+ # Markdown URL Handling
106
111
  url_md = "Check <https://example.com> and <http://test.org/path>"
107
112
  assert escape_html_tags(url_md, allow_bare_md_urls=True) == url_md
108
113
  assert (
@@ -127,7 +132,7 @@ def test_escape_html_tags():
127
132
  == "&lt;/https://example.com>"
128
133
  ) # Closing URL-like is escaped
129
134
 
130
- # 7. Nested/Malformed '<' and Edge Cases
135
+ # Nested/Malformed '<' and Edge Cases
131
136
  assert escape_html_tags("<<script>>") == "&lt;&lt;script>>" # Escaped non-tag <
132
137
  assert escape_html_tags("<div><p>nested</p></div>") == "<div>&lt;p>nested&lt;/p></div>"
133
138
  assert escape_html_tags("<div<span") == "&lt;div&lt;span" # Incomplete tags are escaped
@@ -140,7 +145,7 @@ def test_escape_html_tags():
140
145
  assert escape_html_tags("< >") == "&lt; >"
141
146
  assert escape_html_tags("< / div >") == "< / div >" # Whitelisted closing tag with spaces
142
147
 
143
- # 8. Mixed Content Combination
148
+ # Mixed Content Combination
144
149
  complex_html = "<DiV class='A'>Hello <Br/> <p>World</p> <https://link.com> </DiV>"
145
150
  expected_complex_allowed = (
146
151
  "<DiV class='A'>Hello <Br/> &lt;p>World&lt;/p> <https://link.com> </DiV>"
@@ -151,6 +156,6 @@ def test_escape_html_tags():
151
156
  assert escape_html_tags(complex_html, allow_bare_md_urls=True) == expected_complex_allowed
152
157
  assert escape_html_tags(complex_html, allow_bare_md_urls=False) == expected_complex_disallowed
153
158
 
154
- # 9. Empty/No Tags
159
+ # Empty/No Tags
155
160
  assert escape_html_tags("") == ""
156
161
  assert escape_html_tags("Just plain text, no tags.") == "Just plain text, no tags."
@@ -33,6 +33,7 @@ MARKO_GFM = marko.Markdown(
33
33
 
34
34
 
35
35
  FOOTNOTE_UP_ARROW = "&nbsp;↑&nbsp;"
36
+ FOOTNOTE_DOWN_ARROW = "&nbsp;↓&nbsp;"
36
37
 
37
38
 
38
39
  def html_postprocess(html: str) -> str:
@@ -6,7 +6,7 @@ from typing import Any, TypeAlias
6
6
  import marko
7
7
  import regex
8
8
  from marko.block import Heading, ListItem
9
- from marko.inline import Link
9
+ from marko.inline import AutoLink, Link
10
10
 
11
11
  from kash.utils.common.url import Url
12
12
 
@@ -64,6 +64,9 @@ def _tree_links(element, include_internal=False):
64
64
  case Link():
65
65
  if include_internal or not element.dest.startswith("#"):
66
66
  links.append(element.dest)
67
+ case AutoLink():
68
+ if include_internal or not element.dest.startswith("#"):
69
+ links.append(element.dest)
67
70
  case _:
68
71
  if hasattr(element, "children"):
69
72
  for child in element.children:
@@ -710,3 +713,157 @@ def test_markdown_utils_exceptions() -> None:
710
713
  result_with_internal = extract_links(content, include_internal=True)
711
714
  assert "https://example.com" in result_with_internal
712
715
  assert "#section" in result_with_internal
716
+
717
+
718
+ def test_extract_links_comprehensive() -> None:
719
+ """Test extract_links with various link formats including bare links and footnotes."""
720
+
721
+ # Test regular markdown links
722
+ regular_links = "Check out [this link](https://example.com) and [another](https://test.com)"
723
+ result = extract_links(regular_links)
724
+ assert "https://example.com" in result
725
+ assert "https://test.com" in result
726
+ assert len(result) == 2
727
+
728
+ # Test bare/autolinks in angle brackets
729
+ bare_links = "Visit <https://google.com> and also <https://github.com>"
730
+ result_bare = extract_links(bare_links)
731
+ assert "https://google.com" in result_bare
732
+ assert "https://github.com" in result_bare
733
+ assert len(result_bare) == 2
734
+
735
+ # Test autolinks without brackets (expected to not work with standard markdown)
736
+ auto_links = "Visit https://stackoverflow.com or http://reddit.com"
737
+ result_auto = extract_links(auto_links)
738
+ assert (
739
+ result_auto == []
740
+ ) # Plain URLs without brackets aren't parsed as links in standard markdown
741
+
742
+ # Test GFM footnotes (the original issue)
743
+ footnote_content = """
744
+ [^109]: What Is The Future Of Ketamine Therapy For Mental Health Treatment?
745
+ - The Ko-Op, accessed June 28, 2025,
746
+ <https://psychedelictherapists.co/blog/the-future-of-ketamine-assisted-psychotherapy/>
747
+ """
748
+ result_footnote = extract_links(footnote_content)
749
+ assert (
750
+ "https://psychedelictherapists.co/blog/the-future-of-ketamine-assisted-psychotherapy/"
751
+ in result_footnote
752
+ )
753
+ assert len(result_footnote) == 1
754
+
755
+ # Test mixed content with all types (excluding reference-style which has parsing conflicts with footnotes)
756
+ mixed_content = """
757
+ # Header
758
+
759
+ Regular link: [Example](https://example.com)
760
+ Bare link: <https://bare-link.com>
761
+ Auto link: https://auto-link.com
762
+
763
+ [^1]: Footnote with [regular link](https://footnote-regular.com)
764
+ [^2]: Footnote with bare link <https://footnote-bare.com>
765
+ """
766
+ result_mixed = extract_links(mixed_content)
767
+ expected_links = [
768
+ "https://example.com", # Regular link
769
+ "https://bare-link.com", # Bare link
770
+ "https://footnote-regular.com", # Link in footnote
771
+ "https://footnote-bare.com", # Bare link in footnote
772
+ ]
773
+ for link in expected_links:
774
+ assert link in result_mixed, f"Missing expected link: {link}"
775
+ # Should not include plain auto link (https://auto-link.com) as it's not in angle brackets
776
+ assert "https://auto-link.com" not in result_mixed
777
+ assert len(result_mixed) == len(expected_links)
778
+
779
+
780
+ def test_extract_bare_links() -> None:
781
+ """Test extraction of bare links in angle brackets."""
782
+ content = "Visit <https://example.com> and <https://github.com/user/repo> for more info"
783
+ result = extract_links(content)
784
+ assert "https://example.com" in result
785
+ assert "https://github.com/user/repo" in result
786
+ assert len(result) == 2
787
+
788
+
789
+ def test_extract_footnote_links() -> None:
790
+ """Test extraction of links within footnotes."""
791
+ content = dedent("""
792
+ Main text with reference[^1].
793
+
794
+ [^1]: This footnote has a [regular link](https://example.com) and <https://bare-link.com>
795
+ """)
796
+ result = extract_links(content)
797
+ assert "https://example.com" in result
798
+ assert "https://bare-link.com" in result
799
+ assert len(result) == 2
800
+
801
+
802
+ def test_extract_reference_style_links() -> None:
803
+ """Test extraction of reference-style links."""
804
+ content = dedent("""
805
+ Check out [this article][ref1] and [this other one][ref2].
806
+
807
+ [ref1]: https://example.com/article1
808
+ [ref2]: https://example.com/article2
809
+ """)
810
+ result = extract_links(content)
811
+ assert "https://example.com/article1" in result
812
+ assert "https://example.com/article2" in result
813
+ assert len(result) == 2
814
+
815
+
816
+ def test_extract_links_with_internal_fragments() -> None:
817
+ """Test that internal fragment links are excluded by default but included when requested."""
818
+ content = dedent("""
819
+ See [this section](#introduction) and [external link](https://example.com).
820
+ Also check [another section](#conclusion) here.
821
+ """)
822
+
823
+ # Default behavior: exclude internal links
824
+ result = extract_links(content)
825
+ assert "https://example.com" in result
826
+ assert "#introduction" not in result
827
+ assert "#conclusion" not in result
828
+ assert len(result) == 1
829
+
830
+ # Include internal links
831
+ result_with_internal = extract_links(content, include_internal=True)
832
+ assert "https://example.com" in result_with_internal
833
+ assert "#introduction" in result_with_internal
834
+ assert "#conclusion" in result_with_internal
835
+ assert len(result_with_internal) == 3
836
+
837
+
838
+ def test_extract_links_mixed_real_world() -> None:
839
+ """Test with a realistic mixed document containing various link types."""
840
+ content = dedent("""
841
+ # Research Article
842
+
843
+ This study examines ketamine therapy[^109] and references multiple sources.
844
+
845
+ ## Methods
846
+
847
+ We reviewed literature from [PubMed](https://pubmed.ncbi.nlm.nih.gov)
848
+ and other databases <https://scholar.google.com>.
849
+
850
+ For protocol details, see [our methodology][methodology].
851
+
852
+ [methodology]: https://research.example.com/protocol
853
+
854
+ [^109]: What Is The Future Of Ketamine Therapy For Mental Health Treatment?
855
+ - The Ko-Op, accessed June 28, 2025,
856
+ <https://psychedelictherapists.co/blog/the-future-of-ketamine-assisted-psychotherapy/>
857
+ """)
858
+
859
+ result = extract_links(content)
860
+ expected_links = [
861
+ "https://pubmed.ncbi.nlm.nih.gov",
862
+ "https://scholar.google.com",
863
+ "https://research.example.com/protocol",
864
+ "https://psychedelictherapists.co/blog/the-future-of-ketamine-assisted-psychotherapy/",
865
+ ]
866
+
867
+ for link in expected_links:
868
+ assert link in result, f"Missing expected link: {link}"
869
+ assert len(result) == len(expected_links)
@@ -82,7 +82,7 @@ def tabbed_webpage_config(
82
82
 
83
83
  config_item = Item(
84
84
  title=f"{title} (config)",
85
- type=ItemType.config,
85
+ type=ItemType.data,
86
86
  format=Format.yaml,
87
87
  body=to_yaml_string(asdict(config)),
88
88
  )
@@ -108,7 +108,7 @@ def tabbed_webpage_generate(
108
108
  """
109
109
  Generate a web page using the supplied config.
110
110
  """
111
- config = config_item.read_as_config()
111
+ config = config_item.read_as_data()
112
112
  tabbed_webpage = as_dataclass(config, TabbedWebpage) # Checks the format.
113
113
 
114
114
  _load_tab_content(tabbed_webpage)
@@ -5,7 +5,7 @@
5
5
  --font-sans: "Source Sans 3 Variable", sans-serif, "Hack Nerd Font";
6
6
  --font-serif: "PT Serif", serif, "Hack Nerd Font";
7
7
  /* Source Sans 3 Variable better at these weights. */
8
- --font-weight-sans-medium: 565;
8
+ --font-weight-sans-medium: 550;
9
9
  --font-weight-sans-bold: 650;
10
10
  --font-mono: "Hack Nerd Font", "Menlo", "DejaVu Sans Mono", Consolas, "Lucida Console", monospace;
11
11
 
@@ -156,7 +156,13 @@ h4 {
156
156
  margin-bottom: 0.7rem;
157
157
  }
158
158
 
159
- h5, h6 {
159
+ h5 {
160
+ font-size: 1rem;
161
+ margin-top: 0.7rem;
162
+ margin-bottom: 0.5rem;
163
+ }
164
+
165
+ h6 {
160
166
  font-size: 1rem;
161
167
  margin-top: 0.7rem;
162
168
  margin-bottom: 0.5rem;
@@ -368,15 +374,15 @@ hr:before {
368
374
 
369
375
  .long-text h3 {
370
376
  font-family: var(--font-sans);
371
- font-weight: 565;
377
+ font-weight: 550;
372
378
  text-transform: uppercase;
373
379
  letter-spacing: 0.025em;
374
380
  }
375
381
 
376
382
  .long-text h4 {
377
- font-family: var(--font-sans);
378
- font-weight: 650;
379
- letter-spacing: 0.02em;
383
+ font-family: var(--font-serif);
384
+ font-weight: 400;
385
+ font-style: italic;
380
386
  }
381
387
 
382
388
  .long-text h5 {
@@ -435,6 +441,10 @@ hr:before {
435
441
  {% endblock long_text_styles %}
436
442
 
437
443
  {% block table_styles %}
444
+ table, th, td, tbody tr {
445
+ transition: background-color 0.4s ease-in-out, border-color 0.4s ease-in-out;
446
+ }
447
+
438
448
  table {
439
449
  font-family: var(--font-sans);
440
450
  font-size: var(--font-size-small);
@@ -453,6 +463,7 @@ th {
453
463
  letter-spacing: 0.03em;
454
464
  border-bottom: 1px solid var(--color-border-hint);
455
465
  line-height: 1.2;
466
+ background-color: var(--color-bg-alt-solid);
456
467
  }
457
468
 
458
469
  th, td {
@@ -461,10 +472,6 @@ th, td {
461
472
  min-width: 6rem;
462
473
  }
463
474
 
464
- th {
465
- background-color: var(--color-bg-alt-solid);
466
- }
467
-
468
475
  tbody tr:nth-child(even) {
469
476
  background-color: var(--color-bg-alt-solid);
470
477
  }
@@ -480,7 +487,12 @@ tbody tr:nth-child(even) {
480
487
  transform: translateX(-50%);
481
488
  /* Prevent container from expanding beyond its content area */
482
489
  overflow-x: auto;
483
- overflow-y: visible;
490
+ overflow-y: hidden; /* Whole height of table shown, no vertical scrolling. */
491
+ }
492
+
493
+ .table-container table {
494
+ /* Tricky: Need this to prevent bogus extra horizontal scroll while keeping normal sizing */
495
+ contain: content;
484
496
  }
485
497
 
486
498
  /* When TOC is present, simplify table container positioning */
@@ -540,7 +552,6 @@ sup {
540
552
  width: calc(100vw - 6rem);
541
553
  /* Ensure container doesn't expand beyond its width */
542
554
  max-width: calc(100vw - 6rem);
543
- contain: layout inline-size;
544
555
  }
545
556
 
546
557
  /* Apply shadow to long-text containers on larger screens */
@@ -590,8 +601,6 @@ sup {
590
601
 
591
602
  /* Ensure horizontal scroll works properly without expanding container */
592
603
  overflow-x: auto;
593
- overflow-y: visible;
594
- contain: layout inline-size;
595
604
  }
596
605
 
597
606
  .content-with-toc.has-toc table {
@@ -634,20 +643,17 @@ sup {
634
643
 
635
644
  /* Make table containers scrollable without affecting page layout */
636
645
  .table-container {
637
- width: calc(100vw - 3rem); /* Fixed width instead of max-width */
638
- max-width: calc(100vw - 3rem);
639
646
  overflow-x: auto;
640
- overflow-y: visible; /* Ensure vertical overflow is not hidden */
641
647
  transform: none;
642
648
  left: 0;
643
649
  position: relative;
644
650
  margin-left: auto;
645
- margin-right: auto;
651
+ margin-right: 0; /* Extend to right edge */
646
652
  /* Prevent container from expanding beyond its width */
647
653
  box-sizing: border-box;
648
- contain: layout inline-size; /* CSS containment to prevent width expansion */
654
+ width: calc(100vw - 1.5rem); /* Full width minus left margin */
649
655
  }
650
-
656
+
651
657
  table {
652
658
  font-size: var(--font-size-smaller);
653
659
  /* Tables can be wider than container on mobile */
@@ -6,7 +6,7 @@
6
6
 
7
7
  @media (min-width: 1536px) {
8
8
  :root {
9
- --toc-width: min(21vw, 30rem);
9
+ --toc-width: min(21vw, 26rem);
10
10
  }
11
11
  }
12
12