feat: add context menu and ribbon actions for quick access to common features

Co-authored-by: Copilot <copilot@github.com>
This commit is contained in:
Erik van der Boom 2026-05-04 00:14:18 +02:00
parent e4773fdf28
commit e4e0003cb8
7 changed files with 370 additions and 33 deletions

View file

@ -0,0 +1,159 @@
import * as fs from 'node:fs';
import * as os from 'node:os';
import * as path from 'node:path';
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import { clearLocateCache, locateTool, locateToolPath } from '../src/tool-locate';
const tempDirs: string[] = [];
function makeDir(): string {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'bindery-merge-locate-test-'));
tempDirs.push(dir);
return dir;
}
beforeEach(() => {
clearLocateCache();
});
afterEach(() => {
clearLocateCache();
for (const dir of tempDirs.splice(0, tempDirs.length)) {
fs.rmSync(dir, { recursive: true, force: true });
}
});
describe('locateTool', () => {
it('prefers explicit setting when the file exists', () => {
const dir = makeDir();
const fakePandoc = path.join(dir, process.platform === 'win32' ? 'pandoc.exe' : 'pandoc');
fs.writeFileSync(fakePandoc, '');
const result = locateTool('pandoc', fakePandoc);
expect(result.path).toBe(fakePandoc);
expect(result.source).toBe('setting');
});
it('returns the literal setting even when the file is missing (surfaces clear error to user)', () => {
const dir = makeDir();
const missing = path.join(dir, 'does-not-exist.exe');
const result = locateTool('pandoc', missing);
expect(result.path).toBe(missing);
expect(result.source).toBe('setting');
});
it('treats empty string and default command as "unset"', () => {
const r1 = locateTool('pandoc', '');
const r2 = locateTool('pandoc', 'pandoc');
expect(['path', 'default', 'fallback']).toContain(r1.source);
expect(['path', 'default', 'fallback']).toContain(r2.source);
});
it('falls back to the command name when nothing is found', () => {
const result = locateTool('libreoffice', undefined);
expect(typeof result.path).toBe('string');
expect(result.path.length).toBeGreaterThan(0);
});
it('locateToolPath returns the same string as locateTool.path', () => {
const setting = 'pandoc';
expect(locateToolPath('pandoc', setting)).toBe(locateTool('pandoc', setting).path);
});
it('caches repeated lookups', () => {
const dir = makeDir();
const fake = path.join(dir, process.platform === 'win32' ? 'pandoc.exe' : 'pandoc');
fs.writeFileSync(fake, '');
const r1 = locateTool('pandoc', fake);
fs.rmSync(fake);
const r2 = locateTool('pandoc', fake);
expect(r2.path).toBe(r1.path);
});
it('clearLocateCache forces fresh resolution on next call', () => {
const dir = makeDir();
const fake = path.join(dir, process.platform === 'win32' ? 'pandoc.exe' : 'pandoc');
fs.writeFileSync(fake, '');
locateTool('pandoc', fake);
clearLocateCache();
const result = locateTool('pandoc', fake);
expect(result.path).toBe(fake);
expect(result.source).toBe('setting');
});
it('resolves libreoffice with explicit setting', () => {
const dir = makeDir();
const fakePath = path.join(dir, process.platform === 'win32' ? 'soffice.exe' : 'soffice');
fs.writeFileSync(fakePath, '');
const result = locateTool('libreoffice', fakePath);
expect(result.path).toBe(fakePath);
expect(result.source).toBe('setting');
});
it('treats "libreoffice" as default setting (triggers auto-detect)', () => {
const result = locateTool('libreoffice', 'libreoffice');
expect(['path', 'default', 'fallback']).toContain(result.source);
});
it('treats "soffice" as default setting for libreoffice (triggers auto-detect)', () => {
const result = locateTool('libreoffice', 'soffice');
expect(['path', 'default', 'fallback']).toContain(result.source);
});
it('returns fallback for pandoc when no setting and nothing on PATH or well-known', () => {
const result = locateTool('pandoc', undefined);
expect(typeof result.path).toBe('string');
expect(result.path.length).toBeGreaterThan(0);
expect(['path', 'default', 'fallback']).toContain(result.source);
});
it('returns setting source when path exists but is NOT a known default', () => {
const dir = makeDir();
const customPath = path.join(dir, 'my-custom-pandoc');
fs.writeFileSync(customPath, '');
const result = locateTool('pandoc', customPath);
expect(result.path).toBe(customPath);
expect(result.source).toBe('setting');
});
it('returns setting source even for non-existent custom path (to surface clear error)', () => {
const result = locateTool('pandoc', '/some/custom/path/to/pandoc');
expect(result.path).toBe('/some/custom/path/to/pandoc');
expect(result.source).toBe('setting');
});
it('cache uses different entries for pandoc vs libreoffice', () => {
const dir = makeDir();
const fakePandoc = path.join(dir, 'custom-pandoc');
const fakeSoffice = path.join(dir, 'custom-soffice');
fs.writeFileSync(fakePandoc, '');
fs.writeFileSync(fakeSoffice, '');
const pandocResult = locateTool('pandoc', fakePandoc);
const libreResult = locateTool('libreoffice', fakeSoffice);
expect(pandocResult.path).toBe(fakePandoc);
expect(libreResult.path).toBe(fakeSoffice);
expect(pandocResult.path).not.toBe(libreResult.path);
});
it('re-resolves when setting changes from default to explicit', () => {
locateTool('pandoc', '');
clearLocateCache();
const dir = makeDir();
const explicit = path.join(dir, 'explicit-pandoc');
fs.writeFileSync(explicit, '');
const result = locateTool('pandoc', explicit);
expect(result.path).toBe(explicit);
expect(result.source).toBe('setting');
});
});

