shortcutxl 0.2.15 → 0.2.17

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.
package/CHANGELOG.md ADDED
@@ -0,0 +1,36 @@
1
+ # Changelog
2
+
3
+ ## [0.2.17]
4
+
5
+ - **Better change review dialog** — Cell diffs now show full addresses (e.g. `Sheet1!A1`), formula details, and keyboard navigation with arrow keys for scrolling through large changesets.
6
+ - **Alt+V paste is more reliable** — Fixed cases where Alt+V would fail silently. Now gives clear feedback and handles clipboard errors gracefully.
7
+ - **Skill saving bug fixed** — Skills are not saved in the correct directory and survives application updates.
8
+ - **Clean terminal on launch** — Starting ShortcutXL now clears prior output so you always begin with a fresh screen.
9
+
10
+ ## [0.2.14]
11
+
12
+ - **Python linting & type checking** — Added ruff and pyright checks for the Python modules and tests. Import sorting, unused variables, and type errors are now caught before publish.
13
+ - **Cleaner dead-code detection** — Removed unused TypeScript exports and tightened knip configuration.
14
+
15
+ ## [0.2.13]
16
+
17
+ - **Stay logged in across terminals** — Previously, opening a new terminal while others were running could force you to log in again. Now your session persists reliably no matter how many terminals you have open.
18
+
19
+ ## [0.2.12]
20
+
21
+ - **No more popup windows during updates** — Background updates now run silently instead of flashing a command prompt on your screen.
22
+
23
+ ## [0.2.11]
24
+
25
+ - **More reliable first-time setup** — ShortcutXL now waits for Excel to be fully ready before running its connection test, so setup succeeds on the first try even on slower machines.
26
+ - **Switched to production servers** — All traffic now routes through `shortcut.ai` production endpoints for better performance and reliability.
27
+
28
+ ## [0.2.9]
29
+
30
+ - **Works without Python on your PATH** — ShortcutXL now finds and loads Python automatically. No more fiddling with environment variables or system settings to get things working.
31
+
32
+ ## [0.1.1]
33
+
34
+ - **Update notifications** — ShortcutXL tells you when a new version is available and shows you the exact command to upgrade.
35
+ - **Survives reinstalls** — Updating or reinstalling the npm package no longer breaks your Excel setup. Everything keeps working without reconfiguration.
36
+ - **Handles Excel locks gracefully** — If Excel has ShortcutXL loaded during an update, you get a clear message instead of a crash.
package/dist/cli.js CHANGED
@@ -14,6 +14,8 @@ const __dirname = dirname(fileURLToPath(import.meta.url));
14
14
  const agentRoot = resolve(__dirname, '..');
15
15
  dotenv.config({ path: resolve(agentRoot, '.env.development') });
16
16
  process.title = 'shortcut';
17
+ // Clear terminal (scrollback + visible area) for a clean slate on launch.
18
+ process.stdout.write('\x1b[2J\x1b[3J\x1b[H');
17
19
  const { main } = await import('./main.js');
18
20
  main(process.argv.slice(2));
19
21
  //# sourceMappingURL=cli.js.map
@@ -45,11 +45,6 @@ export function buildSystemPrompt(options = {}) {
45
45
  prompt += `## ${filePath}\n\n${content}\n\n`;
46
46
  }
47
47
  }
48
- // Append skills section (only if read tool is available)
49
- const customPromptHasRead = !selectedTools || selectedTools.includes(READ);
50
- if (customPromptHasRead && skills.length > 0) {
51
- prompt += formatSkillsForPrompt(skills);
52
- }
53
48
  // Add date/time and working directory last
54
49
  prompt += `\n\nCurrent date and time: ${dateTime}`;
55
50
  prompt += `\nCurrent working directory: ${resolvedCwd}`;
@@ -2,15 +2,16 @@
2
2
  * Action-mode system prompt for the Shortcut agent.
3
3
  *
4
4
  * This is the normal post-installation prompt. Passed as `customPrompt` to
