shortcutxl 0.2.0
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/README.md +59 -0
- package/agent-docs/README.md +397 -0
- package/agent-docs/docs/compaction.md +390 -0
- package/agent-docs/docs/custom-provider.md +580 -0
- package/agent-docs/docs/development.md +69 -0
- package/agent-docs/docs/extensions.md +1971 -0
- package/agent-docs/docs/json.md +79 -0
- package/agent-docs/docs/keybindings.md +174 -0
- package/agent-docs/docs/models.md +293 -0
- package/agent-docs/docs/packages.md +209 -0
- package/agent-docs/docs/prompt-templates.md +67 -0
- package/agent-docs/docs/providers.md +186 -0
- package/agent-docs/docs/rpc.md +1317 -0
- package/agent-docs/docs/sdk.md +962 -0
- package/agent-docs/docs/session.md +412 -0
- package/agent-docs/docs/settings.md +223 -0
- package/agent-docs/docs/shell-aliases.md +13 -0
- package/agent-docs/docs/skills.md +231 -0
- package/agent-docs/docs/terminal-setup.md +70 -0
- package/agent-docs/docs/termux.md +127 -0
- package/agent-docs/docs/themes.md +295 -0
- package/agent-docs/docs/tree.md +219 -0
- package/agent-docs/docs/tui.md +887 -0
- package/agent-docs/docs/windows.md +17 -0
- package/agent-docs/examples/README.md +25 -0
- package/agent-docs/examples/extensions/README.md +205 -0
- package/agent-docs/examples/extensions/antigravity-image-gen.ts +447 -0
- package/agent-docs/examples/extensions/auto-commit-on-exit.ts +49 -0
- package/agent-docs/examples/extensions/bash-spawn-hook.ts +30 -0
- package/agent-docs/examples/extensions/bookmark.ts +50 -0
- package/agent-docs/examples/extensions/built-in-tool-renderer.ts +256 -0
- package/agent-docs/examples/extensions/claude-rules.ts +86 -0
- package/agent-docs/examples/extensions/commands.ts +75 -0
- package/agent-docs/examples/extensions/confirm-destructive.ts +59 -0
- package/agent-docs/examples/extensions/custom-compaction.ts +126 -0
- package/agent-docs/examples/extensions/custom-footer.ts +63 -0
- package/agent-docs/examples/extensions/custom-header.ts +73 -0
- package/agent-docs/examples/extensions/custom-provider-anthropic/index.ts +660 -0
- package/agent-docs/examples/extensions/custom-provider-anthropic/package-lock.json +24 -0
- package/agent-docs/examples/extensions/custom-provider-anthropic/package.json +19 -0
- package/agent-docs/examples/extensions/custom-provider-gitlab-duo/index.ts +362 -0
- package/agent-docs/examples/extensions/custom-provider-gitlab-duo/package.json +16 -0
- package/agent-docs/examples/extensions/custom-provider-gitlab-duo/test.ts +88 -0
- package/agent-docs/examples/extensions/custom-provider-qwen-cli/index.ts +349 -0
- package/agent-docs/examples/extensions/custom-provider-qwen-cli/package.json +16 -0
- package/agent-docs/examples/extensions/dirty-repo-guard.ts +56 -0
- package/agent-docs/examples/extensions/doom-overlay/README.md +46 -0
- package/agent-docs/examples/extensions/doom-overlay/doom/build.sh +152 -0
- package/agent-docs/examples/extensions/doom-overlay/doom/doomgeneric_pi.c +72 -0
- package/agent-docs/examples/extensions/doom-overlay/doom-component.ts +133 -0
- package/agent-docs/examples/extensions/doom-overlay/doom-engine.ts +186 -0
- package/agent-docs/examples/extensions/doom-overlay/doom-keys.ts +108 -0
- package/agent-docs/examples/extensions/doom-overlay/index.ts +74 -0
- package/agent-docs/examples/extensions/doom-overlay/wad-finder.ts +51 -0
- package/agent-docs/examples/extensions/dynamic-resources/SKILL.md +8 -0
- package/agent-docs/examples/extensions/dynamic-resources/dynamic.json +79 -0
- package/agent-docs/examples/extensions/dynamic-resources/dynamic.md +5 -0
- package/agent-docs/examples/extensions/dynamic-resources/index.ts +15 -0
- package/agent-docs/examples/extensions/dynamic-tools.ts +77 -0
- package/agent-docs/examples/extensions/event-bus.ts +43 -0
- package/agent-docs/examples/extensions/file-trigger.ts +41 -0
- package/agent-docs/examples/extensions/git-checkpoint.ts +53 -0
- package/agent-docs/examples/extensions/handoff.ts +155 -0
- package/agent-docs/examples/extensions/hello.ts +25 -0
- package/agent-docs/examples/extensions/inline-bash.ts +94 -0
- package/agent-docs/examples/extensions/input-transform.ts +43 -0
- package/agent-docs/examples/extensions/interactive-shell.ts +209 -0
- package/agent-docs/examples/extensions/mac-system-theme.ts +47 -0
- package/agent-docs/examples/extensions/message-renderer.ts +59 -0
- package/agent-docs/examples/extensions/minimal-mode.ts +430 -0
- package/agent-docs/examples/extensions/modal-editor.ts +90 -0
- package/agent-docs/examples/extensions/model-status.ts +31 -0
- package/agent-docs/examples/extensions/notify.ts +55 -0
- package/agent-docs/examples/extensions/overlay-qa-tests.ts +936 -0
- package/agent-docs/examples/extensions/overlay-test.ts +159 -0
- package/agent-docs/examples/extensions/permission-gate.ts +37 -0
- package/agent-docs/examples/extensions/pirate.ts +47 -0
- package/agent-docs/examples/extensions/plan-mode/README.md +65 -0
- package/agent-docs/examples/extensions/plan-mode/index.ts +363 -0
- package/agent-docs/examples/extensions/plan-mode/utils.ts +173 -0
- package/agent-docs/examples/extensions/preset.ts +418 -0
- package/agent-docs/examples/extensions/protected-paths.ts +30 -0
- package/agent-docs/examples/extensions/qna.ts +122 -0
- package/agent-docs/examples/extensions/question.ts +278 -0
- package/agent-docs/examples/extensions/questionnaire.ts +440 -0
- package/agent-docs/examples/extensions/rainbow-editor.ts +90 -0
- package/agent-docs/examples/extensions/reload-runtime.ts +37 -0
- package/agent-docs/examples/extensions/rpc-demo.ts +124 -0
- package/agent-docs/examples/extensions/sandbox/index.ts +324 -0
- package/agent-docs/examples/extensions/sandbox/package-lock.json +92 -0
- package/agent-docs/examples/extensions/sandbox/package.json +19 -0
- package/agent-docs/examples/extensions/send-user-message.ts +97 -0
- package/agent-docs/examples/extensions/session-name.ts +27 -0
- package/agent-docs/examples/extensions/shutdown-command.ts +69 -0
- package/agent-docs/examples/extensions/snake.ts +343 -0
- package/agent-docs/examples/extensions/space-invaders.ts +566 -0
- package/agent-docs/examples/extensions/ssh.ts +233 -0
- package/agent-docs/examples/extensions/status-line.ts +40 -0
- package/agent-docs/examples/extensions/subagent/README.md +172 -0
- package/agent-docs/examples/extensions/subagent/agents/planner.md +37 -0
- package/agent-docs/examples/extensions/subagent/agents/reviewer.md +35 -0
- package/agent-docs/examples/extensions/subagent/agents/scout.md +50 -0
- package/agent-docs/examples/extensions/subagent/agents/worker.md +24 -0
- package/agent-docs/examples/extensions/subagent/agents.ts +130 -0
- package/agent-docs/examples/extensions/subagent/index.ts +1068 -0
- package/agent-docs/examples/extensions/subagent/prompts/implement-and-review.md +10 -0
- package/agent-docs/examples/extensions/subagent/prompts/implement.md +10 -0
- package/agent-docs/examples/extensions/subagent/prompts/scout-and-plan.md +9 -0
- package/agent-docs/examples/extensions/summarize.ts +206 -0
- package/agent-docs/examples/extensions/system-prompt-header.ts +17 -0
- package/agent-docs/examples/extensions/timed-confirm.ts +72 -0
- package/agent-docs/examples/extensions/titlebar-spinner.ts +58 -0
- package/agent-docs/examples/extensions/todo.ts +314 -0
- package/agent-docs/examples/extensions/tool-override.ts +146 -0
- package/agent-docs/examples/extensions/tools.ts +145 -0
- package/agent-docs/examples/extensions/trigger-compact.ts +40 -0
- package/agent-docs/examples/extensions/truncated-tool.ts +194 -0
- package/agent-docs/examples/extensions/widget-placement.ts +17 -0
- package/agent-docs/examples/extensions/with-deps/index.ts +37 -0
- package/agent-docs/examples/extensions/with-deps/package-lock.json +31 -0
- package/agent-docs/examples/extensions/with-deps/package.json +22 -0
- package/agent-docs/examples/rpc-extension-ui.ts +654 -0
- package/agent-docs/examples/sdk/01-minimal.ts +22 -0
- package/agent-docs/examples/sdk/02-custom-model.ts +48 -0
- package/agent-docs/examples/sdk/03-custom-prompt.ts +55 -0
- package/agent-docs/examples/sdk/04-skills.ts +53 -0
- package/agent-docs/examples/sdk/05-tools.ts +56 -0
- package/agent-docs/examples/sdk/06-extensions.ts +88 -0
- package/agent-docs/examples/sdk/07-context-files.ts +40 -0
- package/agent-docs/examples/sdk/08-prompt-templates.ts +47 -0
- package/agent-docs/examples/sdk/09-api-keys-and-oauth.ts +48 -0
- package/agent-docs/examples/sdk/10-settings.ts +54 -0
- package/agent-docs/examples/sdk/11-sessions.ts +48 -0
- package/agent-docs/examples/sdk/12-full-control.ts +82 -0
- package/agent-docs/examples/sdk/README.md +144 -0
- package/agent-docs/xll-skill.md +61 -0
- package/agent-docs/xll-spec.md +110 -0
- package/dist/cli/args.js +290 -0
- package/dist/cli/config-selector.js +31 -0
- package/dist/cli/file-processor.js +79 -0
- package/dist/cli/list-models.js +92 -0
- package/dist/cli/package-commands.js +210 -0
- package/dist/cli/report-settings-errors.js +11 -0
- package/dist/cli/session-picker.js +34 -0
- package/dist/cli.js +19 -0
- package/dist/config.js +288 -0
- package/dist/core/abort.js +15 -0
- package/dist/core/agent-loop.js +352 -0
- package/dist/core/agent-session.js +2019 -0
- package/dist/core/agent.js +410 -0
- package/dist/core/auth-storage.js +456 -0
- package/dist/core/bash-executor.js +222 -0
- package/dist/core/compaction/branch-summarization.js +242 -0
- package/dist/core/compaction/compaction.js +610 -0
- package/dist/core/compaction/index.js +7 -0
- package/dist/core/compaction/utils.js +139 -0
- package/dist/core/defaults.js +6 -0
- package/dist/core/diagnostics.js +2 -0
- package/dist/core/event-bus.js +25 -0
- package/dist/core/exec.js +71 -0
- package/dist/core/export-html/ansi-to-html.js +256 -0
- package/dist/core/export-html/index.js +238 -0
- package/dist/core/export-html/session-view-model.js +342 -0
- package/dist/core/export-html/template.css +1110 -0
- package/dist/core/export-html/template.html +76 -0
- package/dist/core/export-html/template.js +1990 -0
- package/dist/core/export-html/tool-renderer.js +63 -0
- package/dist/core/export-html/vendor/highlight.min.js +7725 -0
- package/dist/core/export-html/vendor/marked.min.js +1803 -0
- package/dist/core/extensions/index.js +9 -0
- package/dist/core/extensions/loader.js +422 -0
- package/dist/core/extensions/runner.js +651 -0
- package/dist/core/extensions/types.js +35 -0
- package/dist/core/extensions/wrapper.js +102 -0
- package/dist/core/footer-data-provider.js +162 -0
- package/dist/core/index.js +9 -0
- package/dist/core/keybindings.js +153 -0
- package/dist/core/messages.js +133 -0
- package/dist/core/model-registry.js +539 -0
- package/dist/core/model-resolver.js +370 -0
- package/dist/core/package-manager.js +1485 -0
- package/dist/core/prompt-templates.js +253 -0
- package/dist/core/resolve-config-value.js +59 -0
- package/dist/core/resource-loader.js +700 -0
- package/dist/core/sdk.js +197 -0
- package/dist/core/session-bash.js +99 -0
- package/dist/core/session-compaction.js +165 -0
- package/dist/core/session-manager.js +1153 -0
- package/dist/core/session-models.js +99 -0
- package/dist/core/session-retry.js +155 -0
- package/dist/core/settings-manager.js +572 -0
- package/dist/core/skills.js +382 -0
- package/dist/core/slash-commands.js +31 -0
- package/dist/core/system-prompt.js +161 -0
- package/dist/core/theme.js +770 -0
- package/dist/core/timings.js +26 -0
- package/dist/core/tools/bash.js +258 -0
- package/dist/core/tools/edit-diff.js +245 -0
- package/dist/core/tools/edit.js +148 -0
- package/dist/core/tools/find.js +208 -0
- package/dist/core/tools/grep.js +246 -0
- package/dist/core/tools/index.js +67 -0
- package/dist/core/tools/ls.js +123 -0
- package/dist/core/tools/path-utils.js +81 -0
- package/dist/core/tools/read.js +160 -0
- package/dist/core/tools/truncate.js +70 -0
- package/dist/core/tools/write.js +82 -0
- package/dist/custom/agents/action.js +13 -0
- package/dist/custom/agents/document-reader.js +70 -0
- package/dist/custom/agents/general.js +26 -0
- package/dist/custom/agents/index.js +49 -0
- package/dist/custom/agents/installation.js +13 -0
- package/dist/custom/agents/types.js +7 -0
- package/dist/custom/auth/refresh-timer.js +33 -0
- package/dist/custom/auth/shortcut-oauth.js +145 -0
- package/dist/custom/constants.js +21 -0
- package/dist/custom/context/workbook-summary.js +73 -0
- package/dist/custom/credits/shortcut-credits.js +29 -0
- package/dist/custom/cron/cron-daemon-entry.js +18 -0
- package/dist/custom/cron/daemon-ipc.js +131 -0
- package/dist/custom/cron/daemon.js +224 -0
- package/dist/custom/cron/jobs.js +226 -0
- package/dist/custom/cron/run-log.js +51 -0
- package/dist/custom/cron/schedule.js +72 -0
- package/dist/custom/cron/status-line.js +98 -0
- package/dist/custom/cron/store.js +87 -0
- package/dist/custom/cron/types.js +8 -0
- package/dist/custom/dev/index.js +59 -0
- package/dist/custom/dev/trace-export.js +58 -0
- package/dist/custom/ensure-excel.js +63 -0
- package/dist/custom/excel-config.js +36 -0
- package/dist/custom/preflight.js +422 -0
- package/dist/custom/prompts/action.js +100 -0
- package/dist/custom/prompts/api.js +66 -0
- package/dist/custom/prompts/installation.js +124 -0
- package/dist/custom/prompts/shared.js +138 -0
- package/dist/custom/providers/llm-usage.js +42 -0
- package/dist/custom/providers/message-converter.js +74 -0
- package/dist/custom/providers/provider-ids.js +9 -0
- package/dist/custom/providers/register-openai-codex-provider.js +27 -0
- package/dist/custom/providers/register-shortcut-provider.js +52 -0
- package/dist/custom/providers/shortcut-invoke.js +117 -0
- package/dist/custom/providers/shortcut-stream.js +252 -0
- package/dist/custom/providers/sse-protocol.js +38 -0
- package/dist/custom/sync-xll.js +130 -0
- package/dist/custom/tools/cron.js +413 -0
- package/dist/custom/tools/excel-exec.js +167 -0
- package/dist/custom/tools/excel-range.js +50 -0
- package/dist/custom/tools/llm-analysis.js +265 -0
- package/dist/custom/tools/render-helpers.js +38 -0
- package/dist/custom/tools/switch-mode.js +94 -0
- package/dist/custom/tools/task/agents.js +6 -0
- package/dist/custom/tools/task/index.js +8 -0
- package/dist/custom/tools/task/render.js +348 -0
- package/dist/custom/tools/task/subprocess.js +320 -0
- package/dist/custom/tools/task/task.js +205 -0
- package/dist/custom/tools/todo-list.js +195 -0
- package/dist/custom/tracing/session-upload.js +93 -0
- package/dist/index.js +45 -0
- package/dist/main.js +613 -0
- package/dist/migrations.js +265 -0
- package/dist/modes/index.js +8 -0
- package/dist/modes/interactive/components/armin.js +337 -0
- package/dist/modes/interactive/components/assistant-message.js +94 -0
- package/dist/modes/interactive/components/bash-execution.js +171 -0
- package/dist/modes/interactive/components/bordered-loader.js +51 -0
- package/dist/modes/interactive/components/branch-summary-message.js +45 -0
- package/dist/modes/interactive/components/compaction-summary-message.js +46 -0
- package/dist/modes/interactive/components/config-selector.js +488 -0
- package/dist/modes/interactive/components/countdown-timer.js +33 -0
- package/dist/modes/interactive/components/custom-editor.js +93 -0
- package/dist/modes/interactive/components/custom-message.js +81 -0
- package/dist/modes/interactive/components/daxnuts.js +140 -0
- package/dist/modes/interactive/components/diff.js +133 -0
- package/dist/modes/interactive/components/dynamic-border.js +21 -0
- package/dist/modes/interactive/components/extension-editor.js +105 -0
- package/dist/modes/interactive/components/extension-input.js +61 -0
- package/dist/modes/interactive/components/extension-selector.js +78 -0
- package/dist/modes/interactive/components/footer.js +309 -0
- package/dist/modes/interactive/components/index.js +33 -0
- package/dist/modes/interactive/components/keybinding-hints.js +61 -0
- package/dist/modes/interactive/components/layout.js +64 -0
- package/dist/modes/interactive/components/login-dialog.js +148 -0
- package/dist/modes/interactive/components/model-selector.js +237 -0
- package/dist/modes/interactive/components/oauth-selector.js +111 -0
- package/dist/modes/interactive/components/session-selector-search.js +157 -0
- package/dist/modes/interactive/components/session-selector.js +860 -0
- package/dist/modes/interactive/components/settings-selector.js +123 -0
- package/dist/modes/interactive/components/show-images-selector.js +35 -0
- package/dist/modes/interactive/components/skill-invocation-message.js +48 -0
- package/dist/modes/interactive/components/theme-selector.js +47 -0
- package/dist/modes/interactive/components/thinking-selector.js +47 -0
- package/dist/modes/interactive/components/tool-execution.js +789 -0
- package/dist/modes/interactive/components/tool-group.js +106 -0
- package/dist/modes/interactive/components/tree-selector.js +962 -0
- package/dist/modes/interactive/components/user-message-selector.js +115 -0
- package/dist/modes/interactive/components/user-message.js +48 -0
- package/dist/modes/interactive/components/visual-truncate.js +33 -0
- package/dist/modes/interactive/file-attachments.js +135 -0
- package/dist/modes/interactive/interactive-mode.js +3775 -0
- package/dist/modes/interactive/theme/dark.json +85 -0
- package/dist/modes/interactive/theme/light.json +85 -0
- package/dist/modes/interactive/theme/theme-schema.json +335 -0
- package/dist/modes/interactive/theme/theme.js +177 -0
- package/dist/modes/print-mode.js +101 -0
- package/dist/modes/rpc/rpc-client.js +387 -0
- package/dist/modes/rpc/rpc-mode.js +509 -0
- package/dist/modes/rpc/rpc-types.js +8 -0
- package/dist/subagent-entry.js +145 -0
- package/dist/tool-names.js +34 -0
- package/dist/tui/autocomplete.js +596 -0
- package/dist/tui/components/box.js +104 -0
- package/dist/tui/components/cancellable-loader.js +35 -0
- package/dist/tui/components/editor.js +1679 -0
- package/dist/tui/components/image.js +69 -0
- package/dist/tui/components/input.js +433 -0
- package/dist/tui/components/loader.js +49 -0
- package/dist/tui/components/markdown.js +629 -0
- package/dist/tui/components/select-list.js +152 -0
- package/dist/tui/components/settings-list.js +185 -0
- package/dist/tui/components/spacer.js +23 -0
- package/dist/tui/components/text.js +89 -0
- package/dist/tui/components/truncated-text.js +51 -0
- package/dist/tui/editor-component.js +2 -0
- package/dist/tui/fuzzy.js +107 -0
- package/dist/tui/get-east-asian-width/index.js +32 -0
- package/dist/tui/get-east-asian-width/lookup.js +404 -0
- package/dist/tui/index.js +32 -0
- package/dist/tui/keybindings.js +114 -0
- package/dist/tui/keys.js +959 -0
- package/dist/tui/kill-ring.js +44 -0
- package/dist/tui/stdin-buffer.js +317 -0
- package/dist/tui/terminal-image.js +288 -0
- package/dist/tui/terminal.js +249 -0
- package/dist/tui/tui/autocomplete.js +596 -0
- package/dist/tui/tui/components/box.js +106 -0
- package/dist/tui/tui/components/cancellable-loader.js +35 -0
- package/dist/tui/tui/components/editor.js +1679 -0
- package/dist/tui/tui/components/image.js +69 -0
- package/dist/tui/tui/components/input.js +433 -0
- package/dist/tui/tui/components/loader.js +49 -0
- package/dist/tui/tui/components/markdown.js +629 -0
- package/dist/tui/tui/components/select-list.js +152 -0
- package/dist/tui/tui/components/settings-list.js +185 -0
- package/dist/tui/tui/components/spacer.js +23 -0
- package/dist/tui/tui/components/text.js +91 -0
- package/dist/tui/tui/components/truncated-text.js +51 -0
- package/dist/tui/tui/editor-component.js +2 -0
- package/dist/tui/tui/fuzzy.js +107 -0
- package/dist/tui/tui/get-east-asian-width/index.js +32 -0
- package/dist/tui/tui/get-east-asian-width/lookup.js +404 -0
- package/dist/tui/tui/index.js +32 -0
- package/dist/tui/tui/keybindings.js +114 -0
- package/dist/tui/tui/keys.js +959 -0
- package/dist/tui/tui/kill-ring.js +44 -0
- package/dist/tui/tui/stdin-buffer.js +317 -0
- package/dist/tui/tui/terminal-image.js +288 -0
- package/dist/tui/tui/terminal.js +249 -0
- package/dist/tui/tui/tui.js +955 -0
- package/dist/tui/tui/undo-stack.js +25 -0
- package/dist/tui/tui/utils.js +800 -0
- package/dist/tui/tui.js +955 -0
- package/dist/tui/undo-stack.js +25 -0
- package/dist/tui/utils.js +800 -0
- package/dist/utils/changelog.js +87 -0
- package/dist/utils/clipboard-image.js +164 -0
- package/dist/utils/clipboard-native.js +14 -0
- package/dist/utils/clipboard.js +67 -0
- package/dist/utils/frontmatter.js +26 -0
- package/dist/utils/git.js +166 -0
- package/dist/utils/image-convert.js +35 -0
- package/dist/utils/image-resize.js +183 -0
- package/dist/utils/mime.js +26 -0
- package/dist/utils/photon.js +121 -0
- package/dist/utils/shell.js +217 -0
- package/dist/utils/sleep.js +17 -0
- package/dist/utils/tools-manager.js +259 -0
- package/package.json +78 -0
- package/skills/excel-com-api/SKILL.md +74 -0
- package/skills/excel-com-api/excel-type-library.py +27767 -0
- package/skills/excel-com-api/office-type-library.py +10867 -0
- package/skills/integrations/SKILL.md +138 -0
- package/skills/integrations/alphasense.md +457 -0
- package/skills/integrations/bloomberg.md +803 -0
- package/skills/integrations/calcbench.md +315 -0
- package/skills/integrations/capiq.md +848 -0
- package/skills/integrations/dynamics-365-finance.md +354 -0
- package/skills/integrations/earnings_transcripts.md +387 -0
- package/skills/integrations/factset.md +758 -0
- package/skills/integrations/ice-fixed-income.md +344 -0
- package/skills/integrations/moodys-analytics.md +313 -0
- package/skills/integrations/morningstar.md +433 -0
- package/skills/integrations/nasdaq-data-link.md +249 -0
- package/skills/integrations/pitchbook.md +413 -0
- package/skills/integrations/preqin.md +422 -0
- package/skills/integrations/quickbooks.md +289 -0
- package/skills/integrations/quickfs.md +314 -0
- package/skills/integrations/refinitiv.md +473 -0
- package/skills/integrations/sage-intacct.md +401 -0
- package/skills/integrations/visible-alpha.md +320 -0
- package/skills/integrations/xero.md +393 -0
- package/skills/integrations/ycharts.md +306 -0
- package/skills/pdf-creation/SKILL.md +93 -0
- package/skills/pdf-extraction/SKILL.md +32 -0
- package/skills/powerpoint-creation/SKILL.md +110 -0
- package/skills/sec-edgar/SKILL.md +127 -0
- package/skills/sec-edgar/sec_to_pdf.py +109 -0
- package/xll/ShortcutXL.xll +0 -0
- package/xll/modules/debug_render.py +272 -0
- package/xll/modules/gameboy.py +241 -0
- package/xll/modules/pong.py +188 -0
- package/xll/modules/shortcut_xl/__init__.py +18 -0
- package/xll/modules/shortcut_xl/_categorize.py +200 -0
- package/xll/modules/shortcut_xl/_com.py +108 -0
- package/xll/modules/shortcut_xl/_format.py +252 -0
- package/xll/modules/shortcut_xl/_log.py +12 -0
- package/xll/modules/shortcut_xl/_managed.py +116 -0
- package/xll/modules/shortcut_xl/_registry.py +44 -0
- package/xll/modules/shortcut_xl/_threading.py +161 -0
- package/xll/modules/shortcut_xl/_tracking.py +283 -0
- package/xll/modules/stocks.py +100 -0
- package/xll/python3.dll +0 -0
- package/xll/python312.dll +0 -0
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
"""Cell diff formatter — port of cell-diff-formatter.ts.
|
|
2
|
+
|
|
3
|
+
Produces a human-readable summary of dirty cells for the LLM,
|
|
4
|
+
with Good / Issues breakdown and row sampling.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import re
|
|
8
|
+
from ._categorize import (
|
|
9
|
+
categorize_cells,
|
|
10
|
+
GOOD, LARGE_PERCENTAGE, HARDCODED_NUMBER,
|
|
11
|
+
HARDCODED_NUMBER_IN_FORMULA, INVALID_FORMULA,
|
|
12
|
+
is_com_error, com_error_to_str,
|
|
13
|
+
)
|
|
14
|
+
from ._tracking import _col_to_letter
|
|
15
|
+
|
|
16
|
+
# Display constants
|
|
17
|
+
MAX_ROWS_TO_SHOW = 10
|
|
18
|
+
MAX_SAMPLES_PER_ROW = 5
|
|
19
|
+
DECIMAL_PLACES = 3
|
|
20
|
+
MAX_FORMAT_DECIMALS = 2
|
|
21
|
+
UNDEFINED_SYMBOL = '\u2205' # ∅
|
|
22
|
+
NO_CHANGES_MSG = 'No changes made.'
|
|
23
|
+
|
|
24
|
+
# (category_key, must_fix)
|
|
25
|
+
_PROBLEM_SECTIONS = (
|
|
26
|
+
(LARGE_PERCENTAGE, False),
|
|
27
|
+
(HARDCODED_NUMBER, False),
|
|
28
|
+
(HARDCODED_NUMBER_IN_FORMULA, False),
|
|
29
|
+
(INVALID_FORMULA, True),
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
# ---------- address parsing ----------
|
|
33
|
+
|
|
34
|
+
_ADDR_RE = re.compile(r'^(\$?)([A-Z]+)(\$?)(\d+)$', re.IGNORECASE)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _parse_address(addr):
|
|
38
|
+
"""Parse 'B12' -> (col_index_0based, row_0based)."""
|
|
39
|
+
m = _ADDR_RE.match(addr)
|
|
40
|
+
if not m:
|
|
41
|
+
return (0, 0)
|
|
42
|
+
letters = m.group(2).upper()
|
|
43
|
+
col = 0
|
|
44
|
+
for ch in letters:
|
|
45
|
+
col = col * 26 + (ord(ch) - 64)
|
|
46
|
+
return (col - 1, int(m.group(4)) - 1)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _col_letter(col_0):
|
|
50
|
+
"""0-based column index to letter(s). 0->A, 25->Z, 26->AA."""
|
|
51
|
+
return _col_to_letter(col_0 + 1)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
# ---------- value formatting ----------
|
|
55
|
+
|
|
56
|
+
def format_value(value, number_format=None):
|
|
57
|
+
"""Format a raw cell value for display."""
|
|
58
|
+
if value is None:
|
|
59
|
+
return UNDEFINED_SYMBOL
|
|
60
|
+
if isinstance(value, bool):
|
|
61
|
+
return 'TRUE' if value else 'FALSE'
|
|
62
|
+
if is_com_error(value):
|
|
63
|
+
return com_error_to_str(value)
|
|
64
|
+
if isinstance(value, str):
|
|
65
|
+
return 'EMPTY' if len(value) == 0 else value
|
|
66
|
+
# COM may return Decimal or other numeric types — coerce to float
|
|
67
|
+
if not isinstance(value, (int, float)):
|
|
68
|
+
try:
|
|
69
|
+
value = float(value)
|
|
70
|
+
except (TypeError, ValueError):
|
|
71
|
+
return str(value)
|
|
72
|
+
|
|
73
|
+
if not number_format:
|
|
74
|
+
return '0' if value == 0 else f'{value:.{DECIMAL_PLACES}f}'
|
|
75
|
+
|
|
76
|
+
fmt = number_format
|
|
77
|
+
is_pct = '%' in fmt
|
|
78
|
+
currency_m = re.search(r'[$\xa3\u20ac\xa5\u20b9\u20a9\u20aa\u20b1\u0e3f]|kr', fmt)
|
|
79
|
+
currency = currency_m.group(0) if currency_m else None
|
|
80
|
+
dec_m = re.search(r'\.([0#]+)', fmt)
|
|
81
|
+
decimals = min(len(dec_m.group(1)), MAX_FORMAT_DECIMALS) if dec_m else MAX_FORMAT_DECIMALS
|
|
82
|
+
use_parens = '(' in fmt
|
|
83
|
+
|
|
84
|
+
num = value * 100 if is_pct else value
|
|
85
|
+
abs_str = f'{abs(num):,.{decimals}f}'
|
|
86
|
+
neg = num < 0
|
|
87
|
+
|
|
88
|
+
if is_pct:
|
|
89
|
+
return f'({abs_str}%)' if neg and use_parens else f'{"-" if neg else ""}{abs_str}%'
|
|
90
|
+
if currency:
|
|
91
|
+
return (f'({currency}{abs_str})' if neg and use_parens
|
|
92
|
+
else f'{"-" if neg else ""}{currency}{abs_str}')
|
|
93
|
+
return f'({abs_str})' if neg and use_parens else f'{"-" if neg else ""}{abs_str}'
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _display_value(cell):
|
|
97
|
+
"""Get display string for a cell — prefer displayValue, then format with numberFormat."""
|
|
98
|
+
dv = cell.get('displayValue')
|
|
99
|
+
if dv is not None:
|
|
100
|
+
return 'EMPTY' if dv == '' else dv
|
|
101
|
+
return format_value(cell.get('value'), cell.get('numberFormat'))
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
# ---------- sampling ----------
|
|
105
|
+
|
|
106
|
+
def _sample_first_last(items, max_n):
|
|
107
|
+
"""Sample up to max_n items, always including first and last.
|
|
108
|
+
|
|
109
|
+
Uses evenly-spaced indices for deterministic output."""
|
|
110
|
+
n = len(items)
|
|
111
|
+
if n <= max_n:
|
|
112
|
+
return list(items)
|
|
113
|
+
if max_n <= 0:
|
|
114
|
+
return []
|
|
115
|
+
if max_n == 1:
|
|
116
|
+
return [items[0]]
|
|
117
|
+
# Evenly spaced indices including 0 and n-1
|
|
118
|
+
indices = [round(i * (n - 1) / (max_n - 1)) for i in range(max_n)]
|
|
119
|
+
return [items[i] for i in indices]
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
# ---------- filtering ----------
|
|
123
|
+
|
|
124
|
+
def _should_include(cell, issues):
|
|
125
|
+
"""Include all changes except trivial None↔None (no actual change)."""
|
|
126
|
+
value = cell.get('value')
|
|
127
|
+
old = cell.get('oldValue')
|
|
128
|
+
if value is None and old is None:
|
|
129
|
+
return False
|
|
130
|
+
return True
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
# ---------- grouping ----------
|
|
134
|
+
|
|
135
|
+
def _group_by_sheet_row(entries):
|
|
136
|
+
"""Group (cell, issues) entries by sheet -> row.
|
|
137
|
+
Returns {sheet: {row: [(cell, issues, col), ...]}}
|
|
138
|
+
"""
|
|
139
|
+
grouped = {}
|
|
140
|
+
for cell, issues in entries:
|
|
141
|
+
if not _should_include(cell, issues):
|
|
142
|
+
continue
|
|
143
|
+
col, row = _parse_address(cell.get('address', 'A1'))
|
|
144
|
+
sheet = cell.get('sheet', 'Sheet1')
|
|
145
|
+
grouped.setdefault(sheet, {}).setdefault(row, []).append((cell, issues, col))
|
|
146
|
+
# Sort within each row by column
|
|
147
|
+
for sheet_rows in grouped.values():
|
|
148
|
+
for row_entries in sheet_rows.values():
|
|
149
|
+
row_entries.sort(key=lambda t: t[2])
|
|
150
|
+
return grouped
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
# ---------- row formatting ----------
|
|
154
|
+
|
|
155
|
+
def _format_row(sheet, row, entries, indent=2, show_issues=True, show_count=True):
|
|
156
|
+
"""Format a single row with sampled cells."""
|
|
157
|
+
ind = ' ' * indent
|
|
158
|
+
sind = ' ' * (indent + 2)
|
|
159
|
+
lines = []
|
|
160
|
+
|
|
161
|
+
cols = [e[2] for e in entries]
|
|
162
|
+
min_c, max_c = min(cols), max(cols)
|
|
163
|
+
col_range = (_col_letter(min_c) if min_c == max_c
|
|
164
|
+
else f'{_col_letter(min_c)}-{_col_letter(max_c)}')
|
|
165
|
+
|
|
166
|
+
count_suffix = f': {len(entries)} cells' if show_count else ':'
|
|
167
|
+
lines.append(f'{ind}{sheet}!Row {row + 1} ({col_range}){count_suffix}')
|
|
168
|
+
|
|
169
|
+
samples = _sample_first_last(entries, MAX_SAMPLES_PER_ROW)
|
|
170
|
+
for cell, issues, _col in samples:
|
|
171
|
+
fmt = cell.get('numberFormat')
|
|
172
|
+
old_str = format_value(cell.get('oldValue'), fmt)
|
|
173
|
+
new_str = _display_value(cell)
|
|
174
|
+
# Show formula text as annotation when relevant (not category names —
|
|
175
|
+
# the section header already tells you the category)
|
|
176
|
+
ann = f' [{cell["formula"]}]' if show_issues and cell.get('formula') else ''
|
|
177
|
+
lines.append(f'{sind}\u2192 {cell.get("address", "?")}: {old_str} -> {new_str}{ann}')
|
|
178
|
+
|
|
179
|
+
if len(entries) > MAX_SAMPLES_PER_ROW:
|
|
180
|
+
lines.append(f'{sind}... and {len(entries) - MAX_SAMPLES_PER_ROW} more cells')
|
|
181
|
+
|
|
182
|
+
return lines
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
# ---------- section formatting ----------
|
|
186
|
+
|
|
187
|
+
def _format_section(entries, title, must_fix=False):
|
|
188
|
+
"""Format a titled section of (cell, issues) entries."""
|
|
189
|
+
filtered = [(c, iss) for c, iss in entries if _should_include(c, iss)]
|
|
190
|
+
if not filtered:
|
|
191
|
+
return ''
|
|
192
|
+
|
|
193
|
+
prefix = 'MUST FIX: ' if must_fix else ''
|
|
194
|
+
lines = [f'{prefix}{title}: {len(filtered)} total cells']
|
|
195
|
+
|
|
196
|
+
grouped = _group_by_sheet_row(filtered)
|
|
197
|
+
# Flatten to sorted list of (sheet, row, entries)
|
|
198
|
+
all_rows = []
|
|
199
|
+
for sheet in sorted(grouped):
|
|
200
|
+
for row in sorted(grouped[sheet]):
|
|
201
|
+
all_rows.append((sheet, row, grouped[sheet][row]))
|
|
202
|
+
|
|
203
|
+
selected = _sample_first_last(all_rows, MAX_ROWS_TO_SHOW)
|
|
204
|
+
for sheet, row, row_entries in selected:
|
|
205
|
+
lines.extend(_format_row(sheet, row, row_entries))
|
|
206
|
+
|
|
207
|
+
if len(all_rows) > MAX_ROWS_TO_SHOW:
|
|
208
|
+
lines.append(f' ... and {len(all_rows) - MAX_ROWS_TO_SHOW} more rows')
|
|
209
|
+
|
|
210
|
+
return '\n'.join(lines)
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
# ---------- public API ----------
|
|
214
|
+
|
|
215
|
+
def format_cell_diff(dirty_cells):
|
|
216
|
+
"""Format a list of dirty cell dicts into a human-readable summary.
|
|
217
|
+
|
|
218
|
+
This is the main entry point — call it with the raw dirty_cells list
|
|
219
|
+
from DirtyTracker.collect_changes(). It categorizes issues and produces
|
|
220
|
+
the same style of output as the Office.js client's cell-diff-formatter.
|
|
221
|
+
|
|
222
|
+
Returns a string ready to be printed to stdout for the LLM to see.
|
|
223
|
+
"""
|
|
224
|
+
if not dirty_cells:
|
|
225
|
+
return NO_CHANGES_MSG
|
|
226
|
+
|
|
227
|
+
cats = categorize_cells(dirty_cells)
|
|
228
|
+
|
|
229
|
+
sections = [
|
|
230
|
+
'--- CELL DIFF SUMMARY ---',
|
|
231
|
+
f'({UNDEFINED_SYMBOL} = undefined/empty value)\n',
|
|
232
|
+
]
|
|
233
|
+
|
|
234
|
+
# Good cells
|
|
235
|
+
if cats[GOOD]:
|
|
236
|
+
good_entries = [(c, []) for c in cats[GOOD]]
|
|
237
|
+
s = _format_section(good_entries, 'Changed without issues')
|
|
238
|
+
if s:
|
|
239
|
+
sections.append(s)
|
|
240
|
+
|
|
241
|
+
# Problem cells — driven by _PROBLEM_SECTIONS config
|
|
242
|
+
problem_parts = []
|
|
243
|
+
for cat_key, must_fix in _PROBLEM_SECTIONS:
|
|
244
|
+
if cats[cat_key]:
|
|
245
|
+
s = _format_section(cats[cat_key], cat_key, must_fix=must_fix)
|
|
246
|
+
if s:
|
|
247
|
+
problem_parts.append(s)
|
|
248
|
+
|
|
249
|
+
if problem_parts:
|
|
250
|
+
sections.append('Cells that need review:\n' + '\n\n'.join(problem_parts))
|
|
251
|
+
|
|
252
|
+
return '\n\n'.join(sections) if len(sections) > 2 else NO_CHANGES_MSG
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"""Logging utility for ShortcutXL."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import datetime
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def xl_log(msg):
|
|
8
|
+
"""Append a timestamped message to %TEMP%\\shortcutxl.log."""
|
|
9
|
+
log_path = os.path.join(os.environ.get("TEMP", "."), "shortcutxl.log")
|
|
10
|
+
timestamp = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S.%f")[:-3]
|
|
11
|
+
with open(log_path, "a", encoding="utf-8") as f:
|
|
12
|
+
f.write(f"[{timestamp}] {msg}\n")
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
"""run_managed — execute a function against Excel on the main thread."""
|
|
2
|
+
|
|
3
|
+
import threading
|
|
4
|
+
from ._log import xl_log
|
|
5
|
+
from ._com import xl_app
|
|
6
|
+
from ._threading import _run_on_main
|
|
7
|
+
from ._tracking import DirtyTracker, TrackedApp
|
|
8
|
+
|
|
9
|
+
# Excel xlCalculation enum values
|
|
10
|
+
_XL_CALCULATION_MANUAL = -4135
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def run_managed(fn):
|
|
14
|
+
"""Run fn(app) on the main thread with Excel state managed.
|
|
15
|
+
|
|
16
|
+
Suspends Calculation/Events/Alerts, runs fn(app) with a tracking
|
|
17
|
+
proxy, collects dirty cells, then restores Excel state.
|
|
18
|
+
|
|
19
|
+
All COM calls happen directly on the main thread — no cross-apartment
|
|
20
|
+
marshaling overhead.
|
|
21
|
+
|
|
22
|
+
Returns the list of dirty cell dicts.
|
|
23
|
+
|
|
24
|
+
Usage::
|
|
25
|
+
|
|
26
|
+
def do_work(app):
|
|
27
|
+
app.ActiveSheet.Range("A1").Value = 42
|
|
28
|
+
|
|
29
|
+
dirty = run_managed(do_work)
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
def _work():
|
|
33
|
+
app = xl_app()
|
|
34
|
+
has_workbooks = app.Workbooks.Count > 0
|
|
35
|
+
orig_ev = app.EnableEvents
|
|
36
|
+
orig_da = app.DisplayAlerts
|
|
37
|
+
orig_calc = app.Calculation if has_workbooks else None
|
|
38
|
+
if has_workbooks:
|
|
39
|
+
app.Calculation = _XL_CALCULATION_MANUAL
|
|
40
|
+
app.EnableEvents = False
|
|
41
|
+
app.DisplayAlerts = False
|
|
42
|
+
tracker = DirtyTracker(app)
|
|
43
|
+
tracked = TrackedApp(app, tracker)
|
|
44
|
+
dirty = []
|
|
45
|
+
try:
|
|
46
|
+
fn(tracked)
|
|
47
|
+
finally:
|
|
48
|
+
# Collect changes BEFORE restoring calc so we only see
|
|
49
|
+
# direct writes, not formula recalculations.
|
|
50
|
+
try:
|
|
51
|
+
dirty.extend(tracker.collect_changes())
|
|
52
|
+
except Exception as e:
|
|
53
|
+
xl_log(f"run_managed: collect_changes failed: {e}")
|
|
54
|
+
# Each restoration is independent — don't let one failure
|
|
55
|
+
# prevent the others from being restored.
|
|
56
|
+
try:
|
|
57
|
+
app.DisplayAlerts = orig_da
|
|
58
|
+
except Exception:
|
|
59
|
+
pass
|
|
60
|
+
try:
|
|
61
|
+
app.EnableEvents = orig_ev
|
|
62
|
+
except Exception:
|
|
63
|
+
pass
|
|
64
|
+
try:
|
|
65
|
+
# If user's calc mode was manual, restoring it won't
|
|
66
|
+
# trigger a recalc — force one so dependents are fresh.
|
|
67
|
+
if orig_calc == _XL_CALCULATION_MANUAL and app.Workbooks.Count > 0:
|
|
68
|
+
app.Calculate()
|
|
69
|
+
except Exception:
|
|
70
|
+
pass
|
|
71
|
+
try:
|
|
72
|
+
# Restore original calc mode if we had workbooks,
|
|
73
|
+
# or reset to automatic if fn created the first workbook.
|
|
74
|
+
if orig_calc is not None:
|
|
75
|
+
app.Calculation = orig_calc
|
|
76
|
+
elif app.Workbooks.Count > 0:
|
|
77
|
+
# fn created workbooks — calc was never saved, reset
|
|
78
|
+
# to automatic so we don't leave it stuck on manual.
|
|
79
|
+
app.Calculation = -4105 # xlCalculationAutomatic
|
|
80
|
+
except Exception:
|
|
81
|
+
pass
|
|
82
|
+
return dirty
|
|
83
|
+
|
|
84
|
+
return _run_on_main(_work)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def xl_batch(fn):
|
|
88
|
+
"""Run fn(app) on the main thread with ScreenUpdating disabled."""
|
|
89
|
+
def _batched():
|
|
90
|
+
import pythoncom
|
|
91
|
+
app = xl_app()
|
|
92
|
+
app.ScreenUpdating = False
|
|
93
|
+
try:
|
|
94
|
+
fn(app)
|
|
95
|
+
finally:
|
|
96
|
+
app.ScreenUpdating = True
|
|
97
|
+
pythoncom.PumpWaitingMessages()
|
|
98
|
+
_run_on_main(_batched)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def schedule_call(fn, delay_seconds=0.0):
|
|
102
|
+
"""Schedule fn() to be called after delay_seconds on a background thread."""
|
|
103
|
+
import time
|
|
104
|
+
|
|
105
|
+
def _run():
|
|
106
|
+
if delay_seconds > 0:
|
|
107
|
+
time.sleep(delay_seconds)
|
|
108
|
+
try:
|
|
109
|
+
fn()
|
|
110
|
+
except Exception as e:
|
|
111
|
+
import traceback
|
|
112
|
+
xl_log(f"schedule_call error: {e}\n{traceback.format_exc()}")
|
|
113
|
+
|
|
114
|
+
t = threading.Thread(target=_run, daemon=True)
|
|
115
|
+
t.start()
|
|
116
|
+
return t
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"""@xl_func decorator — marks functions for registration as Excel UDFs.
|
|
2
|
+
|
|
3
|
+
The C runtime scans _registry at xlAutoOpen time to register functions with Excel.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import inspect
|
|
7
|
+
|
|
8
|
+
# Internal registry: list of (name, callable, n_args)
|
|
9
|
+
_registry = []
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def xl_func(fn=None, *, name=None):
|
|
13
|
+
"""Decorator — mark a function as an Excel UDF.
|
|
14
|
+
|
|
15
|
+
Usage:
|
|
16
|
+
@xl_func
|
|
17
|
+
def my_func(a, b):
|
|
18
|
+
return a + b
|
|
19
|
+
|
|
20
|
+
@xl_func(name="myCustomName")
|
|
21
|
+
def my_func(a, b):
|
|
22
|
+
return a + b
|
|
23
|
+
"""
|
|
24
|
+
def _register(f):
|
|
25
|
+
excel_name = name if name else f.__name__
|
|
26
|
+
sig = inspect.signature(f)
|
|
27
|
+
n_args = len([
|
|
28
|
+
p for p in sig.parameters.values()
|
|
29
|
+
if p.kind in (
|
|
30
|
+
inspect.Parameter.POSITIONAL_OR_KEYWORD,
|
|
31
|
+
inspect.Parameter.POSITIONAL_ONLY,
|
|
32
|
+
)
|
|
33
|
+
])
|
|
34
|
+
# Check if already registered (e.g. after a hot-reload)
|
|
35
|
+
for i, (n, _, _) in enumerate(_registry):
|
|
36
|
+
if n == excel_name:
|
|
37
|
+
_registry[i] = (excel_name, f, n_args)
|
|
38
|
+
return f
|
|
39
|
+
_registry.append((excel_name, f, n_args))
|
|
40
|
+
return f
|
|
41
|
+
|
|
42
|
+
if fn is not None:
|
|
43
|
+
return _register(fn)
|
|
44
|
+
return _register
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
"""Main-thread work queue for ShortcutXL.
|
|
2
|
+
|
|
3
|
+
Python win32com cannot call Excel COM from background threads reliably
|
|
4
|
+
(GetActiveObject fails from MTA, Dispatch creates a new instance).
|
|
5
|
+
This matches the pattern used by PyXLL, Excel-DNA, and xlwings:
|
|
6
|
+
background threads enqueue work, a timer on the main thread drains it.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import threading
|
|
10
|
+
import queue
|
|
11
|
+
import ctypes
|
|
12
|
+
from ctypes import wintypes
|
|
13
|
+
from ._log import xl_log
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
# COM error code: Excel is busy (VBA_E_IGNORE = 0x800AC472)
|
|
17
|
+
_VBA_E_IGNORE = -2146777998
|
|
18
|
+
|
|
19
|
+
_MAX_ITEMS_PER_TICK = 64 # cap per timer tick to avoid starving Excel
|
|
20
|
+
_MAX_BUSY_RETRIES = 2400 # ~120s at 50ms intervals before giving up
|
|
21
|
+
_DRAIN_INTERVAL_MS = 50 # SetTimer interval for queue drain
|
|
22
|
+
_DEFAULT_TIMEOUT_S = 30.0 # max wait for main-thread dispatch + execution
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class ExecutionTimeout(BaseException):
|
|
26
|
+
"""Raised on the main thread when a caller's timeout expires mid-execution.
|
|
27
|
+
|
|
28
|
+
Inherits from BaseException (not Exception) so that user code's
|
|
29
|
+
``except Exception:`` blocks cannot swallow it."""
|
|
30
|
+
pass
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _is_excel_busy(e):
|
|
34
|
+
"""Check if a COM exception is VBA_E_IGNORE (Excel is busy)."""
|
|
35
|
+
if not hasattr(e, 'hresult'):
|
|
36
|
+
return False
|
|
37
|
+
if e.hresult == _VBA_E_IGNORE:
|
|
38
|
+
return True
|
|
39
|
+
ei = getattr(e, 'excepinfo', None)
|
|
40
|
+
if ei and len(ei) > 5 and ei[5] == _VBA_E_IGNORE:
|
|
41
|
+
return True
|
|
42
|
+
return False
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
# Persist main thread ID in builtins so it survives hot-reload.
|
|
46
|
+
import builtins
|
|
47
|
+
_main_thread_id = getattr(builtins, '_shortcutxl_main_thread_id', None)
|
|
48
|
+
if _main_thread_id is None:
|
|
49
|
+
_main_thread_id = threading.current_thread().ident
|
|
50
|
+
builtins._shortcutxl_main_thread_id = _main_thread_id
|
|
51
|
+
|
|
52
|
+
_work_queue = queue.Queue()
|
|
53
|
+
_draining = False
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _drain_queue():
|
|
57
|
+
"""Drain pending work items. Runs on the main thread from the timer.
|
|
58
|
+
|
|
59
|
+
The reentrancy guard is essential: work items may call
|
|
60
|
+
PumpWaitingMessages(), which can dispatch another WM_TIMER and
|
|
61
|
+
re-enter this function. Without the guard the queue can be
|
|
62
|
+
drained out-of-order and done_events can be missed."""
|
|
63
|
+
global _draining
|
|
64
|
+
if _draining:
|
|
65
|
+
return
|
|
66
|
+
_draining = True
|
|
67
|
+
try:
|
|
68
|
+
for _ in range(_MAX_ITEMS_PER_TICK):
|
|
69
|
+
try:
|
|
70
|
+
fn, result_box, done_event = _work_queue.get_nowait()
|
|
71
|
+
except queue.Empty:
|
|
72
|
+
break
|
|
73
|
+
if result_box.get('_cancelled'):
|
|
74
|
+
continue
|
|
75
|
+
result_box['_running'] = True
|
|
76
|
+
try:
|
|
77
|
+
result_box['result'] = fn()
|
|
78
|
+
result_box['error'] = None
|
|
79
|
+
done_event.set()
|
|
80
|
+
except ExecutionTimeout:
|
|
81
|
+
result_box['error'] = RuntimeError(
|
|
82
|
+
"Execution interrupted: caller timed out")
|
|
83
|
+
done_event.set()
|
|
84
|
+
except Exception as e:
|
|
85
|
+
if _is_excel_busy(e):
|
|
86
|
+
result_box['_retries'] = result_box.get('_retries', 0) + 1
|
|
87
|
+
if result_box['_retries'] < _MAX_BUSY_RETRIES:
|
|
88
|
+
_work_queue.put((fn, result_box, done_event))
|
|
89
|
+
break
|
|
90
|
+
xl_log(f"_drain_queue: Excel busy exceeded {_MAX_BUSY_RETRIES} retries")
|
|
91
|
+
result_box['error'] = e
|
|
92
|
+
done_event.set()
|
|
93
|
+
finally:
|
|
94
|
+
_draining = False
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
# SetTimer callback — called by Windows on the main thread during message pump
|
|
98
|
+
_TIMERPROC = ctypes.WINFUNCTYPE(
|
|
99
|
+
None, wintypes.HWND, ctypes.c_uint, ctypes.c_size_t, wintypes.DWORD)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _on_timer(hwnd, msg, id_event, dw_time):
|
|
103
|
+
_drain_queue()
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
# prevent GC of the callback (prevents crash when timer fires)
|
|
107
|
+
_timer_proc_ref = _TIMERPROC(_on_timer)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _start_drain_timer():
|
|
111
|
+
"""Create a 50ms timer on the main thread. Safe to call on hot-reload."""
|
|
112
|
+
try:
|
|
113
|
+
old_id = getattr(builtins, '_shortcutxl_timer_id', None)
|
|
114
|
+
if old_id:
|
|
115
|
+
ctypes.windll.user32.KillTimer(None, ctypes.c_size_t(old_id))
|
|
116
|
+
timer_id = ctypes.windll.user32.SetTimer(
|
|
117
|
+
None, ctypes.c_size_t(0), _DRAIN_INTERVAL_MS, _timer_proc_ref)
|
|
118
|
+
builtins._shortcutxl_timer_id = timer_id
|
|
119
|
+
xl_log(f"_start_drain_timer: timer_id={timer_id}")
|
|
120
|
+
except Exception as e:
|
|
121
|
+
xl_log(f"_start_drain_timer: FAILED — {e}")
|
|
122
|
+
import traceback
|
|
123
|
+
xl_log(traceback.format_exc())
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
# Only start the drain timer on the main thread (xlAutoOpen).
|
|
127
|
+
if threading.current_thread().ident == _main_thread_id:
|
|
128
|
+
_start_drain_timer()
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def _run_on_main(fn, timeout=_DEFAULT_TIMEOUT_S):
|
|
132
|
+
"""Run fn() on the main thread and return its result. Blocks the caller.
|
|
133
|
+
If already on the main thread, calls fn() directly."""
|
|
134
|
+
if threading.current_thread().ident == _main_thread_id:
|
|
135
|
+
return fn()
|
|
136
|
+
result_box = {'_retries': 0}
|
|
137
|
+
done_event = threading.Event()
|
|
138
|
+
_work_queue.put((fn, result_box, done_event))
|
|
139
|
+
if not done_event.wait(timeout=timeout):
|
|
140
|
+
if result_box.get('_running'):
|
|
141
|
+
# fn is mid-execution on the main thread — inject an async
|
|
142
|
+
# exception to interrupt it. This fires at the next Python
|
|
143
|
+
# bytecode boundary (won't interrupt a blocking C call, but
|
|
144
|
+
# will fire between COM calls / sleep increments).
|
|
145
|
+
ctypes.pythonapi.PyThreadState_SetAsyncExc(
|
|
146
|
+
ctypes.c_ulong(_main_thread_id),
|
|
147
|
+
ctypes.py_object(ExecutionTimeout))
|
|
148
|
+
# Race guard: fn may have completed between the timeout and
|
|
149
|
+
# the injection. If done_event is already set, the exception
|
|
150
|
+
# would land on the NEXT work item — clear it immediately.
|
|
151
|
+
if done_event.is_set():
|
|
152
|
+
ctypes.pythonapi.PyThreadState_SetAsyncExc(
|
|
153
|
+
ctypes.c_ulong(_main_thread_id), None)
|
|
154
|
+
elif not done_event.wait(timeout=5.0):
|
|
155
|
+
xl_log("_run_on_main: fn did not stop after async exception")
|
|
156
|
+
else:
|
|
157
|
+
result_box['_cancelled'] = True
|
|
158
|
+
raise RuntimeError("_run_on_main: timed out waiting for main thread")
|
|
159
|
+
if result_box.get('error') is not None:
|
|
160
|
+
raise result_box['error']
|
|
161
|
+
return result_box.get('result')
|