View file

@ -14,10 +14,10 @@ import type { OutputType } from '@bindery/merge';
import { mergeBook } from './merge';
import { formatFile } from './formatter';
import { exportBook, resolveBookRoot } from './exporter';
import { setupAiFiles, type AiTarget, ALL_SKILLS } from './ai-setup';
import { readSettings, writeSettings, readTranslations, addDialectRule, addTranslationEntry, addLanguage, findProbableUsWords } from './workspace';
import { setupAiFiles, ALL_SKILLS } from './ai-setup';
import { readSettings, addDialectRule, addTranslationEntry, addLanguage, findProbableUsWords } from './workspace';
import { BinderySettingsTab, DEFAULT_SETTINGS, type BinderySettings } from './settings-tab';
import { Plugin } from 'obsidian';
import { Notice, Plugin } from 'obsidian';
import type { Editor, TFile } from 'obsidian';
import * as fs from 'node:fs';
import * as path from 'node:path';
@ -28,6 +28,50 @@ const REVIEW_STOP_MARKER = '<!-- Bindery: Review stop -->';
export default class BinderyPlugin extends Plugin {
settings: BinderySettings = { ...DEFAULT_SETTINGS };
private notify(message: string): void {
// Prefer user-facing notices to console logs to keep the developer console clean.
new Notice(message);
}
private addContextMenuItems(menu: unknown, forEditor: boolean): void {
const m = menu as {
addSeparator?: () => void;
addItem: (cb: (item: {
setTitle: (title: string) => unknown;
setIcon?: (icon: string) => unknown;
onClick: (fn: () => void) => unknown;
}) => void) => void;
};
m.addSeparator?.();
m.addItem((item) => {
item.setTitle('Bindery: Merge chapters to all formats');
item.setIcon?.('book-open');
item.onClick(() => { void this.mergeBook(['md', 'docx', 'epub', 'pdf']); });
});
if (forEditor) {
m.addItem((item) => {
item.setTitle('Bindery: Format document');
item.setIcon?.('wand');
item.onClick(() => { void this.formatActive(); });
});
}
m.addItem((item) => {
item.setTitle('Bindery: Find probable US to UK words');
item.setIcon?.('languages');
item.onClick(() => { void this.findUsToUkCommand(); });
});
m.addItem((item) => {
item.setTitle('Bindery: Generate AI assistant files');
item.setIcon?.('sparkles');
item.onClick(() => { void this.setupAiCommand(); });
});
}
private getVaultBasePath(): string {
const basePath = this.app.vault.adapter?.basePath;
if (typeof basePath !== 'string' || !basePath.trim()) {
@ -151,6 +195,27 @@ export default class BinderyPlugin extends Plugin {
callback: () => void this.copyMcpSnippet(),
});
this.addRibbonIcon('book-open', 'Bindery: Merge chapters to all formats', () => {
void this.mergeBook(['md', 'docx', 'epub', 'pdf']);
});
this.addRibbonIcon('wand', 'Bindery: Format active note', () => {
void this.formatActive();
});
this.addRibbonIcon('languages', 'Bindery: Find probable US to UK words', () => {
void this.findUsToUkCommand();
});
// Right-click menus in editor and file explorer.
const workspaceOn = this.app.workspace?.on;
if (workspaceOn) {
this.registerEvent(workspaceOn.call(this.app.workspace, 'editor-menu', (menu: unknown) => {
this.addContextMenuItems(menu, true);
}));
this.registerEvent(workspaceOn.call(this.app.workspace, 'file-menu', (menu: unknown) => {
this.addContextMenuItems(menu, false);
}));
}
this.addSettingTab(new BinderySettingsTab(this.app, this));
}
@ -159,7 +224,6 @@ export default class BinderyPlugin extends Plugin {
const vaultPath = this.getVaultBasePath();
const bookRoot = resolveBookRoot(vaultPath, this.settings.bookRoot);
console.log('Bindery: Merging chapters…');
const result = await mergeBook(
this.app,
this.app.vault,
@ -170,25 +234,27 @@ export default class BinderyPlugin extends Plugin {
);
const names = result.outputs.map((p: string) => path.basename(p)).join(', ');
console.log(`✓ Merged → ${names}`);
this.notify(`Merged: ${names}`);
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
console.error(`✗ Merge failed: ${message}`);
this.notify(`Merge failed: ${message}`);
}
}
private async formatActive(): Promise<void> {
const file = this.app.workspace?.getActiveFile?.();
if (!file || file.extension !== 'md') {
console.log('No markdown file active');
if (file?.extension !== 'md') {
this.notify('No markdown file active');
return;
}
try {
await formatFile(this.app.vault, file);
console.log('✓ Formatted');
this.notify('Formatted active note');
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
console.error(`✗ Format failed: ${message}`);
this.notify(`Format failed: ${message}`);
}
}
@ -199,10 +265,11 @@ export default class BinderyPlugin extends Plugin {
const result = await setupAiFiles(this.app, bookRoot, ['claude', 'copilot'], ALL_SKILLS, false);
const msg = `Generated: ${result.created.length}, Skipped: ${result.skipped.length}`;
console.log(`✓ AI setup: ${msg}`);
this.notify(`AI setup complete. ${msg}`);
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
console.error(`✗ AI setup failed: ${message}`);
this.notify(`AI setup failed: ${message}`);
}
}
@ -221,10 +288,11 @@ export default class BinderyPlugin extends Plugin {
if (!to) return;
addDialectRule(bookRoot, language, from, to);
console.log(`✓ Added dialect rule: ${from} ${to}`);
this.notify(`Added dialect rule: ${from} -> ${to}`);
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
console.error(`✗ Failed: ${message}`);
this.notify(`Action failed: ${message}`);
}
}
@ -245,10 +313,11 @@ export default class BinderyPlugin extends Plugin {
}
addTranslationEntry(bookRoot, term, translations);
console.log(`Added translation entry: ${term}`);
this.notify(`Added translation entry: ${term}`);
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
console.error(`✗ Failed: ${message}`);
this.notify(`Action failed: ${message}`);
}
}
@ -264,10 +333,11 @@ export default class BinderyPlugin extends Plugin {
if (!folderName) return;
addLanguage(bookRoot, code, folderName);
console.log(`Added language: ${code}`);
this.notify(`Added language: ${code}`);
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
console.error(`✗ Failed: ${message}`);
this.notify(`Action failed: ${message}`);
}
}
@ -278,14 +348,15 @@ export default class BinderyPlugin extends Plugin {
const translationsPath = path.join(bookRoot, '.bindery', 'translations.json');
if (!fs.existsSync(translationsPath)) {
console.log('translations.json not found');
this.notify('translations.json not found');
return;
}
console.log('Translations file:', translationsPath);
this.notify(`Translations file: ${translationsPath}`);
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
console.error(`✗ Failed: ${message}`);
this.notify(`Action failed: ${message}`);
}
}
@ -293,7 +364,7 @@ export default class BinderyPlugin extends Plugin {
try {
const file = this.app.workspace?.getActiveFile?.();
if (!file) {
console.log('No file active');
this.notify('No file active');
return;
}
@ -301,21 +372,22 @@ export default class BinderyPlugin extends Plugin {
const words = findProbableUsWords(content);
if (words.length === 0) {
console.log('No probable US words found');
this.notify('No probable US words found');
return;
}
const list = words.slice(0, 10).join(', ') + (words.length > 10 ? `, +${words.length - 10} more` : '');
console.log(`Found: ${list}`);
this.notify(`Found: ${list}`);
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
console.error(`✗ Failed: ${message}`);
this.notify(`Action failed: ${message}`);
}
}
private async promptString(prompt: string, defaultValue: string = ''): Promise<string | null> {
return new Promise((resolve) => {
const result = window.prompt(prompt, defaultValue);
const result = globalThis.prompt(prompt, defaultValue);
resolve(result);
});
}
@ -395,8 +467,7 @@ export default class BinderyPlugin extends Plugin {
await clipboard.writeText(snippet);
return;
}
// Fallback when clipboard API is unavailable in runtime/tests.
console.log(snippet);
this.notify('Clipboard unavailable; unable to copy MCP snippet.');
}
showMcpSnippet(): string {

View file

@ -35,9 +35,14 @@ declare module 'obsidian' {
vault: Vault;
workspace?: {
getActiveFile(): TFile | null;
on(event: string, callback: (...args: unknown[]) => unknown): EventRef;
};
}
export class Notice {
constructor(message: string, timeout?: number);
}
export interface Command {
id: string;
name: string;
@ -50,6 +55,7 @@ declare module 'obsidian' {
constructor(app: App);
loadData(): Promise<unknown>;
saveData(data: unknown): Promise<void>;
addRibbonIcon(icon: string, title: string, callback: () => void): HTMLElement;
addCommand(command: Command): void;
addSettingTab(tab: unknown): void;
registerEvent(eventRef: EventRef): void;

View file

@ -10,6 +10,16 @@ import * as fs from 'node:fs';
// Must be declared before any imports that pull in obsidian transitively.
vi.mock('obsidian', () => {
class Notice {
message: string;
timeout?: number;
constructor(message: string, timeout?: number) {
this.message = message;
this.timeout = timeout;
}
}
class Plugin {
app: App;
@ -32,16 +42,20 @@ vi.mock('obsidian', () => {
editorCallback?: (editor: unknown) => void;
}): void { return; }
addRibbonIcon(_icon: string, _title: string, _callback: () => void): HTMLElement {
return {} as HTMLElement;
}
addSettingTab(_tab: unknown): void { return; }
registerEvent(_eventRef: unknown): void { return; }
}
return { Plugin };
return { Plugin, Notice };
});
import BinderyPlugin from '../src/main';
import type { App, Vault, Command, Editor } from 'obsidian';
import type { App, Vault, Editor } from 'obsidian';
// ─── Mock helpers ─────────────────────────────────────────────────────────────
@ -53,7 +67,13 @@ function makeApp(vaultPath: string, vaultName = 'TestVault'): App {
on: vi.fn().mockReturnValue({}),
adapter: { basePath: vaultPath },
} as unknown as Vault;
return { vault };
return {
vault,
workspace: {
getActiveFile: () => null,
on: vi.fn().mockReturnValue({}),
},
} as unknown as App;
}
function makeEditor(lineText: string, cursorCh: number, selection = ''): Editor {
@ -238,13 +258,14 @@ describe('review marker commands', () => {
const app = makeApp('/vault', 'MyVault');
app.workspace = {
getActiveFile: () => ({ path: 'Story/ch1.md', extension: 'md', name: 'ch1.md', basename: 'ch1' }),
on: vi.fn().mockReturnValue({}),
};
const bp = new BinderyPlugin(app);
const addCommandSpy = vi.spyOn(bp, 'addCommand');
await bp.onload();
const commands = addCommandSpy.mock.calls.map((call) => call[0] as Command);
const commands = addCommandSpy.mock.calls.map((call) => call[0]);
const ids = commands.map((c) => c.id);
expect(ids).toContain('start-review-marker');
expect(ids).toContain('stop-review-marker');
@ -254,12 +275,13 @@ describe('review marker commands', () => {
const app = makeApp('/vault', 'MyVault');
app.workspace = {
getActiveFile: () => ({ path: 'Story/ch1.md', extension: 'md', name: 'ch1.md', basename: 'ch1' }),
on: vi.fn().mockReturnValue({}),
};
const bp = new BinderyPlugin(app);
const addCommandSpy = vi.spyOn(bp, 'addCommand');
await bp.onload();
const commands = addCommandSpy.mock.calls.map((call) => call[0] as Command);
const commands = addCommandSpy.mock.calls.map((call) => call[0]);
const start = commands.find((c) => c.id === 'start-review-marker');
const editor = makeEditor('abc', 0, '');
@ -275,12 +297,13 @@ describe('review marker commands', () => {
const app = makeApp('/vault', 'MyVault');
app.workspace = {
getActiveFile: () => ({ path: 'Story/ch1.md', extension: 'md', name: 'ch1.md', basename: 'ch1' }),
on: vi.fn().mockReturnValue({}),
};
const bp = new BinderyPlugin(app);
const addCommandSpy = vi.spyOn(bp, 'addCommand');
await bp.onload();
const commands = addCommandSpy.mock.calls.map((call) => call[0] as Command);
const commands = addCommandSpy.mock.calls.map((call) => call[0]);
const start = commands.find((c) => c.id === 'start-review-marker');
const editor = makeEditor('abc', 0, 'X');
@ -295,12 +318,13 @@ describe('review marker commands', () => {
const app = makeApp('/vault', 'MyVault');
app.workspace = {
getActiveFile: () => ({ path: 'Story/ch1.md', extension: 'md', name: 'ch1.md', basename: 'ch1' }),
on: vi.fn().mockReturnValue({}),
};
const bp = new BinderyPlugin(app);
const addCommandSpy = vi.spyOn(bp, 'addCommand');
await bp.onload();
const commands = addCommandSpy.mock.calls.map((call) => call[0] as Command);
const commands = addCommandSpy.mock.calls.map((call) => call[0]);
const stop = commands.find((c) => c.id === 'stop-review-marker');
const editor = makeEditor('abc', 0, '');
@ -316,12 +340,13 @@ describe('review marker commands', () => {
const app = makeApp('/vault', 'MyVault');
app.workspace = {
getActiveFile: () => ({ path: 'Story/ch1.txt', extension: 'txt', name: 'ch1.txt', basename: 'ch1' }),
on: vi.fn().mockReturnValue({}),
};
const bp = new BinderyPlugin(app);
const addCommandSpy = vi.spyOn(bp, 'addCommand');
await bp.onload();
const commands = addCommandSpy.mock.calls.map((call) => call[0] as Command);
const commands = addCommandSpy.mock.calls.map((call) => call[0]);
const start = commands.find((c) => c.id === 'start-review-marker');
const editor = makeEditor('abc', 0, '');
@ -335,12 +360,13 @@ describe('review marker commands', () => {
const app = makeApp('/vault', 'MyVault');
app.workspace = {
getActiveFile: () => ({ path: 'Story/ch1.txt', extension: 'txt', name: 'ch1.txt', basename: 'ch1' }),
on: vi.fn().mockReturnValue({}),
};
const bp = new BinderyPlugin(app);
const addCommandSpy = vi.spyOn(bp, 'addCommand');
await bp.onload();
const commands = addCommandSpy.mock.calls.map((call) => call[0] as Command);
const commands = addCommandSpy.mock.calls.map((call) => call[0]);
const stop = commands.find((c) => c.id === 'stop-review-marker');
const editor = makeEditor('abc', 0, '');
@ -353,12 +379,13 @@ describe('review marker commands', () => {
const app = makeApp('/vault', 'MyVault');
app.workspace = {
getActiveFile: () => ({ path: 'Story/ch1.md', extension: 'md', name: 'ch1.md', basename: 'ch1' }),
on: vi.fn().mockReturnValue({}),
};
const bp = new BinderyPlugin(app);
const addCommandSpy = vi.spyOn(bp, 'addCommand');
await bp.onload();
const commands = addCommandSpy.mock.calls.map((call) => call[0] as Command);
const commands = addCommandSpy.mock.calls.map((call) => call[0]);
const stop = commands.find((c) => c.id === 'stop-review-marker');
// Cursor at end of 'abc' (ch=3 === lineText.length=3)
const editor = makeEditor('abc', 3, '');
@ -375,12 +402,13 @@ describe('review marker commands', () => {
const app = makeApp('/vault', 'MyVault');
app.workspace = {
getActiveFile: () => ({ path: 'Story/ch1.md', extension: 'md', name: 'ch1.md', basename: 'ch1' }),
on: vi.fn().mockReturnValue({}),
};
const bp = new BinderyPlugin(app);
const addCommandSpy = vi.spyOn(bp, 'addCommand');
await bp.onload();
const commands = addCommandSpy.mock.calls.map((call) => call[0] as Command);
const commands = addCommandSpy.mock.calls.map((call) => call[0]);
const start = commands.find((c) => c.id === 'start-review-marker');
// Cursor at end of 'abc' (ch=3 === lineText.length=3)
const editor = makeEditor('abc', 3, '');
@ -393,3 +421,32 @@ describe('review marker commands', () => {
);
});
});
describe('ribbon actions', () => {
it('registers ribbon shortcuts for merge, format, and word scan', async () => {
const app = makeApp('/vault', 'MyVault');
const bp = new BinderyPlugin(app);
const ribbonSpy = vi.spyOn(bp, 'addRibbonIcon');
await bp.onload();
const titles = ribbonSpy.mock.calls.map((call) => call[1]);
expect(titles).toContain('Bindery: Merge chapters to all formats');
expect(titles).toContain('Bindery: Format active note');
expect(titles).toContain('Bindery: Find probable US to UK words');
});
});
describe('context menu actions', () => {
it('registers editor-menu and file-menu hooks', async () => {
const app = makeApp('/vault', 'MyVault');
const bp = new BinderyPlugin(app);
await bp.onload();
const workspaceOn = app.workspace?.on as ReturnType<typeof vi.fn>;
const events = workspaceOn.mock.calls.map((c: unknown[]) => c[0]);
expect(events).toContain('editor-menu');
expect(events).toContain('file-menu');
});
});

View file

@ -0,0 +1,3 @@
{
"0.1.0": "1.4.0"
}

View file

@ -157,6 +157,11 @@
"title": "Setup AI Assistant Files",
"category": "Bindery"
},
{
"command": "bindery.quickActions",
"title": "Quick Actions",
"category": "Bindery"
},
{
"command": "bindery.formatDocument",
"title": "Format Typography",
@ -1001,6 +1006,11 @@
}
],
"editor/title": [
{
"command": "bindery.quickActions",
"when": "resourceLangId == markdown",
"group": "navigation@95"
},
{
"submenu": "bindery.exportMenu",
"when": "resourceLangId == markdown",

View file

@ -865,6 +865,36 @@ async function findProbableUsToUkWordsCommand() {
}
}
// ─── Command: Quick actions (status-bar ribbon entry point) ─────────────────
async function quickActionsCommand() {
const picked = await vscode.window.showQuickPick(
[
{ label: 'Merge chapters → All formats', command: 'bindery.mergeAll' },
{ label: 'Merge chapters → Markdown', command: 'bindery.mergeMarkdown' },
{ label: 'Merge chapters → DOCX', command: 'bindery.mergeDocx' },
{ label: 'Merge chapters → EPUB', command: 'bindery.mergeEpub' },
{ label: 'Merge chapters → PDF', command: 'bindery.mergePdf' },
{ label: 'Format document', command: 'bindery.formatDocument' },
{ label: 'Format folder', command: 'bindery.formatFolder' },
{ label: 'Setup AI assistant files', command: 'bindery.setupAI' },
{ label: 'Find probable US to UK words', command: 'bindery.findProbableUsToUkWords' },
{ label: 'Add dialect rule', command: 'bindery.addDialect' },
{ label: 'Add translation (glossary)', command: 'bindery.addTranslation' },
{ label: 'Add language', command: 'bindery.addLanguage' },
{ label: 'Open translations.json', command: 'bindery.openTranslations' },
{ label: 'Initialize workspace', command: 'bindery.init' },
{ label: 'Register MCP server', command: 'bindery.registerMcp' },
],
{
title: 'Bindery quick actions',
placeHolder: 'Choose an action',
}
);
if (!picked) { return; }
await vscode.commands.executeCommand(picked.command);
}
// ─── Merge helpers ────────────────────────────────────────────────────────────
function buildMergeOptions(
@ -1298,6 +1328,7 @@ export function activate(context: vscode.ExtensionContext) {
subscriptions.push(
vscode.commands.registerCommand('bindery.init', () => initWorkspaceCommand(context)),
vscode.commands.registerCommand('bindery.setupAI', () => setupAiCommand(context)),
vscode.commands.registerCommand('bindery.quickActions', quickActionsCommand),
vscode.commands.registerCommand('bindery.formatDocument', formatDocumentCommand),
vscode.commands.registerCommand('bindery.formatFolder', formatFolderCommand),
vscode.commands.registerCommand('bindery.mergeMarkdown', () => mergeCommand(['md'])),
@ -1345,8 +1376,8 @@ export function activate(context: vscode.ExtensionContext) {
// Status bar — shown when a markdown file is active
const statusBar = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Right, 100);
statusBar.text = '$(book) Bindery';
statusBar.tooltip = 'Bindery: Merge Chapters → All Formats';
statusBar.command = 'bindery.mergeAll';
statusBar.tooltip = 'Bindery: Quick actions';
statusBar.command = 'bindery.quickActions';
subscriptions.push(statusBar);
const updateStatusBar = () => {