Add workflow command support to chat view
Integrate WorkflowEngine into ChatView to handle `/workflow` commands. When a message starts with `/workflow`, the workflow engine generates and executes a multi-step plan instead of the standard chat flow. Results are formatted with step-by-step status and output display. Includes workflow result formatting, assistant message updates, and short-term context tracking. Tests verify command parsing and engine invocation.
This commit is contained in:
+111
-4
@@ -4,6 +4,7 @@ import { VaultIndexer } from './vault-indexer';
|
||||
import { VaultVectorStore } from './vault-vector-store';
|
||||
import { ToolExecutor } from './tool-executor';
|
||||
import { ActionPreviewBuilder, isWriteTool } from './action-preview-builder';
|
||||
import { WorkflowEngine } from './workflow-engine';
|
||||
import {
|
||||
PluginSettings,
|
||||
OllamaMessage,
|
||||
@@ -58,6 +59,13 @@ export class ChatView extends ItemView {
|
||||
this.toolExecutor = new ToolExecutor(this.app.vault, this.app);
|
||||
this.actionPreviewBuilder = new ActionPreviewBuilder(this.app.vault);
|
||||
this.conversationStateManager = new ConversationStateManager();
|
||||
this.workflowEngine = new WorkflowEngine(
|
||||
this.app.vault,
|
||||
this.app,
|
||||
settings.ollamaUrl,
|
||||
settings.model,
|
||||
{ cacheConfig: settings.cacheConfig }
|
||||
);
|
||||
}
|
||||
|
||||
updateSettings(newSettings: PluginSettings) {
|
||||
@@ -68,6 +76,13 @@ export class ChatView extends ItemView {
|
||||
undefined,
|
||||
newSettings.cacheConfig
|
||||
);
|
||||
this.workflowEngine = new WorkflowEngine(
|
||||
this.app.vault,
|
||||
this.app,
|
||||
newSettings.ollamaUrl,
|
||||
newSettings.model,
|
||||
{ cacheConfig: newSettings.cacheConfig }
|
||||
);
|
||||
void this.ollamaClient.initializeCache().catch(() => {
|
||||
new Notice(
|
||||
'Semantic cache unavailable: could not connect to ChromaDB. Check the ChromaDB URL in settings.'
|
||||
@@ -777,12 +792,97 @@ export class ChatView extends ItemView {
|
||||
this.chatContainer?.querySelectorAll('.ollama-proposed-actions').forEach((el) => el.remove());
|
||||
}
|
||||
|
||||
private formatWorkflowResult(result: {
|
||||
workflowName: string;
|
||||
success: boolean;
|
||||
stepResults: { stepName: string; success: boolean; data: unknown; error?: string }[];
|
||||
finalOutput: unknown;
|
||||
error?: string;
|
||||
}): string {
|
||||
const lines: string[] = [];
|
||||
lines.push(`## ${result.workflowName}`);
|
||||
lines.push('');
|
||||
|
||||
if (result.stepResults.length > 0) {
|
||||
lines.push('**Steps:**');
|
||||
for (const step of result.stepResults) {
|
||||
const status = step.success ? '✅' : '❌';
|
||||
lines.push(`${status} **${step.stepName}**`);
|
||||
if (!step.success && step.error) {
|
||||
lines.push(` Error: ${step.error}`);
|
||||
}
|
||||
}
|
||||
lines.push('');
|
||||
}
|
||||
|
||||
if (result.error) {
|
||||
lines.push(`**Workflow Error:** ${result.error}`);
|
||||
lines.push('');
|
||||
}
|
||||
|
||||
if (result.finalOutput) {
|
||||
lines.push('**Result:**');
|
||||
if (typeof result.finalOutput === 'string') {
|
||||
lines.push(result.finalOutput);
|
||||
} else {
|
||||
lines.push(JSON.stringify(result.finalOutput, null, 2));
|
||||
}
|
||||
}
|
||||
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
async handleWorkflowRequest(query: string, assistantMessageId: string): Promise<void> {
|
||||
try {
|
||||
this.updateMessageById(assistantMessageId, {
|
||||
content: '🔄 Generating workflow plan...',
|
||||
isStreaming: false,
|
||||
});
|
||||
|
||||
const result = await this.workflowEngine.executeWorkflowFromQuery(query, this.getTools());
|
||||
|
||||
const formatted = this.formatWorkflowResult({
|
||||
workflowName: result.workflowName,
|
||||
success: result.success,
|
||||
stepResults: result.stepResults.map((sr) => ({
|
||||
stepName: sr.stepName,
|
||||
success: sr.success,
|
||||
data: sr.data,
|
||||
error: sr.error,
|
||||
})),
|
||||
finalOutput: result.finalOutput,
|
||||
error: result.error,
|
||||
});
|
||||
|
||||
this.updateMessageById(assistantMessageId, {
|
||||
content: formatted,
|
||||
isStreaming: false,
|
||||
});
|
||||
|
||||
this.conversationStateManager.updateShortTermContext({
|
||||
role: 'assistant',
|
||||
content: formatted,
|
||||
});
|
||||
} catch (error) {
|
||||
ErrorHandler.handleError(error, 'ChatView.handleWorkflowRequest');
|
||||
this.updateMessageById(assistantMessageId, {
|
||||
content: 'An error occurred while executing the workflow.',
|
||||
isStreaming: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async handleUserInput(inputValue?: string): Promise<void> {
|
||||
const userMessage = (inputValue ?? this.inputEl?.value ?? '').trim();
|
||||
if (!userMessage) {
|
||||
return;
|
||||
}
|
||||
|
||||
const isWorkflowCommand = userMessage.toLowerCase().startsWith('/workflow');
|
||||
const actualMessage = isWorkflowCommand
|
||||
? userMessage.slice('/workflow'.length).trim()
|
||||
: userMessage;
|
||||
|
||||
const maxContextLength = this.settings.maxContextLength;
|
||||
const tools = this.getTools();
|
||||
const messageId = crypto.randomUUID();
|
||||
@@ -792,7 +892,7 @@ export class ChatView extends ItemView {
|
||||
const userChatMessage: ChatMessage = {
|
||||
id: userMessageId,
|
||||
role: 'user',
|
||||
content: userMessage,
|
||||
content: actualMessage,
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
|
||||
@@ -823,9 +923,15 @@ export class ChatView extends ItemView {
|
||||
this.lastMessageEl = previousStreamingEl;
|
||||
}
|
||||
|
||||
if (isWorkflowCommand) {
|
||||
await this.handleWorkflowRequest(actualMessage, assistantMessageId);
|
||||
this.cleanupStreamingResources();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const entries = await this.vaultIndexer.searchVault(
|
||||
userMessage,
|
||||
actualMessage,
|
||||
this.settings.vaultSearchLimit
|
||||
);
|
||||
const context = entries
|
||||
@@ -841,8 +947,8 @@ export class ChatView extends ItemView {
|
||||
.join('\n\n')
|
||||
.slice(0, maxContextLength);
|
||||
const userMessageWithContext = context
|
||||
? `Relevant vault context:\n${context}\n\nUser question:\n${userMessage}`
|
||||
: userMessage;
|
||||
? `Relevant vault context:\n${context}\n\nUser question:\n${actualMessage}`
|
||||
: actualMessage;
|
||||
|
||||
// Get the complete messages array for the LLM with all context layers
|
||||
const completeMessages =
|
||||
@@ -932,6 +1038,7 @@ export class ChatView extends ItemView {
|
||||
private vaultIndexer: VaultIndexer;
|
||||
private toolExecutor: ToolExecutor;
|
||||
private actionPreviewBuilder: ActionPreviewBuilder;
|
||||
private workflowEngine: WorkflowEngine;
|
||||
private conversationStateManager: ConversationStateManager;
|
||||
private vectorStore?: VaultVectorStore;
|
||||
|
||||
|
||||
@@ -713,6 +713,37 @@ describe('ChatView', () => {
|
||||
errorHandlerSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('should handle /workflow command', async () => {
|
||||
view['sendButton'] = document.createElement('button');
|
||||
view['inputEl'] = document.createElement('textarea');
|
||||
(view['inputEl'] as HTMLTextAreaElement).value = '/workflow find notes and summarize';
|
||||
|
||||
const workflowSpy = jest
|
||||
.spyOn(view['workflowEngine'], 'executeWorkflowFromQuery')
|
||||
.mockResolvedValue({
|
||||
workflowId: 'wf-1',
|
||||
workflowName: 'Test Workflow',
|
||||
success: true,
|
||||
stepResults: [
|
||||
{
|
||||
stepId: 's1',
|
||||
stepName: 'Search',
|
||||
success: true,
|
||||
data: ['note1'],
|
||||
timestamp: Date.now(),
|
||||
},
|
||||
],
|
||||
finalOutput: 'Summary result',
|
||||
});
|
||||
|
||||
await (view as any).handleUserInput('/workflow find notes and summarize');
|
||||
|
||||
expect(workflowSpy).toHaveBeenCalledWith('find notes and summarize', expect.any(Array));
|
||||
const assistantMsg = (view as any).messages.find((m: any) => m.role === 'assistant');
|
||||
expect(assistantMsg.content).toContain('Test Workflow');
|
||||
expect(assistantMsg.content).toContain('Summary result');
|
||||
});
|
||||
|
||||
it('should limit conversation history to maxMessageHistory', async () => {
|
||||
view['sendButton'] = document.createElement('button');
|
||||
view['inputEl'] = document.createElement('textarea');
|
||||
|
||||
Reference in New Issue
Block a user