shortcutxl 0.2.13 → 0.2.14
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/dist/core/keybindings.js +3 -1
- package/dist/core/settings-manager.js +29 -0
- package/dist/custom/install-utils.js +0 -3
- package/dist/custom/prompts/api.js +1 -1
- package/dist/custom/providers/provider-ids.js +1 -0
- package/dist/custom/tools/excel-approval.js +180 -0
- package/dist/custom/tools/excel-exec.js +66 -11
- package/dist/custom/tools/task/render.js +1 -1
- package/dist/main.js +12 -1
- package/dist/modes/interactive/components/footer.js +10 -0
- package/dist/modes/interactive/components/settings-selector.js +11 -0
- package/dist/modes/interactive/interactive-mode.js +29 -5
- package/package.json +5 -3
- package/xll/ShortcutXL.xll +0 -0
- package/xll/modules/shortcut_xl/__init__.py +29 -14
- package/xll/modules/shortcut_xl/_com.py +1 -0
- package/xll/modules/shortcut_xl/_diff_highlight.py +133 -91
- package/xll/modules/shortcut_xl/_exec_entry.py +150 -0
- package/xll/modules/shortcut_xl/_log.py +1 -1
- package/xll/modules/shortcut_xl/_managed.py +15 -9
- package/xll/modules/shortcut_xl/_threading.py +4 -3
- package/xll/modules/shortcut_xl/_tracking.py +8 -2
- package/xll/modules/shortcut_xl/api/__init__.py +2 -2
- package/xll/modules/shortcut_xl/api/format.py +10 -5
- package/xll/modules/shortcut_xl/api/range_formatter.py +4 -4
- package/xll/modules/shortcut_xl/api/workbook.py +3 -8
- package/xll/modules/shortcut_xl/api/worksheet.py +7 -7
- package/xll/modules/shortcut_xl/api-reference.py +3 -0
- /package/skills/{COM-advanced-api → com-advanced-api}/SKILL.md +0 -0
- /package/skills/{COM-advanced-api → com-advanced-api}/excel-type-library.py +0 -0
- /package/skills/{COM-advanced-api → com-advanced-api}/office-type-library.py +0 -0
package/dist/core/keybindings.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
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,
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
|
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
|
-
| \`${
|
|
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.
|
|
3
|
+
"version": "0.2.14",
|
|
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",
|
package/xll/ShortcutXL.xll
CHANGED
|
Binary file
|
|
@@ -21,19 +21,34 @@ hex_to_bgr — convert "#RRGGBB" to BGR int for COM
|
|
|
21
21
|
index_to_address, address_to_index, parse_range, range_to_excel, col_letter, col_index
|
|
22
22
|
"""
|
|
23
23
|
|
|
24
|
-
from shortcut_xl.
|
|
25
|
-
from shortcut_xl.
|
|
26
|
-
from shortcut_xl.
|
|
27
|
-
from shortcut_xl._managed import
|
|
28
|
-
from shortcut_xl.
|
|
29
|
-
from shortcut_xl.
|
|
30
|
-
from shortcut_xl.
|
|
24
|
+
from shortcut_xl._com import xl_app as xl_app
|
|
25
|
+
from shortcut_xl._log import xl_log as xl_log
|
|
26
|
+
from shortcut_xl._managed import run_managed as run_managed
|
|
27
|
+
from shortcut_xl._managed import schedule_call as schedule_call
|
|
28
|
+
from shortcut_xl._managed import xl_batch as xl_batch
|
|
29
|
+
from shortcut_xl._registry import _registry as _registry
|
|
30
|
+
from shortcut_xl._registry import xl_func as xl_func
|
|
31
|
+
from shortcut_xl.api.format import format_cell_diff as format_cell_diff
|
|
32
|
+
from shortcut_xl.api.utils.helpers import ( # noqa: I001
|
|
33
|
+
address_to_index as address_to_index,
|
|
34
|
+
)
|
|
35
|
+
from shortcut_xl.api.utils.helpers import (
|
|
36
|
+
col_index as col_index,
|
|
37
|
+
)
|
|
38
|
+
from shortcut_xl.api.utils.helpers import (
|
|
39
|
+
col_letter as col_letter,
|
|
40
|
+
)
|
|
41
|
+
from shortcut_xl.api.utils.helpers import (
|
|
42
|
+
hex_to_bgr as hex_to_bgr,
|
|
43
|
+
)
|
|
44
|
+
from shortcut_xl.api.utils.helpers import (
|
|
45
|
+
index_to_address as index_to_address,
|
|
46
|
+
)
|
|
47
|
+
from shortcut_xl.api.utils.helpers import (
|
|
48
|
+
parse_range as parse_range,
|
|
49
|
+
)
|
|
31
50
|
from shortcut_xl.api.utils.helpers import (
|
|
32
|
-
|
|
33
|
-
index_to_address,
|
|
34
|
-
address_to_index,
|
|
35
|
-
parse_range,
|
|
36
|
-
range_to_excel,
|
|
37
|
-
col_letter,
|
|
38
|
-
col_index,
|
|
51
|
+
range_to_excel as range_to_excel,
|
|
39
52
|
)
|
|
53
|
+
from shortcut_xl.api.workbook import Workbook as Workbook
|
|
54
|
+
from shortcut_xl.api.worksheet import Worksheet as Worksheet
|