5
- * the core buildSystemPrompt(), which automatically appends: project context
6
- * files, skills, date/time, and cwd.
5
+ * the core buildSystemPrompt(), which appends only: project context files,
6
+ * date/time, and cwd. Skills are loaded and injected here directly.
7
7
  *
8
8
  * The prompt does NOT embed runtime-discovered paths (xllDir, modulesPath).
9
9
  * Those are shown in the TUI status line. The agent discovers them dynamically
10
10
  * via the health check endpoint or excel_exec.
11
11
  */
12
12
  import { join, resolve } from 'path';
13
- import { getAgentDir, getInstalledMarkerPath, getPackageDir, getSessionsDir } from '../../config.js';
13
+ import { getAgentDir, getBundledSkillsDir, getInstalledMarkerPath, getPackageDir, getSessionsDir } from '../../config.js';
14
+ import { loadSkills } from '../../core/skills.js';
14
15
  import { EXCEL_HTTP_URL } from '../constants.js';
15
16
  import { EXCEL_COM_API_GUIDELINES } from './api.js';
16
17
  import { CODING_GUIDELINES, COMMUNICATION_GUIDELINES, DATA_FORMATTING_GUIDELINES, EXPLORATION_GUIDELINES, NOTES_COMMENTS_GUIDELINES, NUMBER_FORMAT_REFERENCE } from './shared.js';
@@ -57,6 +58,7 @@ Excel Processing:
57
58
  - if Excel is not running, we need to run it -- ask the user if they want a specific file open or if you can just open a blank workbook
58
59
  - if Excel is running, we need to close the excel process and restart it. Ask the user to save work, close Excel, and then re-open
59
60
  - wait a couple seconds to health check
61
+ - if health check passes but excel_exec fails, it is probably blocked by a pop-up. Confirm and debug however you can and then stop. Don't do anything fancy, just tell the user that nothing can proceed until popups are gone
60
62
 
61
63
  ## Extensibility — UDFs vs Skills vs Extensions
62
64
  For any extension requests, ask the user which approach fits their need, or recommend one based on context. Prefer skills by default — they're lightweight and don't pollute context
@@ -66,10 +68,15 @@ For any extension requests, ask the user which approach fits their need, or reco
66
68
  const INSTALLATION = (markerPath) => `\
67
69
  ## Installation
68
70
  The installed marker file is at \`${markerPath}\`. If the user needs to re-run installation (e.g. something is broken), delete this file and use switch_mode to switch to "installation" mode`;
69
- const DOCS = (paths) => `\
71
+ const SELF_REFERENCE = (paths) => `\
72
+ ======================
73
+ ## SELF REFERENCE
74
+ ======================
75
+
70
76
  ## Key Directories
71
77
  - Package directory (shipped code, read-only): ${paths.packageDir}
72
78
  - Agent directory (user config, state, XLL binaries): ${paths.agentDir}
79
+ - User skills (create new skills here): ${paths.userSkillsDir}
73
80
  - Sessions (prior conversation JSONL transcripts): ${paths.sessionsDir}
74
81
  - Temp directory (dump temporary/scratch files here, e.g. exported Excel files): ${paths.tempDir}
75
82
 
@@ -86,33 +93,56 @@ Read these for introspection, customization, debugging of Shortcut itself, its S
86
93
  - Examples: ${paths.examples} (extensions, custom tools, SDK)
87
94
  - Topics: extensions (docs/extensions.md, examples/extensions/), themes (docs/themes.md), skills (docs/skills.md), prompt templates (docs/prompt-templates.md), TUI (docs/tui.md), keybindings (docs/keybindings.md), SDK (docs/sdk.md), custom providers (docs/custom-provider.md), models (docs/models.md), packages (docs/packages.md)
