kash-shell 0.3.15__py3-none-any.whl → 0.3.16__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.
@@ -27,7 +27,9 @@ def render_as_html(input: ActionInput, no_title: bool = False) -> ActionResult:
27
27
  """
28
28
  if len(input.items) == 1:
29
29
  input_item = input.items[0]
30
- html_body = simple_webpage_render(input_item, add_title_h1=not no_title)
30
+ html_body = simple_webpage_render(
31
+ input_item, add_title_h1=not no_title, show_theme_toggle=True
32
+ )
31
33
  result_item = input_item.derived_copy(
32
34
  type=ItemType.export, format=Format.html, body=html_body
33
35
  )
@@ -17,7 +17,9 @@ def tabbed_webpage_generate(input: ActionInput, add_title: bool = False) -> Acti
17
17
  Generate a tabbed web page from a config item for the tabbed template.
18
18
  """
19
19
  config_item = input.items[0]
20
- html = tabbed_webpage.tabbed_webpage_generate(config_item, add_title_h1=add_title)
20
+ html = tabbed_webpage.tabbed_webpage_generate(
21
+ config_item, add_title_h1=add_title, show_theme_toggle=True
22
+ )
21
23
 
22
24
  webpage_item = Item(
23
25
  title=config_item.title,
kash/config/colors.py CHANGED
@@ -148,12 +148,40 @@ web_light_translucent = SimpleNamespace(
148
148
  tooltip_bg=hsl_to_hex("hsla(188, 6%, 37%, 0.7)"),
149
149
  popover_bg=hsl_to_hex("hsla(188, 6%, 37%, 0.7)"),
150
150
  bright=hsl_to_hex("hsl(134, 43%, 60%)"),
151
+ success=hsl_to_hex("hsl(134, 43%, 60%)"),
151
152
  selection="hsla(225, 61%, 82%, 0.80)",
152
153
  scrollbar=hsl_to_hex("hsla(189, 12%, 55%, 0.9)"),
153
154
  scrollbar_hover=hsl_to_hex("hsla(190, 12%, 38%, 0.9)"),
154
155
  )
155
156
 
156
157
 
158
+ # Web dark colors
159
+ web_dark_translucent = SimpleNamespace(
160
+ primary=hsl_to_hex("hsl(188, 40%, 62%)"),
161
+ primary_light=hsl_to_hex("hsl(188, 50%, 72%)"),
162
+ secondary=hsl_to_hex("hsl(188, 12%, 65%)"),
163
+ bg=hsl_to_hex("hsla(220, 14%, 7%, 0.95)"),
164
+ bg_solid=hsl_to_hex("hsl(220, 14%, 7%)"),
165
+ bg_header=hsl_to_hex("hsla(188, 42%, 20%, 0.3)"),
166
+ bg_alt=hsl_to_hex("hsla(220, 14%, 12%, 0.5)"),
167
+ bg_alt_solid=hsl_to_hex("hsl(220, 14%, 12%)"),
168
+ text=hsl_to_hex("hsl(188, 20%, 90%)"),
169
+ border=hsl_to_hex("hsl(188, 8%, 25%)"),
170
+ border_hint=hsl_to_hex("hsla(188, 8%, 35%, 0.7)"),
171
+ border_accent=hsl_to_hex("hsla(305, 30%, 55%, 0.85)"),
172
+ hover=hsl_to_hex("hsl(188, 12%, 35%)"),
173
+ hover_bg=hsl_to_hex("hsla(188, 20%, 25%, 0.4)"),
174
+ hint=hsl_to_hex("hsl(188, 11%, 55%)"),
175
+ tooltip_bg=hsl_to_hex("hsla(188, 6%, 20%, 0.9)"),
176
+ popover_bg=hsl_to_hex("hsla(188, 6%, 20%, 0.9)"),
177
+ bright=hsl_to_hex("hsl(134, 43%, 60%)"),
178
+ success=hsl_to_hex("hsl(134, 43%, 60%)"),
179
+ selection=hsl_to_hex("hsla(225, 61%, 40%, 0.40)"),
180
+ scrollbar=hsl_to_hex("hsla(189, 12%, 35%, 0.9)"),
181
+ scrollbar_hover=hsl_to_hex("hsla(190, 12%, 50%, 0.9)"),
182
+ )
183
+
184
+
157
185
  rich_terminal_dark = TerminalTheme(
158
186
  hex_to_int(terminal_dark.background),
159
187
  hex_to_int(terminal_dark.foreground),
@@ -211,9 +239,6 @@ rich_terminal_light = TerminalTheme(
211
239
  # We default to light colors for Rich content in HTML.
212
240
  rich_terminal = rich_terminal_light
213
241
 
214
- # Only support light web colors for now.
215
- web = web_light_translucent
216
-
217
242
  # Logical colors
218
243
  logical = SimpleNamespace(
219
244
  concept_dark=terminal.green_dark,
@@ -234,18 +259,23 @@ logical = SimpleNamespace(
234
259
  )
235
260
 
236
261
 
237
- def consolidate_color_vars(overrides: dict[str, str] | None = None) -> dict[str, str]:
262
+ def consolidate_color_vars(
263
+ overrides: dict[str, str] | None = None, web_colors: SimpleNamespace | None = None
264
+ ) -> dict[str, str]:
238
265
  """
239
266
  Consolidate all color variables into a single dictionary with appropriate prefixes.
240
267
  Terminal variables have no prefix, while web and logical variables have "color-" prefix.
241
268
  """
242
269
  if overrides is None:
243
270
  overrides = {}
271
+ if web_colors is None:
272
+ web_colors = web_light_translucent
273
+
244
274
  return {
245
275
  # Terminal variables (no prefix)
246
276
  **terminal.__dict__,
247
277
  # Web and logical variables with "color-" prefix
248
- **{f"color-{k}": v for k, v in web.__dict__.items()},
278
+ **{f"color-{k}": v for k, v in web_colors.__dict__.items()},
249
279
  **{f"color-{k}": v for k, v in logical.__dict__.items()},
250
280
  # Overrides take precedence (assume they already have correct prefixes)
251
281
  **overrides,
@@ -262,19 +292,63 @@ def normalize_var_names(variables: dict[str, str]) -> dict[str, str]:
262
292
 
263
293
  def generate_css_vars(overrides: dict[str, str] | None = None) -> str:
264
294
  """
265
- Generate CSS variables for the terminal and web colors.
295
+ Generate CSS variables for terminal and both light and dark themes.
266
296
  """
267
297
  if overrides is None:
268
298
  overrides = {}
269
- normalized_vars = normalize_var_names(consolidate_color_vars(overrides))
270
299
 
271
- # Generate the CSS.
272
- css_variables = ":root {\n"
273
- for name, value in normalized_vars.items():
274
- css_variables += f" --{name}: {value};\n"
275
- css_variables += "}"
300
+ # Get base variables (terminal colors stay the same)
301
+ base_vars = normalize_var_names({k: v for k, v in terminal.__dict__.items()})
302
+
303
+ # Get light theme color variables
304
+ light_color_vars = normalize_var_names(
305
+ {f"color-{k}": v for k, v in web_light_translucent.__dict__.items()}
306
+ )
307
+ light_color_vars.update(
308
+ normalize_var_names({f"color-{k}": v for k, v in logical.__dict__.items()})
309
+ )
310
+
311
+ # Get dark theme color variables
312
+ dark_color_vars = normalize_var_names(
313
+ {f"color-{k}": v for k, v in web_dark_translucent.__dict__.items()}
314
+ )
315
+ dark_color_vars.update(
316
+ normalize_var_names({f"color-{k}": v for k, v in logical.__dict__.items()})
317
+ )
276
318
 
277
- return css_variables
319
+ # Apply overrides
320
+ if overrides:
321
+ normalized_overrides = normalize_var_names(overrides)
322
+ light_color_vars.update(normalized_overrides)
323
+ dark_color_vars.update(normalized_overrides)
324
+
325
+ # Generate CSS
326
+ css_parts = []
327
+
328
+ # Root with all variables (defaults to light)
329
+ css_parts.append(":root {")
330
+ css_parts.extend(f" --{k}: {v};" for k, v in base_vars.items())
331
+ css_parts.extend(f" --{k}: {v};" for k, v in light_color_vars.items())
332
+ css_parts.append("}\n")
333
+
334
+ # Light theme (only color- variables)
335
+ css_parts.append('[data-theme="light"] {')
336
+ css_parts.extend(f" --{k}: {v};" for k, v in light_color_vars.items())
337
+ css_parts.append("}\n")
338
+
339
+ # Dark theme (only color- variables)
340
+ css_parts.append('[data-theme="dark"] {')
341
+ css_parts.extend(f" --{k}: {v};" for k, v in dark_color_vars.items())
342
+ css_parts.append("}\n")
343
+
344
+ # Print media
345
+ css_parts.append("@media print {")
346
+ css_parts.append(' :root, [data-theme="dark"] {')
347
+ css_parts.extend(f" --{k}: {v} !important;" for k, v in light_color_vars.items())
348
+ css_parts.append(" }")
349
+ css_parts.append("}")
350
+
351
+ return "\n".join(css_parts)
278
352
 
279
353
 
280
354
  if __name__ == "__main__":
@@ -16,7 +16,11 @@ from kash.config.logger import get_log_settings, get_logger
16
16
  from kash.config.text_styles import EMOJI_SAVED
17
17
  from kash.file_storage.item_file_format import read_item, write_item
18
18
  from kash.file_storage.metadata_dirs import MetadataDirs
19
- from kash.file_storage.store_filenames import folder_for_type, join_suffix, parse_item_filename
19
+ from kash.file_storage.store_filenames import (
20
+ folder_for_type,
21
+ join_suffix,
22
+ parse_item_filename,
23
+ )
20
24
  from kash.model.items_model import Item, ItemId, ItemType
21
25
  from kash.model.paths_model import StorePath
22
26
  from kash.shell.output.shell_output import PrintHooks
@@ -39,7 +43,9 @@ T = TypeVar("T")
39
43
  P = ParamSpec("P")
40
44
 
41
45
 
42
- def synchronized(method: Callable[Concatenate[SelfT, P], T]) -> Callable[Concatenate[SelfT, P], T]:
46
+ def synchronized(
47
+ method: Callable[Concatenate[SelfT, P], T],
48
+ ) -> Callable[Concatenate[SelfT, P], T]:
43
49
  """
44
50
  Simple way to synchronize a few methods.
45
51
  """
@@ -142,7 +148,10 @@ class FileStore(Workspace):
142
148
  """
143
149
  name, item_type, _format, file_ext = parse_item_filename(store_path)
144
150
  if not file_ext:
145
- log.debug("Skipping file with unrecognized name or extension: %s", fmt_path(store_path))
151
+ log.debug(
152
+ "Skipping file with unrecognized name or extension: %s",
153
+ fmt_path(store_path),
154
+ )
146
155
  return None
147
156
 
148
157
  full_suffix = join_suffix(item_type.name, file_ext.name) if item_type else file_ext.name
@@ -158,11 +167,17 @@ class FileStore(Workspace):
158
167
  if old_path and old_path != store_path:
159
168
  dup_path = old_path
160
169
  log.info(
161
- "Duplicate items (%s):\n%s", item_id, fmt_lines([old_path, store_path])
170
+ "Duplicate items (%s):\n%s",
171
+ item_id,
172
+ fmt_lines([old_path, store_path]),
162
173
  )
163
174
  self.id_map[item_id] = store_path
164
- except SkippableError as e:
165
- log.warning("Could not read file, skipping: %s: %s", fmt_path(store_path), e)
175
+ except (ValueError, SkippableError) as e:
176
+ log.warning(
177
+ "Could not load file, skipping from store index: %s: %s",
178
+ fmt_path(store_path),
179
+ e,
180
+ )
166
181
 
167
182
  return dup_path
168
183
 
@@ -219,13 +234,17 @@ class FileStore(Workspace):
219
234
  @synchronized
220
235
  def _pick_filename_for(self, item: Item, *, overwrite: bool = False) -> tuple[str, str | None]:
221
236
  """
222
- Get a suitable filename for this item.
223
- If `overwrite` is true, use the the slugified title.
224
- If it is false, use the slugified title with a suffix to make it unique
225
- and in this case returns the old filename for this item, if it is different.
237
+ Get a suitable filename for this item. If `overwrite` is true, use the the slugified
238
+ title, regardless of whether it is already in the store.
239
+ If `overwrite` is false, use the slugified title with a suffix to make it unique
240
+ (and in this case also return the old filename for this item).
226
241
  """
227
242
  if overwrite:
228
- log.info("Picked default filename: %s for item: %s", item.default_filename(), item)
243
+ log.info(
244
+ "Picked default filename: %s for item: %s",
245
+ item.default_filename(),
246
+ item,
247
+ )
229
248
  return item.default_filename(), None
230
249
 
231
250
  slug = item.slug_name()
@@ -283,14 +302,13 @@ class FileStore(Workspace):
283
302
  @synchronized
284
303
  def store_path_for(
285
304
  self, item: Item, *, as_tmp: bool = False, overwrite: bool = False
286
- ) -> tuple[StorePath, bool, StorePath | None]:
305
+ ) -> tuple[StorePath, StorePath | None]:
287
306
  """
288
307
  Return the store path for an item. If the item already has a `store_path`, we use that.
289
308
  Otherwise we need to find the store path or generate a new one that seems suitable.
290
309
 
291
- Returns `store_path, found, old_store_path` where `found` indicates whether the path was
292
- already found (in the item or in the store by checking for identity) and `old_store_path`
293
- is the previous similarly named item with a different identity (or None there is none).
310
+ Returns `store_path, old_store_path` where `old_store_path` is the previous similarly
311
+ named item with a different identity (or None there is none).
294
312
 
295
313
  If `as_tmp` is true, will return a path from the temporary directory in the store.
296
314
  Normally an item is always saved to a unique store path but if `overwrite` is true,
@@ -299,17 +317,17 @@ class FileStore(Workspace):
299
317
  item_id = item.item_id()
300
318
  old_filename = None
301
319
  if as_tmp:
302
- return self._tmp_path_for(item), False, None
320
+ return self._tmp_path_for(item), None
303
321
  elif item.store_path:
304
- return StorePath(item.store_path), True, None
322
+ return StorePath(item.store_path), None
305
323
  elif item_id in self.id_map and self.exists(self.id_map[item_id]):
306
324
  # If this item has an identity and we've saved under that id before, use the same store path.
307
325
  store_path = self.id_map[item_id]
308
326
  log.info(
309
- "Found existing item with same id:\n%s",
327
+ "When picking a store path, found an existing item with same id:\n%s",
310
328
  fmt_lines([fmt_loc(store_path), item_id]),
311
329
  )
312
- return store_path, True, None
330
+ return store_path, None
313
331
  else:
314
332
  # We need to pick the path and filename.
315
333
  folder_path = folder_for_type(item.type)
@@ -320,14 +338,14 @@ class FileStore(Workspace):
320
338
  if old_filename and Path(self.base_dir / folder_path / old_filename).exists():
321
339
  old_store_path = StorePath(folder_path / old_filename)
322
340
 
323
- return StorePath(store_path), False, old_store_path
341
+ return StorePath(store_path), old_store_path
324
342
 
325
343
  def _tmp_path_for(self, item: Item) -> StorePath:
326
344
  """
327
345
  Find a path for an item in the tmp directory.
328
346
  """
329
347
  if not item.store_path:
330
- store_path, _found, _old = self.store_path_for(item, as_tmp=False)
348
+ store_path, _old = self.store_path_for(item, as_tmp=False)
331
349
  return StorePath(self.dirs.tmp_dir / store_path)
332
350
  elif (self.base_dir / item.store_path).is_relative_to(self.dirs.tmp_dir):
333
351
  return StorePath(item.store_path)
@@ -346,14 +364,14 @@ class FileStore(Workspace):
346
364
  item: Item,
347
365
  *,
348
366
  overwrite: bool = False,
349
- skip_dup_names: bool = False,
350
367
  as_tmp: bool = False,
351
368
  no_format: bool = False,
352
369
  no_frontmatter: bool = False,
353
370
  ) -> StorePath:
354
371
  """
355
372
  Save the item. Uses the `store_path` if it's already set or generates a new one.
356
- Updates `item.store_path`.
373
+ Updates `item.store_path`. An existing file can be added by having the item's
374
+ `external_path` set to a location (inside or outside the store).
357
375
 
358
376
  Unless `no_format` is true, also normalizes body text formatting (for Markdown)
359
377
  and updates the item's body to match.
@@ -363,65 +381,62 @@ class FileStore(Workspace):
363
381
  If `overwrite` is true, will overwrite a file that has the same path.
364
382
 
365
383
  If `as_tmp` is true, will save the item to a temporary file.
366
-
367
- If `skip_dup_names` is true, will skip saving if an item if an item with a
368
- matching path (based on its title) already exists.
369
384
  """
370
- if overwrite and skip_dup_names:
371
- raise ValueError("Cannot both overwrite and skip duplicate names.")
372
385
  if overwrite and as_tmp:
373
386
  raise ValueError("Cannot both overwrite and save to a temporary file.")
374
387
 
375
- # If external file already exists within the workspace, the file is already saved (without metadata).
388
+ # If external path already exists and is within the workspace, the file was
389
+ # already saved (e.g. by an action that wrote the item directly to the store).
376
390
  external_path = item.external_path and Path(item.external_path).resolve()
377
391
  if external_path and self._is_in_store(external_path):
378
- log.message("External file already saved: %s", fmt_loc(external_path))
392
+ log.info("Item with external_path already saved: %s", fmt_loc(external_path))
379
393
  rel_path = external_path.relative_to(self.base_dir)
380
- # Indicate this is really an item with a store path, not an external path.
394
+ # Indicate this is an item with a store path, not an external path.
395
+ # Keep external_path set so we know body is in that file.
381
396
  item.store_path = str(rel_path)
382
- item.external_path = None
383
397
  return StorePath(rel_path)
384
398
  else:
385
399
  # Otherwise it's still in memory or in a file outside the workspace and we need to save it.
386
- store_path, found, old_store_path = self.store_path_for(
400
+ store_path, old_store_path = self.store_path_for(
387
401
  item, as_tmp=as_tmp, overwrite=overwrite
388
402
  )
389
403
 
390
- if skip_dup_names and found:
391
- log.message(
392
- "Skipping save because an item of the same name already exists: %s",
393
- fmt_loc(store_path),
394
- )
395
- item.store_path = str(store_path)
396
- return store_path
397
-
398
404
  full_path = self.base_dir / store_path
399
405
 
400
- log.info("Saving item to %s: %s", fmt_loc(full_path), item)
406
+ supports_frontmatter = item.format and item.format.supports_frontmatter
407
+ log.info(
408
+ "Saving item in format %s (supports_frontmatter=%s) to %s: %s",
409
+ item.format,
410
+ supports_frontmatter,
411
+ fmt_loc(full_path),
412
+ item,
413
+ )
401
414
 
402
- # If we're overwriting an existing file, archive it first.
415
+ # If we're overwriting an existing file, archive it first so it is in the archive, not lost.
403
416
  if full_path.exists():
404
417
  try:
418
+ log.info(
419
+ "Previous file exists so will archive it: %s",
420
+ fmt_loc(store_path),
421
+ )
405
422
  self.archive(store_path, quiet=True)
406
423
  except Exception as e:
407
424
  log.info("Exception archiving existing file: %s", e)
408
425
 
409
426
  # Now save the new item.
410
427
  try:
411
- supports_frontmatter = item.format and item.format.supports_frontmatter
412
- # For binary or unknown formats or if we're not adding frontmatter, copy the file exactly.
428
+ # For binary or unknown formats or if we're not adding frontmatter, copy the file.
413
429
  if item.external_path and (no_frontmatter or not supports_frontmatter):
430
+ log.info(
431
+ "Path is an external path, so copying: %s -> %s",
432
+ fmt_path(item.external_path),
433
+ fmt_path(full_path),
434
+ )
414
435
  copyfile_atomic(item.external_path, full_path, make_parents=True)
415
436
  else:
416
437
  # Save as a text item with frontmatter.
417
438
  if item.external_path:
418
439
  item.body = Path(item.external_path).read_text()
419
- if overwrite and full_path.exists():
420
- log.info(
421
- "Overwrite is enabled and a previous file exists so will archive it: %s",
422
- fmt_loc(store_path),
423
- )
424
- self.archive(store_path, quiet=True)
425
440
  write_item(item, full_path, normalize=not no_format)
426
441
  except OSError as e:
427
442
  log.error("Error saving item: %s", e)
@@ -545,7 +560,7 @@ class FileStore(Workspace):
545
560
  else:
546
561
  # Binary or other files we just copy over as-is, preserving the name.
547
562
  # We know the extension is recognized.
548
- store_path, _found, old_store_path = self.store_path_for(item)
563
+ store_path, old_store_path = self.store_path_for(item)
549
564
  if self.exists(store_path):
550
565
  raise FileExists(f"Resource already in store: {fmt_loc(store_path)}")
551
566
 
@@ -690,7 +705,10 @@ class FileStore(Workspace):
690
705
  for warning in self.warnings:
691
706
  log.warning("%s", warning)
692
707
 
693
- log.info("File store startup took %s.", format_duration(self.end_time - self.start_time))
708
+ log.info(
709
+ "File store startup took %s.",
710
+ format_duration(self.end_time - self.start_time),
711
+ )
694
712
  # TODO: Log more info like number of items by type.
695
713
  return True
696
714
 
@@ -28,8 +28,6 @@ def write_item(item: Item, path: Path, normalize: bool = True):
28
28
  By default normalizes formatting of the body text and updates the item's body.
29
29
  """
30
30
  item.validate()
31
- if item.is_binary:
32
- raise ValueError(f"Binary items should be external files: {item}")
33
31
  if item.format and not item.format.supports_frontmatter:
34
32
  raise ValueError(f"Item format `{item.format.value}` does not support frontmatter: {item}")
35
33
 
@@ -244,7 +244,7 @@ def _serve_item(
244
244
  media_type=mime_type,
245
245
  )
246
246
  else:
247
- display_title = item.display_title() if item else str(path)
247
+ display_title = item.pick_title() if item else str(path)
248
248
 
249
249
  # For HEAD requests, return header with mime type only.
250
250
  if request.method == "HEAD":
@@ -22,7 +22,7 @@ from kash.exec_model.shell_model import ShellResult
22
22
  from kash.llm_utils import LLM, LLMName
23
23
  from kash.llm_utils.llm_messages import Message, MessageTemplate
24
24
  from kash.model.exec_model import ExecContext
25
- from kash.model.items_model import UNTITLED, Item, ItemType
25
+ from kash.model.items_model import UNTITLED, Format, Item, ItemType
26
26
  from kash.model.operations_model import Operation, Source
27
27
  from kash.model.params_model import (
28
28
  ALL_COMMON_PARAMS,
@@ -102,6 +102,10 @@ class ActionResult:
102
102
  shell_result: ShellResult | None = None
103
103
  """Customize control of how the action's result is displayed in the shell."""
104
104
 
105
+ def get_by_format(self, format: Format) -> Item:
106
+ """Convenience method to get an item for actions that return multiple formats."""
107
+ return next(item for item in self.items if item.format == format)
108
+
105
109
  def has_hints(self) -> bool:
106
110
  return bool(
107
111
  self.replaces_input or self.skip_duplicates or self.path_ops or self.shell_result
kash/model/items_model.py CHANGED
@@ -219,6 +219,12 @@ class Item:
219
219
  a text document, PDF or other resource, URL, etc.
220
220
  """
221
221
 
222
+ # TODO: A few cleanups:
223
+ # - Consider adding aliases and tags. See also Obsidian frontmatter format:
224
+ # https://help.obsidian.md/Editing+and+formatting/Properties#Default%20properties
225
+ # - Can eliminate context here as we now have ExectContext in a contextvar.
226
+ # - Change store_path and external_path to a StorePath and Path instead of a str.
227
+
222
228
  type: ItemType
223
229
  state: State = State.draft
224
230
  title: str | None = None
@@ -230,17 +236,15 @@ class Item:
230
236
  created_at: datetime = field(default_factory=lambda: datetime.now(UTC))
231
237
  modified_at: datetime | None = None
232
238
 
233
- # TODO: Consider adding aliases and tags. See also Obsidian frontmatter format:
234
- # https://help.obsidian.md/Editing+and+formatting/Properties#Default%20properties
235
-
236
239
  # Content of the item.
237
240
  # Text items are in body. Large or binary items may be stored externally.
241
+ # The external_path if present should always hold the current body of the content
242
+ # (and body will not be set). This is necessary for large or binary files.
238
243
  body: str | None = None
239
244
  external_path: str | None = None
240
245
  original_filename: str | None = None
241
246
 
242
247
  # Path to the item in the store, if it has been saved.
243
- # TODO: Migrate this to StorePath.
244
248
  store_path: str | None = None
245
249
 
246
250
  # Optionally, relations to other items, including any time this item is derived from.
@@ -547,7 +551,7 @@ class Item:
547
551
  if filename_stem and not prefer_title:
548
552
  return slugify_snake(filename_stem)
549
553
  else:
550
- return slugify_snake(self.abbrev_title(max_len=max_len, add_ops_suffix=True))
554
+ return slugify_snake(self.pick_title(max_len=max_len, add_ops_suffix=True))
551
555
 
552
556
  def default_filename(self) -> str:
553
557
  """
@@ -560,7 +564,16 @@ class Item:
560
564
  full_suffix = self.get_full_suffix()
561
565
  return join_suffix(slug, full_suffix)
562
566
 
563
- def abbrev_title(
567
+ def body_heading(self, allowed_tags: tuple[str, ...] = ("h1", "h2")) -> str | None:
568
+ """
569
+ Get the first heading (by default h1 or h2) from the body text, if present.
570
+ """
571
+ if self.format in [Format.markdown, Format.md_html]:
572
+ return first_heading(self.body_text(), allowed_tags=allowed_tags)
573
+ # TODO: Support HTML <h1> and <h2> as well.
574
+ return None
575
+
576
+ def pick_title(
564
577
  self,
565
578
  *,
566
579
  max_len: int = 100,
@@ -619,41 +632,20 @@ class Item:
619
632
 
620
633
  return final_text
621
634
 
622
- def display_title(self) -> str:
623
- """
624
- A display title for this item. Same as abbrev_title() but will fall back
625
- to the filename if it is available.
626
- """
627
- display_title = self.title
628
- if not display_title and self.store_path:
629
- display_title = Path(self.store_path).name
630
- if not display_title:
631
- display_title = self.abbrev_title(pull_body_heading=True)
632
- return display_title
633
-
634
- def abbrev_description(self, max_len: int = 1000) -> str:
635
+ def pick_description(self, max_len: int = 1000) -> str:
635
636
  """
636
637
  Get or infer description.
637
638
  """
638
639
  return abbrev_on_words(html_to_plaintext(self.description or self.body or ""), max_len)
639
640
 
640
- def body_heading(self) -> str | None:
641
- """
642
- Get the first h1 or h2 heading from the body text, if present.
643
- """
644
- if self.format in [Format.markdown, Format.md_html]:
645
- return first_heading(self.body_text(), allowed_tags=("h1", "h2"))
646
- # TODO: Support HTML <h1> and <h2> as well.
647
- return None
648
-
649
641
  def abbrev_body(self, max_len: int) -> str:
650
642
  """
651
643
  Get an abbreviated version of the body text. Must not be a binary Item.
652
644
  Abbreviates YAML bodies like {"role": "user", "content": "Hello"} to "user Hello".
653
645
  """
654
- body_text = self.body_text()[:max_len]
646
+ body_text = abbrev_str(self.body_text(), max_len)
655
647
 
656
- # Just for aesthetics especially for titles of chat files.
648
+ # Just for aesthetics, especially for titles of chat files.
657
649
  if self.type in [ItemType.chat, ItemType.config] or self.format == Format.yaml:
658
650
  try:
659
651
  yaml_obj = list(new_yaml().load_all(self.body_text()))
@@ -662,7 +654,7 @@ class Item:
662
654
  except Exception as e:
663
655
  log.info("Error parsing YAML body: %s", e)
664
656
 
665
- return body_text[:max_len]
657
+ return abbrev_str(body_text, max_len)
666
658
 
667
659
  @property
668
660
  def has_body(self) -> bool:
@@ -745,9 +737,14 @@ class Item:
745
737
  self,
746
738
  other: Item | None = None,
747
739
  update_timestamp: bool = False,
740
+ clear_fields=(
741
+ "store_path", # Will be set at save time.
742
+ "source", # Should be cleared so the ItemId of a copy is not the same as the original.
743
+ "modified_at",
744
+ ),
748
745
  **other_updates: Unpack[ItemUpdateOptions],
749
746
  ) -> dict[str, Any]:
750
- overrides: dict[str, Any] = {"store_path": None, "modified_at": None}
747
+ overrides: dict[str, Any] = {f: None for f in clear_fields}
751
748
  if update_timestamp:
752
749
  overrides["created_at"] = datetime.now()
753
750
 
@@ -767,8 +764,9 @@ class Item:
767
764
  self, update_timestamp: bool = True, **other_updates: Unpack[ItemUpdateOptions]
768
765
  ) -> Item:
769
766
  """
770
- Copy item with the given field updates. Resets `store_path` to None but preserves
771
- other fields, including the body. Updates created time if requested.
767
+ Copy item with the given field updates. Resets `store_path` and `source` to None
768
+ since those should be set explicitly later. Preserves other fields, including
769
+ the body.
772
770
  """
773
771
  new_fields = self._copy_and_update(update_timestamp=update_timestamp, **other_updates)
774
772
  return Item(**new_fields)
@@ -783,9 +781,12 @@ class Item:
783
781
 
784
782
  def derived_copy(self, **updates: Unpack[ItemUpdateOptions]) -> Item:
785
783
  """
786
- Same as `new_copy_with()`, but also makes any other updates and updates the
787
- `derived_from` relation. If we also have an action context, then use the
788
- `title_template` to derive a new title.
784
+ Copy item with the given field updates. Resets `store_path` and `source` to None
785
+ since those should be set explicitly later. Preserves other fields, including
786
+ the body.
787
+
788
+ Same as `new_copy_with` but also updates the `derived_from` relation. If we also
789
+ have an action context, then use the `title_template` to derive a new title.
789
790
  """
790
791
  if not self.store_path:
791
792
  if self.relations.derived_from:
@@ -898,7 +899,7 @@ class Item:
898
899
  elif self.external_path:
899
900
  return fmt_loc(self.external_path)
900
901
  else:
901
- return repr(self.abbrev_title())
902
+ return repr(self.pick_title())
902
903
 
903
904
  def as_chat_history(self) -> ChatHistory:
904
905
  if self.type != ItemType.chat:
@@ -270,7 +270,7 @@ def get_item_completions(
270
270
  f"{fmt_store_path(not_none(item.store_path))}",
271
271
  COMPLETION_DISPLAY_MAX_LEN,
272
272
  ),
273
- description=item.abbrev_title(),
273
+ description=item.pick_title(),
274
274
  append_space=True,
275
275
  score=score,
276
276
  )
@@ -106,7 +106,9 @@ class Format(Enum):
106
106
  @property
107
107
  def is_simple_text(self) -> bool:
108
108
  """
109
- Is this plaintext or close to it, like Markdown?
109
+ Is this plaintext or close to it, like Markdown or Markdown with limited HTML?
110
+ "Simple text" should be a format that converts canonically to clean HTML.
111
+ Does not include full-page general HTML.
110
112
  """
111
113
  return self in [self.plaintext, self.markdown, self.md_html]
112
114
 
@@ -174,8 +176,8 @@ class Format(Enum):
174
176
  """
175
177
  Is this format compatible with frontmatter format metadata?
176
178
  PDF and docx unfortunately won't work with frontmatter.
177
- CSV does to some degree, depending on the tool, and this is useful so we support it.
178
- Perhaps we could include JSON here (assuming it's JSON5), but currently we do not.
179
+ CSV does to some degree, depending on the tool, and this can be useful so we support it.
180
+ Including JSON here (assuming it's JSON5) for similar reasons.
179
181
  """
180
182
  return self in [
181
183
  self.url,
@@ -189,7 +191,7 @@ class Format(Enum):
189
191
  self.python,
190
192
  self.shellscript,
191
193
  self.xonsh,
192
- self.csv,
194
+ self.csv, # Often but not always supported.
193
195
  self.log,
194
196
  ]
195
197
 
@@ -7,6 +7,7 @@ def simple_webpage_render(
7
7
  item: Item,
8
8
  page_template: str = "simple_webpage.html.jinja",
9
9
  add_title_h1: bool = True,
10
+ show_theme_toggle: bool = False,
10
11
  ) -> str:
11
12
  """
12
13
  Generate a simple web page from a single item.
@@ -15,10 +16,12 @@ def simple_webpage_render(
15
16
  return render_web_template(
16
17
  template_filename=page_template,
17
18
  data={
18
- "title": item.abbrev_title(),
19
+ "title": item.pick_title(),
19
20
  "add_title_h1": add_title_h1,
20
21
  "content_html": item.body_as_html(),
21
22
  "thumbnail_url": item.thumbnail_url,
23
+ "enable_themes": show_theme_toggle,
24
+ "show_theme_toggle": show_theme_toggle,
22
25
  },
23
26
  )
24
27
 
@@ -68,14 +68,14 @@ def tabbed_webpage_config(
68
68
 
69
69
  tabs = [
70
70
  TabInfo(
71
- label=clean_label(item.abbrev_title()),
71
+ label=clean_label(item.pick_title()),
72
72
  store_path=item.store_path,
73
73
  thumbnail_url=get_thumbnail_url(item),
74
74
  )
75
75
  for item in items
76
76
  ]
77
77
  _fill_in_ids(tabs)
78
- title = summary_heading([item.abbrev_title() for item in items])
78
+ title = summary_heading([item.pick_title() for item in items])
79
79
  config = TabbedWebpage(
80
80
  title=title, tabs=tabs, show_tabs=len(tabs) > 1, add_title_h1=add_title_h1
81
81
  )
@@ -100,7 +100,10 @@ def _load_tab_content(config: TabbedWebpage):
100
100
 
101
101
 
102
102
  def tabbed_webpage_generate(
103
- config_item: Item, page_template: str = "base_webpage.html.jinja", add_title_h1: bool = True
103
+ config_item: Item,
104
+ page_template: str = "base_webpage.html.jinja",
105
+ add_title_h1: bool = True,
106
+ show_theme_toggle: bool = False,
104
107
  ) -> str:
105
108
  """
106
109
  Generate a web page using the supplied config.
@@ -121,6 +124,8 @@ def tabbed_webpage_generate(
121
124
  "title": tabbed_webpage.title,
122
125
  "add_title_h1": add_title_h1,
123
126
  "content": content,
127
+ "enable_themes": show_theme_toggle,
128
+ "show_theme_toggle": show_theme_toggle,
124
129
  },
125
130
  )
126
131
 
@@ -176,11 +176,10 @@ ol > li {
176
176
  }
177
177
 
178
178
  blockquote {
179
- border-left: 4px solid var(--color-primary);
179
+ border-left: 2px solid var(--color-primary);
180
180
  padding-left: 1rem;
181
181
  margin-left: 0;
182
182
  margin-right: 0;
183
- color: var(--color-secondary);
184
183
  }
185
184
 
186
185
  /* Inline code styling */
@@ -188,6 +187,7 @@ code {
188
187
  font-family: var(--font-mono);
189
188
  font-size: var(--font-size-mono);
190
189
  letter-spacing: -0.025em;
190
+ transition: color 0.4s ease-in-out;
191
191
  }
192
192
 
193
193
  /* Code blocks (pre + code) */
@@ -201,6 +201,7 @@ pre {
201
201
  padding: 0.2rem 0.2rem 0.1rem 0.2rem;
202
202
  overflow-x: auto; /* Enable horizontal scrolling */
203
203
  position: relative; /* Create new stacking context */
204
+ transition: background-color 0.4s ease-in-out, border-color 0.4s ease-in-out;
204
205
  }
205
206
 
206
207
  /* Reset code styling when inside pre blocks */
@@ -209,6 +210,44 @@ pre > code {
209
210
  line-height: 1.5; /* Improve readability */
210
211
  }
211
212
 
213
+ /* Copy button for code blocks */
214
+ .code-copy-button {
215
+ position: absolute;
216
+ top: 0;
217
+ right: 0;
218
+ background: var(--color-bg-alt);
219
+ color: var(--color-hint);
220
+ border: none;
221
+ border-radius: 0.25rem;
222
+ padding: 0.25rem;
223
+ cursor: pointer;
224
+ font-size: 0.75rem;
225
+ z-index: 10;
226
+ transition: all 0.2s ease-in-out;
227
+ display: flex;
228
+ align-items: center;
229
+ justify-content: center;
230
+ width: 1.5rem;
231
+ height: 1.5rem;
232
+ opacity: 0.6;
233
+ }
234
+
235
+ .code-copy-button:hover {
236
+ background: var(--color-hover-bg);
237
+ color: var(--color-primary);
238
+ opacity: 1;
239
+ }
240
+
241
+ .code-copy-button.copied {
242
+ color: var(--color-success);
243
+ opacity: 1;
244
+ }
245
+
246
+ .code-copy-button svg {
247
+ width: 0.875rem;
248
+ height: 0.875rem;
249
+ }
250
+
212
251
  hr {
213
252
  border: none;
214
253
  height: 1.5rem;
@@ -5,10 +5,32 @@
5
5
  {% block meta %}
6
6
  <meta charset="UTF-8" />
7
7
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
8
+ <meta name="color-scheme" content="light dark">
8
9
  {% endblock meta %}
9
10
 
10
11
  {% block title %}<title>{{ title }}</title>{% endblock title %}
11
12
 
13
+ {% block dark_mode_script %}
14
+ <script>
15
+ // Set theme before body renders to prevent flash of unstyled content
16
+ function applyTheme(theme) {
17
+ document.documentElement.dataset.theme = theme;
18
+ localStorage.setItem('theme', theme);
19
+ }
20
+
21
+ // If theme toggle is enabled, respect stored preference or system preference.
22
+ // Otherwise default to light mode.
23
+ {% if enable_themes %}
24
+ const storedTheme = localStorage.getItem('theme');
25
+ const systemPrefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
26
+ const initialTheme = storedTheme || (systemPrefersDark ? 'dark' : 'light');
27
+ {% else %}
28
+ const initialTheme = 'light';
29
+ {% endif %}
30
+ applyTheme(initialTheme);
31
+ </script>
32
+ {% endblock dark_mode_script %}
33
+
12
34
  {% block head_basic %}
13
35
  <link rel="preconnect" href="https://fonts.googleapis.com" />
14
36
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
@@ -35,6 +57,52 @@
35
57
  {% block head_extra %}{% endblock head_extra %}
36
58
 
37
59
  <style>
60
+
61
+ body {
62
+ background: var(--color-bg);
63
+ color: var(--color-text);
64
+ transition: background 0.4s ease-in-out, color 0.4s ease-in-out;
65
+ }
66
+
67
+ .theme-toggle {
68
+ position: fixed;
69
+ top: 1rem;
70
+ right: 1rem;
71
+ background: transparent;
72
+ color: var(--color-hint);
73
+ border: none;
74
+ padding: 0;
75
+ border-radius: 0.3rem;
76
+ cursor: pointer;
77
+ font-size: 1rem;
78
+ z-index: 100;
79
+ transition: all 0.2s ease-in-out;
80
+ display: flex;
81
+ align-items: center;
82
+ justify-content: center;
83
+ width: 2.5rem;
84
+ height: 2.5rem;
85
+ }
86
+
87
+ .theme-toggle:hover {
88
+ background: var(--color-hover-bg);
89
+ color: var(--color-primary);
90
+ }
91
+
92
+ [data-theme="dark"] .theme-toggle {
93
+ color: var(--color-primary-light);
94
+ }
95
+
96
+ [data-theme="dark"] .theme-toggle:hover {
97
+ color: var(--color-bright);
98
+ background: var(--color-hover-bg);
99
+ }
100
+
101
+ .theme-toggle svg {
102
+ width: 1rem;
103
+ height: 1rem;
104
+ }
105
+
38
106
  {% block font_faces %}
39
107
  /* https://fontsource.org/fonts/pt-serif/cdn */
40
108
  /* pt-serif-latin-400-normal */
@@ -108,6 +176,14 @@
108
176
  </head>
109
177
 
110
178
  <body>
179
+ {% block theme_toggle %}
180
+ {% if show_theme_toggle %}
181
+ <button class="theme-toggle" aria-label="toggle dark mode">
182
+ <i data-feather="moon"></i>
183
+ </button>
184
+ {% endif %}
185
+ {% endblock theme_toggle %}
186
+
111
187
  {% block body_header %}{% endblock body_header %}
112
188
 
113
189
  {% block main_content %}
@@ -119,6 +195,53 @@
119
195
  {% block scripts %}
120
196
  <script>
121
197
  document.addEventListener('DOMContentLoaded', () => {
198
+ // Add copy buttons to code blocks
199
+ document.querySelectorAll('pre').forEach(pre => {
200
+ // Skip if already has a copy button
201
+ if (pre.querySelector('.code-copy-button')) {
202
+ return;
203
+ }
204
+
205
+ const copyButton = document.createElement('button');
206
+ copyButton.className = 'code-copy-button';
207
+ copyButton.setAttribute('aria-label', 'Copy code');
208
+
209
+ const copyIcon = typeof feather !== 'undefined' ? feather.icons.copy.toSvg() : '<i data-feather="copy"></i>';
210
+ const checkIcon = typeof feather !== 'undefined' ? feather.icons.check.toSvg() : '<i data-feather="check"></i>';
211
+
212
+ copyButton.innerHTML = copyIcon;
213
+ copyButton.addEventListener('click', async () => {
214
+ const codeElement = pre.querySelector('code') || pre;
215
+ const textToCopy = (codeElement.textContent || codeElement.innerText).trim();
216
+
217
+ // Works on modern browsers.
218
+ navigator.clipboard.writeText(textToCopy).then(() => {
219
+ copyButton.innerHTML = checkIcon;
220
+ copyButton.classList.add('copied');
221
+
222
+ // Reset after 2 seconds
223
+ setTimeout(() => {
224
+ copyButton.innerHTML = copyIcon;
225
+ copyButton.classList.remove('copied');
226
+ }, 2000);
227
+ }).catch(err => {
228
+ console.error('Failed to copy text: ', err);
229
+ });
230
+ });
231
+
232
+ pre.appendChild(copyButton);
233
+ });
234
+
235
+ // Theme toggle (if present on page)
236
+ const themeToggleButton = document.querySelector('.theme-toggle');
237
+ if (themeToggleButton) {
238
+ themeToggleButton.addEventListener('click', () => {
239
+ const currentTheme = document.documentElement.dataset.theme;
240
+ const newTheme = currentTheme === 'dark' ? 'light' : 'dark';
241
+ applyTheme(newTheme);
242
+ });
243
+ }
244
+
122
245
  // Send messages to the parent window, in case we are in a viewport where that matters
123
246
  // (e.g. an iframe tooltip).
124
247
  // Request a resize of the parent viewport. This iframe size message format isn't
@@ -159,6 +282,11 @@
159
282
  }
160
283
  });
161
284
  });
285
+
286
+ // Initialize Feather icons once at the end, after all DOM manipulation.
287
+ if (typeof feather !== 'undefined') {
288
+ feather.replace();
289
+ }
162
290
  });
