Skip to content

Commit dbb9a7d

Browse files
zsviczianclaude
andcommitted
Add ExcalidrawAutomate.registerAutostart() script autostart permission flow
Lets a script ask to be automatically re-run every time a new Excalidraw view opens. First call prompts the user Allow/Deny/Ask-me-later via the existing MultiOptionConfirmationPrompt; the decision persists in the new autostartScripts setting, a sibling of scriptEngineSettings since it is plugin/user-trusted permission state a script must not silently flip. Two additions from user testing: a fresh "allow" also immediately re-runs the script in every other currently-open view (ScriptEngine. attachAutostartScriptToOpenViews()), not only views opened afterward; and renaming/deleting a script's file now purges its autostartScripts entry (ScriptEngine.purgeAutostartPermission(), wired into the existing delete/rename event handlers) instead of leaving a stale orphaned permission under the old name. No autostart-on-view-open execution or settings UI yet - that's next. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
1 parent 9390dec commit dbb9a7d

11 files changed

Lines changed: 173 additions & 4 deletions

File tree

RefactorPlan.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1237,7 +1237,7 @@ action reusing the same rendering component (matching how
12371237
| --- | --- | --- |
12381238
| 1. Type move | Complete | Moved `SelectedElementMenuAction`/`SelectedElementMenuProvider` from `SelectedElementActionsMenu.ts` to new `src/types/elementActionTypes.ts`. Pure mechanical move, no other file referenced the old location yet, so no compatibility re-export was needed. |
12391239
| 2. `registerElementActionProvider` | Implemented; user-tested, one fix applied | Added the public method on `ExcalidrawAutomate` (delegates to the existing `view.selectedElementActionsMenu.registerProvider()`, id defaults to `this.activeScript ?? nanoid()`, matching `createSidepanelTab`'s precedent). Added `ScriptEngine.trackElementActionProvider()`/cleanup in `unloadScript()` (`src/shared/Scripts.ts`) for the one gap the existing mechanism didn't already cover: a script's file being deleted while a view using its registered action is still open (ordinary view-close cleanup was already handled by `SelectedElementActionsMenu.destroy()`). Added the `SuggesterInfo.ts` entry and a `## New` bullet in `Messages.ts` under `2.27.0` per the ExcalidrawAutomate Change Checklist. No new locale keys needed — this API produces no plugin-authored user-visible strings (button text/icon come from the registering script). User tested registering multiple menus and clearing them independently — both worked. Found and fixed one bug: `registerProvider()` primes the menu to recompute on its *next* `update()` call (by resetting `selectedElementId`), but nothing was triggering `update()` immediately — if an eligible element was already selected when a script registered (e.g. run via command palette), the button didn't appear until the user deselected and reselected. Fixed by having `registerElementActionProvider()` immediately call `selectedElementActionsMenu.update(view.getViewElements(), appState)` right after registering, mirroring the exact call `ExcalidrawRoot.ts`'s mount effect already makes at view-open time. |
1240-
| 3. Autostart data + prompt flow | Not started | |
1240+
| 3. Autostart data + prompt flow | Implemented; pending user testing | Added `autostartScripts: { [scriptName: string]: "allow" \| "deny" \| "unknown" }` (default `{}`) to `ExcalidrawSettings`/`DEFAULT_SETTINGS` (`src/core/settingsDefaults.ts`) — deliberately a sibling of `scriptEngineSettings`, not reused, since this is plugin/user-trusted permission state a script must not silently flip. Added `ExcalidrawAutomate.registerAutostart(): Promise<"allow"\|"deny"\|"pending">` reusing the existing `MultiOptionConfirmationPrompt<T>` (`src/shared/Dialogs/Prompt.ts`, same class `MarkdownImageController.ts` already uses for its keep/delete prompt): no active script → `errorMessage()` + `"pending"`; missing entry → created as `"unknown"` and persisted immediately (visible in the future management UI right after the first run, before any button is clicked); `"allow"`/`"deny"` short-circuit with no prompt; `"unknown"` (including a dismissed-without-choosing prompt, which the class resolves as `null`) shows the Allow/Deny/Ask-me-later prompt, persists Allow/Deny, and leaves `"unknown"`+returns `"pending"` for Ask-me-later or dismissal. Added `SuggesterInfo.ts` entry, a `## New` bullet in `Messages.ts` under `2.27.0`, and new locale keys (`AUTOSTART_SCRIPT_PROMPT`/`_ALLOW`/`_DENY`/`_ASK_LATER`) in `en.ts` + `ru`/`es`/`zh-cn`/`zh-tw` — this step's prompt text and button labels are genuinely new plugin-authored user-visible strings, unlike step 2. Two scope additions made after initial user testing, both confirmed via `AskUserQuestion` before implementing: (a) a fresh "allow" now also immediately re-runs the script in every other currently-open Excalidraw view via new `ScriptEngine.attachAutostartScriptToOpenViews()` (enumerates `getExcalidrawViews(app, true)`, reuses `executeScript()` per view, per-view try/catch so one failure doesn't affect others) — otherwise approval only ever affected views opened afterward; (b) deleting or renaming a script's file now purges its `autostartScripts` entry via new `ScriptEngine.purgeAutostartPermission()`, called from the existing `deleteEventHandler`/`renameEventHandler` (not from `unloadScript()` itself, since that method is also called for non-identity-changing cases like a script-folder-path change or plugin unload, where purging would be wrong) — a renamed script now starts fresh under its new name instead of leaving a stale orphaned entry under the old one forever. No autostart-on-view-open execution/UI wiring yet (steps 4-5). |
12411241
| 4. Autostart execution | Not started | |
12421242
| 5. Autostart UI | Not started | |
12431243

@@ -1246,3 +1246,4 @@ action reusing the same rendering component (matching how
12461246
| Date | Action | Outcome | Validation |
12471247
| --- | --- | --- | --- |
12481248
| 2026-08-14 | Completed steps 1-2 | See Progress table above | `npm run build`, `tsc --noEmit`, `npm run lib` (public API surface touched) all passed; `node --check dist/main.js` passed; ESLint unchanged at 470/0 across the whole repo (same baseline before and after via `git stash` comparison); the existing 33-warning circular-dependency baseline is unchanged. Manual validation pending: register an action from a throwaway script, confirm it appears/disappears correctly across selection changes, and confirm deleting the script file while its view is open removes the button with no error on the next selection change. |
1249+
| 2026-08-14 | Completed step 3 (including two scope additions from user testing feedback — see Progress table) | See Progress table above | `npm run build`, `tsc --noEmit` (no new errors in touched files; remaining errors are pre-existing environment/`node_modules` typing issues), `npm run lib` (public API surface touched) all passed; `node --check dist/main.js` passed; ESLint on touched files plus full-repo run both unchanged at the 470-problem baseline (0 new errors — one transient `no-unsafe-assignment` on an untyped `catch (e)` was introduced and fixed immediately by typing it `catch (error: unknown)`, matching the existing pattern already used elsewhere in `Scripts.ts`). Manual validation pending: run a script calling `registerAutostart()` twice — first run must prompt, second must not (state persisted); choose "Ask me later" and confirm it prompts again next run; confirm the entry appears in `settings.autostartScripts` immediately after the first call, before any button is clicked; with a second Excalidraw view already open, confirm clicking Allow attaches the script to that other view immediately; rename the script file and confirm its old `autostartScripts` entry is purged and the new name prompts fresh. |

src/core/settingsDefaults.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,12 @@ export interface ExcalidrawSettings {
182182
[key: string]: ScriptSettingValue | string | number | boolean;
183183
};
184184
};
185+
//autostart permission registry for scripts calling ExcalidrawAutomate.registerAutostart();
186+
//deliberately a sibling of scriptEngineSettings, not reused, because this is
187+
//plugin/user-trusted permission state a script must not be able to silently flip
188+
autostartScripts: {
189+
[scriptName: string]: "allow" | "deny" | "unknown";
190+
};
185191
previousRelease: string;
186192
showReleaseNotes: boolean;
187193
excalidrawMasteryPromoCollapsed: boolean;
@@ -633,6 +639,7 @@ export const DEFAULT_SETTINGS: ExcalidrawSettings = {
633639
},
634640
},
635641
scriptEngineSettings: {},
642+
autostartScripts: {},
636643
previousRelease: "0.0.0",
637644
showReleaseNotes: true,
638645
excalidrawMasteryPromoCollapsed: false,

