Configurable action buttons with preset templates
Convert the hardcoded Claude / Visual Studio / Redeploy buttons into a configurable action-button system with an editor UI. - New config field actions[] with kinds: terminal, detached, open (open uses Windows shell association — used by the .sln preset) - requiresFile supports * globs (e.g. *.sln matches solution files) - Migration seeds Claude + Visual Studio + Redeploy on first run, with a one-time vsActionBackfilled pass to add VS to users who already migrated under the previous two-seed flow - ActionEditor modal: list with reorder/edit/delete, add form with preset dropdown (Claude, Visual Studio, VS Code, Cursor, Run 1ReDeploy.bat, Commit & Push) and a curated icon dropdown plus free-text override - runAction IPC dispatches by kind; isCommandAvailable generalizes the per-CLI PATH probe used to disable buttons whose CLI is missing - Built-in VS button removed from BookmarkRow + MinimizedList; Claude visibility toggle removed from settings (now an editable action) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Binary file not shown.
+121
-8
@@ -60,6 +60,46 @@ function getBookmarks() {
|
||||
return Array.isArray(cfg.bookmarks) ? cfg.bookmarks.filter((b) => b && b.id && b.alias && b.path) : [];
|
||||
}
|
||||
|
||||
function getActions() {
|
||||
const cfg = readConfig();
|
||||
if (!Array.isArray(cfg.actions)) return [];
|
||||
return cfg.actions.filter((a) => a && a.id && a.kind && a.label);
|
||||
}
|
||||
|
||||
// Default action seeds. Mirror the previously-hardcoded buttons (Claude,
|
||||
// Visual Studio, Redeploy) so new installs see the same defaults that
|
||||
// existing users had as built-ins.
|
||||
function defaultActionSeeds() {
|
||||
return [
|
||||
{
|
||||
id: crypto.randomUUID(),
|
||||
label: 'Claude',
|
||||
icon: 'C',
|
||||
kind: 'terminal',
|
||||
command: 'claude',
|
||||
requiresCommand: 'claude',
|
||||
hideOnNetwork: true,
|
||||
},
|
||||
{
|
||||
id: crypto.randomUUID(),
|
||||
label: 'Visual Studio',
|
||||
icon: 'VS',
|
||||
kind: 'open',
|
||||
requiresFile: '*.sln',
|
||||
hideOnNetwork: true,
|
||||
},
|
||||
{
|
||||
id: crypto.randomUUID(),
|
||||
label: 'Run 1ReDeploy.bat',
|
||||
icon: '▶',
|
||||
kind: 'detached',
|
||||
command: '',
|
||||
requiresFile: '1ReDeploy.bat',
|
||||
keepOpen: true,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
function getActiveTabId() {
|
||||
const cfg = readConfig();
|
||||
const tabs = getTabs();
|
||||
@@ -69,15 +109,13 @@ function getActiveTabId() {
|
||||
return tabs[0].id;
|
||||
}
|
||||
|
||||
// Per-button visibility for the row action bar. Defaults to true so existing
|
||||
// installs see no behavior change.
|
||||
// Per-button visibility for the built-in action buttons. Claude and Redeploy
|
||||
// have moved into the configurable `actions` list — Terminal stays built-in.
|
||||
function getButtonVisibility() {
|
||||
const cfg = readConfig();
|
||||
const v = cfg.buttonVisibility || {};
|
||||
return {
|
||||
claude: v.claude !== false,
|
||||
terminal: v.terminal !== false,
|
||||
redeploy: v.redeploy !== false,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -166,7 +204,45 @@ function migrateConfigIfNeeded() {
|
||||
}
|
||||
}
|
||||
|
||||
if (mutated) writeConfig({ columns, bookmarks, tabs, activeTabId });
|
||||
// Seed default action buttons if the user has none configured. Honor the
|
||||
// legacy buttonVisibility flags: if claude/redeploy were toggled off, skip
|
||||
// seeding that action so we don't resurrect a button the user dismissed.
|
||||
const seedPatch = {};
|
||||
if (!Array.isArray(cfg.actions)) {
|
||||
const visibility = cfg.buttonVisibility || {};
|
||||
const seeds = defaultActionSeeds().filter((a) => {
|
||||
if (a.requiresCommand === 'claude') return visibility.claude !== false;
|
||||
if (a.requiresFile === '1ReDeploy.bat') return visibility.redeploy !== false;
|
||||
return true;
|
||||
});
|
||||
seedPatch.actions = seeds;
|
||||
mutated = true;
|
||||
} else if (!cfg.vsActionBackfilled) {
|
||||
// Older users got Claude + Redeploy seeded before VS became a configurable
|
||||
// action. Backfill VS once so they don't silently lose the built-in button.
|
||||
const hasVs = cfg.actions.some((a) => a && /\*\.sln$/i.test(a.requiresFile || ''));
|
||||
if (!hasVs) {
|
||||
const vs = defaultActionSeeds().find((a) => /\*\.sln$/i.test(a.requiresFile || ''));
|
||||
if (vs) seedPatch.actions = [...cfg.actions, vs];
|
||||
}
|
||||
seedPatch.vsActionBackfilled = true;
|
||||
mutated = true;
|
||||
}
|
||||
// The claude/redeploy visibility keys are now expressed as the
|
||||
// presence/absence of the matching action. Strip them so the settings
|
||||
// panel doesn't render stale toggles.
|
||||
if (cfg.buttonVisibility && (
|
||||
Object.prototype.hasOwnProperty.call(cfg.buttonVisibility, 'claude') ||
|
||||
Object.prototype.hasOwnProperty.call(cfg.buttonVisibility, 'redeploy')
|
||||
)) {
|
||||
const v = { ...cfg.buttonVisibility };
|
||||
delete v.claude;
|
||||
delete v.redeploy;
|
||||
seedPatch.buttonVisibility = v;
|
||||
mutated = true;
|
||||
}
|
||||
|
||||
if (mutated) writeConfig({ columns, bookmarks, tabs, activeTabId, ...seedPatch });
|
||||
}
|
||||
|
||||
function getCurrentHeight() {
|
||||
@@ -301,6 +377,7 @@ function broadcastConfig() {
|
||||
tabs: getTabs(),
|
||||
columns: getColumns(),
|
||||
bookmarks: getBookmarks(),
|
||||
actions: getActions(),
|
||||
activeTabId: getActiveTabId(),
|
||||
buttonVisibility: getButtonVisibility(),
|
||||
autoStart: getAutoStart(),
|
||||
@@ -487,6 +564,7 @@ ipcMain.handle('get-config', () => {
|
||||
tabs: getTabs(),
|
||||
columns: getColumns(),
|
||||
bookmarks: getBookmarks(),
|
||||
actions: getActions(),
|
||||
activeTabId: getActiveTabId(),
|
||||
buttonVisibility: getButtonVisibility(),
|
||||
autoStart: getAutoStart(),
|
||||
@@ -506,15 +584,50 @@ ipcMain.handle('set-button-visibility', (_event, payload) => {
|
||||
if (!payload || typeof payload !== 'object') return getButtonVisibility();
|
||||
const current = getButtonVisibility();
|
||||
const next = {
|
||||
claude: typeof payload.claude === 'boolean' ? payload.claude : current.claude,
|
||||
terminal: typeof payload.terminal === 'boolean' ? payload.terminal : current.terminal,
|
||||
redeploy: typeof payload.redeploy === 'boolean' ? payload.redeploy : current.redeploy,
|
||||
};
|
||||
writeConfig({ buttonVisibility: next });
|
||||
broadcastConfig();
|
||||
return getButtonVisibility();
|
||||
});
|
||||
|
||||
ipcMain.handle('set-actions', (_event, list) => {
|
||||
if (!Array.isArray(list)) return getActions();
|
||||
const cleaned = list
|
||||
.filter((a) => a && typeof a === 'object' && a.id && a.label && (a.kind === 'terminal' || a.kind === 'detached' || a.kind === 'open'))
|
||||
.map((a) => {
|
||||
const out = {
|
||||
id: String(a.id),
|
||||
label: String(a.label).slice(0, 40),
|
||||
icon: typeof a.icon === 'string' ? a.icon.slice(0, 4) : '?',
|
||||
kind: a.kind,
|
||||
};
|
||||
if (typeof a.command === 'string' && a.command) out.command = a.command;
|
||||
if (Array.isArray(a.args) && a.args.length > 0) out.args = a.args.map(String);
|
||||
if (typeof a.requiresFile === 'string' && a.requiresFile) out.requiresFile = a.requiresFile;
|
||||
if (typeof a.requiresCommand === 'string' && a.requiresCommand) out.requiresCommand = a.requiresCommand;
|
||||
if (a.hideOnNetwork === true) out.hideOnNetwork = true;
|
||||
if (a.keepOpen === true) out.keepOpen = true;
|
||||
return out;
|
||||
});
|
||||
writeConfig({ actions: cleaned });
|
||||
broadcastConfig();
|
||||
return getActions();
|
||||
});
|
||||
|
||||
ipcMain.handle('run-action', (_event, payload) => {
|
||||
if (!payload || typeof payload !== 'object') return { ok: false, error: 'Invalid payload.' };
|
||||
const action = (getActions()).find((a) => a.id === payload.actionId);
|
||||
if (!action) return { ok: false, error: 'Action not found.' };
|
||||
return launchers.runAction(action, {
|
||||
targetPath: payload.targetPath,
|
||||
alias: payload.alias,
|
||||
color: payload.color,
|
||||
});
|
||||
});
|
||||
|
||||
ipcMain.handle('is-command-available', (_event, name) => launchers.isCommandAvailable(name));
|
||||
|
||||
ipcMain.handle('set-config', (_event, patch) => {
|
||||
return writeConfig(patch || {});
|
||||
});
|
||||
@@ -699,7 +812,7 @@ ipcMain.handle('set-active-tab', (_event, id) => {
|
||||
});
|
||||
|
||||
ipcMain.handle('pick-folder', () => launchers.pickFolder());
|
||||
ipcMain.handle('inspect-folder', (_event, p) => launchers.inspectFolder(p));
|
||||
ipcMain.handle('inspect-folder', (_event, p, actionFiles) => launchers.inspectFolder(p, actionFiles));
|
||||
|
||||
ipcMain.handle('open-in-explorer', (_event, p) => launchers.openInExplorer(p));
|
||||
ipcMain.handle('open-terminal', (_event, payload) => {
|
||||
|
||||
+4
-1
@@ -27,7 +27,7 @@ contextBridge.exposeInMainWorld('bookmarks', {
|
||||
|
||||
// Folder picking + inspection
|
||||
pickFolder: () => ipcRenderer.invoke('pick-folder'),
|
||||
inspectFolder: (path) => ipcRenderer.invoke('inspect-folder', path),
|
||||
inspectFolder: (path, actionFiles) => ipcRenderer.invoke('inspect-folder', path, actionFiles),
|
||||
|
||||
// Launchers
|
||||
openInExplorer: (path) => ipcRenderer.invoke('open-in-explorer', path),
|
||||
@@ -37,6 +37,8 @@ contextBridge.exposeInMainWorld('bookmarks', {
|
||||
runRedeploy: (path) => ipcRenderer.invoke('run-redeploy', path),
|
||||
copyPath: (text) => ipcRenderer.invoke('copy-path', text),
|
||||
isClaudeAvailable: () => ipcRenderer.invoke('is-claude-available'),
|
||||
isCommandAvailable: (name) => ipcRenderer.invoke('is-command-available', name),
|
||||
runAction: (payload) => ipcRenderer.invoke('run-action', payload),
|
||||
|
||||
// Window
|
||||
setPinned: (value) => ipcRenderer.invoke('set-pinned', value),
|
||||
@@ -47,6 +49,7 @@ contextBridge.exposeInMainWorld('bookmarks', {
|
||||
// Settings
|
||||
setButtonVisibility: (payload) => ipcRenderer.invoke('set-button-visibility', payload),
|
||||
setAutoStart: (value) => ipcRenderer.invoke('set-auto-start', value),
|
||||
setActions: (list) => ipcRenderer.invoke('set-actions', list),
|
||||
|
||||
// Events from main → renderer
|
||||
onConfigUpdated: (cb) => {
|
||||
|
||||
Reference in New Issue
Block a user