shortcutxl 0.2.13 → 0.2.15

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 (33) hide show
  1. package/README.md +22 -7
  2. package/dist/core/keybindings.js +3 -1
  3. package/dist/core/settings-manager.js +29 -0
  4. package/dist/custom/install-utils.js +0 -3
  5. package/dist/custom/prompts/api.js +1 -1
  6. package/dist/custom/providers/provider-ids.js +1 -0
  7. package/dist/custom/tools/excel-approval.js +180 -0
  8. package/dist/custom/tools/excel-exec.js +66 -11
  9. package/dist/custom/tools/task/render.js +1 -1
  10. package/dist/main.js +12 -1
  11. package/dist/modes/interactive/components/footer.js +10 -0
  12. package/dist/modes/interactive/components/settings-selector.js +11 -0
  13. package/dist/modes/interactive/interactive-mode.js +29 -5
  14. package/package.json +5 -3
  15. package/xll/ShortcutXL.xll +0 -0
  16. package/xll/modules/shortcut_xl/__init__.py +29 -14
  17. package/xll/modules/shortcut_xl/_com.py +1 -0
  18. package/xll/modules/shortcut_xl/_diff_highlight.py +133 -91
  19. package/xll/modules/shortcut_xl/_exec_entry.py +191 -0
  20. package/xll/modules/shortcut_xl/_log.py +1 -1
  21. package/xll/modules/shortcut_xl/_managed.py +15 -9
  22. package/xll/modules/shortcut_xl/_navigate.py +115 -0
  23. package/xll/modules/shortcut_xl/_threading.py +4 -3
  24. package/xll/modules/shortcut_xl/_tracking.py +15 -3
  25. package/xll/modules/shortcut_xl/api/__init__.py +2 -2
  26. package/xll/modules/shortcut_xl/api/format.py +10 -5
  27. package/xll/modules/shortcut_xl/api/range_formatter.py +4 -4
  28. package/xll/modules/shortcut_xl/api/workbook.py +3 -8
  29. package/xll/modules/shortcut_xl/api/worksheet.py +7 -7
  30. package/xll/modules/shortcut_xl/api-reference.py +3 -0
  31. /package/skills/{COM-advanced-api → com-advanced-api}/SKILL.md +0 -0
  32. /package/skills/{COM-advanced-api → com-advanced-api}/excel-type-library.py +0 -0
  33. /package/skills/{COM-advanced-api → com-advanced-api}/office-type-library.py +0 -0