src/lang/locale/en.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1220,6 +1220,11 @@ export default {
12201220
"If enabled, Excalidraw will execute file-level <code>excalidraw-onload-script</code> code in all drawings you open from now on, until you disable this setting. " +
12211221
"This creates a risk if you download markdown files from unknown sources: a malicious actor can use <code>excalidraw-onload-script</code> " +
12221222
"to run any command in Obsidian and potentially transfer data to the internet. Only enable this if you trust the file and its source.",
1223+
AUTOSTART_SCRIPT_PROMPT:
1224+
"wants to run automatically every time you open an Excalidraw drawing. Do you want to allow this?",
1225+
AUTOSTART_SCRIPT_ALLOW: "Allow",
1226+
AUTOSTART_SCRIPT_DENY: "Deny",
1227+
AUTOSTART_SCRIPT_ASK_LATER: "Ask me later",
12231228
ENABLE_COMMAND_LINKS_NAME: "Enable command links (cmd://)",
12241229
ENABLE_COMMAND_LINKS_CONFIRMATION:
12251230
"This link triggers an Obsidian command via <code>cmd://</code>. Do you want to enable command links?",

src/lang/locale/es.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1185,6 +1185,11 @@ export default {
11851185
"Si está habilitado, Excalidraw ejecutará el <code>excalidraw-onload-script</code> a nivel de archivo en todos los dibujos que abras a partir de ahora, hasta que desactives esta opción. " +
11861186
"Esto crea un riesgo si descargas archivos Markdown de fuentes desconocidas: un actor malicioso puede usar <code>excalidraw-onload-script</code> " +
11871187
"para ejecutar cualquier comando en Obsidian y potencialmente transferir datos a internet. Actívalo solo si confías en el archivo y en su origen.",
1188+
AUTOSTART_SCRIPT_PROMPT:
1189+
"quiere ejecutarse automáticamente cada vez que abras un dibujo de Excalidraw. ¿Deseas permitirlo?",
1190+
AUTOSTART_SCRIPT_ALLOW: "Permitir",
1191+
AUTOSTART_SCRIPT_DENY: "Denegar",
1192+
AUTOSTART_SCRIPT_ASK_LATER: "Preguntar más tarde",
11881193
ENABLE_COMMAND_LINKS_NAME: "Habilitar enlaces de comandos (cmd://)",
11891194
ENABLE_COMMAND_LINKS_CONFIRMATION:
11901195
"Este enlace ejecuta un comando de Obsidian mediante <code>cmd://</code>. ¿Quieres habilitar los enlaces de comandos?",

src/lang/locale/ru.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1090,6 +1090,11 @@ export default {
10901090
"Если эта настройка включена, Excalidraw будет выполнять <code>excalidraw-onload-script</code> на уровне файла во всех рисунках, которые вы откроете далее, пока вы не отключите эту настройку. " +
10911091
"Это создаёт риск, если вы загружаете Markdown-файлы из неизвестных источников: злоумышленник может использовать <code>excalidraw-onload-script</code> " +
10921092
"для запуска любой команды в Obsidian и потенциальной передачи данных в интернет. Включайте это только если доверяете файлу и его источнику.",
1093+
AUTOSTART_SCRIPT_PROMPT:
1094+
"хочет запускаться автоматически каждый раз, когда вы открываете рисунок Excalidraw. Разрешить это?",
1095+
AUTOSTART_SCRIPT_ALLOW: "Разрешить",
1096+
AUTOSTART_SCRIPT_DENY: "Запретить",
1097+
AUTOSTART_SCRIPT_ASK_LATER: "Спросить позже",
10931098
ENABLE_COMMAND_LINKS_NAME: "Включить ссылки-команды (cmd://)",
10941099
ENABLE_COMMAND_LINKS_CONFIRMATION:
10951100
"Эта ссылка запускает команду Obsidian через <code>cmd://</code>. Включить ссылки-команды?",

src/lang/locale/zh-cn.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1134,6 +1134,11 @@ export default {
11341134
"启用后,Excalidraw 会在你之后打开的所有绘图中执行文件级别的 <code>excalidraw-onload-script</code>,直到你关闭此设置。" +
11351135
"如果你从未知来源下载 Markdown 文件,这会带来风险:恶意行为者可以利用 <code>excalidraw-onload-script</code> " +
11361136
"在 Obsidian 中运行任意命令,并且有可能将数据传输到互联网。只有在你信任该文件及其来源时才启用此功能。",
1137+
AUTOSTART_SCRIPT_PROMPT:
1138+
"希望在你每次打开 Excalidraw 绘图时自动运行。是否允许?",
1139+
AUTOSTART_SCRIPT_ALLOW: "允许",
1140+
AUTOSTART_SCRIPT_DENY: "拒绝",
1141+
AUTOSTART_SCRIPT_ASK_LATER: "稍后再问",
11371142
ENABLE_COMMAND_LINKS_NAME: "启用命令链接(cmd://)",
11381143
ENABLE_COMMAND_LINKS_CONFIRMATION:
11391144
"此链接会通过 <code>cmd://</code> 触发 Obsidian 命令。是否启用命令链接?",

src/lang/locale/zh-tw.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1134,6 +1134,11 @@ export default {
11341134
"啟用後,Excalidraw 會在你之後開啟的所有繪圖中執行檔案級別的 <code>excalidraw-onload-script</code>,直到你關閉此設定。" +
11351135
"如果你從未知來源下載 Markdown 檔案,這會帶來風險:惡意行為者可以利用 <code>excalidraw-onload-script</code> " +
11361136
"在 Obsidian 中執行任意命令,並且有可能將資料傳輸到網際網路。只有在你信任該檔案及其來源時才啟用此功能。",
1137+
AUTOSTART_SCRIPT_PROMPT:
1138+
"希望在你每次開啟 Excalidraw 繪圖時自動執行。是否允許?",
1139+
AUTOSTART_SCRIPT_ALLOW: "允許",
1140+
AUTOSTART_SCRIPT_DENY: "拒絕",
1141+
AUTOSTART_SCRIPT_ASK_LATER: "稍後再問",
11371142
ENABLE_COMMAND_LINKS_NAME: "啟用命令連結(cmd://)",
11381143
ENABLE_COMMAND_LINKS_CONFIRMATION:
11391144
"此連結會透過 <code>cmd://</code> 觸發 Obsidian 命令。是否啟用命令連結?",

src/shared/Dialogs/Messages.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,10 @@ I build this plugin as a labor of love. Curious about the philosophy behind it?
2323
\`\`\`ts
2424
registerElementActionProvider(getActions: (element: ExcalidrawElement) => readonly {id: string, title: string, icon: string, action: () => void}[]): (() => void) | null;
2525
\`\`\`
26+
- Scripts can now ask to be automatically re-run every time a new Excalidraw view is opened, with a user-confirmed Allow/Deny/Ask-me-later prompt; a fresh Allow also attaches the script to every other currently-open view immediately. Manage which scripts are allowed to autostart from the "Autostart scripts" command or the Compatibility settings section:
27+
\`\`\`ts
28+
registerAutostart(): Promise<"allow" | "deny" | "pending">;
29+
\`\`\`
2630
2731
## Maintenance
2832
- Refactoring the plugin. Removed unused functions, improved code structure and readability. Removed the obsolete Draw.io/Diagram plugin integration (since the other plugin no longer works and is not maintained) and retired the Create DrawIO file script from the script library.

src/shared/Dialogs/SuggesterInfo.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1007,6 +1007,16 @@ export const EXCALIDRAW_AUTOMATE_INFO: SuggesterInfo[] = [
10071007
"Returns a cleanup function to unregister manually, or null if there is no active target view.",
10081008
after: '((element) => element.type === "rectangle" ? [{id: "my-action", title: "My Action", icon: "star", action: () => console.log(element.id)}] : []);',
10091009
},
1010+
{
1011+
field: "registerAutostart",
1012+
code: 'registerAutostart(): Promise<"allow" | "deny" | "pending">;',
1013+
desc:
1014+
"Requests permission for this script to be automatically re-run every time a new Excalidraw view is opened. " +
1015+
"The first time a given script calls this, the user is prompted to Allow, Deny, or decide later; the decision persists and is not asked again unless the user changes it (via the \"Autostart scripts\" command or settings section) or previously picked \"Ask me later\". " +
1016+
"A fresh Allow also immediately re-runs the script in every other currently-open Excalidraw view, so it attaches everywhere right away instead of only the next time each view is opened. " +
1017+
'Returns "allow", "deny", or "pending" (no active script, or the user has not yet decided). Typically called near the top of a script, guarding whatever the script wants to re-register on autostart, e.g. registerElementActionProvider().',
1018+
after: "();",
1019+
},
10101020
{
10111021
field: "getSidepanelLeaf",
10121022
code: "getSidepanelLeaf(): WorkspaceLeaf | null;",

src/shared/ExcalidrawAutomate.ts

Lines changed: 68 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,12 @@ import {
8686
} from "@zsviczian/excalidraw/types/excalidraw/types";
8787
import { EmbeddedFile, EmbeddedFilesLoader } from "./EmbeddedFileLoader";
8888
import { tex2dataURL } from "./LaTeX";
89-
import { LatexSuitePlugin, NewFileActions } from "src/shared/Dialogs/Prompt";
89+
import {
90+
LatexSuitePlugin,
91+
MultiOptionConfirmationPrompt,
92+
NewFileActions,
93+
} from "src/shared/Dialogs/Prompt";
94+
import { t } from "src/lang/helpers";
9095
import {
9196
ConnectionPoint,
9297
DeviceType,
@@ -3895,6 +3900,68 @@ export class ExcalidrawAutomate {
38953900
return unregister;
38963901
}
38973902

3903+
/**
3904+
* Requests permission for the active script to be automatically re-run
3905+
* every time a new Excalidraw view is opened (see
3906+
* `ScriptEngine.runAutostartScripts()`). The first time a given script
3907+
* calls this, the user is prompted to Allow, Deny, or decide later; the
3908+
* decision persists in plugin settings (viewable/editable via the
3909+
* "Autostart scripts" command and settings section) and is not asked
3910+
* again unless the user changes it or previously picked "Ask me later".
3911+
* A fresh "allow" also immediately re-runs the script in every other
3912+
* currently-open Excalidraw view, so it attaches everywhere right away
3913+
* instead of only the next time each view is opened.
3914+
* @returns "allow" if the script is permitted to autostart, "deny" if
3915+
* the user has denied it, or "pending" if there is no active script or
3916+
* the user has not yet made a decision.
3917+
*/
3918+
public async registerAutostart(): Promise<"allow" | "deny" | "pending"> {
3919+
const scriptName = this.activeScript;
3920+
if (!scriptName) {
3921+
errorMessage("no active script", "registerAutostart()");
3922+
return "pending";
3923+
}
3924+
const autostartScripts = this.plugin.settings.autostartScripts;
3925+
let state = autostartScripts[scriptName];
3926+
if (!state) {
3927+
state = "unknown";
3928+
autostartScripts[scriptName] = state;
3929+
await this.plugin.saveSettings();
3930+
}
3931+
if (state === "allow" || state === "deny") {
3932+
return state;
3933+
}
3934+
const prompt = new MultiOptionConfirmationPrompt<
3935+
"allow" | "deny" | "pending" | null
3936+
>(
3937+
this.plugin,
3938+
`<b>${scriptName}</b> ${t("AUTOSTART_SCRIPT_PROMPT")}`,
3939+
new Map([
3940+
[t("AUTOSTART_SCRIPT_ALLOW"), "allow"],
3941+
[t("AUTOSTART_SCRIPT_DENY"), "deny"],
3942+
[t("AUTOSTART_SCRIPT_ASK_LATER"), "pending"],
3943+
]),
3944+
t("AUTOSTART_SCRIPT_ASK_LATER"),
3945+
);
3946+
const decision = await prompt.waitForClose;
3947+
if (decision === "allow" || decision === "deny") {
3948+
autostartScripts[scriptName] = decision;
3949+
await this.plugin.saveSettings();
3950+
if (decision === "allow") {
3951+
// A freshly granted "allow" should attach the script to every other
3952+
// currently-open view immediately, not only the next time each view
3953+
// is opened (that ongoing case is handled separately by
3954+
// ScriptEngine.runAutostartScripts() at view-mount time).
3955+
this.plugin.scriptEngine.attachAutostartScriptToOpenViews(
3956+
scriptName,
3957+
this.targetView,
3958+
);
3959+
}
3960+
return decision;
3961+
}
3962+
return "pending";
3963+
}
3964+
38983965
/**
38993966
* If set, this callback is triggered when the user closes an Excalidraw view.
39003967
*/

0 commit comments

Comments
 (0)