163
291
 
164
292
  // Double-click to expand (e.g. expand tooltip to popover).
@@ -10,7 +10,7 @@
10
10
  }
11
11
 
12
12
  .item-header {
13
- padding: 0.5rem 1rem 0rem 1rem;
13
+ padding: 0 1rem 0rem 1rem;
14
14
  border-bottom: 1px solid var(--color-border-hint);
15
15
  }
16
16
 
@@ -109,7 +109,7 @@ h1.item-title {
109
109
 
110
110
  .item-file-info {
111
111
  {# font-family: var(--font-sans); #}
112
- padding: 0.5rem 0;
112
+ padding: 0;
113
113
  {# font-size: var(--font-size-mono-small); #}
114
114
  word-break: break-word;
115
115
  }
@@ -118,6 +118,8 @@ h1.item-title {
118
118
  font-family: var(--font-sans);
119
119
  font-size: var(--font-size-small);
120
120
  letter-spacing: 0;
121
+ border: none;
122
+ background-color: transparent;
121
123
  }
122
124
 
123
125
  .item-url {
@@ -1,5 +1,29 @@
1
1
  {% extends "base_webpage.html.jinja" %}
2
2
 
3
+ {% block custom_styles %}
4
+ {{ super() }}
5
+ <style>
6
+ /* Override Tailwind's bg-white in dark mode */
7
+ [data-theme="dark"] .bg-white {
8
+ background-color: var(--color-bg-alt-solid) !important;
9
+ }
10
+ .long-text {
11
+ transition: background 0.4s ease-in-out, color 0.4s ease-in-out;
12
+ }
13
+
14
+ /* Ensure long-text containers respect theme */
15
+ [data-theme="dark"] .long-text {
16
+ background-color: var(--color-bg-alt-solid);
17
+ color: var(--color-text);
18
+ }
19
+
20
+ /* Adjust shadow for dark mode */
21
+ [data-theme="dark"] .long-text {
22
+ box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.3), 0 -2px 6px -1px rgba(0, 0, 0, 0.2);
23
+ }
24
+ </style>
25
+ {% endblock custom_styles %}
26
+
3
27
  <!-- simple_webpage begin main_content block -->
4
28
  {% block main_content %}
5
29
  <div class="long-text container max-w-3xl mx-auto bg-white py-4 px-6 md:px-16">
@@ -12,7 +12,6 @@ Can run from the custom kash shell (main.py) or from a regular xonsh shell.
12
12
  import kash.exec.command_registry
13
13
  import kash.xonsh_custom.load_into_xonsh
14
14
  import kash.xonsh_custom.xonsh_env
15
- from kash.config.logger import get_logger
16
15
 
17
16
 
18
17
  # We add action loading here directly in the xontrib so we expose `load` and
@@ -55,6 +54,8 @@ kash.xonsh_custom.xonsh_env.set_alias("load", load)
55
54
  try:
56
55
  kash.xonsh_custom.load_into_xonsh.load_into_xonsh()
57
56
  except Exception as e:
57
+ from kash.config.logger import get_logger
58
+
58
59
  log = get_logger(__name__)
59
- log.error("Could not initialize kash: %s", e)
60
+ log.error("Could not initialize kash: %s", e, exc_info=True)
60
61
  raise
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: kash-shell
3
- Version: 0.3.15
3
+ Version: 0.3.16
4
4
  Summary: The knowledge agent shell (core)
5
5
  Project-URL: Repository, https://github.com/jlevy/kash-shell
6
6
  Author-email: Joshua Levy <joshua@cal.berkeley.edu>
@@ -6,12 +6,12 @@ kash/actions/core/chat.py,sha256=yCannBFa0cSpR_in-XSSuMm1x2ZZQUCKmlqzhsUfpOo,269
6
6
  kash/actions/core/format_markdown_template.py,sha256=ZJbtyTSypPo2ewLiGRSyIpVf711vQMhI_-Ng-FgCs80,2991
7
7
  kash/actions/core/markdownify.py,sha256=KjdUeY4c9EhZ5geQrn22IoBv0P_p62q4zyyOYE0NRHM,1270
8
8
  kash/actions/core/readability.py,sha256=ljdB2rOpzfKU2FpEJ2UELIzcdOAWvdUjFsxoHRTE3xo,989
9
- kash/actions/core/render_as_html.py,sha256=bSyZdX9nZnP33QBdGSzWhInRREWXWayMG2oyiKn4rxw,1824
9
+ kash/actions/core/render_as_html.py,sha256=CIPGKCjUEVNsnXmpqHCUnjGwTfEfOyCXxlYFUN8mahY,1870
10
10
  kash/actions/core/show_webpage.py,sha256=Ggba9jkx9U-FZOcuL0lkS-SwtPNUyxVsGdeQrqwWs1s,887
11
11
  kash/actions/core/strip_html.py,sha256=FDLN_4CKB11q5cU4NixTf7PGrAq92AjQNbKAdvQDwCY,849
12
12
  kash/actions/core/summarize_as_bullets.py,sha256=Zwr8lNzL77pwpnW_289LQjNBijNDpTPANfFdOJA-PZ4,2070
13
13
  kash/actions/core/tabbed_webpage_config.py,sha256=rIbzEhBTmnkbSiRZC-Rj46T1J6c0jOztiKE9Usa4nsc,980
14
- kash/actions/core/tabbed_webpage_generate.py,sha256=_w_4LsgDqNnVvtX6Y4Txq56HAwEVMAY7RooWB29Okdk,954
14
+ kash/actions/core/tabbed_webpage_generate.py,sha256=oYx9fdgY8NndFekXsZbil0luGa2OFDi2e0s5l7Toh7E,992
15
15
  kash/actions/meta/write_instructions.py,sha256=zeKKX-Yi8jSyjvZ4Ii_4MNBRtM2MENuHyrD0Vxsaos8,1277
16
16
  kash/actions/meta/write_new_action.py,sha256=w7SJZ2FjzRbKwqKX9PeozUrh8cNJAumX7F80wW7dQts,6356
17
17
  kash/commands/__init__.py,sha256=MhdPSluWGE3XVQ7LSv2L8_aAxaey8CLjQBjGC9B4nRM,191
@@ -37,7 +37,7 @@ kash/commands/workspace/selection_commands.py,sha256=nZzA-H7Pk8kqSJVRlX7j1m6cZX-
37
37
  kash/commands/workspace/workspace_commands.py,sha256=ZJ3aPsnQ0FOkaA6stpV4YPEOQRCOKTazbMCIQkk9Cmk,25119
38
38
  kash/config/__init__.py,sha256=ytly9Typ1mWV4CXfV9G3CIPtPQ02u2rpZ304L3GlFro,148
39
39
  kash/config/capture_output.py,sha256=ud3uUVNuDicHj3mI_nBUBO-VmOrxtBdA3z-I3D1lSCU,2398
40
- kash/config/colors.py,sha256=6lqrB2RQYF2OLw-njfOqVHO9Bwiq7bW6K1ROCOAd1EM,9949
40
+ kash/config/colors.py,sha256=XrvSpEMJ345e0oCpsr-mASgjROvct9a3JyYkhkaP0Ww,12831
41
41
  kash/config/env_settings.py,sha256=uhCdfs9-TzJ15SzbuIQP1yIORaLUqYXCxh9qq_Z8cJc,996
42
42
  kash/config/init.py,sha256=aE4sZ6DggBmmoZEx9C5mQKrEbcDiswX--HF7pfCFKzc,526
43
43
  kash/config/lazy_imports.py,sha256=MCZXLnKvNyfHi0k7MU5rNwcdJtUF28naCixuogsAOAA,805
@@ -103,8 +103,8 @@ kash/exec_model/commands_model.py,sha256=iM8QhzA0tAas5OwF5liUfHtm45XIH1LcvCviuh3
103
103
  kash/exec_model/script_model.py,sha256=1VG3LhkTmlKzHOYouZ92ZpOSKSCcsz3-tHNcFMQF788,5031
104
104
  kash/exec_model/shell_model.py,sha256=LUhQivbpXlerM-DUzNY7BtctNBbn08Wto8CSSxQDxRU,568
105
105
  kash/file_storage/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
106
- kash/file_storage/file_store.py,sha256=c9Vt40JwleHOVqk-2b7yayW80RRiH-AApD-HZ3gdebo,30021
107
- kash/file_storage/item_file_format.py,sha256=YAz7VqyfIoiSLQOoFdWsp-FI_2tTLXAPi8V8QXbo5ag,5475
106
+ kash/file_storage/file_store.py,sha256=7KUOtEKuwSyz8uP61I8hcgB5TC8yd9oSeU9wzt_dLo4,30093
107
+ kash/file_storage/item_file_format.py,sha256=_o2CjWstk_Z__qMr-Inct9wJm2VEUK0GZvF-fDZ8bcc,5377
108
108
  kash/file_storage/metadata_dirs.py,sha256=9AqO3S3SSY1dtvP2iLX--E4ui0VIzXttG8R040otfyg,3820
109
109
  kash/file_storage/persisted_yaml.py,sha256=4-4RkFqdlBUkTOwkdA4vRKUywEE9TaDo13OGaDUyU9M,1309
110
110
  kash/file_storage/store_cache_warmer.py,sha256=cQ_KwxkBPWT3lMmYOCTkXgo7CKaGINns2YzIH32ExSU,1013
@@ -136,7 +136,7 @@ kash/llm_utils/llms.py,sha256=Zz45v7qrmBNGnmyF-Mn5Q4F1dZGFT2xf_kmWGUMSUUw,3518
136
136
  kash/local_server/__init__.py,sha256=AyNpvCOJlQF6A4DnlYKRMbbfRNzdizEA-ytp-F2SLZU,162
137
137
  kash/local_server/local_server.py,sha256=EugjL30VM0pWdZDsiQxU-o6EdEa082qlGd_7RHvI5tk,5863
138
138
  kash/local_server/local_server_commands.py,sha256=ZMp1DpYgg-MJ0iqH0DHfhWKDpFqNRG_txkRdODIr9mU,1661
139
- kash/local_server/local_server_routes.py,sha256=SgYB_BQ8SRED_4Wtba6iwl_5y3Yjrn-5s9q3zEPsgC8,10533
139
+ kash/local_server/local_server_routes.py,sha256=JlIVsrbsU0yiwv7vAoD9BctqiBI0w6u8Ld3BYY4jmo8,10530
140
140
  kash/local_server/local_url_formatters.py,sha256=SqHjGMEufvm43n34SCa_8Asdwm7utx91Wwymj15TuSY,5327
141
141
  kash/local_server/port_tools.py,sha256=oFfOvO6keqS5GowTpVg2FTu5KqkPHBq-dWAEomUIgGo,2008
142
142
  kash/local_server/rich_html_template.py,sha256=O9CnkMYkWuMvKJkqD0P8jaZqfUe6hMP4LXFvcLpwN8Q,196
@@ -158,13 +158,13 @@ kash/media_base/transcription_format.py,sha256=rOVPTpwvW22c27BRwYF-Tc_xzqK_wOtUZ
158
158
  kash/media_base/transcription_whisper.py,sha256=GqvroW9kBAH4-gcbYkMgNCfs2MpMIgm1ip3NMWtJ0IE,1169
159
159
  kash/media_base/services/local_file_media.py,sha256=-A-tK6XP7XDCqXQIAoohesFZ3OIvpXWsDoktYBvBuNA,5399
160
160
  kash/model/__init__.py,sha256=kFfBKb5N70NWYUfpRRxn_Sb9p_vXlB6BBaTCqWmSReo,2978
161
- kash/model/actions_model.py,sha256=7HbcEjm_-wckFE2hhec6oh7n8sqZ58G14yKahSEiOG8,21859
161
+ kash/model/actions_model.py,sha256=nOo-xbPp_J0aVKul2zBvG7zRaqvz6F-wFUH4oBYU2q0,22085
162
162
  kash/model/assistant_response_model.py,sha256=6eDfC27nyuBDFjv5nCYMa_Qb2mPbKwDzZy7uLOIyskI,2653
163
163
  kash/model/compound_actions_model.py,sha256=HiDK5wwCu3WwZYHATZoLEguiqwR9V6V296wiKtGIX8s,6926
164
164
  kash/model/concept_model.py,sha256=we2qOcy9Mv1q7XPfkDLp_CyO_-8DwAUfUYlpgy_jrFs,1011
165
165
  kash/model/exec_model.py,sha256=IlfvtQyoFRRWhWju7vdXp9J-w_NGcGtL5DhDLy9gRd8,2250
166
166
  kash/model/graph_model.py,sha256=jnctrPiBZ0xwAR8D54JMAJPanA1yZdaxSFQoIpe8anA,2662
167
- kash/model/items_model.py,sha256=RLbRTo36AZR5QLHotcYo4s6na8u40rcLXA0F6POUHyw,34913
167
+ kash/model/items_model.py,sha256=oH6qsexLTrjYwE_wS_pLalYRV8bX1avHXha4klBK8UI,35237
168
168
  kash/model/language_list.py,sha256=I3RIbxTseVmPdhExQimimEv18Gmy2ImMbpXe0-_t1Qw,450
169
169
  kash/model/llm_actions_model.py,sha256=a29uXVNfS2CiqvM7HPdC6H9A23rSQQihAideuBLMH8g,2110
170
170
  kash/model/media_model.py,sha256=64Zic4cRjQpgf_-tOuZlZZe59mz_qu0s6OQSU0YlDUI,3357
@@ -177,7 +177,7 @@ kash/shell/shell_main.py,sha256=nqbP8NyxtxAwh787YWhpbWelt_ObfIRZzCMK1aaQcmg,2094
177
177
  kash/shell/version.py,sha256=FRGs_jmzk6QiRPZfZv36SPzeyJCsVWZ_E2CKRkExazw,835
178
178
  kash/shell/completions/completion_scoring.py,sha256=-svesm2cR1AA86jYcxlynXCBZON26eUJce93FlL2nQo,10209
179
179
  kash/shell/completions/completion_types.py,sha256=FocRXd6Df3Df0nL2Y1GevMx3FsljJwbQdVgWsIngpaQ,4793
180
- kash/shell/completions/shell_completions.py,sha256=yHCFs-XRNjReuEsmhEUi-iTE2MoZrUa3RDSKhVe5EfQ,8846
180
+ kash/shell/completions/shell_completions.py,sha256=A0WWrm2lEwguasDMnDuaI8xGny9MhwGfNvCY3rHNUnw,8844
181
181
  kash/shell/file_icons/color_for_format.py,sha256=bFuE1lwiyUkgWB3Gk3jrch-9EIQz9thILQiX_a5dGb8,2034
182
182
  kash/shell/file_icons/nerd_icons.py,sha256=qF1Awc5cWIwaOWo7k_nmG9_A6XXwdNNcaluUUlde3KM,36046
183
183
  kash/shell/input/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -217,7 +217,7 @@ kash/utils/file_utils/__init__.py,sha256=loL_iW0oOZs0mJ5GelBPptBcqzYKSWdsGcHrpRy
217
217
  kash/utils/file_utils/dir_info.py,sha256=HamMr58k_DanTLifj7A2JDxTGWXEZZx2pQuE6Hjcm8g,1856
218
218
  kash/utils/file_utils/file_ext.py,sha256=-H63vlrVI3pfE2Cn_9qF7-QLDaUIu_njc4TieNgAHSY,1860
219
219
  kash/utils/file_utils/file_formats.py,sha256=vnihRFLl85G1uzpqDc_uiGH9SIvbFTYVszz3srdSSz0,4949
220
- kash/utils/file_utils/file_formats_model.py,sha256=M1KTGJdwC91SiQkBbEkext-hjMcjYpSnoNGUIuWiJKo,15448
220
+ kash/utils/file_utils/file_formats_model.py,sha256=KA697MPW-V0Xw82eeNATW1UrnUMnzldFKA1XWM3Obq0,15628
221
221
  kash/utils/file_utils/file_sort_filter.py,sha256=_k1chT3dJl5lSmKA2PW90KaoG4k4zftGdtwWoNEljP4,7136
222
222
  kash/utils/file_utils/file_walk.py,sha256=cpwVDPuaVm95_ZwFJiAdIuZAGhASI3gJ3ZUsCGP75b8,5527
223
223
  kash/utils/file_utils/filename_parsing.py,sha256=drHrH2B9W_5yAbXURNGJxNqj9GmTe8FayH6Gjw9e4-U,4194
@@ -248,15 +248,15 @@ kash/web_content/web_extract_readabilipy.py,sha256=IT7ET5IoU2-Nf37-Neh6CkKMvLL3W
248
248
  kash/web_content/web_fetch.py,sha256=J8DLFP1vzp7aScanFq0Bd7xCP6AVL4JgMMBqyRPtZjQ,4720
249
249
  kash/web_content/web_page_model.py,sha256=9bPuqZxXo6hSUB_llEcz8bs3W1lW0r-Y3Q7pZgknlQU,693
250
250
  kash/web_gen/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
251
- kash/web_gen/simple_webpage.py,sha256=c_kLXAsjP9wB9-ppF7MMJMw5VHXzVwkcHymfo5YOfS0,1402
252
- kash/web_gen/tabbed_webpage.py,sha256=Q_Htw2QO0O9H3A9OFrWw9GBD73cbwB6hOKF-W6mO6YE,4807
251
+ kash/web_gen/simple_webpage.py,sha256=ks_0ljxCeS2-gAAEaUc1JEnzY3JY0nzqGFiyyqyRuZs,1537
252
+ kash/web_gen/tabbed_webpage.py,sha256=DiZV48TVvcjOf31g3nzTAtGKpH5Cek1Unksr7Cwcwog,4949
253
253
  kash/web_gen/template_render.py,sha256=aypo6UanouftV4RpxgNm6JdquelI52fV0IlihdA3yjE,1908
254
- kash/web_gen/templates/base_styles.css.jinja,sha256=xrA7567wXHp9oMRqXhiMcEjH_IMeo6Ny1_pThYWpe4o,9202
255
- kash/web_gen/templates/base_webpage.html.jinja,sha256=Nvdd8pLSG2OdbrDQRvYcexYIZoMdr1G_7MdUVHNDoA8,7945
254
+ kash/web_gen/templates/base_styles.css.jinja,sha256=xSC0eACITRz1IcIizanhmzjH0aLB28fKdkSmvTEAJUY,9975
255
+ kash/web_gen/templates/base_webpage.html.jinja,sha256=xQ2p4Kv42VPfGUnGLedEjPn6WOnq8G8Fly5XjJUdnXc,12043
256
256
  kash/web_gen/templates/content_styles.css.jinja,sha256=3qcIwIt3DipCDJa9z6oIM_BMxmwoT7E_loTK0F3L9Vo,3629
257
257
  kash/web_gen/templates/explain_view.html.jinja,sha256=DNw5Iw5SrhIUFRGB4qNvfcKXsBHVbEJVURGdhvyC75Q,949
258
- kash/web_gen/templates/item_view.html.jinja,sha256=-Rqf2qS9KNbcJp9q1OOtl1aBGgPuwG7jc2Bgw_6YfBg,6791
259
- kash/web_gen/templates/simple_webpage.html.jinja,sha256=_59sVXjRUdrArz_l9S4SYJQubxMJg61XDhYs-lE1Qoo,708
258
+ kash/web_gen/templates/item_view.html.jinja,sha256=cYGyGKFcX8-5L2SM7-BC5oK6GLuH6blrzcxw2DxX-Q8,6828
259
+ kash/web_gen/templates/simple_webpage.html.jinja,sha256=MVXbs0359TTMAqY_A_T8-fP4mrkH5GalwwRBbAzWPeE,1333
260
260
  kash/web_gen/templates/tabbed_webpage.html.jinja,sha256=u7gabDsCs3neQQlH1y1rIaqT8UumPKVPPZG4VKg5R_g,1823
261
261
  kash/workspaces/__init__.py,sha256=q1gFERRZLJMA9-XSUKvB1ulauHDKqyzzc86GFLbxAuk,700
262
262
  kash/workspaces/param_state.py,sha256=vT_eGWqg2SRviIM5jqEAauznX2B5Xt2nHHu2oRxTcIU,746
@@ -278,9 +278,9 @@ kash/xonsh_custom/xonsh_keybindings.py,sha256=pSnYoZdJmyvC32U1JeqSADV-wReXEHC_Le
278
278
  kash/xonsh_custom/xonsh_modern_tools.py,sha256=mj_b34LZXfE8MJe9EpDmp5JZ0tDM1biYNPAS8jdcURo,1467
279
279
  kash/xonsh_custom/xonsh_ranking_completer.py,sha256=ZRGiAfoEgqgnlq2-ReUVEaX5oOgW1DQ9WxIv2OJLuTo,5620
280
280
  kash/xontrib/fnm.py,sha256=V2tsOdmIDgbFbZSfMLpsvDIwwJJqiYnOkOySD1cXNXw,3700
281
- kash/xontrib/kash_extension.py,sha256=JRRJC3cZSMOl4sSWEdKAQ_dVRMubWaOltKr8G0dWt6Y,1876
282
- kash_shell-0.3.15.dist-info/METADATA,sha256=nxMDgU5r65l0fnmvMfJkeIXQeUMxXKdvjk6EFTpgyQY,31258
283
- kash_shell-0.3.15.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
284
- kash_shell-0.3.15.dist-info/entry_points.txt,sha256=SQraWDAo8SqYpthLXThei0mf_hGGyhYBUO-Er_0HcwI,85
285
- kash_shell-0.3.15.dist-info/licenses/LICENSE,sha256=rCh2PsfYeiU6FK_0wb58kHGm_Fj5c43fdcHEexiVzIo,34562
286
- kash_shell-0.3.15.dist-info/RECORD,,
281
+ kash/xontrib/kash_extension.py,sha256=FLIMlgR3C_6A1fwKE-Ul0nmmpJSszVPbAriinUyQ8Zg,1896
282
+ kash_shell-0.3.16.dist-info/METADATA,sha256=4nG6g2qUrGSvUHNGVQe389ak5Kv20uV5JBM_ByIkqKk,31258
283
+ kash_shell-0.3.16.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
284
+ kash_shell-0.3.16.dist-info/entry_points.txt,sha256=SQraWDAo8SqYpthLXThei0mf_hGGyhYBUO-Er_0HcwI,85
285
+ kash_shell-0.3.16.dist-info/licenses/LICENSE,sha256=rCh2PsfYeiU6FK_0wb58kHGm_Fj5c43fdcHEexiVzIo,34562
286
+ kash_shell-0.3.16.dist-info/RECORD,,