@@ -0,0 +1,115 @@
1
+ """Navigate Excel to a specific cell — used during diff review.
2
+
3
+ Highlights the cursor cell with a colored border via direct Range
4
+ formatting. Visible even when Excel doesn't have focus (unlike
5
+ .Select() which only shows when Excel is foreground).
6
+
7
+ Thread safety: called via _run_on_main, serialized by s_exec_lock.
8
+ """
9
+
10
+ from shortcut_xl._log import xl_log
11
+ from shortcut_xl.api.utils.helpers import extract_sheet_and_range
12
+
13
+ # Excel COM constants
14
+ _XL_EDGE_LEFT = 7
15
+ _XL_EDGE_TOP = 8
16
+ _XL_EDGE_BOTTOM = 9
17
+ _XL_EDGE_RIGHT = 10
18
+ _XL_CONTINUOUS = 1
19
+ _XL_LINE_STYLE_NONE = -4142
20
+ _XL_MEDIUM = -4138
21
+
22
+ _CURSOR_BLUE = 0xFF0000 # BGR — distinct from yellow change highlights
23
+ _BORDER_EDGES = (_XL_EDGE_LEFT, _XL_EDGE_TOP, _XL_EDGE_BOTTOM, _XL_EDGE_RIGHT)
24
+
25
+ # Current cursor state — at most one cell highlighted at a time.
26
+ _cursor_ws = None # COM Worksheet of the current cursor cell
27
+ _cursor_address = None # e.g. "B5"
28
+
29
+
30
+ def parse_cell_ref(cell_ref):
31
+ """Parse a cell reference into (workbook, sheet, address).
32
+
33
+ Supports single cells and ranges:
34
+ "[Book1]Sheet1!A1" → ("Book1", "Sheet1", "A1")
35
+ "Sheet1!A1:E1" → (None, "Sheet1", "A1:E1")
36
+
37
+ Returns (None, None, None) if the reference is invalid.
38
+ """
39
+ rest = cell_ref
40
+ workbook = None
41
+
42
+ if rest.startswith('['):
43
+ close = rest.find(']')
44
+ if close <= 1: # missing or empty workbook name
45
+ return None, None, None
46
+ workbook = rest[1:close]
47
+ rest = rest[close + 1:]
48
+
49
+ sheet, address = extract_sheet_and_range(rest)
50
+ if not sheet or not address:
51
+ return None, None, None
52
+
53
+ return workbook, sheet, address
54
+
55
+
56
+ def _resolve_worksheet(app, workbook, sheet):
57
+ """Get the COM Worksheet object for the given workbook/sheet names."""
58
+ if workbook:
59
+ return app.Workbooks(workbook).Worksheets(sheet)
60
+ return app.Worksheets(sheet)
61
+
62
+
63
+ def _set_border(rng, color, line_style, weight):
64
+ """Apply border styling to all four edges of a range."""
65
+ for edge in _BORDER_EDGES:
66
+ b = rng.Borders(edge)
67
+ b.LineStyle = line_style
68
+ if line_style != _XL_LINE_STYLE_NONE:
69
+ b.Color = color
70
+ b.Weight = weight
71
+
72
+
73
+ def _clear_cursor():
74
+ """Remove the border from the current cursor cell, if any."""
75
+ global _cursor_ws, _cursor_address
76
+ if _cursor_ws is None or _cursor_address is None:
77
+ return
78
+ try:
79
+ _set_border(_cursor_ws.Range(_cursor_address), 0, _XL_LINE_STYLE_NONE, 0)
80
+ except Exception as e:
81
+ xl_log(f"_clear_cursor({_cursor_address}): {e}")
82
+ _cursor_ws = None
83
+ _cursor_address = None
84
+
85
+
86
+ def navigate_to_cell(app, cell_ref):
87
+ """Highlight the cell with a colored border.
88
+
89
+ *cell_ref* is "[Book1]Sheet1!A1" or "Sheet1!A1".
90
+ Clears the previous cursor border before applying the new one.
91
+ Returns True on success, False on failure.
92
+ """
93
+ global _cursor_ws, _cursor_address
94
+
95
+ workbook, sheet, address = parse_cell_ref(cell_ref)
96
+ if not sheet or not address:
97
+ return False
98
+
99
+ _clear_cursor()
100
+
101
+ try:
102
+ ws = _resolve_worksheet(app, workbook, sheet)
103
+ rng = ws.Range(address)
104
+ _set_border(rng, _CURSOR_BLUE, _XL_CONTINUOUS, _XL_MEDIUM)
105
+ _cursor_ws = ws
106
+ _cursor_address = address
107
+ return True
108
+ except Exception as e:
109
+ xl_log(f"navigate_to_cell({cell_ref}): {e}")
110
+ return False
111
+
112
+
113
+ def clear_navigate_cursor():
114
+ """Remove the cursor border. Called on approval accept/reject."""
115
+ _clear_cursor()
@@ -6,12 +6,12 @@ This matches the pattern used by PyXLL, Excel-DNA, and xlwings:
6
6
  background threads enqueue work, a timer on the main thread drains it.
7
7
  """
8
8
 
9
- import threading
10
- import queue
11
9
  import ctypes
10
+ import queue
11
+ import threading
12
12
  from ctypes import wintypes
13
- from shortcut_xl._log import xl_log
14
13
 
14
+ from shortcut_xl._log import xl_log
15
15
 
16
16
  # COM error code: Excel is busy (VBA_E_IGNORE = 0x800AC472)
17
17
  _VBA_E_IGNORE = -2146777998
@@ -44,6 +44,7 @@ def _is_excel_busy(e):
44
44
 
45
45
  # Persist main thread ID in builtins so it survives hot-reload.
46
46
  import builtins
47
+
47
48
  _main_thread_id = getattr(builtins, '_shortcutxl_main_thread_id', None)
48
49
  if _main_thread_id is None:
49
50
  _main_thread_id = threading.current_thread().ident
@@ -7,9 +7,9 @@ sheets) not O(workbook), and the agent gets raw COM objects for everything
7
7
  below the Worksheet level — zero overhead on Range reads/writes.
8
8
  """
