mirror of
https://github.com/andy-stack/vaultkeeper-ai.git
synced 2026-07-22 06:42:03 +00:00
feat: add plan approval UI with user review workflow
Add PlanApprovalService and PlanApprovalView to enable user review and approval of AI-generated execution plans before execution begins. Users can approve, reject, or suggest changes to plans through a new input mode and dedicated view.
This commit is contained in:
parent
4f57798295
commit
fa92e15b8c
22 changed files with 671 additions and 33 deletions
|
|
@ -3,7 +3,7 @@ import type { IAIToolDefinition } from "../IAIToolDefinition";
|
|||
|
||||
export const SubmitPlan: IAIToolDefinition = {
|
||||
name: AITool.SubmitPlan,
|
||||
description: `Submits an execution plan with ordered, actionable steps.
|
||||
description: `Submits an execution plan with ordered, actionable steps for the user to review.
|
||||
|
||||
Call this function:
|
||||
- After analyzing the goal and vault context to provide a structured plan
|
||||
|
|
@ -26,11 +26,11 @@ Do NOT use this function:
|
|||
properties: {
|
||||
description: {
|
||||
type: "string",
|
||||
description: "Brief summary of what this step accomplishes (e.g., 'Search for ML notes', 'Create index file'). This is user-facing and should be very concise."
|
||||
description: "Brief title of what this step accomplishes (e.g., 'Search for ML notes', 'Create index file'). This is just a heading and should be very concise."
|
||||
},
|
||||
instruction: {
|
||||
type: "string",
|
||||
description: "Detailed instructions for executing this step. Should be specific enough to guide the execution without ambiguity. Examples: 'Search vault for all notes with tag #machine-learning using search_vault_files', 'Create new file ML-Index.md in /Research folder with heading structure', 'Update frontmatter in daily note 2024-01-15 to add tag #reviewed'"
|
||||
description: "Detailed instructions for executing this step. Should be specific enough to guide the execution without ambiguity. This is user facing and should be formatted using markdown. Examples: 'Search vault for all notes tagged **#machine-learning**', 'Create new file `ML-Index.md` in `/Research` folder with the following heading structure:\\n- Overview\\n- Key Papers\\n- Open Questions', 'Update frontmatter in daily note `2024-01-15` to add tag **#reviewed**'"
|
||||
},
|
||||
context: {
|
||||
type: "string",
|
||||
|
|
|
|||
|
|
@ -12,9 +12,11 @@
|
|||
import UserInstruction from "./UserInstruction.svelte";
|
||||
import ChatModeSelector from "./ChatModeSelector.svelte";
|
||||
import DiffControls from "./DiffControls.svelte";
|
||||
import PlanApprovalControls from "./PlanApprovalControls.svelte";
|
||||
import type { EventService } from "Services/EventService";
|
||||
import { Event } from "Enums/Event";
|
||||
import type { DiffService } from "Services/DiffService";
|
||||
import type { PlanApprovalService } from "Services/PlanApprovalService";
|
||||
import type { Attachment } from "Conversations/Attachment";
|
||||
import ChatAttachments from "./ChatAttachments.svelte";
|
||||
import InputDisplay from "./InputDisplay.svelte";
|
||||
|
|
@ -40,6 +42,7 @@
|
|||
const userInputService: UserInputService = Resolve<UserInputService>(Services.UserInputService);
|
||||
const searchStateStore: SearchStateStore = Resolve<SearchStateStore>(Services.SearchStateStore);
|
||||
const diffService: DiffService = Resolve<DiffService>(Services.DiffService);
|
||||
const planApprovalService: PlanApprovalService = Resolve<PlanApprovalService>(Services.PlanApprovalService);
|
||||
const eventService: EventService = Resolve<EventService>(Services.EventService);
|
||||
const aiPrompt: IPrompt = Resolve<IPrompt>(Services.IPrompt);
|
||||
|
||||
|
|
@ -74,6 +77,8 @@
|
|||
|
||||
const diffOpenedRef: EventRef = eventService.on(Event.DiffOpened, () => { inputMode = InputMode.Diff; focusInput(); });
|
||||
const diffClosedRef: EventRef = eventService.on(Event.DiffClosed, () => { inputMode = InputMode.Normal; focusInput(); });
|
||||
const planApprovalOpenedRef: EventRef = eventService.on(Event.PlanApprovalOpened, () => { inputMode = InputMode.PlanApproval; focusInput(); });
|
||||
const planApprovalClosedRef: EventRef = eventService.on(Event.PlanApprovalClosed, () => { inputMode = InputMode.Normal; focusInput(); });
|
||||
const rateLimitCountdownRef: EventRef = eventService.on(Event.RateLimitCountdown, (delayMs: number) => { startCountdown(delayMs); });
|
||||
|
||||
const settingsSubscription: object = settingsService.subscribeToSettingsChanged(changed => {
|
||||
|
|
@ -97,6 +102,8 @@
|
|||
onDestroy(() => {
|
||||
eventService.offref(diffOpenedRef);
|
||||
eventService.offref(diffClosedRef);
|
||||
eventService.offref(planApprovalOpenedRef);
|
||||
eventService.offref(planApprovalClosedRef);
|
||||
eventService.offref(rateLimitCountdownRef);
|
||||
settingsService.unsubscribe(settingsSubscription);
|
||||
stopCountdown();
|
||||
|
|
@ -215,7 +222,7 @@
|
|||
})();
|
||||
|
||||
$: if (submitButton) {
|
||||
if (inputMode === InputMode.Question || inputMode === InputMode.Diff) {
|
||||
if (inputMode === InputMode.Question || inputMode === InputMode.Diff || inputMode === InputMode.PlanApproval) {
|
||||
setIcon(submitButton, userRequest.trim() === "" ? "square" : "send-horizontal");
|
||||
} else {
|
||||
setIcon(submitButton, isSubmitting ? "square" : "send-horizontal");
|
||||
|
|
@ -233,11 +240,12 @@
|
|||
$: inputPlaceholder = (() => {
|
||||
if (inputMode === InputMode.Question) return Copy.InputPlaceholderQuestion;
|
||||
if (inputMode === InputMode.Diff) return Copy.InputPlaceholderDiff;
|
||||
if (inputMode === InputMode.PlanApproval) return Copy.InputPlaceholderPlanApproval;
|
||||
return Copy.InputPlaceholderNormal;
|
||||
})();
|
||||
|
||||
$: submitDisabled = (() => {
|
||||
if (inputMode === InputMode.Diff || inputMode === InputMode.Question) {
|
||||
if (inputMode === InputMode.Diff || inputMode === InputMode.Question || inputMode === InputMode.PlanApproval) {
|
||||
return false;
|
||||
}
|
||||
return !isSubmitting && userRequest.trim() === "";
|
||||
|
|
@ -250,6 +258,9 @@
|
|||
if (inputMode === InputMode.Diff) {
|
||||
return userRequest.trim() === "" ? Copy.ButtonCancel : Copy.ButtonMakeSuggestion;
|
||||
}
|
||||
if (inputMode === InputMode.PlanApproval) {
|
||||
return userRequest.trim() === "" ? Copy.ButtonCancel : Copy.ButtonMakeSuggestion;
|
||||
}
|
||||
return isSubmitting ? Copy.ButtonCancel : Copy.ButtonSendMessage;
|
||||
})();
|
||||
|
||||
|
|
@ -273,6 +284,14 @@
|
|||
diffService.onSuggest(suggestion.formattedRequest);
|
||||
}
|
||||
|
||||
function handlePlanSuggestion() {
|
||||
if (userRequest.trim() === "" || inputMode !== InputMode.PlanApproval) {
|
||||
return;
|
||||
}
|
||||
const suggestion = requestFromInput();
|
||||
planApprovalService.onSuggest(suggestion.formattedRequest);
|
||||
}
|
||||
|
||||
function handleAnswer() {
|
||||
if (userRequest.trim() === "" || inputMode !== InputMode.Question) {
|
||||
return;
|
||||
|
|
@ -341,6 +360,8 @@
|
|||
handleAnswer();
|
||||
} else if (inputMode === InputMode.Diff) {
|
||||
handleSuggestion();
|
||||
} else if (inputMode === InputMode.PlanApproval) {
|
||||
handlePlanSuggestion();
|
||||
} else {
|
||||
handleSubmit();
|
||||
}
|
||||
|
|
@ -555,8 +576,9 @@
|
|||
<ChatAttachments bind:attachments={attachments}/>
|
||||
</div>
|
||||
|
||||
<div id="diff-controls-container" style:padding-top={inputMode === InputMode.Diff ? "var(--size-4-2)" : 0}>
|
||||
<div id="diff-controls-container" style:padding-top={(inputMode === InputMode.Diff || inputMode === InputMode.PlanApproval) ? "var(--size-4-2)" : 0}>
|
||||
<DiffControls diffOpen={inputMode === InputMode.Diff}/>
|
||||
<PlanApprovalControls planApprovalOpen={inputMode === InputMode.PlanApproval}/>
|
||||
</div>
|
||||
|
||||
<div id="input-search-results-container" style:padding-top={$searchState.results.length > 0 ? "var(--size-4-2)" : 0}>
|
||||
|
|
@ -637,6 +659,8 @@
|
|||
userRequest.trim() === "" ? handleStop() : handleAnswer();
|
||||
} else if (inputMode === InputMode.Diff) {
|
||||
userRequest.trim() === "" ? handleStop() : handleSuggestion();
|
||||
} else if (inputMode === InputMode.PlanApproval) {
|
||||
userRequest.trim() === "" ? handleStop() : handlePlanSuggestion();
|
||||
} else {
|
||||
isSubmitting ? handleStop() : handleSubmit();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -39,20 +39,6 @@
|
|||
scrollToActiveStep();
|
||||
}
|
||||
|
||||
$: if (wrapperDiv && expanded !== undefined) {
|
||||
handleExpandTransition();
|
||||
}
|
||||
|
||||
function handleExpandTransition() {
|
||||
isTransitioning = true;
|
||||
const onTransitionEnd = () => {
|
||||
isTransitioning = false;
|
||||
wrapperDiv.removeEventListener('transitionend', onTransitionEnd);
|
||||
scrollToActiveStep();
|
||||
};
|
||||
wrapperDiv.addEventListener('transitionend', onTransitionEnd);
|
||||
}
|
||||
|
||||
function setupResizeObserver() {
|
||||
if (resizeObserver) {
|
||||
resizeObserver.disconnect();
|
||||
|
|
@ -110,6 +96,17 @@
|
|||
requestAnimationFrame(() => {
|
||||
updateHeight();
|
||||
});
|
||||
|
||||
isTransitioning = true;
|
||||
const finishTransition = () => {
|
||||
wrapperDiv.removeEventListener('transitionend', finishTransition);
|
||||
clearTimeout(fallbackTimeoutId);
|
||||
isTransitioning = false;
|
||||
scrollToActiveStep();
|
||||
};
|
||||
// Fallback in case height doesn't actually change (e.g. <=3 steps), so transitionend never fires
|
||||
const fallbackTimeoutId = setTimeout(finishTransition, 250);
|
||||
wrapperDiv.addEventListener('transitionend', finishTransition, { once: true });
|
||||
}
|
||||
</script>
|
||||
|
||||
|
|
|
|||
|
|
@ -20,11 +20,13 @@
|
|||
import type { StreamingMarkdownService } from "Services/StreamingMarkdownService";
|
||||
import { AITool, fromString } from "Enums/AITool";
|
||||
import { AIProvider } from "Enums/ApiProvider";
|
||||
import type { PlanApprovalService } from "Services/PlanApprovalService";
|
||||
|
||||
const plugin: VaultkeeperAIPlugin = Resolve<VaultkeeperAIPlugin>(Services.VaultkeeperAIPlugin);
|
||||
const executionPlanStore: ExecutionPlanStore = Resolve<ExecutionPlanStore>(Services.ExecutionPlanStore);
|
||||
const settingsService: SettingsService = Resolve<SettingsService>(Services.SettingsService);
|
||||
const chatService: ChatService = Resolve<ChatService>(Services.ChatService);
|
||||
const planApprovalService: PlanApprovalService = Resolve<PlanApprovalService>(Services.PlanApprovalService);
|
||||
const workSpaceService: WorkSpaceService = Resolve<WorkSpaceService>(Services.WorkSpaceService);
|
||||
const conversationService: ConversationFileSystemService = Resolve<ConversationFileSystemService>(Services.ConversationFileSystemService);
|
||||
const streamingMarkdownService: StreamingMarkdownService = Resolve<StreamingMarkdownService>(Services.StreamingMarkdownService);
|
||||
|
|
@ -144,6 +146,9 @@
|
|||
chatInput.enterQuestionMode(resolve);
|
||||
});
|
||||
},
|
||||
onPlanApprovalRequest: async (plan) => {
|
||||
return planApprovalService.requestApproval(plan);
|
||||
},
|
||||
onPlanUpdate: (executionPlan) => {
|
||||
executionPlanStore.setPlan(executionPlan);
|
||||
},
|
||||
|
|
|
|||
97
Components/PlanApprovalControls.svelte
Normal file
97
Components/PlanApprovalControls.svelte
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
<script lang="ts">
|
||||
import { Resolve } from "Services/DependencyService";
|
||||
import type { PlanApprovalService } from "Services/PlanApprovalService";
|
||||
import { Services } from "Services/Services";
|
||||
import { Copy } from "Enums/Copy";
|
||||
import { tick } from "svelte";
|
||||
|
||||
export let planApprovalOpen = false;
|
||||
|
||||
const planApprovalService: PlanApprovalService = Resolve<PlanApprovalService>(Services.PlanApprovalService);
|
||||
|
||||
let contentDiv: HTMLDivElement;
|
||||
let height = 0;
|
||||
|
||||
$: planApprovalOpen, updateHeight();
|
||||
|
||||
function updateHeight() {
|
||||
tick().then(() => {
|
||||
if (contentDiv) {
|
||||
height = contentDiv.scrollHeight;
|
||||
}
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<div id="plan-approval-controls-wrapper" style:height="{height}px">
|
||||
<div id="plan-approval-controls" bind:this={contentDiv}>
|
||||
{#if planApprovalOpen}
|
||||
<button
|
||||
id="plan-approve"
|
||||
class="plan-approval-button"
|
||||
aria-label={Copy.ButtonApprove}
|
||||
on:click={() => planApprovalService.onApprove()}>
|
||||
{Copy.ButtonApprove}
|
||||
</button>
|
||||
<button
|
||||
id="plan-reject"
|
||||
class="plan-approval-button"
|
||||
aria-label={Copy.ButtonReject}
|
||||
on:click={() => planApprovalService.onReject()}>
|
||||
{Copy.ButtonReject}
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
#plan-approval-controls-wrapper {
|
||||
transition: height 0.2s ease-out;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#plan-approval-controls {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr var(--size-4-2) 1fr;
|
||||
grid-template-rows: auto;
|
||||
}
|
||||
|
||||
#plan-approve {
|
||||
grid-column: 1;
|
||||
background-color: color-mix(
|
||||
in srgb,
|
||||
var(--color-green) 75%,
|
||||
var(--background-primary) 25%
|
||||
);
|
||||
}
|
||||
|
||||
#plan-approve:hover {
|
||||
background-color: var(--color-green);
|
||||
}
|
||||
|
||||
#plan-approve:focus {
|
||||
background-color: var(--color-green);
|
||||
}
|
||||
|
||||
#plan-reject {
|
||||
grid-column: 3;
|
||||
background-color: color-mix(
|
||||
in srgb,
|
||||
var(--color-red) 75%,
|
||||
var(--background-primary) 25%
|
||||
);
|
||||
}
|
||||
|
||||
#plan-reject:hover {
|
||||
background-color: var(--color-red);
|
||||
}
|
||||
|
||||
#plan-reject:focus {
|
||||
background-color: var(--color-red);
|
||||
}
|
||||
|
||||
.plan-approval-button {
|
||||
border-radius: var(--button-radius);
|
||||
transition-duration: 0.5s;
|
||||
}
|
||||
</style>
|
||||
44
Components/PlanApprovalWindow.svelte
Normal file
44
Components/PlanApprovalWindow.svelte
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
<script lang="ts">
|
||||
import { Resolve } from "Services/DependencyService";
|
||||
import { Services } from "Services/Services";
|
||||
import type { StreamingMarkdownService } from "Services/StreamingMarkdownService";
|
||||
import type { ExecutionPlan } from "Types/ExecutionPlan";
|
||||
|
||||
export let plan: ExecutionPlan;
|
||||
|
||||
const streamingMarkdownService: StreamingMarkdownService = Resolve<StreamingMarkdownService>(Services.StreamingMarkdownService);
|
||||
|
||||
function instructionRenderAction(element: HTMLElement, instruction: string) {
|
||||
streamingMarkdownService.render(instruction, element, true);
|
||||
return {
|
||||
update(newInstruction: string) {
|
||||
streamingMarkdownService.render(newInstruction, element, true);
|
||||
}
|
||||
};
|
||||
}
|
||||
</script>
|
||||
|
||||
<div id="plan-approval-container">
|
||||
{#each plan.executionSteps as step, index}
|
||||
<div class="plan-approval-step">
|
||||
<h3 class="plan-approval-step-heading">{`${index + 1}. ${step.description}`}</h3>
|
||||
<div class="plan-approval-step-instruction" use:instructionRenderAction={step.instruction}></div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
#plan-approval-container {
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
padding: var(--size-4-4);
|
||||
}
|
||||
|
||||
.plan-approval-step {
|
||||
margin-bottom: var(--size-4-6);
|
||||
}
|
||||
|
||||
.plan-approval-step-heading {
|
||||
margin-bottom: var(--size-4-2);
|
||||
}
|
||||
</style>
|
||||
|
|
@ -124,6 +124,7 @@ export enum Copy {
|
|||
// Chat Input Placeholders
|
||||
InputPlaceholderQuestion = "Provide an answer...",
|
||||
InputPlaceholderDiff = "Make a suggestion...",
|
||||
InputPlaceholderPlanApproval = "Suggest a change to the plan...",
|
||||
InputPlaceholderNormal = "Type a message...",
|
||||
|
||||
// Chat Input Button Labels
|
||||
|
|
@ -140,6 +141,13 @@ export enum Copy {
|
|||
ButtonUserInstruction = "User Instruction",
|
||||
ButtonAttachFiles = "Attach Files",
|
||||
|
||||
// Plan Approval View
|
||||
PlanApprovalViewTitle = "Vaultkeeper AI plan",
|
||||
|
||||
ButtonApprove = "Approve",
|
||||
ButtonReject = "Reject",
|
||||
ButtonSuggest = "Suggest",
|
||||
|
||||
// Agent file message
|
||||
AttachedFile = `The file {fileName} is attached and its full contents follow below. This is the actual content of the file — read it directly to answer the user. This attachment may be a file the user uploaded to the chat, or a vault file you retrieved with a tool; either way, the content below is authoritative and you do NOT need to read or fetch this file again.`,
|
||||
|
||||
|
|
@ -169,8 +177,11 @@ The following context explains why you are doing the task. It is NOT an instruct
|
|||
PlanningFailedNoSteps = "The planned workflow has failed, however steps may have been completed. Consult with the user on how to continue.",
|
||||
WorkflowFailedAtStep = "The planned workflow failed when executing step '{stepDescription}'. Consult with the user on how to continue.",
|
||||
WorkflowAborted = "The planned workflow was aborted. Result: {abortContext}",
|
||||
PlanReceived = "Plan received, now attempting to execute plan",
|
||||
PlanReceived = "Plan received, now awaiting user approval",
|
||||
PlanRejected = "The user has rejected the plan and chosen not to continue at this time",
|
||||
PlanRejectedWithSuggestion = "The user has rejected the current plan. You should replan accounting for their feedback: {suggestion}",
|
||||
PlanningModeError = "First create a plan before executing any functions!",
|
||||
PlanSubmissionRequired = "Error: Attempted to exit planning but plan has not yet been submitted!",
|
||||
UpdateMemoriesWithoutReadError = "Memories must be read before they can be updated. Retrieve the current memory contents first, then provide the complete revised content.",
|
||||
MemoriesInjectionHeader = `\n\n---\n\n## Current Memories\n\nThe following memories were recorded from previous sessions. Use them as context for this conversation.\n\n{memories}`,
|
||||
MemoriesEmpty = "No memories have been created yet.",
|
||||
|
|
@ -199,8 +210,7 @@ The following context explains why you are doing the task. It is NOT an instruct
|
|||
DirectiveWebSearchDisabled = "- **Web Search**: DISABLED — the web search tool is unavailable; if the user requests it, inform them it is currently turned off in settings",
|
||||
DirectiveWebViewerEnabled = "- **Web Viewer**: ENABLED — you may call the web viewer tool to read the content of the page currently open in the Obsidian web viewer; call it proactively when the user asks about a web page",
|
||||
DirectiveWebViewerDisabled = "- **Web Viewer**: DISABLED — the web viewer tool is unavailable; if the user requests it, inform them it is currently turned off in settings",
|
||||
|
||||
PlanSubmissionRequired = "Error: Attempted to exit planning but plan has not yet been submitted!",
|
||||
|
||||
MaxExecutionDepthReached = "Exceeded maximum plan execution attempts - consult with the user on how to continue.",
|
||||
|
||||
// Execution Plan Request Templates
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
export enum Event {
|
||||
DiffOpened = "diffOpened",
|
||||
DiffClosed = "diffClosed",
|
||||
PlanApprovalOpened = "planApprovalOpened",
|
||||
PlanApprovalClosed = "planApprovalClosed",
|
||||
RateLimitCountdown = "rateLimitCountdown"
|
||||
}
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
export enum InputMode {
|
||||
Normal = "normal",
|
||||
Diff = "diff",
|
||||
Question = "question"
|
||||
Question = "question",
|
||||
PlanApproval = "planApproval"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ import { DebugColor } from "Enums/DebugColor";
|
|||
import { AIToolUsageMode } from "Enums/AIToolUsageMode";
|
||||
import { AIToolResponsePayload } from "AIClasses/ToolDefinitions/AIToolResponsePayload";
|
||||
import type { IAIToolDefinition } from "AIClasses/ToolDefinitions/IAIToolDefinition";
|
||||
import type { ExecutionPlan } from "Types/ExecutionPlan";
|
||||
|
||||
export class OrchestrationAgent extends BaseAgent {
|
||||
|
||||
|
|
@ -39,13 +40,49 @@ export class OrchestrationAgent extends BaseAgent {
|
|||
callbacks.onPlanReset();
|
||||
callbacks.onPlanningStarted();
|
||||
this.debugService?.log("OrchestrationAgent", "Spawning PlanningAgent to generate execution plan");
|
||||
const executionPlan = await planningAgent.runPlanningAgent(planningConversation, callbacks);
|
||||
callbacks.onPlanningFinished();
|
||||
|
||||
let approved = false;
|
||||
|
||||
let executionPlan: ExecutionPlan | undefined;
|
||||
|
||||
while (!approved) {
|
||||
executionPlan = await planningAgent.runPlanningAgent(planningConversation, callbacks);
|
||||
|
||||
if (!executionPlan) {
|
||||
callbacks.onPlanningFinished();
|
||||
this.debugService?.log("OrchestrationAgent", "Planning failed - no execution plan generated");
|
||||
return new AIToolResponsePayload({ message: Copy.PlanningFailedNoSteps });
|
||||
}
|
||||
|
||||
this.debugService?.log("OrchestrationAgent", "Plan awaiting user approval");
|
||||
let response = await callbacks.onPlanApprovalRequest(executionPlan);
|
||||
|
||||
if (response.approved) {
|
||||
this.debugService?.log("OrchestrationAgent", "Execution plan approved");
|
||||
break;
|
||||
}
|
||||
|
||||
if (response.suggestion.trim() === "") {
|
||||
callbacks.onPlanningFinished();
|
||||
this.debugService?.log("OrchestrationAgent", "Execution plan rejected");
|
||||
return new AIToolResponsePayload({ message: Copy.PlanRejected });
|
||||
}
|
||||
|
||||
this.debugService?.log("OrchestrationAgent", "Execution plan rejected with user suggestion, commencing replanning");
|
||||
planningConversation.contents.push(new ConversationContent({
|
||||
role: Role.User,
|
||||
content: replaceCopy(Copy.PlanRejectedWithSuggestion, [response.suggestion]),
|
||||
shouldDisplayContent: false
|
||||
}));
|
||||
}
|
||||
|
||||
if (!executionPlan) {
|
||||
callbacks.onPlanningFinished();
|
||||
this.debugService?.log("OrchestrationAgent", "Planning failed - no execution plan generated");
|
||||
return new AIToolResponsePayload({ message: Copy.PlanningFailedNoSteps });
|
||||
}
|
||||
callbacks.onPlanningFinished();
|
||||
|
||||
this.debugService?.log("OrchestrationAgent", `Execution plan received with ${executionPlan.executionSteps.length} steps`);
|
||||
callbacks.onPlanUpdate(executionPlan);
|
||||
|
||||
|
|
@ -93,6 +130,7 @@ export class OrchestrationAgent extends BaseAgent {
|
|||
: orchestrationResult.continueContext;
|
||||
}
|
||||
stepIndex++;
|
||||
callbacks.onPlanStepUpdate(stepIndex);
|
||||
continue;
|
||||
}
|
||||
|
||||
|
|
@ -125,6 +163,7 @@ export class OrchestrationAgent extends BaseAgent {
|
|||
content: `Step ${stepIndex + 1} was skipped. Reason: ${orchestrationResult.skipReason}`
|
||||
}));
|
||||
stepIndex++;
|
||||
callbacks.onPlanStepUpdate(stepIndex);
|
||||
continue;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -47,6 +47,7 @@ export class QuickAgent extends BaseAgent {
|
|||
onPlanningStarted: () => {},
|
||||
onPlanningFinished: () => {},
|
||||
onUserQuestion: async () => new Promise<string>(() => {}),
|
||||
onPlanApprovalRequest: async () => new Promise(() => {}),
|
||||
onPlanUpdate: () => {},
|
||||
onPlanStepUpdate: () => {},
|
||||
onPlanReset: () => {},
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ import type { WorkSpaceService } from "./WorkSpaceService";
|
|||
import type { ExecutionPlan } from "Types/ExecutionPlan";
|
||||
import type { MainAgent } from "./AIServices/MainAgent";
|
||||
import type { ChatMode } from "Enums/ChatMode";
|
||||
import type { PlanApprovalResponse } from "Types/PlanApprovalResponse";
|
||||
|
||||
export interface IChatServiceCallbacks {
|
||||
onSubmit: () => void;
|
||||
|
|
@ -26,6 +27,7 @@ export interface IChatServiceCallbacks {
|
|||
onPlanningStarted: () => void;
|
||||
onPlanningFinished: () => void;
|
||||
onUserQuestion: (question: string) => Promise<string>;
|
||||
onPlanApprovalRequest: (plan: ExecutionPlan) => Promise<PlanApprovalResponse>
|
||||
onPlanUpdate: (executionPlan: ExecutionPlan) => void;
|
||||
onPlanStepUpdate: (currentStepIndex: number) => void;
|
||||
onPlanReset: () => void;
|
||||
|
|
|
|||
|
|
@ -5,6 +5,8 @@ export class EventService extends Events {
|
|||
|
||||
public on(name: Event.DiffOpened, callback: () => void): EventRef;
|
||||
public on(name: Event.DiffClosed, callback: () => void): EventRef;
|
||||
public on(name: Event.PlanApprovalOpened, callback: () => void): EventRef;
|
||||
public on(name: Event.PlanApprovalClosed, callback: () => void): EventRef;
|
||||
public on(name: Event.RateLimitCountdown, callback: (delayMs: number) => void): EventRef;
|
||||
|
||||
public on<T extends unknown[]>(name: string, callback: (...data: T) => unknown): EventRef {
|
||||
|
|
@ -13,6 +15,8 @@ export class EventService extends Events {
|
|||
|
||||
public trigger(name: Event.DiffOpened, data?: unknown): void;
|
||||
public trigger(name: Event.DiffClosed, data?: unknown): void;
|
||||
public trigger(name: Event.PlanApprovalOpened, data?: unknown): void;
|
||||
public trigger(name: Event.PlanApprovalClosed, data?: unknown): void;
|
||||
public trigger(name: Event.RateLimitCountdown, delayMs: number): void;
|
||||
|
||||
public trigger(name: string, ...data: unknown[]): void {
|
||||
|
|
|
|||
95
Services/PlanApprovalService.ts
Normal file
95
Services/PlanApprovalService.ts
Normal file
|
|
@ -0,0 +1,95 @@
|
|||
import type VaultkeeperAIPlugin from 'main';
|
||||
import { Resolve } from './DependencyService';
|
||||
import { Services } from './Services';
|
||||
import type { EventService } from './EventService';
|
||||
import { Event } from 'Enums/Event';
|
||||
import { Component } from 'obsidian';
|
||||
import { AbortService } from './AbortService';
|
||||
import type { ExecutionPlan } from 'Types/ExecutionPlan';
|
||||
import { PlanApprovalResponse } from 'Types/PlanApprovalResponse';
|
||||
|
||||
export class PlanApprovalService extends Component {
|
||||
|
||||
private readonly plugin: VaultkeeperAIPlugin;
|
||||
private readonly eventService: EventService;
|
||||
private readonly abortService: AbortService;
|
||||
|
||||
private planResolve?: (response: PlanApprovalResponse) => void;
|
||||
|
||||
private ongoingApproval: boolean = false;
|
||||
|
||||
public constructor() {
|
||||
super();
|
||||
this.plugin = Resolve<VaultkeeperAIPlugin>(Services.VaultkeeperAIPlugin);
|
||||
this.eventService = Resolve<EventService>(Services.EventService);
|
||||
this.abortService = Resolve<AbortService>(Services.AbortService);
|
||||
|
||||
this.registerEvent(this.eventService.on(Event.PlanApprovalClosed, () => {
|
||||
this.cancelPendingApproval();
|
||||
}));
|
||||
}
|
||||
|
||||
public async requestApproval(plan: ExecutionPlan): Promise<PlanApprovalResponse> {
|
||||
this.ongoingApproval = true;
|
||||
|
||||
const signal = this.abortService.signal();
|
||||
return new Promise<PlanApprovalResponse>((resolve, reject) => {
|
||||
if (signal.aborted) {
|
||||
this.finishApproval();
|
||||
reject(this.abortService.reason());
|
||||
return;
|
||||
}
|
||||
|
||||
const abortHandler = () => {
|
||||
this.finishApproval();
|
||||
reject(this.abortService.reason());
|
||||
};
|
||||
signal.addEventListener("abort", abortHandler, { once: true });
|
||||
|
||||
this.planResolve = (response: PlanApprovalResponse) => {
|
||||
signal.removeEventListener("abort", abortHandler);
|
||||
resolve(response);
|
||||
};
|
||||
|
||||
void this.plugin.activatePlanApprovalView(plan);
|
||||
this.eventService.trigger(Event.PlanApprovalOpened);
|
||||
});
|
||||
}
|
||||
|
||||
public onApprove() {
|
||||
if (this.planResolve) {
|
||||
this.planResolve(new PlanApprovalResponse(true));
|
||||
}
|
||||
this.finishApproval();
|
||||
}
|
||||
|
||||
public onReject() {
|
||||
if (this.planResolve) {
|
||||
this.planResolve(new PlanApprovalResponse(false));
|
||||
}
|
||||
this.finishApproval();
|
||||
}
|
||||
|
||||
public onSuggest(suggestion: string) {
|
||||
if (this.planResolve) {
|
||||
this.planResolve(new PlanApprovalResponse(false, suggestion));
|
||||
}
|
||||
this.finishApproval();
|
||||
}
|
||||
|
||||
private cancelPendingApproval() {
|
||||
if (this.ongoingApproval) {
|
||||
if (this.planResolve) {
|
||||
this.planResolve(new PlanApprovalResponse(false));
|
||||
}
|
||||
this.finishApproval();
|
||||
}
|
||||
}
|
||||
|
||||
private finishApproval() {
|
||||
this.ongoingApproval = false;
|
||||
this.planResolve = undefined;
|
||||
this.eventService.trigger(Event.PlanApprovalClosed);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -15,6 +15,7 @@ import { ConversationNamingService } from "./ConversationNamingService";
|
|||
import { DebugService } from "./DebugService";
|
||||
import { DiffService } from "./DiffService";
|
||||
import { EventService } from "./EventService";
|
||||
import { PlanApprovalService } from "./PlanApprovalService";
|
||||
import { FileSystemService } from "./FileSystemService";
|
||||
import { HTMLService } from "./HTMLService";
|
||||
import { InputService } from "./InputService";
|
||||
|
|
@ -81,6 +82,7 @@ export function RegisterDependencies() {
|
|||
RegisterSingleton<HTMLService>(Services.HTMLService, new HTMLService());
|
||||
RegisterSingleton<SanitiserService>(Services.SanitiserService, new SanitiserService());
|
||||
RegisterSingleton<DiffService>(Services.DiffService, new DiffService());
|
||||
RegisterSingleton<PlanApprovalService>(Services.PlanApprovalService, new PlanApprovalService());
|
||||
RegisterSingleton<VaultService>(Services.VaultService, new VaultService());
|
||||
RegisterSingleton<VaultCacheService>(Services.VaultCacheService, new VaultCacheService());
|
||||
RegisterSingleton<FileSystemService>(Services.FileSystemService, new FileSystemService());
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ export class Services {
|
|||
static InputService = Symbol("InputService");
|
||||
static WebViewerService = Symbol("WebViewerService");
|
||||
static DiffService = Symbol("DiffService");
|
||||
static PlanApprovalService = Symbol("PlanApprovalService");
|
||||
static MemoriesService = Symbol("MemoriesService");
|
||||
static DebugService = Symbol("DebugService");
|
||||
|
||||
|
|
|
|||
|
|
@ -83,6 +83,16 @@ body:not(.is-tablet) .workspace-drawer.mod-right {
|
|||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* ============================== */
|
||||
/* Plan Approval View Customization */
|
||||
/* ============================== */
|
||||
|
||||
.workspace-leaf-content[data-type="vaultkeeper-ai-plan-approval-view"] .view-content {
|
||||
container-type: size;
|
||||
container-name: plan-approval-container;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* ============================== */
|
||||
/* Settings Styles */
|
||||
/* ============================== */
|
||||
|
|
|
|||
92
Styles/plan_approval_styles.css
Normal file
92
Styles/plan_approval_styles.css
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
/* ============================== */
|
||||
/* Structural / layout fixes */
|
||||
/* ============================== */
|
||||
|
||||
.plan-approval-wrapper {
|
||||
container-type: size;
|
||||
container-name: plan-approval-wrapper;
|
||||
transform: translateZ(0);
|
||||
height: 100cqh;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
/* ============================== */
|
||||
/* Mobile Controls */
|
||||
/* ============================== */
|
||||
|
||||
.plan-approval-mobile-controls {
|
||||
display: none; /* Hidden by default (desktop) */
|
||||
}
|
||||
|
||||
/* Avoid the floating obsidian controls */
|
||||
@media (max-width: 600px) {
|
||||
.plan-approval-mobile-controls {
|
||||
margin-bottom: 85px;
|
||||
}
|
||||
}
|
||||
|
||||
.plan-approval-view-mobile .plan-approval-mobile-controls {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr 1fr;
|
||||
gap: var(--size-4-2);
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
padding: var(--size-4-3);
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.plan-approval-mobile-controls .plan-approval-mobile-button {
|
||||
min-height: 44px;
|
||||
font-size: var(--font-ui-medium);
|
||||
font-weight: var(--font-semibold);
|
||||
border-radius: var(--button-radius);
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
color: var(--text-on-accent);
|
||||
}
|
||||
|
||||
.plan-approval-mobile-controls .plan-approval-mobile-approve {
|
||||
background-color: color-mix(
|
||||
in srgb,
|
||||
var(--color-green) 75%,
|
||||
var(--background-primary) 25%
|
||||
);
|
||||
}
|
||||
|
||||
.plan-approval-mobile-controls .plan-approval-mobile-approve:active {
|
||||
background-color: var(--color-green);
|
||||
}
|
||||
|
||||
.plan-approval-mobile-controls .plan-approval-mobile-suggest {
|
||||
background-color: var(--interactive-accent);
|
||||
}
|
||||
|
||||
.plan-approval-mobile-controls .plan-approval-mobile-suggest:active {
|
||||
background-color: var(--interactive-accent-hover);
|
||||
}
|
||||
|
||||
.plan-approval-mobile-controls .plan-approval-mobile-reject {
|
||||
background-color: color-mix(
|
||||
in srgb,
|
||||
var(--color-red) 75%,
|
||||
var(--background-primary) 25%
|
||||
);
|
||||
}
|
||||
|
||||
.plan-approval-mobile-controls .plan-approval-mobile-reject:active {
|
||||
background-color: var(--color-red);
|
||||
}
|
||||
|
||||
/* Adjust plan height on mobile to account for buttons */
|
||||
.plan-approval-view-mobile .plan-approval-wrapper {
|
||||
height: calc(100cqh - 40px);
|
||||
}
|
||||
|
||||
/* Avoid the floating obsidian controls */
|
||||
@media (max-width: 600px) {
|
||||
.plan-approval-view-mobile .plan-approval-wrapper {
|
||||
height: calc(100cqh - 120px);
|
||||
}
|
||||
}
|
||||
11
Types/PlanApprovalResponse.ts
Normal file
11
Types/PlanApprovalResponse.ts
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
export class PlanApprovalResponse {
|
||||
|
||||
public approved: boolean;
|
||||
public suggestion: string;
|
||||
|
||||
public constructor(approved: boolean, suggestion: string = "") {
|
||||
this.approved = approved;
|
||||
this.suggestion = suggestion;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -7,6 +7,7 @@ import type { DiffService } from "Services/DiffService";
|
|||
import { Services } from "Services/Services";
|
||||
import { VIEW_TYPE_MAIN, type MainView } from "./MainView";
|
||||
import { tick } from "svelte";
|
||||
import { Copy } from "Enums/Copy";
|
||||
|
||||
export const VIEW_TYPE_DIFF = 'vaultkeeper-ai-diff-view';
|
||||
|
||||
|
|
@ -111,21 +112,21 @@ export class DiffView extends ItemView {
|
|||
|
||||
const acceptButton = container.createEl('button', {
|
||||
cls: 'diff-mobile-button diff-mobile-accept',
|
||||
text: 'Accept'
|
||||
text: Copy.ButtonApprove
|
||||
});
|
||||
acceptButton.setAttribute('aria-label', 'Accept changes');
|
||||
acceptButton.setAttribute('aria-label', Copy.ButtonApprove);
|
||||
|
||||
const suggestButton = container.createEl('button', {
|
||||
cls: 'diff-mobile-button diff-mobile-suggest',
|
||||
text: 'Suggest'
|
||||
text: Copy.ButtonSuggest
|
||||
});
|
||||
suggestButton.setAttribute('aria-label', 'Make a suggestion');
|
||||
suggestButton.setAttribute('aria-label', Copy.ButtonSuggest);
|
||||
|
||||
const rejectButton = container.createEl('button', {
|
||||
cls: 'diff-mobile-button diff-mobile-reject',
|
||||
text: 'Reject'
|
||||
text: Copy.ButtonReject
|
||||
});
|
||||
rejectButton.setAttribute('aria-label', 'Reject changes');
|
||||
rejectButton.setAttribute('aria-label', Copy.ButtonReject);
|
||||
|
||||
this.registerDomEvent(acceptButton, 'click', async () => {
|
||||
this.diffService.onAccept();
|
||||
|
|
|
|||
177
Views/PlanApprovalView.ts
Normal file
177
Views/PlanApprovalView.ts
Normal file
|
|
@ -0,0 +1,177 @@
|
|||
import { Event } from "Enums/Event";
|
||||
import { Copy } from "Enums/Copy";
|
||||
import { ItemView, WorkspaceLeaf, Platform, type ViewStateResult } from "obsidian";
|
||||
import { mount, unmount } from "svelte";
|
||||
import { Resolve } from "Services/DependencyService";
|
||||
import type { EventService } from "Services/EventService";
|
||||
import type { PlanApprovalService } from "Services/PlanApprovalService";
|
||||
import { Services } from "Services/Services";
|
||||
import { VIEW_TYPE_MAIN, type MainView } from "./MainView";
|
||||
import PlanApprovalWindow from "Components/PlanApprovalWindow.svelte";
|
||||
import type { ExecutionPlan } from "Types/ExecutionPlan";
|
||||
import { tick } from "svelte";
|
||||
|
||||
export const VIEW_TYPE_PLAN_APPROVAL = 'vaultkeeper-ai-plan-approval-view';
|
||||
|
||||
interface PlanApprovalViewState {
|
||||
plan: ExecutionPlan;
|
||||
}
|
||||
|
||||
export class PlanApprovalView extends ItemView {
|
||||
|
||||
private readonly eventService: EventService;
|
||||
private readonly planApprovalService: PlanApprovalService;
|
||||
|
||||
private plan: ExecutionPlan | undefined;
|
||||
|
||||
private planWindow: ReturnType<typeof PlanApprovalWindow> | undefined;
|
||||
private planContainer: HTMLElement | null = null;
|
||||
private mobileControlsContainer: HTMLElement | null = null;
|
||||
|
||||
constructor(leaf: WorkspaceLeaf) {
|
||||
super(leaf);
|
||||
|
||||
this.eventService = Resolve<EventService>(Services.EventService);
|
||||
this.planApprovalService = Resolve<PlanApprovalService>(Services.PlanApprovalService);
|
||||
|
||||
this.registerEvent(this.eventService.on(Event.PlanApprovalClosed, () => {
|
||||
this.leaf.detach();
|
||||
}));
|
||||
}
|
||||
|
||||
protected override onClose(): Promise<void> {
|
||||
// trigger PlanApprovalClosed event in case the user closed the tab
|
||||
this.eventService.trigger(Event.PlanApprovalClosed);
|
||||
|
||||
if (this.planWindow) {
|
||||
void unmount(this.planWindow);
|
||||
this.planWindow = undefined;
|
||||
}
|
||||
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
public getViewType(): string {
|
||||
return VIEW_TYPE_PLAN_APPROVAL;
|
||||
}
|
||||
|
||||
public getDisplayText(): string {
|
||||
return Copy.PlanApprovalViewTitle;
|
||||
}
|
||||
|
||||
public async setState(state: PlanApprovalViewState, result: ViewStateResult): Promise<void> {
|
||||
this.plan = state.plan;
|
||||
|
||||
this.renderPlan();
|
||||
|
||||
return super.setState(state, result);
|
||||
}
|
||||
|
||||
public getState(): Record<string, unknown> {
|
||||
return {
|
||||
plan: this.plan
|
||||
};
|
||||
}
|
||||
|
||||
private renderPlan() {
|
||||
const container = this.resetContainer();
|
||||
|
||||
if (!this.plan) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (Platform.isMobile) {
|
||||
container.addClass('plan-approval-view-mobile');
|
||||
}
|
||||
|
||||
this.planContainer = container.createDiv({ cls: 'plan-approval-wrapper' });
|
||||
|
||||
this.planWindow = mount(PlanApprovalWindow, {
|
||||
target: this.planContainer,
|
||||
props: {
|
||||
plan: this.plan
|
||||
}
|
||||
});
|
||||
|
||||
if (Platform.isMobile) {
|
||||
this.mobileControlsContainer = this.createMobileButtons();
|
||||
}
|
||||
}
|
||||
|
||||
private resetContainer(): HTMLElement {
|
||||
const container = this.contentEl;
|
||||
container.empty();
|
||||
|
||||
if (this.planWindow) {
|
||||
void unmount(this.planWindow);
|
||||
this.planWindow = undefined;
|
||||
}
|
||||
|
||||
if (this.planContainer) {
|
||||
this.planContainer.remove();
|
||||
this.planContainer = null;
|
||||
}
|
||||
|
||||
if (this.mobileControlsContainer) {
|
||||
this.mobileControlsContainer.remove();
|
||||
this.mobileControlsContainer = null;
|
||||
}
|
||||
|
||||
return container;
|
||||
}
|
||||
|
||||
private createMobileButtons(): HTMLElement {
|
||||
const container = this.contentEl.createDiv({ cls: 'plan-approval-mobile-controls' });
|
||||
|
||||
const approveButton = container.createEl('button', {
|
||||
cls: 'plan-approval-mobile-button plan-approval-mobile-approve',
|
||||
text: Copy.ButtonApprove
|
||||
});
|
||||
approveButton.setAttribute('aria-label', Copy.ButtonApprove);
|
||||
|
||||
const suggestButton = container.createEl('button', {
|
||||
cls: 'plan-approval-mobile-button plan-approval-mobile-suggest',
|
||||
text: Copy.ButtonSuggest
|
||||
});
|
||||
suggestButton.setAttribute('aria-label', Copy.ButtonSuggest);
|
||||
|
||||
const rejectButton = container.createEl('button', {
|
||||
cls: 'plan-approval-mobile-button plan-approval-mobile-reject',
|
||||
text: Copy.ButtonReject
|
||||
});
|
||||
rejectButton.setAttribute('aria-label', Copy.ButtonReject);
|
||||
|
||||
this.registerDomEvent(approveButton, 'click', async () => {
|
||||
this.planApprovalService.onApprove();
|
||||
await this.refocusMainView();
|
||||
});
|
||||
|
||||
this.registerDomEvent(suggestButton, 'click', async () => {
|
||||
await this.refocusMainView(true);
|
||||
});
|
||||
|
||||
this.registerDomEvent(rejectButton, 'click', async () => {
|
||||
this.planApprovalService.onReject();
|
||||
await this.refocusMainView();
|
||||
});
|
||||
|
||||
return container;
|
||||
}
|
||||
|
||||
private async refocusMainView(focusChatInput: boolean = false): Promise<void> {
|
||||
await tick().then(async () => {
|
||||
const { workspace } = this.app;
|
||||
const leaves = workspace.getLeavesOfType(VIEW_TYPE_MAIN);
|
||||
|
||||
if (leaves.length > 0) {
|
||||
await workspace.revealLeaf(leaves[0]);
|
||||
workspace.setActiveLeaf(leaves[0], { focus: true });
|
||||
|
||||
if (focusChatInput) {
|
||||
(leaves[0].view as MainView).input?.focusInput(true);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
25
main.ts
25
main.ts
|
|
@ -3,6 +3,7 @@ import { MainView, VIEW_TYPE_MAIN } from "Views/MainView";
|
|||
import { RegisterDependencies, RegisterPlugin } from "Services/ServiceRegistration";
|
||||
import { VaultkeeperAISettingTab } from "Views/VaultkeeperAISettingTab";
|
||||
import { DiffView, VIEW_TYPE_DIFF } from "Views/DiffView";
|
||||
import { PlanApprovalView, VIEW_TYPE_PLAN_APPROVAL } from "Views/PlanApprovalView";
|
||||
import { Services } from "Services/Services";
|
||||
import { DeregisterAllServices, Resolve } from "Services/DependencyService";
|
||||
import type { VaultService } from "Services/VaultService";
|
||||
|
|
@ -10,6 +11,7 @@ import { Path } from "Enums/Path";
|
|||
import { Copy } from "Enums/Copy";
|
||||
import type { SettingsService } from "Services/SettingsService";
|
||||
import type { Diff2HtmlUIConfig } from "diff2html/lib/ui/js/diff2html-ui";
|
||||
import type { ExecutionPlan } from "Types/ExecutionPlan";
|
||||
|
||||
import "katex/dist/katex.min.css";
|
||||
import 'highlight.js/styles/monokai.min.css';
|
||||
|
|
@ -30,6 +32,10 @@ export default class VaultkeeperAIPlugin extends Plugin {
|
|||
VIEW_TYPE_DIFF,
|
||||
(leaf) => new DiffView(leaf)
|
||||
);
|
||||
this.registerView(
|
||||
VIEW_TYPE_PLAN_APPROVAL,
|
||||
(leaf) => new PlanApprovalView(leaf)
|
||||
);
|
||||
|
||||
this.addCommand({
|
||||
id: "open",
|
||||
|
|
@ -79,7 +85,7 @@ export default class VaultkeeperAIPlugin extends Plugin {
|
|||
const leaves = workspace.getLeavesOfType(VIEW_TYPE_DIFF);
|
||||
const leaf = leaves.length > 0 ? leaves[0] : workspace.getLeaf("tab");
|
||||
|
||||
await leaf?.setViewState({
|
||||
await leaf?.setViewState({
|
||||
type: VIEW_TYPE_DIFF,
|
||||
active: true,
|
||||
state: { diffString, config }
|
||||
|
|
@ -90,6 +96,23 @@ export default class VaultkeeperAIPlugin extends Plugin {
|
|||
}
|
||||
}
|
||||
|
||||
public async activatePlanApprovalView(plan: ExecutionPlan) {
|
||||
const { workspace } = this.app;
|
||||
|
||||
const leaves = workspace.getLeavesOfType(VIEW_TYPE_PLAN_APPROVAL);
|
||||
const leaf = leaves.length > 0 ? leaves[0] : workspace.getLeaf("tab");
|
||||
|
||||
await leaf?.setViewState({
|
||||
type: VIEW_TYPE_PLAN_APPROVAL,
|
||||
active: true,
|
||||
state: { plan }
|
||||
});
|
||||
|
||||
if (leaf != null) {
|
||||
await workspace.revealLeaf(leaf);
|
||||
}
|
||||
}
|
||||
|
||||
// create example user instruction (on first launch only)
|
||||
private async setup() {
|
||||
const settingsService = Resolve<SettingsService>(Services.SettingsService);
|
||||
|
|
|
|||
Loading…
Reference in a new issue