andy-stack_vaultkeeper-ai/Components/ChatWindow.svelte

272 lines
8.8 KiB
Svelte
Raw Normal View History

<script lang="ts">
import { Resolve } from "Services/DependencyService";
import { Services } from "Services/Services";
import ChatArea from "./ChatArea.svelte";
import ChatInput from "./ChatInput.svelte";
import { tick, onMount } from "svelte";
import { conversationStore } from "../Stores/ConversationStore";
import { Conversation } from "Conversations/Conversation";
2025-11-10 08:03:27 +00:00
import type VaultkeeperAIPlugin from "main";
import { openPluginSettings } from "Helpers/Helpers";
import type { WorkSpaceService } from "Services/WorkSpaceService";
import type { ChatService } from "Services/ChatService";
import type { ConversationFileSystemService } from "Services/ConversationFileSystemService";
import type { SettingsService } from "Services/SettingsService";
import { Copy } from "Enums/Copy";
import { AbortService } from "Services/AbortService";
import type { Attachment } from "Conversations/Attachment";
import ChatPlanArea from "./ChatPlanArea.svelte";
import type { ExecutionPlanStore } from "Stores/ExecutionPlanStore";
import type { StreamingMarkdownService } from "Services/StreamingMarkdownService";
import { AITool, fromString } from "Enums/AITool";
2026-07-04 11:16:08 +00:00
import { AIProvider } from "Enums/ApiProvider";
import type { PlanApprovalService } from "Services/PlanApprovalService";
import type { Artifact } from "Conversations/Artifact";
import { ConversationContent } from "Conversations/ConversationContent";
import { Role } from "Enums/Role";
2025-11-10 08:03:27 +00:00
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);
const abortService: AbortService = Resolve<AbortService>(Services.AbortService);
let collectedArtifacts: Artifact[] = [];
let chatContainer: HTMLDivElement;
let chatArea: ChatArea;
let chatInput: ChatInput;
let hasNoApiKey = false;
let isSubmitting = false;
let busyPlanning = false;
let conversation: Conversation = new Conversation();
let attachments: Attachment[] = [];
let currentThought: string | null = null;
export function focusInput(force: boolean = false) {
chatInput?.focusInput(force);
}
export function resetChatArea() {
chatArea.resetChatArea();
}
onMount(() => {
if (chatContainer) {
plugin.registerDomEvent(chatContainer, 'click', handleLinkClick);
}
});
async function handleLinkClick(evt: MouseEvent) {
const target = evt.target as HTMLElement;
const link = target.closest('.internal-link') as HTMLAnchorElement | null;
if (!link) {
return;
}
const notePath = link.getAttribute('data-href');
if (!notePath) {
return;
}
evt.preventDefault();
evt.stopPropagation();
await workSpaceService.openNote(notePath);
}
function handleNoApiKey(): boolean {
2026-07-04 11:16:08 +00:00
hasNoApiKey = settingsService.settings.provider !== AIProvider.Local
&& settingsService.getApiKeyForCurrentProvider().trim() == "";
if (hasNoApiKey) {
openPluginSettings(plugin);
}
2026-07-04 11:16:08 +00:00
return hasNoApiKey;
}
function handleStop() {
chatService.stop();
currentThought = null;
}
async function handleSubmit(userRequest: string, formattedRequest: string) {
if (handleNoApiKey()) {
return;
}
collectedArtifacts = [];
const currentRequest = userRequest;
await chatService.submit(conversation, settingsService.settings.chatMode, currentRequest, formattedRequest, attachments, {
onSubmit: () => {
isSubmitting = true;
attachments = [];
chatArea.updateChatAreaLayout("smooth");
},
onStreamingUpdate: () => {
conversation = conversation;
chatArea.updateChatAreaLayout();
},
onThoughtUpdate: (thought) => {
if (thought !== Copy.AIThoughtMessage) {
currentThought = thought;
} else if (currentThought !== null) {
// we are in-between thoughts so use generic copy
currentThought = thought;
}
},
onToolCallStarted: (toolName: string) => {
const tool = fromString(toolName);
switch(tool) {
case AITool.WriteVaultFile:
case AITool.PatchVaultFile:
currentThought = Copy.AIThoughtGeneratingNote;
break;
case AITool.AskUserQuestionPlanning:
case AITool.AskUserQuestionExecution:
currentThought = Copy.AIThoughtPreparingQuery;
break;
}
},
onArtifactProduced: (artifact: Artifact) => {
const collectedArtifact = collectedArtifacts.find(a => a.filePath === artifact.filePath);
if (!collectedArtifact) {
collectedArtifacts.push(artifact);
return;
}
collectedArtifact.updatedContent = artifact.updatedContent;
},
onPlanningStarted: () => {
busyPlanning = true;
},
onPlanningFinished: () => {
busyPlanning = false;
},
onUserQuestion: async (question) => {
const displayEl = createEl("div");
await streamingMarkdownService.render(question, displayEl, true);
chatInput.setDisplayItem(displayEl);
return new Promise<string>((resolve) => {
chatInput.enterQuestionMode(resolve);
});
},
onPlanApprovalRequest: async (plan) => {
return planApprovalService.requestApproval(plan);
},
onPlanUpdate: (executionPlan) => {
executionPlanStore.setPlan(executionPlan);
},
refactor: implement multi-agent orchestration architecture Restructure the AI workflow from a single-agent model to a specialized multi-agent system with distinct roles: - Add AgentType enum (Main, Orchestration, Planning, Execution) to define agent specializations - Replace AIControllerService and AIFunctionService with modular agent classes in Services/AIServices/: - MainAgent: Handles user interaction and delegates to orchestration - OrchestrationAgent: Coordinates plan execution and step-by-step workflow - PlanningAgent: Creates and revises execution plans - ExecutionAgent: Executes individual plan steps - AIController: Base class providing common agent loop functionality - Create specialized prompts (OrchestrationPrompt, ExecutionPrompt) for agent-specific behavior - Add OrchestrationResult type to communicate workflow control decisions (continue, abort, replan) - Introduce agent-specific function scoping: - ExecuteWorkflow for main agent - CompleteTask for execution agent - CompleteStep/Replan/CancelPlan for orchestration agent - SubmitPlan/AskUserQuestionPlanning for planning agent - Update BaseAIClass to use agentType property instead of isPlanningAgent boolean for model selection - Simplify ExecutionPlan and ExecutionStep types by removing execution state tracking (moved to agent coordination) - Remove PlanningEnabledAppendix and ExecutionStatus enum (superseded by agent architecture) - Add comprehensive integration tests for agent workflows This architecture provides better separation of concerns, clearer agent responsibilities, and more robust plan execution with explicit orchestration control flow.
2026-01-27 20:29:20 +00:00
onPlanStepUpdate: (currentStepIndex) => {
executionPlanStore.setCurrentStepIndex(currentStepIndex);
},
onPlanReset: () => {
executionPlanStore.clearPlan();
},
onComplete: async () => {
saveCollectedArtifects(conversation);
conversationService.saveConversation(conversation);
isSubmitting = false;
busyPlanning = false;
currentThought = null;
executionPlanStore.clearPlan();
chatInput.clearDisplayItem();
abortService.reset();
chatArea.updateChatAreaLayout();
},
});
}
function saveCollectedArtifects(conversation: Conversation): void {
let lastMessage = conversation.contents.last();
if (lastMessage?.role !== Role.Assistant || !lastMessage.shouldDisplayContent) {
lastMessage = new ConversationContent({ role: Role.Assistant });
conversation.contents.push(lastMessage);
}
lastMessage.artifacts = collectedArtifacts;
}
$: if ($conversationStore.shouldReset) {
conversation = new Conversation();
conversationService.resetCurrentConversation();
isSubmitting = false;
currentThought = null;
chatArea?.resetChatArea();
chatService.onNameChanged?.("");
conversationStore.clearResetFlag();
}
$: if ($conversationStore.conversationToLoad) {
conversation.contents = [];
isSubmitting = false;
currentThought = null;
chatArea.resetChatArea();
tick().then(() => {
if ($conversationStore.conversationToLoad) {
const { conversation: loadedConversation, filePath } = $conversationStore.conversationToLoad;
conversation = loadedConversation;
conversationService.setCurrentConversationPath(filePath);
chatService.onNameChanged?.(loadedConversation.title);
conversationStore.clearLoadFlag();
chatArea.updateChatAreaLayout("instant", true);
}
});
}
</script>
<main class="container">
<ChatPlanArea executionPlanState={executionPlanStore.executionPlanState} {busyPlanning}/>
<div id="chat-container">
<ChatArea messages={conversation.contents} bind:this={chatArea} bind:currentThought bind:isSubmitting bind:chatContainer/>
</div>
<ChatInput
bind:this={chatInput}
bind:attachments
{hasNoApiKey}
{isSubmitting}
onSubmit={handleSubmit}
onStop={handleStop}
/>
</main>
<style>
.container {
display: grid;
grid-template-rows: auto 1fr auto var(--size-2-1);
grid-template-columns: 1fr;
height: calc(100% - var(--size-4-16));
border-radius: var(--radius-m);
color: var(--font-interface-theme);
}
#chat-container {
height: 100%;
width: 100%;
max-width: 1000px;
justify-self: center;
user-select: text;
grid-row: 2;
grid-column: 1;
overflow: hidden;
}
</style>