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,9 +1,12 @@
1
+ from prettyfmt import abbrev_on_words
2
+
1
3
  from kash.config.logger import get_logger
2
4
  from kash.exec import kash_action
3
5
  from kash.exec.preconditions import has_html_body, is_url_resource
4
6
  from kash.exec.runtime_settings import current_runtime_settings
5
7
  from kash.model import Format, Item
6
8
  from kash.model.items_model import ItemType
9
+ from kash.utils.text_handling.markdown_utils import first_heading
7
10
  from kash.utils.text_handling.markdownify_utils import markdownify_custom
8
11
  from kash.web_content.file_cache_utils import get_url_html
9
12
  from kash.web_content.web_extract_readabilipy import extract_text_readabilipy
@@ -25,6 +28,14 @@ def markdownify_html(item: Item) -> Item:
25
28
  assert page_data.clean_html
26
29
  markdown_content = markdownify_custom(page_data.clean_html)
27
30
 
31
+ # Sometimes readability doesn't include the title, in which case we add it.
32
+ first_h1 = first_heading(markdown_content, allowed_tags=("h1",))
33
+ title = page_data.title and abbrev_on_words(page_data.title.strip(), 80)
34
+ if not first_h1 and title:
35
+ log.message(f"No h1 found, inserting h1: {title}")
36
+ # Insert a h1 at the top of the document
37
+ markdown_content = f"# {title}\n\n{markdown_content}"
38
+
28
39
  output_item = item.derived_copy(
29
40
  type=ItemType.doc, format=Format.markdown, body=markdown_content
30
41
  )
@@ -1,6 +1,6 @@
1
1
  from kash.config.logger import get_logger
2
2
  from kash.exec import kash_action
3
- from kash.exec.preconditions import is_config
3
+ from kash.exec.preconditions import is_data
4
4
  from kash.model import ONE_ARG, ActionInput, ActionResult, FileExt, Format, Item, ItemType, Param
5
5
  from kash.web_gen import tabbed_webpage
6
6
 
@@ -9,7 +9,7 @@ log = get_logger(__name__)
9
9
 
10
10
  @kash_action(
11
11
  expected_args=ONE_ARG,
12
- precondition=is_config,
12
+ precondition=is_data,
13
13
  params=(Param("add_title", "Add a title to the page body.", type=bool),),
14
14
  )
15
15
  def tabbed_webpage_generate(input: ActionInput, add_title: bool = False) -> ActionResult:
@@ -1,8 +1,6 @@
1
- from rich import get_console
2
-
3
1
  from kash.commands.base.basic_file_commands import trash
4
2
  from kash.commands.workspace.selection_commands import select
5
- from kash.config.logger import get_logger
3
+ from kash.config.logger import get_console, get_logger
6
4
  from kash.config.text_styles import PROMPT_ASSIST, SPINNER
7
5
  from kash.docs.all_docs import DocSelection
8
6
  from kash.exec import kash_command
@@ -45,7 +43,7 @@ def assist(
45
43
  help()
46
44
  return
47
45
 
48
- with get_console().status("Thinking…", spinner=SPINNER):
46
+ with get_console().status("Thinking…", spinner=SPINNER): # noqa: F821
49
47
  shell_context_assistance(input, model=model, assistance_type=type)
50
48
 
51
49
 
@@ -2,7 +2,8 @@ import re
2
2
  from pathlib import Path
3
3
 
4
4
  from rich.box import SQUARE
5
- from rich.console import Group
5
+ from rich.console import Group, RenderableType
6
+ from rich.padding import Padding
6
7
  from rich.panel import Panel
7
8
  from rich.text import Text
8
9
 
@@ -62,7 +63,13 @@ def color_logo() -> Group:
62
63
  )
63
64
 
64
65
 
