shortcutxl 0.2.15 → 0.2.16
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/cli.js +2 -0
- package/dist/core/system-prompt.js +0 -5
- package/dist/custom/prompts/action.js +47 -18
- package/dist/custom/tools/excel-approval.js +51 -34
- package/dist/custom/tools/switch-mode.js +1 -1
- package/dist/modes/interactive/interactive-mode.js +4 -2
- package/dist/utils/clipboard-image.js +45 -1
- package/package.json +2 -1
- package/xll/modules/debug_render.py +2 -5
- package/xll/modules/shortcut_xl/_diff_highlight.py +229 -218
- package/xll/modules/shortcut_xl/_exec_entry.py +18 -34
- package/xll/modules/shortcut_xl/_tracking.py +24 -2
- package/xll/modules/shortcut_xl/api/categorize.py +4 -3
- package/xll/modules/shortcut_xl/api/format.py +2 -1
- package/xll/modules/shortcut_xl/api/utils/helpers.py +36 -0
- package/xll/modules/gameboy.py +0 -241
- package/xll/modules/pong.py +0 -188
- package/xll/modules/shortcut_xl/_navigate.py +0 -115
- package/xll/modules/stocks.py +0 -100
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
|
|
6
|
-
*
|
|
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';
|
|
@@ -66,10 +67,15 @@ For any extension requests, ask the user which approach fits their need, or reco
|
|
|
66
67
|
const INSTALLATION = (markerPath) => `\
|
|
67
68
|
## Installation
|
|
68
69
|
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
|
|
70
|
+
const SELF_REFERENCE = (paths) => `\
|
|
71
|
+
======================
|
|
72
|
+
## SELF REFERENCE
|
|
73
|
+
======================
|
|
74
|
+
|
|
70
75
|
## Key Directories
|
|
71
76
|
- Package directory (shipped code, read-only): ${paths.packageDir}
|
|
72
77
|
- Agent directory (user config, state, XLL binaries): ${paths.agentDir}
|
|
78
|
+
- User skills (create new skills here): ${paths.userSkillsDir}
|
|
73
79
|
- Sessions (prior conversation JSONL transcripts): ${paths.sessionsDir}
|
|
74
80
|
- Temp directory (dump temporary/scratch files here, e.g. exported Excel files): ${paths.tempDir}
|
|
75
81
|
|
|
@@ -86,33 +92,56 @@ Read these for introspection, customization, debugging of Shortcut itself, its S
|
|
|
86
92
|
- Examples: ${paths.examples} (extensions, custom tools, SDK)
|
|
87
93
|
- 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
94
|
- Always read Shortcut .md files completely and follow links to related docs`;
|
|
95
|
+
const SKILLS = (skills, userSkillsDir) => {
|
|
96
|
+
const visible = skills.filter((s) => !s.disableModelInvocation);
|
|
97
|
+
const lines = [
|
|
98
|
+
'======================',
|
|
99
|
+
'## SKILLS',
|
|
100
|
+
'======================',
|
|
101
|
+
'The following skills provide specialized instructions for specific tasks.',
|
|
102
|
+
'When a skill file references a relative path, resolve it against the skill directory (parent of SKILL.md).',
|
|
103
|
+
'',
|
|
104
|
+
`To create a new user skill: ${userSkillsDir}/<skill-name>/SKILL.md`,
|
|
105
|
+
'Format:',
|
|
106
|
+
' ---',
|
|
107
|
+
' name: <skill-name> # must match the parent directory name',
|
|
108
|
+
' description: "..." # one-line summary shown here; max 1024 chars',
|
|
109
|
+
' ---',
|
|
110
|
+
' <skill body — clear and concise instructions the agent reads when the skill is invoked>',
|
|
111
|
+
''
|
|
112
|
+
];
|
|
113
|
+
for (const skill of visible) {
|
|
114
|
+
lines.push(`- ${skill.name} (${skill.filePath}): ${skill.description}`);
|
|
115
|
+
}
|
|
116
|
+
return lines.join('\n');
|
|
117
|
+
};
|
|
89
118
|
// ---------------------------------------------------------------------------
|
|
90
|
-
// Assembly — resolves runtime paths, joins sections
|
|
119
|
+
// Assembly — resolves runtime paths, loads skills, joins sections
|
|
91
120
|
// ---------------------------------------------------------------------------
|
|
92
121
|
export function buildActionPrompt() {
|
|
122
|
+
const agentDir = getAgentDir();
|
|
93
123
|
const agentDocsDir = resolve(join(getPackageDir(), 'agent-docs'));
|
|
94
124
|
const installedMarker = getInstalledMarkerPath();
|
|
95
|
-
|
|
125
|
+
const userSkillsDir = join(agentDir, 'skills');
|
|
126
|
+
const { skills } = loadSkills({ agentDir, skillPaths: [getBundledSkillsDir()] });
|
|
127
|
+
const skillsSection = SKILLS(skills, userSkillsDir);
|
|
128
|
+
const sections = [
|
|
96
129
|
CORE(),
|
|
97
130
|
INSTALLATION(installedMarker),
|
|
98
|
-
|
|
99
|
-
DATA_FORMATTING_GUIDELINES,
|
|
100
|
-
NUMBER_FORMAT_REFERENCE,
|
|
101
|
-
NOTES_COMMENTS_GUIDELINES,
|
|
102
|
-
CODING_GUIDELINES,
|
|
103
|
-
COMMUNICATION_GUIDELINES,
|
|
104
|
-
DOCS({
|
|
131
|
+
SELF_REFERENCE({
|
|
105
132
|
packageDir: getPackageDir(),
|
|
106
|
-
agentDir
|
|
133
|
+
agentDir,
|
|
134
|
+
userSkillsDir: join(agentDir, 'skills'),
|
|
107
135
|
sessionsDir: getSessionsDir(),
|
|
108
|
-
tempDir: join(
|
|
136
|
+
tempDir: join(agentDir, 'temp'),
|
|
109
137
|
xllSpec: join(agentDocsDir, 'xll-spec.md'),
|
|
110
138
|
xllSkill: join(agentDocsDir, 'xll-skill.md'),
|
|
111
139
|
readme: join(agentDocsDir, 'README.md'),
|
|
112
140
|
docs: join(agentDocsDir, 'docs'),
|
|
113
141
|
examples: join(agentDocsDir, 'examples')
|
|
114
|
-
})
|
|
115
|
-
|
|
116
|
-
|
|
142
|
+
})
|
|
143
|
+
];
|
|
144
|
+
sections.push(skillsSection, COMMUNICATION_GUIDELINES, CODING_GUIDELINES, EXPLORATION_GUIDELINES, DATA_FORMATTING_GUIDELINES, NUMBER_FORMAT_REFERENCE, NOTES_COMMENTS_GUIDELINES, EXCEL_COM_API_GUIDELINES);
|
|
145
|
+
return sections.join('\n\n');
|
|
117
146
|
}
|
|
118
147
|
//# 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
|
-
|
|
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 = {
|
|
82
|
+
const colWidths = { addr: 7, before: 6, after: 5, detail: 7 };
|
|
81
83
|
for (const c of visible) {
|
|
82
|
-
colWidths.
|
|
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
|
-
|
|
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.
|
|
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.
|
|
92
|
+
colWidths.detail = Math.min(colWidths.detail, 30);
|
|
92
93
|
// Header
|
|
93
94
|
const header = ' ' +
|
|
94
|
-
this.theme.fg('dim', padVisible('
|
|
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('
|
|
101
|
+
padVisible('Detail', colWidths.detail));
|
|
101
102
|
this.tableContainer.addChild(new Text(header, 0, 0));
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
const
|
|
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
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
161
|
-
|
|
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
|
|
167
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
3
|
+
"version": "0.2.16",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"files": [
|
|
6
6
|
"dist/",
|
|
@@ -33,6 +33,7 @@
|
|
|
33
33
|
"format:check": "prettier --check 'src/**/*.{ts,tsx,js,json}'",
|
|
34
34
|
"test": "vitest --run",
|
|
35
35
|
"test:e2e": "vitest --run --config vitest.e2e.config.ts",
|
|
36
|
+
"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
37
|
"test:watch": "vitest",
|
|
37
38
|
"knip": "knip",
|
|
38
39
|
"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(
|
|
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"
|