9
9
 
10
- from shortcut_xl.api.utils.helpers import col_letter
11
- from shortcut_xl.api.utils.com_utils import normalize_2d
12
10
  from shortcut_xl._log import xl_log
11
+ from shortcut_xl.api.utils.com_utils import normalize_2d
12
+ from shortcut_xl.api.utils.helpers import col_letter
13
13
 
14
14
 
15
15
  class _SheetSnapshot:
@@ -69,6 +69,9 @@ def _diff_snapshots(sheet_name, before, after):
69
69
  formula = _get_formula(after, ri, ci)
70
70
  if formula:
71
71
  entry['formula'] = formula
72
+ old_formula = _get_formula(before, ri, ci)
73
+ if old_formula:
74
+ entry['oldFormula'] = old_formula
72
75
  changes.append(entry)
73
76
  return changes
74
77
 
@@ -105,6 +108,9 @@ def _diff_snapshots(sheet_name, before, after):
105
108
  formula = _get_formula_abs(after, row, col)
106
109
  if formula:
107
110
  entry['formula'] = formula
111
+ old_formula = _get_formula_abs(before, row, col)
112
+ if old_formula:
113
+ entry['oldFormula'] = old_formula
108
114
  changes.append(entry)
109
115
  return changes
110
116
 
@@ -136,8 +142,14 @@ class DirtyTracker:
136
142
  try:
137
143
  after = _SheetSnapshot(ws)
138
144
  changes = _diff_snapshots(name, before, after)
139
- # Enrich with numberFormat (can't bulk-read mixed ranges)
145
+ # Enrich with workbook name + numberFormat
146
+ try:
147
+ wb_name = ws.Parent.Name
148
+ except Exception:
149
+ wb_name = None
140
150
  for entry in changes:
151
+ if wb_name:
152
+ entry['workbook'] = wb_name
141
153
  try:
142
154
  fmt = ws.Range(entry['address']).NumberFormat
143
155
  if fmt and fmt != 'General':
@@ -1,4 +1,4 @@
1
1
  """API layer — Workbook/Worksheet wrappers and supporting modules."""
2
2
 
3
- from shortcut_xl.api.workbook import Workbook
4
- from shortcut_xl.api.worksheet import Worksheet
3
+ from shortcut_xl.api.workbook import Workbook as Workbook
4
+ from shortcut_xl.api.worksheet import Worksheet as Worksheet
@@ -5,14 +5,19 @@ with Good / Issues breakdown and row sampling.
5
5
  """
6
6
 
7
7
  import re
8
- from shortcut_xl.api.utils.numerical import NUMERIC_TYPES
8
+
9
9
  from shortcut_xl.api.categorize import (
10
+ GOOD,
11
+ HARDCODED_NUMBER,
12
+ HARDCODED_NUMBER_IN_FORMULA,
13
+ INVALID_FORMULA,
14
+ LARGE_PERCENTAGE,
10
15
  categorize_cells,
11
- GOOD, LARGE_PERCENTAGE, HARDCODED_NUMBER,
12
- HARDCODED_NUMBER_IN_FORMULA, INVALID_FORMULA,
13
- is_com_error, com_error_to_str,
16
+ com_error_to_str,
17
+ is_com_error,
14
18
  )
15
19
  from shortcut_xl.api.utils.helpers import col_letter
20
+ from shortcut_xl.api.utils.numerical import NUMERIC_TYPES
16
21
 
17
22
  # Display constants
18
23
  MAX_ROWS_TO_SHOW = 10
@@ -165,7 +170,7 @@ def _format_row(sheet, row, entries, indent=2, show_issues=True, show_count=True
165
170
  lines.append(f'{ind}{sheet}!Row {row + 1} ({col_range}){count_suffix}')
166
171
 
167
172
  samples = _sample_first_last(entries, MAX_SAMPLES_PER_ROW)
168
- for cell, issues, _col in samples:
173
+ for cell, _issues, _col in samples:
169
174
  fmt = cell.get('numberFormat')
170
175
  old_str = format_value(cell.get('oldValue'), fmt)
171
176
  new_str = _display_value(cell)
@@ -9,11 +9,11 @@ Replicates the TypeScript processCellRange / RangeFormatter logic:
9
9
 
10
10
  from __future__ import annotations
11
11
 
12
- from shortcut_xl.api.utils.helpers import index_to_address
13
12
  from shortcut_xl.api.style import read_style
14
- from shortcut_xl.api.utils.numerical import is_numerical, NUMERIC_TYPES
13
+ from shortcut_xl.api.utils.helpers import index_to_address
14
+ from shortcut_xl.api.utils.numerical import NUMERIC_TYPES, is_numerical
15
15
  from shortcut_xl.api.utils.ranges import consolidate_ranges
16
- from shortcut_xl.api.utils.style_utils import get_style_key, get_style_description
16
+ from shortcut_xl.api.utils.style_utils import get_style_description, get_style_key
17
17
 
18
18
  # ---------------------------------------------------------------------------
19
19
  # Constants (match TypeScript range-processing/types.ts)
@@ -349,8 +349,8 @@ def format_cell_range(
349
349
  5. Context from cells to the left
350
350
  6. Context from cells above
351
351
  """