65
- def branded_box(content: Group | None, version: str | None = None) -> Panel:
66
+ def simple_box(content: RenderableType) -> Panel:
67
+ return Panel(
68
+ content, border_style=COLOR_HINT, padding=(0, 1), width=CONSOLE_WRAP_WIDTH, box=SQUARE
69
+ )
70
+
71
+
72
+ def logo_box(content: Group | None = None) -> Padding:
66
73
  panel_width = CONSOLE_WRAP_WIDTH
67
74
 
68
75
  logo_lines = LOGO_LARGE.split("\n")
@@ -70,29 +77,17 @@ def branded_box(content: Group | None, version: str | None = None) -> Panel:
70
77
  tagline_offset = (panel_width - 4 - len(TAGLINE_STYLED)) // 2
71
78
 
72
79
  colored_lines = [logo_colorize_line(line, " ", rest_offset) for line in logo_lines]
73
- header = None
74
- if version:
75
- footer = Text(version, style=COLOR_HINT, justify="right")
76
- else:
77
- footer = None
78
80
 
79
81
  body = ["", content] if content else []
80
82
 
81
- return Panel(
83
+ return Padding(
82
84
  Group(
83
85
  Text.assemble(" " * tagline_offset, LOGO_SPACER),
84
86
  *colored_lines,
85
87
  Text.assemble(" " * tagline_offset, TAGLINE_STYLED),
86
88
  *body,
87
89
  ),
88
- title=header,
89
- title_align="center",
90
- subtitle=footer,
91
- subtitle_align="right",
92
- border_style=COLOR_HINT,
93
- padding=(0, 1),
94
- width=panel_width,
95
- box=SQUARE,
90
+ pad=(1, 1),
96
91
  )
97
92
 
98
93
 
@@ -101,7 +96,7 @@ def kash_logo(box: bool = False, svg_out: str | None = None, html_out: str | Non
101
96
  """
102
97
  Show the kash logo.
103
98
  """
104
- logo = branded_box(None) if box else color_logo()
99
+ logo = logo_box(None) if box else color_logo()
105
100
 
106
101
  cprint(logo)
107
102
 
@@ -2,7 +2,7 @@ from rich.box import SQUARE
2
2
  from rich.console import Group
3
3
  from rich.panel import Panel
4
4
 
5
- from kash.commands.help.logo import branded_box
5
+ from kash.commands.help.logo import logo_box, simple_box
6
6
  from kash.config.text_styles import (
7
7
  COLOR_HINT,
8
8
  )
@@ -20,14 +20,15 @@ def welcome() -> None:
20
20
  """
21
21
 
22
22
  help_topics = all_docs.help_topics
23
- version = get_full_version_name()
23
+
24
24
  # Create header with logo and right-justified version
25
25
 
26
26
  PrintHooks.before_welcome()
27
+ cprint(logo_box())
27
28
  cprint(
28
- branded_box(
29
+ simple_box(
29
30
  Group(Markdown(help_topics.welcome)),
30
- version,
31
31
  )
32
32
  )
33
33
  cprint(Panel(Markdown(help_topics.warning), box=SQUARE, border_style=COLOR_HINT))
34
+ cprint("%s", get_full_version_name())
kash/config/colors.py CHANGED
@@ -139,14 +139,15 @@ web_light_translucent = SimpleNamespace(
139
139
  bg_header=hsl_to_hex("hsla(188, 42%, 70%, 0.2)"),
140
140
  bg_alt=hsl_to_hex("hsla(39, 24%, 90%, 0.3)"),
141
141
  bg_alt_solid=hsl_to_hex("hsla(39, 24%, 97%, 1)"),
142
- bg_selected=hsl_to_hex("hsla(188, 44%, 94%, 0.95)"),
142
+ bg_meta_solid=hsl_to_hex("hsla(39, 24%, 94%, 1)"),
143
+ bg_selected=hsl_to_hex("hsla(188, 21%, 94%, 0.9)"),
143
144
  text=hsl_to_hex("hsl(188, 39%, 11%)"),
144
145
  code=hsl_to_hex("hsl(44, 38%, 23%)"),
145
146
  border=hsl_to_hex("hsl(188, 8%, 50%)"),
146
147
  border_hint=hsl_to_hex("hsla(188, 8%, 72%, 0.3)"),
147
148
  border_accent=hsl_to_hex("hsla(305, 18%, 65%, 0.85)"),
148
149
  hover=hsl_to_hex("hsl(188, 12%, 84%)"),
149
- hover_bg=hsl_to_hex("hsla(188, 44%, 94%, 1)"),
150
+ hover_bg=hsl_to_hex("hsla(188, 18%, 97%, 1)"),
150
151
  hint=hsl_to_hex("hsl(188, 11%, 65%)"),
151
152
  hint_strong=hsl_to_hex("hsl(188, 11%, 46%)"),
152
153
  hint_gentle=hsl_to_hex("hsla(188, 11%, 65%, 0.2)"),
@@ -165,14 +166,15 @@ web_light_translucent = SimpleNamespace(
165
166
  web_dark_translucent = SimpleNamespace(
166
167
  primary=hsl_to_hex("hsl(188, 40%, 62%)"),
167
168
  primary_light=hsl_to_hex("hsl(188, 50%, 72%)"),
168
- secondary=hsl_to_hex("hsl(188, 12%, 65%)"),
169
- tertiary=hsl_to_hex("hsl(188, 7%, 40%)"),
169
+ secondary=hsl_to_hex("hsl(188, 12%, 70%)"),
170
+ tertiary=hsl_to_hex("hsl(188, 7%, 45%)"),
170
171
  bg=hsl_to_hex("hsla(220, 14%, 7%, 0.95)"),
171
172
  bg_solid=hsl_to_hex("hsl(220, 14%, 7%)"),
172
173
  bg_header=hsl_to_hex("hsla(188, 42%, 20%, 0.3)"),
173
174
  bg_alt=hsl_to_hex("hsla(220, 14%, 12%, 0.5)"),
174
- bg_alt_solid=hsl_to_hex("hsl(220, 14%, 12%)"),
175
- bg_selected=hsl_to_hex("hsla(188, 12%, 50%, 0.95)"),
175
+ bg_alt_solid=hsl_to_hex("hsl(220, 15%, 16%)"),
176
+ bg_meta_solid=hsl_to_hex("hsl(220, 14%, 25%)"),
177
+ bg_selected=hsl_to_hex("hsla(188, 13%, 33%, 0.95)"),
176
178
  text=hsl_to_hex("hsl(188, 10%, 90%)"),
177
179
  code=hsl_to_hex("hsl(44, 38%, 72%)"),
178
180
  border=hsl_to_hex("hsl(188, 8%, 25%)"),
@@ -277,6 +277,8 @@ EMOJI_HELP = "?"
277
277
 
278
278
  EMOJI_ACTION = "⛭"
279
279
 
280
+ EMOJI_TASK = "⚒"
281
+
280
282
  EMOJI_COMMAND = "⧁" # More ideas: ⦿⧁⧀⦿⦾⟐⦊⟡
281
283
 
282
284
  EMOJI_SHELL = "⦊"
@@ -165,48 +165,37 @@ works on readable text such as Markdown.
165
165
  This catches errors and allows you to find actions that might apply to a given selected
166
166
  set of items using `suggest_actions`.
167
167
 
168
- ### Programmatic Use
169
-
170
- Since commands and actions are really just Python functions.
171
-
172
- ### Useful Features
173
-
174
- Kash makes a few kinds of messy text manipulations easier:
175
-
176
- - Reusable LLM actions: A common kind of action is to invoke an LLM (like GPT-4o or o1)
177
- on a text item, with a given system and user prompt template.
178
- New LLM actions can be added with a few lines of Python by subclassing an action base
179
- class, typically `Action`, `CachedItemAction` (for any action that doesn't need to be
180
- rerun if it has the same single output), `CachedLLMAction` (if it also is performing
181
- an LLM-based transform), or `ChunkedLLMAction` (if it will be processing a document
182
- broken into <div class="chunk"> elements).
183
-
184
- - Sliding window transformations: LLMs can have trouble processing large inputs, not
185
- just because of context window and because they may make more mistakes when making
186
- lots of changes at once.
187
- Kash supports running actions in a sliding window across the document, then stitching
188
- the results back together when done.
189
-
190
- - Checking and enforcing changes: LLMs do not reliably do what they are asked to do.
191
- So a key part of making them useful is to save outputs at each step of the way and
192
- have a way to review their outputs or provide guardrails on what they can do with
193
- content.
194
-
195
- - Fine-grainded diffs with word tokens: Documents can be represented at the word level,
196
- using “word tokens” to represent words and normalized whitespace (word, sentence, and
197
- paragraph breaks, but not line breaks).
198
- This allows diffs of similar documents regardless of formatting.
199
- For example, it is possible to ask an LLM only to add paragraph breaks, then drop any
200
- other changes it makes to other words.
201
- You can use this intelligent matching of words to “backfill” specific content from one
202
- doc into an edited document, such as pulling timestamps from a full transcript back
203
- into an edited transcript or summary.
204
-
205
- - Paragraph and sentence operations: A lot of operations within actions should be done
206
- in chunks at the paragraph or sentence level.
207
- Kash offers simple tools to subdivide documents into paragraphs and sentences and
208
- these can be used together with sliding windows to process large documents.
209
-
210
- In addition, there are built-in kash commands that are part of the kash tool itself.
211
- These allow you to list items in the workspace, see or change the current selection,
212
- archive items, view logs, etc.
168
+ ### Programmatic Usage
169
+
170
+ Kash can be used entirely programmatically, so that actions are called just like
171
+ functions from Python, but the additional functionality of the items model, saving files
172
+ to a workspace, and so on, are all automatic.
173
+
174
+ This means you can use Kash to build your own CLI apps much more quickly.
175
+
176
+ For an example of this, see [textpress](https://github.com/jlevy/textpress), which wraps
177
+ quite a few kash actions to allow clean publishing of docx or PDF files on
178
+ [textpress.md](https://textpress.md/).
179
+
180
+ ### Utilities and Supporting Libraries
181
+
182
+ Kash includes a number of utility libraries to help with common tasks, either in the
183
+ base `kash-shell` package or or smaller dependencies:
184
+
185
+ - See [frontmatter-format](https://github.com/jlevy/frontmatter-format) for the spec and
186
+ implementation we use of frontmatter YAML format.
187
+
188
+ - See
189
+ [utils/file_utils](https://github.com/jlevy/kash/tree/main/src/kash/utils/file_utils)
190
+ for file format detection, conversion, filename handling, etc.
191
+
192
+ - See [chopdiff](https://github.com/jlevy/chopdiff) for a simple text doc data model
193
+ that includes sentences and paragraphs and fairly advanced diffing, filtered diffing,
194
+ and windowed transformations of text via LLM calls.
195
+
196
+ - See [clideps](https://github.com/jlevy/clideps) for utilities for helping with dot-env
197
+ files, API key setup, and dependency checks.
198
+
199
+ - See [utils/common](https://github.com/jlevy/kash/tree/main/src/kash/utils/common) the
200
+ rest of [utils/](https://github.com/jlevy/kash/tree/main/src/kash/utils) for a variety
201
+ of other general utilities.
@@ -1,3 +1,3 @@
1
- **Important:** *Kash will not execute commands without confirmation.
2
- But this is a shell so commands can be destructive or dangerous.
3
- Review commands carefully!*
1
+ **Important:** This is a shell.
2
+ Commands can be destructive.
3
+ Review commands carefully!
@@ -1,5 +1,6 @@
1
- **Welcome to kash!** Use `help` for the manual and full list of available commands.
1
+ **Welcome to Kash!**
2
2
 
3
+ Use `help` for the manual and full list of available commands.
3
4
  Press **tab** for contextual autocomplete of commands, questions, actions, and files.
4
5
  You may simply ask a question and the kash assistant will help you.
5
6
  Press **space** (or type **?**), then write your question or request.
@@ -105,7 +105,10 @@ def kash_action_class(cls: type[A]) -> type[A]:
105
105
 
106
106
 
107
107
  def _register_dynamic_action(
108
- action_cls: type[A], action_name: str, action_description: str, source_path: Path | None
108
+ action_cls: type[A],
109
+ action_name: str,
110
+ action_description: str,
111
+ source_path: Path | None,
109
112
  ) -> type[A]:
110
113
  # Set class fields for name and description for convenience.
111
114
  action_cls.name = action_name
@@ -206,6 +209,7 @@ def kash_action(
206
209
  run_per_item: bool | None = None,
207
210
  uses_selection: bool = True,
208
211
  interactive_input: bool = False,
212
+ live_output: bool = False,
209
213
  mcp_tool: bool = False,
210
214
  title_template: TitleTemplate = TitleTemplate("{title}"),
211
215
  llm_options: LLMOptions = LLMOptions(),
@@ -235,13 +239,17 @@ def kash_action(
235
239
  def decorator(orig_func: AF) -> AF:
236
240
  if hasattr(orig_func, "__action_class__"):
237
241
  log.warning(
238
- "Function `%s` is already decorated with `@kash_action`", orig_func.__name__
242
+ "Function `%s` is already decorated with `@kash_action`",
243
+ orig_func.__name__,
239
244
  )
240
245
  return orig_func
241
246
 
242
247
  # Inspect and sanity check the formal params.
243
248
  func_params = inspect_function_params(orig_func)
244
- if len(func_params) == 0 or func_params[0].effective_type not in (ActionInput, Item):
249
+ if len(func_params) == 0 or func_params[0].effective_type not in (
250
+ ActionInput,
251
+ Item,
252
+ ):
245
253
  raise InvalidDefinition(
246
254
  f"Decorator `@kash_action` requires exactly one positional parameter, "
247
255
  f"`input` of type `ActionInput` or `Item` on function `{orig_func.__name__}` but "
@@ -311,6 +319,7 @@ def kash_action(
311
319
  self.uses_selection = uses_selection
312
320
  self.output_type = output_type
313
321
  self.interactive_input = interactive_input
322
+ self.live_output = live_output
314
323
  self.mcp_tool = mcp_tool
315
324
  self.title_template = title_template
316
325
  self.llm_options = llm_options
@@ -332,8 +341,14 @@ def kash_action(
332
341
  kw_args[fp.name] = self.get_param(fp.name)
333
342
 
334
343
  if self.params:
335
- log.info("Action function param declarations:\n%s", fmt_lines(self.params))
336
- log.info("Action function param values:\n%s", self.param_value_summary_str())
344
+ log.info(
345
+ "Action function param declarations:\n%s",
346
+ fmt_lines(self.params),
347
+ )
348
+ log.info(
349
+ "Action function param values:\n%s",
350
+ self.param_value_summary_str(),
351
+ )
337
352
  else:
338
353
  log.info("Action function has no declared params")
339
354
 
@@ -48,9 +48,10 @@ def fetch_url_item_content(item: Item, *, save_content: bool = True, refetch: bo
48
48
  from kash.workspaces import current_ws
49
49
 
50
50
  ws = current_ws()
51
- if not refetch and item.title and item.description:
51
+ if not refetch and item.title and item.description and item.body:
52
52
  log.message(
53
- "Already have title and description, will not fetch metadata: %s", item.fmt_loc()
53
+ "Already have title, description, and body, will not fetch: %s",
54
+ item.fmt_loc(),
54
55
  )
55
56
  return item
56
57
 
@@ -100,10 +101,11 @@ def fetch_url_item_content(item: Item, *, save_content: bool = True, refetch: bo
100
101
  # Now save the updated URL item and also the content item if we have one.
101
102
  ws.save(url_item)
102
103
  assert url_item.store_path
103
- log.debug("Saved URL item: %s", url_item.fmt_loc())
104
104
  if content_item:
105
105
  ws.save(content_item)
106
106
  assert content_item.store_path
107
- log.debug("Saved content item: %s", content_item.fmt_loc())
107
+ log.info("Saved content item: %s", content_item.fmt_loc())
108
+ else:
109
+ log.info("Saved URL item: %s", url_item.fmt_loc())
108
110
 
109
111
  return content_item or url_item
@@ -68,7 +68,7 @@ def llm_transform_str(options: LLMOptions, input_str: str, check_no_results: boo
68
68
  diff_filter=options.diff_filter,
69
69
  ).reassemble()
70
70
  else:
71
- log.message(
71
+ log.info(
72
72
  "Running simple LLM transform action %s with model %s",
73
73
  options.op_name,
74
74
  options.model.litellm_name,
@@ -48,8 +48,13 @@ def is_concept(item: Item) -> bool:
48
48
 
49
49
 
50
50
  @kash_precondition
51
- def is_config(item: Item) -> bool:
52
- return item.type == ItemType.config
51
+ def is_data(item: Item) -> bool:
52
+ return item.type == ItemType.data
53
+
54
+
55
+ @kash_precondition
56
+ def is_table(item: Item) -> bool:
57
+ return item.type == ItemType.table
53
58
 
54
59
 
55
60
  @kash_precondition
@@ -56,7 +56,7 @@ class ShellCallableAction:
56
56
 
57
57
  log.info("Action shell args: %s", shell_args)
58
58
  explicit_values = RawParamValues(shell_args.options)
59
- if not action.interactive_input:
59
+ if not action.interactive_input and not action.live_output:
60
60
  with get_console().status(f"Running action {action.name}…", spinner=SPINNER):
61
61
  result = run_action_with_shell_context(
62
62
  action_cls,
@@ -173,7 +173,7 @@ def llm_template_completion(
173
173
  )
174
174
 
175
175
  if check_no_results and is_no_results(result.content):
176
- log.message("No results for LLM transform, will ignore: %r", result.content)
176
+ log.info("No results for LLM transform, will ignore: %r", result.content)
177
177
  result.content = ""
178
178
 
179
179
  return result
@@ -270,6 +270,12 @@ class Action(ABC):
270
270
  Does this action ask for input interactively?
271
271
  """
272
272
 
273
+ live_output: bool = False
274
+ """
275
+ Does this action have live output (e.g., progress bars, spinners)?
276
+ If True, the shell should not show its own status spinner.
277
+ """
278
+
273
279
  mcp_tool: bool = False
274
280
  """
275
281
  If True, this action is published as an MCP tool.
kash/model/items_model.py CHANGED
@@ -55,7 +55,8 @@ class ItemType(Enum):
55
55
  concept = "concept"
56
56
  resource = "resource"
57
57
  asset = "asset"
58
- config = "config"
58
+ data = "data"
59
+ table = "table"
59
60
  export = "export"
60
61
  chat = "chat"
61
62
  extension = "extension"
@@ -86,7 +87,9 @@ class ItemType(Enum):
86
87
  Format.diff: ItemType.doc,
87
88
  Format.python: ItemType.extension,
88
89
  Format.json: ItemType.doc,
89
- Format.csv: ItemType.doc,
90
+ Format.csv: ItemType.table,
91
+ Format.xlsx: ItemType.table,
92
+ Format.npz: ItemType.table,
90
93
  Format.log: ItemType.log,
91
94
  Format.pdf: ItemType.resource,
92
95
  Format.jpeg: ItemType.asset,
@@ -646,7 +649,7 @@ class Item:
646
649
  body_text = abbrev_str(self.body_text(), max_len)
647
650
 
648
651
  # Just for aesthetics, especially for titles of chat files.
649
- if self.type in [ItemType.chat, ItemType.config] or self.format == Format.yaml:
652
+ if self.type in [ItemType.chat, ItemType.data] or self.format == Format.yaml:
650
653
  try:
651
654
  yaml_obj = list(new_yaml().load_all(self.body_text()))
652
655
  if len(yaml_obj) > 0:
@@ -663,16 +666,16 @@ class Item:
663
666
  """
664
667
  return bool(self.body and self.body.strip())
665
668
 
666
- def read_as_config(self) -> Any:
669
+ def read_as_data(self) -> Any:
667
670
  """
668
- If it is a config Item, return the parsed YAML.
671
+ If it is a data Item, return the parsed YAML.
669
672
  """
670
- if not self.type == ItemType.config:
671
- raise FileFormatError(f"Item is not a config: {self}")
673
+ if not self.type == ItemType.data:
674
+ raise FileFormatError(f"Item is not a data item: {self}")
672
675
  if not self.body:
673
- raise FileFormatError(f"Config item has no body: {self}")
676
+ raise FileFormatError(f"Data item has no body: {self}")
674
677
  if self.format != Format.yaml:
675
- raise FileFormatError(f"Config item is not YAML: {self.format}: {self}")
678
+ raise FileFormatError(f"Data item is not YAML: {self.format}: {self}")
676
679
  return from_yaml_string(self.body)
677
680
 
678
681
  def get_filename(self) -> str | None:
@@ -709,8 +712,8 @@ class Item:
709
712
  elif self.type == ItemType.script:
710
713
  # Same for kash/xonsh scripts.
711
714
  return f"{self.type.value}.{FileExt.xsh.value}"
712
- elif self.type == ItemType.export:
713
- # For exports, skip the item type to keep it maximally compatible for external tools.
715
+ elif self.type in [ItemType.export, ItemType.data, ItemType.table]:
716
+ # For exports, data, and tables, skip the item type to keep it maximally compatible for external tools.
714
717
  return f"{self.get_file_ext().value}"
715
718
  else:
716
719
  return f"{self.type.value}.{self.get_file_ext().value}"
@@ -4,7 +4,7 @@ Output to the shell UI. These are for user interaction, not logging.
4
4
 
5
5
  import contextvars
6
6
  from collections.abc import Callable
7
- from contextlib import contextmanager
7
+ from contextlib import contextmanager, nullcontext
8
8
  from enum import Enum, auto
9
9
 
10
10
  import rich
@@ -28,6 +28,7 @@ from kash.config.text_styles import (
28
28
  STYLE_HINT,
29
29
  )
30
30
  from kash.shell.output.kmarkdown import KMarkdown
31
+ from kash.utils.rich_custom.multitask_status import MultiTaskStatus, StatusSettings
31
32
  from kash.utils.rich_custom.rich_indent import Indent
32
33
  from kash.utils.rich_custom.rich_markdown_fork import Markdown
33
34
 
@@ -80,6 +81,24 @@ def console_pager(use_pager: bool = True):
80
81
  PrintHooks.after_pager()
81
82
 
82
83
 
84
+ def multitask_status(
85
+ settings: StatusSettings | None = None, *, auto_summary: bool = True, enabled: bool = True
86
+ ) -> MultiTaskStatus | nullcontext:
87
+ """
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.
91
+ """
92
+ if not enabled:
93
+ return nullcontext()
94
+
95
+ return MultiTaskStatus(
96
+ console=get_console(),
97
+ settings=settings,
98
+ auto_summary=auto_summary,
99
+ )
100
+
101
+
83
102
  null_style = rich.style.Style.null()
84
103
 
85
104