From 7be9b3f3218cd247d3c7928441298b885b9637a0 Mon Sep 17 00:00:00 2001 From: Andrew Beal Date: Fri, 24 Apr 2026 14:15:56 +0100 Subject: [PATCH] feat: add AI quick actions button to view header Add sparkles button to view-actions bar that provides access to beautify and apply template quick actions. Refactor QuickActionsService to use WorkSpaceService instead of AbortService for workspace interactions and clear chat name on conversation reset. --- Components/ChatWindow.svelte | 1 + Services/QuickActionsService.ts | 58 ++++++++++++++++++++++++++------- Services/WorkSpaceService.ts | 12 ++++++- 3 files changed, 58 insertions(+), 13 deletions(-) diff --git a/Components/ChatWindow.svelte b/Components/ChatWindow.svelte index 84aac5e..804ae3e 100644 --- a/Components/ChatWindow.svelte +++ b/Components/ChatWindow.svelte @@ -181,6 +181,7 @@ currentStreamingMessageId = null; currentThought = null; + chatService.onNameChanged?.(""); conversationStore.clearResetFlag(); } diff --git a/Services/QuickActionsService.ts b/Services/QuickActionsService.ts index e9d189c..963338e 100644 --- a/Services/QuickActionsService.ts +++ b/Services/QuickActionsService.ts @@ -1,29 +1,30 @@ import { Resolve } from "./DependencyService"; import { Services } from "./Services"; -import { AbortService } from "./AbortService"; import type { QuickAgent } from "./AIServices/QuickAgent"; import type VaultkeeperAIPlugin from "main"; -import { FuzzySuggestModal, Notice, TFile, type Editor, type MarkdownFileInfo, type MarkdownView, type Menu } from "obsidian"; +import { FuzzySuggestModal, MarkdownView, Menu, Notice, setIcon, TFile, type Editor, type MarkdownFileInfo } from "obsidian"; import { FileSystemService } from "./FileSystemService"; import { BeautifyPrompt } from "AIPrompts/QuickActionPrompts/Beautify"; import Spinner from "Components/Spinner.svelte"; import { mount } from "svelte"; import { ApplyTemplatePrompt } from "AIPrompts/QuickActionPrompts/ApplyTemplatePrompt"; import { Copy } from "Enums/Copy"; +import type { WorkSpaceService } from "./WorkSpaceService"; export class QuickActionsService { private readonly actionTimeout: number = 30000; private plugin: VaultkeeperAIPlugin; - private abortService: AbortService; + private workSpaceService: WorkSpaceService private fileSystemService: FileSystemService; public constructor() { this.plugin = Resolve(Services.VaultkeeperAIPlugin); - this.abortService = Resolve(Services.AbortService); + this.workSpaceService = Resolve(Services.WorkSpaceService); this.fileSystemService = Resolve(Services.FileSystemService); + this.registerViewActions(); this.registerEditorMenuActions(); } @@ -117,6 +118,41 @@ export class QuickActionsService { ); } + private registerViewActions() { + this.plugin.registerEvent( + this.plugin.app.workspace.on("layout-change", () => { + const actionsView = this.workSpaceService.getViewActionsView(); + if (actionsView) { + if (actionsView.querySelector(".vault-keeper-ai-actions")) { + return; + } + const button = createEl("button", { cls: "clickable-icon view-action vault-keeper-ai-actions" }); + button.setAttribute('aria-label', 'AI Quick Actions'); + button.addEventListener('click', (evt) => { + const view = this.workSpaceService.getActiveViewOfType(MarkdownView); + if (view) { + const { editor } = view; + const menu = new Menu(); + menu.addItem((item) => + item.setTitle("Beautify") + .setIcon("palette") + .onClick(async () => this.beautify(menu, editor, view)) + ); + menu.addItem((item) => + item.setTitle("Apply template") + .setIcon("notepad-text-dashed") + .onClick(async () => this.applyTemplate(menu, editor, view)) + ); + menu.showAtMouseEvent(evt); + } + }); + setIcon(button, "sparkles"); + actionsView.prepend(button); + } + }) + ); + } + /* Helpers */ private userSelectFile(plugin: VaultkeeperAIPlugin, onSelected: (file: TFile) => Promise): void { @@ -130,16 +166,14 @@ export class QuickActionsService { } private async newAction(action: string, context: string): Promise { - return this.abortService.abortableOperation(async () => { - const agent = Resolve(Services.QuickAgent); - agent.resolveAIProvider(); + const agent = Resolve(Services.QuickAgent); + agent.resolveAIProvider(); - const timeoutPromise = new Promise((_, reject) => - activeWindow.setTimeout(() => reject(new DOMException(`Action timed out after ${this.actionTimeout}s`, "AbortError")), this.actionTimeout) - ); + const timeoutPromise = new Promise((_, reject) => + activeWindow.setTimeout(() => reject(new DOMException(`Action timed out after ${this.actionTimeout}s`, "AbortError")), this.actionTimeout) + ); - return Promise.race([agent.quickAction(action, context), timeoutPromise]); - }); + return Promise.race([agent.quickAction(action, context), timeoutPromise]); } private showNotice(message: string): Notice { diff --git a/Services/WorkSpaceService.ts b/Services/WorkSpaceService.ts index b16243f..2ccca7b 100644 --- a/Services/WorkSpaceService.ts +++ b/Services/WorkSpaceService.ts @@ -1,7 +1,7 @@ import type VaultkeeperAIPlugin from "main"; import { Resolve } from "./DependencyService"; import { Services } from "./Services"; -import { Notice, TFile, type WorkspaceLeaf } from "obsidian"; +import { Notice, TFile, type View, type WorkspaceLeaf } from "obsidian"; import type { VaultService } from "./VaultService"; export class WorkSpaceService { @@ -39,4 +39,14 @@ export class WorkSpaceService { return activeFile; } + + public getActiveViewOfType(type: new (...args: any[]) => ViewType): ViewType | null { + return this.plugin.app.workspace.getActiveViewOfType(type); + } + + + public getViewActionsView(): Element | null | undefined { + const leaf = this.plugin.app.workspace.getMostRecentLeaf(); + return leaf?.view?.containerEl?.querySelector('.view-actions'); + } } \ No newline at end of file