88
95
  - Always read Shortcut .md files completely and follow links to related docs`;
96
+ const SKILLS = (skills, userSkillsDir) => {
97
+ const visible = skills.filter((s) => !s.disableModelInvocation);
98
+ const lines = [
99
+ '======================',
100
+ '## SKILLS',
101
+ '======================',
102
+ 'The following skills provide specialized instructions for specific tasks.',
103
+ 'When a skill file references a relative path, resolve it against the skill directory (parent of SKILL.md).',
104
+ '',
105
+ `To create a new user skill: ${userSkillsDir}/<skill-name>/SKILL.md`,
106
+ 'Format:',
107
+ ' ---',
108
+ ' name: <skill-name> # must match the parent directory name',
109
+ ' description: "..." # one-line summary shown here; max 1024 chars',
110
+ ' ---',
111
+ ' <skill body — clear and concise instructions the agent reads when the skill is invoked>',
112
+ ''
113
+ ];
114
+ for (const skill of visible) {
115
+ lines.push(`- ${skill.name} (${skill.filePath}): ${skill.description}`);
116
+ }
117
+ return lines.join('\n');
118
+ };
89
119
  // ---------------------------------------------------------------------------
90
- // Assembly — resolves runtime paths, joins sections
120
+ // Assembly — resolves runtime paths, loads skills, joins sections
91
121
  // ---------------------------------------------------------------------------
92
122
  export function buildActionPrompt() {
123
+ const agentDir = getAgentDir();
93
124
  const agentDocsDir = resolve(join(getPackageDir(), 'agent-docs'));
94
125
  const installedMarker = getInstalledMarkerPath();
95
- return [
126
+ const userSkillsDir = join(agentDir, 'skills');
127
+ const { skills } = loadSkills({ agentDir, skillPaths: [getBundledSkillsDir()] });
128
+ const skillsSection = SKILLS(skills, userSkillsDir);
129
+ const sections = [
96
130
  CORE(),
97
131
  INSTALLATION(installedMarker),
98
- EXPLORATION_GUIDELINES,
99
- DATA_FORMATTING_GUIDELINES,
100
- NUMBER_FORMAT_REFERENCE,
101
- NOTES_COMMENTS_GUIDELINES,
102
- CODING_GUIDELINES,
103
- COMMUNICATION_GUIDELINES,
104
- DOCS({
132
+ SELF_REFERENCE({
105
133
  packageDir: getPackageDir(),
106
- agentDir: getAgentDir(),
134
+ agentDir,
135
+ userSkillsDir: join(agentDir, 'skills'),
107
136
  sessionsDir: getSessionsDir(),
108
- tempDir: join(getAgentDir(), 'temp'),
137
+ tempDir: join(agentDir, 'temp'),
109
138
  xllSpec: join(agentDocsDir, 'xll-spec.md'),
110
139
  xllSkill: join(agentDocsDir, 'xll-skill.md'),
111
140
  readme: join(agentDocsDir, 'README.md'),
112
141
  docs: join(agentDocsDir, 'docs'),
113
142
  examples: join(agentDocsDir, 'examples')
114
- }),
115
- EXCEL_COM_API_GUIDELINES
116
- ].join('\n\n');
143
+ })
144
+ ];
145
+ sections.push(skillsSection, COMMUNICATION_GUIDELINES, CODING_GUIDELINES, EXPLORATION_GUIDELINES, DATA_FORMATTING_GUIDELINES, NUMBER_FORMAT_REFERENCE, NOTES_COMMENTS_GUIDELINES, EXCEL_COM_API_GUIDELINES);
146
+ return sections.join('\n\n');
117
147
  }
118
148
  //# sourceMappingURL=action.js.map
@@ -42,6 +42,7 @@ function padVisible(text, targetWidth) {
42
42
  export class ExcelApprovalComponent extends Container {
43
43
  selectedButton = 0;
44
44
  scrollOffset = 0;
45
+ cursorRow = 0;
45
46
  diff;
46
47
  theme;
47
48
  onDone;
@@ -63,7 +64,8 @@ export class ExcelApprovalComponent extends Container {
63
64
  this.tableContainer = new Container();
64
65
  this.addChild(this.tableContainer);
65
66
  if (diff.total > diff.cells.length) {
66
- this.addChild(new Text(theme.fg('dim', ` ... and ${diff.total - diff.cells.length} more cells`), 0, 0));
67
+ const hidden = diff.total - diff.cells.length;
68
+ this.addChild(new Text(theme.fg('dim', ` ... ${hidden} more cell${hidden !== 1 ? 's' : ''} not shown`), 0, 0));
67
69
  }
68
70
  this.addChild(new Spacer(1));
69
71
  this.buttonContainer = new Container();
@@ -77,51 +79,58 @@ export class ExcelApprovalComponent extends Container {
77
79
  const cells = this.diff.cells;
78
80
  const visible = cells.slice(this.scrollOffset, this.scrollOffset + MAX_VISIBLE_ROWS);
79
81
  // Compute column widths from visible data
80
- const colWidths = { cell: 4, before: 6, after: 5, formula: 7 };
82
+ const colWidths = { addr: 7, before: 6, after: 5, detail: 7 };
81
83
  for (const c of visible) {
82
- colWidths.cell = Math.max(colWidths.cell, String(c.cell).length);
84
+ colWidths.addr = Math.max(colWidths.addr, String(c.address).length);
83
85
  colWidths.before = Math.max(colWidths.before, String(c.before).length);
84
86
  colWidths.after = Math.max(colWidths.after, String(c.after).length);
85
- const formulaText = c.formula || c.oldFormula || '';
86
- colWidths.formula = Math.max(colWidths.formula, formulaText.length);
87
+ colWidths.detail = Math.max(colWidths.detail, (c.formula || c.oldFormula || '').length);
87
88
  }
88
- colWidths.cell = Math.min(colWidths.cell, 20);
89
+ colWidths.addr = Math.min(colWidths.addr, 24);
89
90
  colWidths.before = Math.min(colWidths.before, 16);
90
91
  colWidths.after = Math.min(colWidths.after, 16);
91
- colWidths.formula = Math.min(colWidths.formula, 30);
92
+ colWidths.detail = Math.min(colWidths.detail, 30);
92
93
  // Header
93
94
  const header = ' ' +
94
- this.theme.fg('dim', padVisible('Cell', colWidths.cell) +
95
+ this.theme.fg('dim', padVisible('Address', colWidths.addr) +
95
96
  ' ' +
96
97
  padVisible('Before', colWidths.before) +
97
98
  ' ' +
98
99
  padVisible('After', colWidths.after) +
99
100
  ' ' +
100
- padVisible('Formula', colWidths.formula));
101
+ padVisible('Detail', colWidths.detail));
101
102
  this.tableContainer.addChild(new Text(header, 0, 0));
102
- // Rows
103
- for (const c of visible) {
104
- const cellStr = truncateToWidth(String(c.cell), colWidths.cell, '..', true);
103
+ for (let idx = 0; idx < visible.length; idx++) {
104
+ const c = visible[idx];
105
+ const absIdx = this.scrollOffset + idx;
106
+ const isCursor = absIdx === this.cursorRow;
107
+ const marker = isCursor ? '▸ ' : ' ';
108
+ const addrStr = truncateToWidth(String(c.address), colWidths.addr, '..', true);
105
109
  const beforeStr = truncateToWidth(String(c.before), colWidths.before, '..', true);
106
110
  const afterStr = truncateToWidth(String(c.after), colWidths.after, '..', true);
107
- const formulaStr = c.formula
108
- ? truncateToWidth(c.formula, colWidths.formula, '..', true)
109
- : c.oldFormula
110
- ? truncateToWidth(this.theme.fg('dim', c.oldFormula), colWidths.formula, '..', true)
111
- : padVisible(this.theme.fg('dim', 'hardcoded'), colWidths.formula);
112
- const row = ' ' +
113
- this.theme.fg('text', padVisible(cellStr, colWidths.cell)) +
111
+ let detailStr;
112
+ if (c.formula) {
113
+ detailStr = truncateToWidth(c.formula, colWidths.detail, '..', true);
114
+ }
115
+ else if (c.oldFormula) {
116
+ detailStr = truncateToWidth(this.theme.fg('dim', c.oldFormula), colWidths.detail, '..', true);
117
+ }
118
+ else {
119
+ detailStr = padVisible(this.theme.fg('dim', 'hardcoded'), colWidths.detail);
120
+ }
121
+ const row = marker +
122
+ this.theme.fg(isCursor ? 'accent' : 'text', padVisible(addrStr, colWidths.addr)) +
114
123
  ' ' +
115
124
  this.theme.fg(c.before !== 'empty' ? 'error' : 'dim', padVisible(beforeStr, colWidths.before)) +
116
125
  ' ' +
117
126
  this.theme.fg('accent', padVisible(afterStr, colWidths.after)) +
118
127
  ' ' +
119
- formulaStr;
128
+ detailStr;
120
129
  this.tableContainer.addChild(new Text(row, 0, 0));
121
130
  }
122
131
  if (cells.length > MAX_VISIBLE_ROWS) {
123
132
  const scrollInfo = ` ${this.scrollOffset + 1}-${Math.min(this.scrollOffset + MAX_VISIBLE_ROWS, cells.length)} of ${cells.length}`;
124
- this.tableContainer.addChild(new Text(this.theme.fg('dim', scrollInfo + ' (\u2191\u2193 scroll)'), 0, 0));
133
+ this.tableContainer.addChild(new Text(this.theme.fg('dim', scrollInfo + ' (\u2191\u2193 navigate)'), 0, 0));
125
134
  }
126
135
  }
127
136
  updateButtons() {
@@ -145,29 +154,37 @@ export class ExcelApprovalComponent extends Container {
145
154
  }
146
155
  }
147
156
  this.buttonContainer.addChild(new Text(' ' + parts.join(' '), 0, 0));
148
- this.buttonContainer.addChild(new Text(this.theme.fg('dim', ' \u2190\u2192 navigate enter select esc cancel'), 0, 0));
157
+ this.buttonContainer.addChild(new Text(this.theme.fg('dim', ' \u2191\u2193 navigate \u2190\u2192 select enter confirm esc cancel'), 0, 0));
158
+ }
159
+ moveCursor(delta) {
160
+ const maxRow = this.diff.cells.length - 1;
161
+ const next = Math.max(0, Math.min(maxRow, this.cursorRow + delta));
162
+ if (next === this.cursorRow)
163
+ return;
164
+ this.cursorRow = next;
165
+ if (this.cursorRow < this.scrollOffset) {
166
+ this.scrollOffset = this.cursorRow;
167
+ }
168
+ else if (this.cursorRow >= this.scrollOffset + MAX_VISIBLE_ROWS) {
169
+ this.scrollOffset = this.cursorRow - MAX_VISIBLE_ROWS + 1;
170
+ }
171
+ this.updateTable();
149
172
  }
150
173
  handleInput(keyData) {
151
174
  const kb = getEditorKeybindings();
152
- if (keyData === '\x1b[D' || keyData === 'h') {
175
+ if (kb.matches(keyData, 'cursorLeft') || keyData === 'h') {
153
176
  this.selectedButton = Math.max(0, this.selectedButton - 1);
154
177
  this.updateButtons();
155
178
  }
156
- else if (keyData === '\x1b[C' || keyData === 'l') {
179
+ else if (kb.matches(keyData, 'cursorRight') || keyData === 'l') {
157
180
  this.selectedButton = Math.min(BUTTONS.length - 1, this.selectedButton + 1);
158
181
  this.updateButtons();
159
182
  }
160
- else if (keyData === '\x1b[A' || keyData === 'k') {
161
- if (this.scrollOffset > 0) {
162
- this.scrollOffset--;
163
- this.updateTable();
164
- }
183
+ else if (kb.matches(keyData, 'selectUp') || keyData === 'k') {
184
+ this.moveCursor(-1);
165
185
  }
166
- else if (keyData === '\x1b[B' || keyData === 'j') {
167
- if (this.scrollOffset + MAX_VISIBLE_ROWS < this.diff.cells.length) {
168
- this.scrollOffset++;
169
- this.updateTable();
170
- }
186
+ else if (kb.matches(keyData, 'selectDown') || keyData === 'j') {
187
+ this.moveCursor(1);
171
188
  }
172
189
  else if (kb.matches(keyData, 'selectConfirm') || keyData === '\n' || keyData === '\r') {
173
190
  this.onDone(BUTTONS[this.selectedButton]);
@@ -68,8 +68,8 @@ export function createSwitchModeTool(session) {
68
68
  };
69
69
  }
70
70
  const s = session();
71
- s.setBaseSystemPrompt(target.systemPrompt);
72
71
  s.setActiveToolsByName([...target.tools]);
72
+ s.setBaseSystemPrompt(target.systemPrompt);
73
73
  currentModeRef = target.name;
74
74
  // Snapshot the new system prompt + tools into the session trace so the
75
75
  // JSONL reflects the mode switch (otherwise only the initial prompt is recorded).
@@ -1603,10 +1603,12 @@ export class InteractiveMode {
1603
1603
  // No image — check if clipboard contains a file path
1604
1604
  if (this.tryAttachClipboardFilePath()) {
1605
1605
  this.ui.requestRender();
1606
+ return;
1606
1607
  }
1608
+ this.showStatus('Nothing to attach — clipboard has no image or file path');
1607
1609
  }
1608
- catch {
1609
- // Silently ignore clipboard errors (may not have permission, etc.)
1610
+ catch (err) {
1611
+ this.showError(`Clipboard paste failed: ${err instanceof Error ? err.message : String(err)}`);
1610
1612
  }
1611
1613
  }
1612
1614
  /**
@@ -83,6 +83,36 @@ function runCommand(command, args, options) {
83
83
  : Buffer.from(result.stdout ?? '', typeof result.stdout === 'string' ? 'utf-8' : undefined);
84
84
  return { ok: true, stdout };
85
85
  }
86
+ /**
87
+ * PowerShell fallback for Windows image reading.
88
+ * Handles BMP/DIB format (Snipping Tool, Win+Shift+S) and cases where the
89
+ * native @mariozechner/clipboard module is unavailable or returns hasImage()=false.
90
+ * Saves clipboard image as PNG via System.Windows.Forms and returns base64-encoded bytes.
91
+ */
92
+ function readClipboardImageViaPowerShell() {
93
+ const result = runCommand('powershell.exe', [
94
+ '-NoProfile',
95
+ '-Command',
96
+ 'Add-Type -AssemblyName System.Windows.Forms; ' +
97
+ '$img = [System.Windows.Forms.Clipboard]::GetImage(); ' +
98
+ 'if ($img -eq $null) { exit 1 }; ' +
99
+ '$ms = New-Object System.IO.MemoryStream; ' +
100
+ '$img.Save($ms, [System.Drawing.Imaging.ImageFormat]::Png); ' +
101
+ '[System.Convert]::ToBase64String($ms.ToArray())'
102
+ ], { timeoutMs: 8000 });
103
+ if (!result.ok || result.stdout.length === 0)
104
+ return null;
105
+ try {
106
+ const base64 = result.stdout.toString('utf-8').trim();
107
+ const bytes = Buffer.from(base64, 'base64');
108
+ if (bytes.length === 0)
109
+ return null;
110
+ return { bytes, mimeType: 'image/png' };
111
+ }
112
+ catch {
113
+ return null;
114
+ }
115
+ }
86
116
  function readClipboardImageViaWlPaste() {
87
117
  const list = runCommand('wl-paste', ['--list-types'], { timeoutMs: DEFAULT_LIST_TIMEOUT_MS });
88
118
  if (!list.ok) {
@@ -137,8 +167,22 @@ export async function readClipboardImage(options) {
137
167
  if (platform === 'linux' && isWaylandSession(env)) {
138
168
  image = readClipboardImageViaWlPaste() ?? readClipboardImageViaXclip();
139
169
  }
170
+ else if (platform === 'win32') {
171
+ // Try native module first (fast), then PowerShell fallback.
172
+ // PowerShell catches BMP/DIB format (Snipping Tool, Win+Shift+S) which
173
+ // hasImage() misses, and also works when the native module fails to load.
174
+ if (clipboard?.hasImage()) {
175
+ const imageData = await clipboard.getImageBinary();
176
+ if (imageData && imageData.length > 0) {
177
+ const bytes = imageData instanceof Uint8Array ? imageData : Uint8Array.from(imageData);
178
+ image = { bytes, mimeType: 'image/png' };
179
+ }
180
+ }
181
+ image ??= readClipboardImageViaPowerShell();
182
+ }
140
183
  else {
141
- if (!clipboard || !clipboard.hasImage()) {
184
+ // macOS and other platforms
185
+ if (!clipboard?.hasImage()) {
142
186
  return null;
143
187
  }
144
188
  const imageData = await clipboard.getImageBinary();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "shortcutxl",
3
- "version": "0.2.15",
3
+ "version": "0.2.17",
4
4
  "type": "module",
5
5
  "files": [
6
6
  "dist/",
@@ -10,7 +10,8 @@
10
10
  "!dist/test-helpers/",
11
11
  "skills/",
12
12
  "agent-docs/",
13
- "xll/"
13
+ "xll/",
14
+ "CHANGELOG.md"
14
15
  ],
15
16
  "shortcutConfig": {
16
17
  "name": "shortcut",
@@ -33,6 +34,7 @@
33
34
  "format:check": "prettier --check 'src/**/*.{ts,tsx,js,json}'",
34
35
  "test": "vitest --run",
35
36
  "test:e2e": "vitest --run --config vitest.e2e.config.ts",
37
+ "release": "node -e \"const v=require('./package.json').version;const t='shortcutxl/v'+v;require('child_process').execSync('git tag '+t,{stdio:'inherit'});require('child_process').execSync('git push origin '+t,{stdio:'inherit'});console.log('Tagged and pushed '+t)\"",
36
38
  "test:watch": "vitest",
37
39
  "knip": "knip",
38
40
  "lint:python": "cd .. && uvx ruff check modules/ tests/",
@@ -71,7 +71,6 @@ def debug_grid_test():
71
71
 
72
72
  def _do(app):
73
73
  sheet = app.ActiveSheet
74
- top = sheet.Range("Z5").Cells(1, 1)
75
74
  sheet.Range("Z5:AD9").Value = grid
76
75
 
77
76
  shortcut_xl.xl_batch(_do)
@@ -107,7 +106,7 @@ def debug_grid_tuple_test():
107
106
  sheet.Range("Z5:AD9").Value = grid
108
107
 
109
108
  shortcut_xl.xl_batch(_do)
110
- xl_log(f"debug_grid_tuple_test: wrote tuple grid to Z5:AD9")
109
+ xl_log("debug_grid_tuple_test: wrote tuple grid to Z5:AD9")
111
110
  return "Wrote tuple-of-tuples 5x5 to Z5:AD9"
112
111
  except Exception as e:
113
112
  xl_log(f"debug_grid_tuple_test FAILED: {e}")
@@ -207,9 +206,7 @@ def debug_frame_ascii():
207
206
  row = []
208
207
  for c in range(WIDTH):
209
208
  ch = "." # visible empty (not "")
210
- if c == 0 and paddle_l <= r < paddle_l + PADDLE_H:
211
- ch = "|"
212
- elif c == WIDTH - 1 and paddle_r <= r < paddle_r + PADDLE_H:
209
+ if c == 0 and paddle_l <= r < paddle_l + PADDLE_H or c == WIDTH - 1 and paddle_r <= r < paddle_r + PADDLE_H:
213
210
  ch = "|"
214
211
  elif r == ball_y and c == ball_x:
215
212
  ch = "O"