352
+ from shortcut_xl.api.utils.com_utils import make_range, normalize_2d
352
353
  from shortcut_xl.api.utils.helpers import parse_range
353
- from shortcut_xl.api.utils.com_utils import normalize_2d, make_range
354
354
 
355
355
  sr, sc, er, ec = parse_range(range_addr)
356
356
  rows = er - sr + 1
@@ -5,17 +5,14 @@ Output formats match the SpreadJS equivalents.
5
5
 
6
6
  from __future__ import annotations
7
7
 
8
- from typing import Any
9
-
8
+ from shortcut_xl.api.categorize import com_error_to_str, is_com_error
9
+ from shortcut_xl.api.named_ranges import get_named_range_info
10
10
  from shortcut_xl.api.utils.helpers import (
11
- col_letter,
11
+ extract_sheet_and_range,
12
12
  index_to_address,
13
13
  is_error_value,
14
14
  parse_range,
15
- extract_sheet_and_range,
16
15
  )
17
- from shortcut_xl.api.categorize import is_com_error, com_error_to_str
18
- from shortcut_xl.api.named_ranges import get_named_range_info
19
16
  from shortcut_xl.api.worksheet import Worksheet
20
17
 
21
18
  # Error check constants (match SpreadJS error-checker.ts)
@@ -263,8 +260,6 @@ class Workbook:
263
260
  rng.Value = com_error_to_str(data)
264
261
  return
265
262
  # Multi-cell range
266
- rows = rng.Rows.Count
267
- cols = rng.Columns.Count
268
263
  for r_idx, row in enumerate(data):
269
264
  if not isinstance(row, tuple):
270
265
  row = (row,)
@@ -11,17 +11,17 @@ import json
11
11
  import re
12
12
  from typing import Any
13
13
 
14
- from shortcut_xl.api.categorize import is_com_error, com_error_to_str
15
- from shortcut_xl.api.utils.numerical import NUMERIC_TYPES
14
+ from shortcut_xl._log import xl_log
15
+ from shortcut_xl.api.categorize import com_error_to_str, is_com_error
16
+ from shortcut_xl.api.picture import add_picture
17
+ from shortcut_xl.api.range_formatter import format_cell_range
18
+ from shortcut_xl.api.style import read_style
19
+ from shortcut_xl.api.utils.com_utils import make_range, normalize_2d
16
20
  from shortcut_xl.api.utils.helpers import (
17
21
  index_to_address,
18
22
  parse_range,
19
23
  )
20
- from shortcut_xl.api.utils.com_utils import normalize_2d, make_range
21
- from shortcut_xl._log import xl_log
22
- from shortcut_xl.api.range_formatter import format_cell_range
23
- from shortcut_xl.api.style import read_style
24
- from shortcut_xl.api.picture import add_picture
24
+ from shortcut_xl.api.utils.numerical import NUMERIC_TYPES
25
25
 
26
26
  # Excel xlAutoFillType enum values
27
27
  _XL_FILL_DEFAULT = 0
@@ -3,6 +3,9 @@ shortcut_xl API Reference
3
3
  All symbols are auto-imported in excel_exec — no import statements needed.
4
4
  """
5
5
 
6
+ from typing import Any
7
+
8
+
6
9
  class Workbook:
7
10
  """
8
11
  Workbook wrapper.