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
package/README.md CHANGED
@@ -1,13 +1,33 @@
1
1
  # ShortcutXL
2
2
 
3
- An AI agent that lives on your computer and has Excel superpowers. Made by the Shortcut team [Shortcut](https://shortcut.ai).
3
+ An AI agent that lives on your computer and has Excel superpowers. Made by the [Shortcut](https://shortcut.ai) team.
4
+
5
+ ## Install
6
+
7
+ ### 1. Open Command Prompt or PowerShell and install Node.js
8
+
9
+ ```bash
10
+ winget install OpenJS.NodeJS.LTS
11
+ ```
12
+
13
+ ### 2. Install ShortcutXL
4
14
 
5
15
  ```bash
6
16
  npm install -g shortcutxl
17
+ ```
18
+
19
+ ### 3. Launch ShortcutXL
20
+
21
+ Type `shortcut` in your terminal:
22
+
23
+ ```bash
7
24
  shortcut
8
25
  ```
9
26
 
10
- > **Important:** Install globally with `-g`. Do not use `npm install shortcutxl` without it.
27
+ ## Requirements
28
+
29
+ - **Windows 10/11** with **Excel 2016+** (64-bit)
30
+ - **Node.js >= 20**
11
31
 
12
32
  ## Capabilities
13
33
 
@@ -19,8 +39,3 @@ shortcut
19
39
  - **Extensible** — Integrate any API or data source by adding a skill file or a custom tool extension.
20
40
  - **User-defined functions (UDFs)** — Custom Excel formulas powered by Python for live data, calculations, or database queries.
21
41
  - **External data connections** — ODBC, OLE DB, QueryTables, Power Query.
22
-
23
- ## Prerequisites
24
-
25
- - **Windows 10/11** with **Excel 2016+** (64-bit)
26
- - **Node.js >= 20** — If missing: `winget install OpenJS.NodeJS.LTS`
@@ -10,7 +10,8 @@ export const DEFAULT_APP_KEYBINDINGS = {
10
10
  clear: 'ctrl+c',
11
11
  exit: 'ctrl+d',
12
12
  suspend: 'ctrl+z',
13
- cycleThinkingLevel: 'shift+tab',
13
+ cycleThinkingLevel: 'shift+ctrl+tab',
14
+ toggleAutoApprove: 'shift+tab',
14
15
  cycleModelForward: 'ctrl+p',
15
16
  cycleModelBackward: 'shift+ctrl+p',
16
17
  selectModel: 'ctrl+l',
@@ -40,6 +41,7 @@ const APP_ACTIONS = [
40
41
  'exit',
41
42
  'suspend',
42
43
  'cycleThinkingLevel',
44
+ 'toggleAutoApprove',
43
45
  'cycleModelForward',
44
46
  'cycleModelBackward',
45
47
  'selectModel',
@@ -2,6 +2,11 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
2
2
  import { dirname, join } from 'path';
3
3
  import lockfile from 'proper-lockfile';
4
4
  import { CONFIG_DIR_NAME, getAgentDir } from '../config.js';
5
+ /** Display labels for the approval mode (footer + /settings). */
6
+ export const APPROVAL_MODE = {
7
+ ASK: 'Ask before edits',
8
+ AUTO: 'Approve all edits',
9
+ };
5
10
  /** Deep merge settings: project/overrides take precedence, nested objects merge recursively */
6
11
  function deepMergeSettings(base, overrides) {
7
12
  const result = { ...base };
@@ -568,5 +573,29 @@ export class SettingsManager {
568
573
  getCodeBlockIndent() {
569
574
  return this.settings.markdown?.codeBlockIndent ?? ' ';
570
575
  }
576
+ // Session-scoped auto-approve. Initialized from the persistent
577
+ // settings.autoApprove default, then toggled freely via Shift+Tab.
578
+ _autoApprove;
579
+ getAutoApprove() {
580
+ if (this._autoApprove === undefined) {
581
+ this._autoApprove = this.settings.autoApprove ?? false;
582
+ }
583
+ return this._autoApprove;
584
+ }
585
+ /** Toggle session-scoped auto-approve (Shift+Tab / Accept All). */
586
+ setAutoApprove(autoApprove) {
587
+ this._autoApprove = autoApprove;
588
+ }
589
+ /** Get the persistent default (what new sessions start with). */
590
+ getAutoApproveDefault() {
591
+ return this.settings.autoApprove ?? false;
592
+ }
593
+ /** Persist the default and update the current session to match. */
594
+ setAutoApproveDefault(autoApprove) {
595
+ this.globalSettings.autoApprove = autoApprove;
596
+ this._autoApprove = autoApprove;
597
+ this.markModified('autoApprove');
598
+ this.save();
599
+ }
571
600
  }
572
601
  //# sourceMappingURL=settings-manager.js.map
@@ -7,9 +7,6 @@
7
7
  import chalk from 'chalk';
8
8
  import { spawnSync } from 'child_process';
9
9
  // ── Logging ──────────────────────────────────────────────────────────────
10
- export function log(msg) {
11
- console.log(chalk.cyan(' → ') + msg);
12
- }
13
10
  export function ok(msg) {
14
11
  console.log(chalk.green(' ✓ ') + msg);
15
12
  }
@@ -68,7 +68,7 @@ function readApiReference() {
68
68
  // ---------------------------------------------------------------------------
69
69
  // Exported prompt string
70
70
  // ---------------------------------------------------------------------------
71
- export function getExcelComApiGuidelines() {
71
+ function getExcelComApiGuidelines() {
72
72
  const apiRef = readApiReference();
73
73
  return `${COM_GUIDELINES}
74
74
 
@@ -5,5 +5,6 @@
5
5
  * and used in auth checks, model scoping, and provider-specific logic.
6
6
  */
7
7
  export const SHORTCUT_PROVIDER_ID = 'shortcut';
8
+ /** @public — parked until multi-provider support */
8
9
  export const OPENAI_CODEX_PROVIDER_ID = 'openai';
9
10
  //# sourceMappingURL=provider-ids.js.map
@@ -0,0 +1,180 @@
1
+ /**
2
+ * Compact approval component for Excel cell changes.
3
+ *
4
+ * Renders a table (Cell | Before | After | Formula) with horizontal
5
+ * buttons: [Accept] [Accept All] [Reject].
6
+ */
7
+ import { Container, getEditorKeybindings, Spacer, Text, truncateToWidth, visibleWidth } from '../../tui/index.js';
8
+ /** User's response to the approval prompt. */
9
+ export const APPROVAL = {
10
+ ACCEPT: 'Accept',
11
+ ACCEPT_ALL: 'Accept All',
12
+ REJECT: 'Reject',
13
+ };
14
+ /** Control actions sent from TS → Python over HTTP. */
15
+ export const CONTROL = {
16
+ REVERT: 'revert',
17
+ CLEANUP: 'cleanup',
18
+ };
19
+ /** Outcome of the approval flow (internal to excel-exec). */
20
+ export const OUTCOME = {
21
+ ACCEPTED: 'accepted',
22
+ REVERTED: 'reverted',
23
+ REVERT_FAILED: 'revert-failed',
24
+ };
25
+ const MAX_VISIBLE_ROWS = 8;
26
+ const BUTTONS = [APPROVAL.ACCEPT, APPROVAL.ACCEPT_ALL, APPROVAL.REJECT];
27
+ /** Inline border — avoids importing from modes/ (boundary rule). */
28
+ class Border {
29
+ color;
30
+ constructor(color) {
31
+ this.color = color;
32
+ }
33
+ invalidate() { }
34
+ render(width) {
35
+ return [this.color('\u2500'.repeat(Math.max(1, width)))];
36
+ }
37
+ }
38
+ function padVisible(text, targetWidth) {
39
+ const w = visibleWidth(text);
40
+ return w >= targetWidth ? text : text + ' '.repeat(targetWidth - w);
41
+ }
42
+ export class ExcelApprovalComponent extends Container {
43
+ selectedButton = 0;
44
+ scrollOffset = 0;
45
+ diff;
46
+ theme;
47
+ onDone;
48
+ tableContainer;
49
+ buttonContainer;
50
+ borderColor;
51
+ constructor(diff, theme, onDone) {
52
+ super();
53
+ this.diff = diff;
54
+ this.theme = theme;
55
+ this.onDone = onDone;
56
+ this.borderColor = (s) => theme.fg('border', s);
57
+ this.addChild(new Border(this.borderColor));
58
+ const count = diff.total;
59
+ const title = theme.bold(theme.fg('accent', ' Review Changes')) +
60
+ theme.fg('dim', ` ${count} cell${count !== 1 ? 's' : ''} modified`);
61
+ this.addChild(new Text(title, 0, 0));
62
+ this.addChild(new Spacer(1));
63
+ this.tableContainer = new Container();
64
+ this.addChild(this.tableContainer);
65
+ 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
+ }
68
+ this.addChild(new Spacer(1));
69
+ this.buttonContainer = new Container();
70
+ this.addChild(this.buttonContainer);
71
+ this.addChild(new Border(this.borderColor));
72
+ this.updateTable();
73
+ this.updateButtons();
74
+ }
75
+ updateTable() {
76
+ this.tableContainer.clear();
77
+ const cells = this.diff.cells;
78
+ const visible = cells.slice(this.scrollOffset, this.scrollOffset + MAX_VISIBLE_ROWS);
79
+ // Compute column widths from visible data
80
+ const colWidths = { cell: 4, before: 6, after: 5, formula: 7 };
81
+ for (const c of visible) {
82
+ colWidths.cell = Math.max(colWidths.cell, String(c.cell).length);
83
+ colWidths.before = Math.max(colWidths.before, String(c.before).length);
84
+ 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
+ }
88
+ colWidths.cell = Math.min(colWidths.cell, 20);
89
+ colWidths.before = Math.min(colWidths.before, 16);
90
+ colWidths.after = Math.min(colWidths.after, 16);
91
+ colWidths.formula = Math.min(colWidths.formula, 30);
92
+ // Header
93
+ const header = ' ' +
94
+ this.theme.fg('dim', padVisible('Cell', colWidths.cell) +
95
+ ' ' +
96
+ padVisible('Before', colWidths.before) +
97
+ ' ' +
98
+ padVisible('After', colWidths.after) +
99
+ ' ' +
100
+ padVisible('Formula', colWidths.formula));
101
+ 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);
105
+ const beforeStr = truncateToWidth(String(c.before), colWidths.before, '..', true);
106
+ 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)) +
114
+ ' ' +
115
+ this.theme.fg(c.before !== 'empty' ? 'error' : 'dim', padVisible(beforeStr, colWidths.before)) +
116
+ ' ' +
117
+ this.theme.fg('accent', padVisible(afterStr, colWidths.after)) +
118
+ ' ' +
119
+ formulaStr;
120
+ this.tableContainer.addChild(new Text(row, 0, 0));
121
+ }
122
+ if (cells.length > MAX_VISIBLE_ROWS) {
123
+ 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));
125
+ }
126
+ }
127
+ updateButtons() {
128
+ this.buttonContainer.clear();
129
+ const parts = [];
130
+ for (let i = 0; i < BUTTONS.length; i++) {
131
+ const label = BUTTONS[i];
132
+ if (i === this.selectedButton) {
133
+ if (label === APPROVAL.ACCEPT) {
134
+ parts.push(this.theme.fg('success', this.theme.bold(`[ ${label} ]`)));
135
+ }
136
+ else if (label === APPROVAL.REJECT) {
137
+ parts.push(this.theme.fg('error', this.theme.bold(`[ ${label} ]`)));
138
+ }
139
+ else {
140
+ parts.push(this.theme.bold(`[ ${label} ]`));
141
+ }
142
+ }
143
+ else {
144
+ parts.push(this.theme.fg('dim', ` ${label} `));
145
+ }
146
+ }
147
+ 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));
149
+ }
150
+ handleInput(keyData) {
151
+ const kb = getEditorKeybindings();
152
+ if (keyData === '\x1b[D' || keyData === 'h') {
153
+ this.selectedButton = Math.max(0, this.selectedButton - 1);
154
+ this.updateButtons();
155
+ }
156
+ else if (keyData === '\x1b[C' || keyData === 'l') {
157
+ this.selectedButton = Math.min(BUTTONS.length - 1, this.selectedButton + 1);
158
+ this.updateButtons();
159
+ }
160
+ else if (keyData === '\x1b[A' || keyData === 'k') {
161
+ if (this.scrollOffset > 0) {
162
+ this.scrollOffset--;
163
+ this.updateTable();
164
+ }
165
+ }
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
+ }
171
+ }
172
+ else if (kb.matches(keyData, 'selectConfirm') || keyData === '\n' || keyData === '\r') {
173
+ this.onDone(BUTTONS[this.selectedButton]);
174
+ }
175
+ else if (kb.matches(keyData, 'selectCancel')) {
176
+ this.onDone('Reject');
177
+ }
178
+ }
179
+ }
180
+ //# sourceMappingURL=excel-approval.js.map
@@ -2,13 +2,16 @@
2
2
  * Excel exec tool — POST /exec with Python code to ShortcutXL's HTTP server.
3
3
  *
4
4
  * Request: POST /exec { "code": "..." }
5
- * Response: { "ok": true, "output": "..." } or { "ok": false, "error": "..." }
5
+ * POST /exec { "control": "revert"|"cleanup", "cfTxId": "..." }
6
+ * Response: { "ok": true, "output": "...", "diff"?: {...}, "cfTxId"?: "..." }
7
+ * or { "ok": false, "error": "..." }
6
8
  */
7
9
  import { Type } from '@sinclair/typebox';
8
10
  import { truncateOutput } from '../../core/tools/truncate.js';
9
11
  import { EXCEL_EXEC } from '../../tool-names.js';
10
12
  import { Text } from '../../tui/index.js';
11
13
  import { formatNotation, parseExcelLocation } from './excel-range.js';
14
+ import { APPROVAL, CONTROL, OUTCOME } from './excel-approval.js';
12
15
  import { highlightCode } from './render-helpers.js';
13
16
  const TOOL_NAME = EXCEL_EXEC;
14
17
  const NO_OUTPUT_FALLBACK = '(no output)';
@@ -32,7 +35,48 @@ const schema = Type.Object({
32
35
  description: 'Python code to execute in Excel. `app` (Excel.Application) is already available — do NOT import xl_app. Use print() to return values.'
33
36
  })
34
37
  });
35
- export function createExcelExecTool(httpUrl) {
38
+ /**
39
+ * Resolve approval for cell changes: show UI if needed, then revert or cleanup.
40
+ * Extracted from execute() so the approval flow is independently testable.
41
+ */
42
+ async function resolveApproval(result, approval, postControl, ctx) {
43
+ const { diff, cfTxId } = result;
44
+ const needsUI = diff && approval && !approval.isAutoApproved();
45
+ if (needsUI) {
46
+ const choice = await approval.requestApproval(diff, ctx);
47
+ if (choice === APPROVAL.REJECT || choice === undefined) {
48
+ try {
49
+ await postControl({ control: CONTROL.REVERT, cfTxId });
50
+ return OUTCOME.REVERTED;
51
+ }
52
+ catch {
53
+ // Revert failed — try to at least clean up CF highlights.
54
+ if (cfTxId != null) {
55
+ await postControl({ control: CONTROL.CLEANUP, cfTxId }).catch(() => { });
56
+ }
57
+ return OUTCOME.REVERT_FAILED;
58
+ }
59
+ }
60
+ if (choice === APPROVAL.ACCEPT_ALL) {
61
+ approval.onAcceptAll();
62
+ }
63
+ }
64
+ // Accepted (explicitly or auto) — clean up CF highlights.
65
+ if (cfTxId != null) {
66
+ await postControl({ control: CONTROL.CLEANUP, cfTxId }).catch((e) => {
67
+ const msg = e instanceof Error ? e.message : String(e);
68
+ console.error(`excel-exec: CF cleanup failed for tx ${cfTxId}: ${msg}`);
69
+ });
70
+ }
71
+ return OUTCOME.ACCEPTED;
72
+ }
73
+ export function createExcelExecTool(httpUrl, approval) {
74
+ const postControl = (body, signal) => fetch(`${httpUrl}/exec`, {
75
+ method: 'POST',
76
+ headers: { 'Content-Type': 'application/json' },
77
+ body: JSON.stringify(body),
78
+ signal
79
+ });
36
80
  return {
37
81
  name: TOOL_NAME,
38
82
  label: 'Excel exec',
@@ -76,15 +120,11 @@ export function createExcelExecTool(httpUrl) {
76
120
  return undefined;
77
121
  return new Text(theme.fg('toolOutput', output), 0, 0);
78
122
  },
79
- async execute(_toolCallId, { code, description: _description }, signal, _onUpdate, _ctx) {
123
+ async execute(_toolCallId, { code, description: _description }, signal, _onUpdate, ctx) {
124
+ const autoApprove = !approval || approval.isAutoApproved();
80
125
  let response;
81
126
  try {
82
- response = await fetch(`${httpUrl}/exec`, {
83
- method: 'POST',
84
- headers: { 'Content-Type': 'application/json' },
85
- body: JSON.stringify({ code }),
86
- signal
87
- });
127
+ response = await postControl({ code, autoApprove }, signal);
88
128
  }
89
129
  catch (e) {
90
130
  const msg = e instanceof Error ? e.message : String(e);
@@ -101,9 +141,24 @@ export function createExcelExecTool(httpUrl) {
101
141
  if (!result.ok) {
102
142
  throw new Error(result.error ?? 'Unknown error from Excel');
103
143
  }
104
- const output = result.output ?? NO_OUTPUT_FALLBACK;
144
+ const outcome = await resolveApproval(result, approval, (b) => postControl(b), ctx);
145
+ if (outcome === OUTCOME.REVERT_FAILED) {
146
+ return {
147
+ content: [{
148
+ type: 'text',
149
+ text: 'Revert failed — changes are still applied in Excel. You may need to undo manually (Ctrl+Z).'
150
+ }],
151
+ details: result
152
+ };
153
+ }
154
+ if (outcome === OUTCOME.REVERTED) {
155
+ return {
156
+ content: [{ type: 'text', text: 'Changes reverted by user.' }],
157
+ details: result
158
+ };
159
+ }
105
160
  return {
106
- content: [{ type: 'text', text: truncateOutput(output) }],
161
+ content: [{ type: 'text', text: truncateOutput(result.output ?? NO_OUTPUT_FALLBACK) }],
107
162
  details: result
108
163
  };
109
164
  }
@@ -347,7 +347,7 @@ function groupQueryPreview(query, theme) {
347
347
  export function formatAgentTypeLabel(agentType) {
348
348
  return agentType.replace(/_/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase()) + ' Subagent';
349
349
  }
350
- export function formatAgentGroupLabel(agentType, count) {
350
+ function formatAgentGroupLabel(agentType, count) {
351
351
  const name = agentType.replace(/_/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase());
352
352
  return `${count} ${name} Agent${count > 1 ? 's' : ''}`;
353
353
  }
package/dist/main.js CHANGED
@@ -46,6 +46,7 @@ import { registerShortcutProvider } from './custom/providers/register-shortcut-p
46
46
  import { syncXll } from './custom/sync-xll.js';
47
47
  import { createCronTool } from './custom/tools/cron.js';
48
48
  import { createExcelExecTool } from './custom/tools/excel-exec.js';
49
+ import { ExcelApprovalComponent } from './custom/tools/excel-approval.js';
49
50
  import { createLlmAnalysisTool } from './custom/tools/llm-analysis.js';
50
51
  import { createSwitchModeTool, setCurrentMode } from './custom/tools/switch-mode.js';
51
52
  import { createTaskTool } from './custom/tools/task/index.js';
@@ -236,7 +237,16 @@ function buildSessionOptions(parsed, sessionManager, modelRegistry, _settingsMan
236
237
  if (!parsed.noCustomTools) {
237
238
  const customToolSet = parsed.customTools ? new Set(parsed.customTools) : null;
238
239
  if (!customToolSet || customToolSet.has(EXCEL_EXEC)) {
239
- availableCustomTools.push(createExcelExecTool(EXCEL_HTTP_URL));
240
+ const approvalCallbacks = {
241
+ isAutoApproved: () => _settingsManager.getAutoApprove(),
242
+ requestApproval: async (diff, ctx) => {
243
+ if (!ctx.hasUI)
244
+ return undefined;
245
+ return ctx.ui.custom((_tui, uiTheme, _kb, done) => new ExcelApprovalComponent(diff, uiTheme, done));
246
+ },
247
+ onAcceptAll: () => _settingsManager.setAutoApprove(true),
248
+ };
249
+ availableCustomTools.push(createExcelExecTool(EXCEL_HTTP_URL, approvalCallbacks));
240
250
  }
241
251
  if (!customToolSet || customToolSet.has(TASK)) {
242
252
  availableCustomTools.push(createTaskTool());
@@ -509,6 +519,7 @@ export async function main(args) {
509
519
  sessionManager = SessionManager.open(selectedPath);
510
520
  }
511
521
  const { options: sessionOptions, cliThinkingFromModel } = buildSessionOptions(parsed, sessionManager, modelRegistry, settingsManager);
522
+ sessionOptions.settingsManager = settingsManager;
512
523
  sessionOptions.authStorage = authStorage;
513
524
  sessionOptions.modelRegistry = modelRegistry;
514
525
  sessionOptions.resourceLoader = resourceLoader;
@@ -1,4 +1,5 @@
1
1
  import { truncateToWidth, visibleWidth } from '../../../tui/index.js';
2
+ import { APPROVAL_MODE } from '../../../core/settings-manager.js';
2
3
  import { theme } from '../../../core/theme.js';
3
4
  /**
4
5
  * Sanitize text for display in a single-line status.
@@ -293,6 +294,15 @@ export class FooterComponent {
293
294
  lines.push(theme.fg('dim', infoLine));
294
295
  }
295
296
  lines.push(styledStatsLeft + styledRemainder);
297
+ // SHORTCUT PATCH: show approval mode indicator
298
+ // Read from settings directly so "Accept All" in excel_exec is reflected immediately.
299
+ const autoApprove = this.session.settingsManager.getAutoApprove();
300
+ if (autoApprove) {
301
+ lines.push(theme.fg('success', `\u23F5\u23F5 ${APPROVAL_MODE.AUTO}`) + theme.fg('dim', ' (shift+tab to toggle)'));
302
+ }
303
+ else {
304
+ lines.push(theme.fg('dim', `\u23F8 ${APPROVAL_MODE.ASK} (shift+tab to toggle)`));
305
+ }
296
306
  // SHORTCUT PATCH: render each extension status on its own line (for dev trace paths)
297
307
  const extensionStatuses = this.footerData.getExtensionStatuses();
298
308
  if (extensionStatuses.size > 0) {
@@ -1,4 +1,5 @@
1
1
  import { Container, getCapabilities, SelectList, SettingsList, Spacer, Text } from '../../../tui/index.js';
2
+ import { APPROVAL_MODE } from '../../../core/settings-manager.js';
2
3
  import { theme } from '../../../core/theme.js';
3
4
  import { getSelectListTheme, getSettingsListTheme } from '../theme/theme.js';
4
5
  import { DynamicBorder } from './dynamic-border.js';
@@ -52,6 +53,13 @@ export class SettingsSelectorComponent extends Container {
52
53
  super();
53
54
  const supportsImages = getCapabilities().images;
54
55
  const items = [
56
+ {
57
+ id: 'auto-approve',
58
+ label: 'Auto-approve edits',
59
+ description: 'Whether the agent can edit Excel without asking',
60
+ currentValue: config.autoApprove ? APPROVAL_MODE.AUTO : APPROVAL_MODE.ASK,
61
+ values: [APPROVAL_MODE.ASK, APPROVAL_MODE.AUTO]
62
+ },
55
63
  {
56
64
  id: 'collapse-changelog',
57
65
  label: 'Collapse changelog',
@@ -102,6 +110,9 @@ export class SettingsSelectorComponent extends Container {
102
110
  this.addChild(new DynamicBorder());
103
111
  this.settingsList = new SettingsList(items, 10, getSettingsListTheme(), (id, newValue) => {
104
112
  switch (id) {
113
+ case 'auto-approve':
114
+ callbacks.onAutoApproveChange(newValue === APPROVAL_MODE.AUTO);
115
+ break;
105
116
  case 'show-images':
106
117
  callbacks.onShowImagesChange(newValue === 'true');
107
118
  break;
@@ -1551,6 +1551,7 @@ export class InteractiveMode {
1551
1551
  this.defaultEditor.onCtrlD = () => this.handleCtrlD();
1552
1552
  this.defaultEditor.onAction('suspend', () => this.handleCtrlZ());
1553
1553
  this.defaultEditor.onAction('cycleThinkingLevel', () => this.cycleThinkingLevel());
1554
+ this.defaultEditor.onAction('toggleAutoApprove', () => this.toggleAutoApprove());
1554
1555
  this.defaultEditor.onAction('cycleModelForward', () => this.cycleModel('forward'));
1555
1556
  this.defaultEditor.onAction('cycleModelBackward', () => this.cycleModel('backward'));
1556
1557
  // Global debug handler on TUI (works regardless of focus)
@@ -2063,10 +2064,23 @@ export class InteractiveMode {
2063
2064
  this.defaultEditor.onEscape = () => {
2064
2065
  this.session.abortRetry();
2065
2066
  };
2066
- // Show retry indicator
2067
+ // SHORTCUT PATCH: Remove the error assistant message from the chat.
2068
+ // The retry will remove it from conversation history and produce a new
2069
+ // response — no point leaving the stale "[Request failed: ...]" visible.
2070
+ // Safe: on auto_retry_start the last assistant message is always the
2071
+ // failed one — successful responses don't trigger retries.
2072
+ const retryChildren = this.chatContainer.children;
2073
+ for (let i = retryChildren.length - 1; i >= 0; i--) {
2074
+ if (retryChildren[i] instanceof AssistantMessageComponent) {
2075
+ this.chatContainer.removeChild(retryChildren[i]);
2076
+ break;
2077
+ }
2078
+ }
2079
+ // Show retry indicator with the error reason
2067
2080
  this.statusContainer.clear();
2068
2081
  const delaySeconds = Math.round(event.delayMs / 1000);
2069
- this.retryLoader = new Loader(this.ui, (spinner) => theme.fg('warning', spinner), (text) => theme.fg('muted', text), `Retrying (${event.attempt}/${event.maxAttempts}) in ${delaySeconds}s... (${appKey(this.keybindings, 'interrupt')} to cancel)`);
2082
+ const prefix = event.errorMessage ? `${event.errorMessage} ` : '';
2083
+ this.retryLoader = new Loader(this.ui, (spinner) => theme.fg('warning', spinner), (text) => theme.fg('muted', text), `${prefix}Retrying (${event.attempt}/${event.maxAttempts}) in ${delaySeconds}s... (${appKey(this.keybindings, 'interrupt')} to cancel)`);
2070
2084
  this.statusContainer.addChild(this.retryLoader);
2071
2085
  this.ui.requestRender();
2072
2086
  break;
@@ -2445,6 +2459,12 @@ export class InteractiveMode {
2445
2459
  this.showStatus(`Thinking level: ${newLevel}`);
2446
2460
  }
2447
2461
  }
2462
+ toggleAutoApprove() {
2463
+ const current = this.settingsManager.getAutoApprove();
2464
+ const next = !current;
2465
+ this.settingsManager.setAutoApprove(next);
2466
+ this.ui.requestRender();
2467
+ }
2448
2468
  async cycleModel(direction) {
2449
2469
  try {
2450
2470
  const result = await this.session.cycleModel(direction);
@@ -2763,7 +2783,8 @@ export class InteractiveMode {
2763
2783
  currentTheme: this.settingsManager.getTheme() || 'dark',
2764
2784
  availableThemes: getAvailableThemes(),
2765
2785
  collapseChangelog: this.settingsManager.getCollapseChangelog(),
2766
- quietStartup: this.settingsManager.getQuietStartup()
2786
+ quietStartup: this.settingsManager.getQuietStartup(),
2787
+ autoApprove: this.settingsManager.getAutoApproveDefault()
2767
2788
  }, {
2768
2789
  onShowImagesChange: (enabled) => {
2769
2790
  this.settingsManager.setShowImages(enabled);
@@ -2794,6 +2815,9 @@ export class InteractiveMode {
2794
2815
  onQuietStartupChange: (enabled) => {
2795
2816
  this.settingsManager.setQuietStartup(enabled);
2796
2817
  },
2818
+ onAutoApproveChange: (enabled) => {
2819
+ this.settingsManager.setAutoApproveDefault(enabled);
2820
+ },
2797
2821
  onCancel: () => {
2798
2822
  done();
2799
2823
  this.ui.requestRender();
@@ -3445,7 +3469,7 @@ export class InteractiveMode {
3445
3469
  const clear = this.getAppKeyDisplay('clear');
3446
3470
  const exit = this.getAppKeyDisplay('exit');
3447
3471
  const suspend = this.getAppKeyDisplay('suspend');
3448
- const cycleThinkingLevel = this.getAppKeyDisplay('cycleThinkingLevel');
3472
+ const toggleAutoApprove = this.getAppKeyDisplay('toggleAutoApprove');
3449
3473
  const cycleModelForward = this.getAppKeyDisplay('cycleModelForward');
3450
3474
  const selectModel = this.getAppKeyDisplay('selectModel');
3451
3475
  const expandTools = this.getAppKeyDisplay('expandTools');
@@ -3486,7 +3510,7 @@ export class InteractiveMode {
3486
3510
  | \`${clear}\` | Clear editor (first) / exit (second) |
3487
3511
  | \`${exit}\` | Exit (when editor is empty) |
3488
3512
  | \`${suspend}\` | Suspend to background |
3489
- | \`${cycleThinkingLevel}\` | Cycle thinking level |
3513
+ | \`${toggleAutoApprove}\` | Toggle auto-approve for Excel edits |
3490
3514
  | \`${cycleModelForward}\` | Cycle models |
3491
3515
  | \`${selectModel}\` | Open model selector |
3492
3516
  | \`${expandTools}\` | Toggle tool output expansion |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "shortcutxl",
3
- "version": "0.2.13",
3
+ "version": "0.2.15",
4
4
  "type": "module",
5
5
  "files": [
6
6
  "dist/",
@@ -25,7 +25,7 @@
25
25
  "copy-assets": "npm run sync-modules && cp -r src/tui dist/tui && cp src/modes/interactive/theme/*.json dist/modes/interactive/theme/ && cp src/core/export-html/*.{html,css,js} dist/core/export-html/ && cp src/core/export-html/vendor/*.js dist/core/export-html/vendor/",
26
26
  "test-install": "npm uninstall -g shortcutxl & npm run build && npm pack && node -e \"const g=require('glob').globSync('shortcutxl-*.tgz')[0];require('child_process').execSync('npm install -g '+g,{stdio:'inherit'});require('fs').unlinkSync(g)\"",
27
27
  "test-uninstall": "npm uninstall -g shortcutxl",
28
- "dev": "npm run sync-modules && tsx src/cli.ts",
28
+ "dev": "npm run sync-modules && node -e \"try{require('fs').unlinkSync(require('path').join(require('os').homedir(),'.shortcut','agent','xll','.sync-version'))}catch{}\" && tsx src/cli.ts",
29
29
  "start": "node dist/cli.js",
30
30
  "lint": "eslint src/",
31
31
  "check-types": "tsc --noEmit",
@@ -34,7 +34,9 @@
34
34
  "test": "vitest --run",
35
35
  "test:e2e": "vitest --run --config vitest.e2e.config.ts",
36
36
  "test:watch": "vitest",
37
- "knip": "knip"
37
+ "knip": "knip",
38
+ "lint:python": "cd .. && uvx ruff check modules/ tests/",
39
+ "check-types:python": "cd .. && uvx pyright modules/ tests/"
38
40
  },
39
41
  "dependencies": {
40
42
  "@mariozechner/jiti": "^2.6.5",
Binary file