Skip to content

Refactor Command Palette to Generic Extensible Workflow System #33

@tamaygz

Description

@tamaygz

Overview

Transform the command palette from a hardcoded state machine into a generic, extensible workflow system inspired by Alfred for macOS. This will enable easy addition of new commands and workflows without modifying core code, while maintaining clean separation of concerns.

Current Issues

The current CommandPaletteForm implementation has several limitations:

  1. Hardcoded workflows - WorkflowState enum with specific states (SelectingPrompt, SelectingAction, FillingPlaceholder, etc.)
  2. Tight coupling - Actions are tightly coupled to PromptActionType enum
  3. Manual state management - Each workflow requires specific handler methods (ShowActionsForPrompt(), StartFillPlaceholdersWorkflow(), etc.)
  4. Limited extensibility - Adding new workflows requires modifying CommandPaletteForm and adding new enum values
  5. Built-in components hardcoded - Components like TextDisplayPanel are hardcoded into specific workflows
  6. Difficult to test - Business logic mixed with UI code

Proposed Architecture

1. Core Architecture Layers

Workflow Layer

Defines workflows as composable chains of nodes.

public class Workflow
{
    public string Id { get; set; }
    public string Name { get; set; }
    public string Description { get; set; }
    public string Icon { get; set; }
    public List<WorkflowNodeDefinition> Nodes { get; set; }
    public Dictionary<string, string> Connections { get; set; } // NodeId -> NextNodeId
    public string EntryNodeId { get; set; }
    public WorkflowMetadata Metadata { get; set; }
}

public class WorkflowNodeDefinition
{
    public string Id { get; set; }
    public string NodeType { get; set; } // Type name for instantiation
    public Dictionary<string, object> Configuration { get; set; }
    public bool AllowBackNavigation { get; set; } = true;
    public string? CustomBackNodeId { get; set; }
}

public class WorkflowMetadata
{
    public string Author { get; set; }
    public Version Version { get; set; }
    public string[] Tags { get; set; }
    public string[] RequiredServices { get; set; }
}

Node Layer

Reusable workflow steps with clear interfaces.

public interface IWorkflowNode
{
    string Id { get; }
    string Name { get; }
    Task<WorkflowResult> ExecuteAsync(WorkflowContext context);
}

public enum NodeUIType
{
    ItemList,          // Show list of items (prompts, actions, etc.)
    TextInput,         // Show text input box
    TextDisplay,       // Show TextDisplayPanel
    MultiStepInput,    // Multiple input fields
    Confirmation,      // Yes/No dialog
    Custom             // Custom UI component
}

public interface INodeUIProvider
{
    NodeUIType UIType { get; }
    string HintText { get; }
    bool ReadOnly { get; }
    IEnumerable<object> GetItems(WorkflowContext context);
    string GetDisplayText(object item);
    string GetSecondaryText(object item);
    string GetIcon(object item);
    Color? GetItemColor(object item);
}

public class WorkflowContext
{
    public Dictionary<string, object> Data { get; }
    public CancellationToken CancellationToken { get; }
    public IServiceProvider Services { get; }
    
    public T Get<T>(string key);
    public T GetOrDefault<T>(string key, T defaultValue);
    public void Set<T>(string key, T value);
    public bool Has(string key);
    public void Remove(string key);
    public WorkflowContext Clone(); // For navigation history
}

public class WorkflowResult
{
    public bool Success { get; set; }
    public WorkflowContext Context { get; set; }
    public string? NextNodeId { get; set; } // For branching
    public object? Output { get; set; }
    public string? ErrorMessage { get; set; }
    
    public static WorkflowResult Success(WorkflowContext context, string? nextNodeId = null, object? output = null);
    public static WorkflowResult Error(WorkflowContext context, string errorMessage);
}

Engine Layer

Executes workflows and manages state.

public class WorkflowEngine
{
    private readonly IWorkflowRegistry _registry;
    private readonly IServiceProvider _services;
    private readonly WorkflowNavigationStack _navigationStack;
    
    public event EventHandler<NodeExecutedEventArgs>? NodeExecuted;
    public event EventHandler<NodeErrorEventArgs>? NodeError;
    
    public async Task<WorkflowResult> StartWorkflowAsync(string workflowId, WorkflowContext initialContext);
    public async Task<WorkflowResult> ExecuteNodeAsync(IWorkflowNode node, WorkflowContext context);
    public async Task<WorkflowResult> MoveToNextNodeAsync(string nodeId, WorkflowContext context);
    public NavigationFrame? NavigateBack();
    public void Reset();
}

public class WorkflowNavigationStack
{
    private readonly Stack<NavigationFrame> _history;
    
    public void Push(string nodeId, WorkflowContext context);
    public NavigationFrame? Pop();
    public void Clear();
    public int Count { get; }
}

public class NavigationFrame
{
    public string NodeId { get; set; }
    public WorkflowContext Context { get; set; }
    public DateTime Timestamp { get; set; }
}

Registry Layer

Discovers and manages workflows and nodes.

public interface IWorkflowRegistry
{
    void RegisterWorkflow(Workflow workflow);
    void RegisterNode(string nodeType, Type nodeClass);
    void RegisterPlugin(IWorkflowPlugin plugin);
    
    Workflow GetWorkflow(string workflowId);
    IEnumerable<Workflow> GetAllWorkflows();
    IWorkflowNode CreateNode(string nodeType, Dictionary<string, object>? config = null);
}

public interface IWorkflowPlugin
{
    string PluginId { get; }
    string Name { get; }
    Version Version { get; }
    IEnumerable<Workflow> GetWorkflows();
    IEnumerable<(string NodeType, Type NodeClass)> GetNodes();
}

2. Node Categories

Following Alfred's architecture, nodes are organized into categories:

Input Nodes

Accept user input and provide filtering/searching.

  • SearchPromptsNode - Search and filter prompts
  • TextInputNode - Single text input
  • MultiTextInputNode - Multiple text inputs
  • SelectionNode - Select from predefined options
  • FillPlaceholderNode - Fill template placeholders

Action Nodes

Perform operations and transformations.

  • ExecuteLLMNode - Execute prompt through LLM
  • CopyToClipboardNode - Copy text to clipboard
  • PasteNode - Paste text to active window
  • OpenInEditorNode - Open prompt in editor
  • ExportNode - Export data to file
  • HttpRequestNode - Make HTTP requests

UI Nodes

Display information to user.

  • ShowTextPanelNode - Display text in TextDisplayPanel
  • ShowActionsNode - Display action list
  • ShowNotificationNode - Show toast notification
  • ShowConfirmationNode - Show confirmation dialog

Utility Nodes

Control flow and data transformation.

  • ConditionalNode - Branch based on conditions
  • FilterNode - Filter items based on predicate
  • TransformNode - Transform data
  • LoopNode - Iterate through items
  • DelayNode - Add delay between steps
  • AggregateNode - Aggregate multiple results

Output Nodes

Final output and side effects.

  • CloseCommandPaletteNode - Close the palette
  • RecordHistoryNode - Record action in history
  • LogNode - Log to console/file
  • ChainWorkflowNode - Launch another workflow

3. Refactored CommandPaletteForm

The form becomes a generic UI shell:

public partial class CommandPaletteForm : BorderlessFormBase
{
    private WorkflowEngine _engine;
    private IWorkflowRegistry _registry;
    private Workflow? _currentWorkflow;
    private IWorkflowNode? _currentNode;
    private WorkflowContext _context;
    
    // UI Components (unchanged)
    private TextBox _searchBox;
    private ListBox _resultsList;
    private Label _hintLabel;
    private TextDisplayPanel _textDisplayPanel;
    
    public void ShowPalette()
    {
        var workflows = _registry.GetAllWorkflows();
        if (workflows.Count() == 1)
        {
            StartWorkflow(workflows.First().Id);
        }
        else
        {
            ShowWorkflowSelection(workflows);
        }
    }
    
    private async void StartWorkflow(string workflowId)
    {
        _currentWorkflow = _registry.GetWorkflow(workflowId);
        _context = new WorkflowContext(_services);
        _currentNode = _registry.CreateNode(_currentWorkflow.EntryNodeId);
        await ExecuteCurrentNode();
    }
    
    private async Task ExecuteCurrentNode()
    {
        if (_currentNode is INodeUIProvider uiProvider)
        {
            RenderNodeUI(uiProvider);
        }
        
        // UI now waits for user input via events (Enter, selection, etc.)
    }
    
    private void RenderNodeUI(INodeUIProvider uiProvider)
    {
        _hintLabel.Text = uiProvider.HintText;
        _searchBox.ReadOnly = uiProvider.ReadOnly;
        
        switch (uiProvider.UIType)
        {
            case NodeUIType.ItemList:
                RenderItemList(uiProvider);
                break;
            case NodeUIType.TextInput:
                RenderTextInput(uiProvider);
                break;
            case NodeUIType.TextDisplay:
                RenderTextDisplay(uiProvider);
                break;
            // ... other UI types
        }
    }
    
    private async void HandleEnter()
    {
        BuildContextFromUI(); // Populate context with current UI state
        var result = await _engine.ExecuteNodeAsync(_currentNode, _context);
        
        if (result.Success && result.NextNodeId != null)
        {
            _currentNode = _registry.CreateNode(result.NextNodeId);
            _context = result.Context;
            await ExecuteCurrentNode();
        }
        else if (!result.Success)
        {
            ShowError(result.ErrorMessage);
        }
        else
        {
            // Workflow complete
            ResetState();
        }
    }
    
    private void HandleEscape()
    {
        var previousFrame = _engine.NavigateBack();
        if (previousFrame != null)
        {
            _currentNode = _registry.CreateNode(previousFrame.NodeId);
            _context = previousFrame.Context;
            ExecuteCurrentNode();
        }
        else
        {
            Hide();
        }
    }
}

4. Example: Converting "Fill Placeholders" Workflow

Old Implementation:

  • ~500 lines of hardcoded state management
  • Specific methods for each step
  • Tight coupling between UI and logic

New Implementation:

var fillPlaceholdersWorkflow = new Workflow
{
    Id = "fill-placeholders",
    Name = "Fill Placeholders and Execute",
    Description = "Fill template variables and execute prompt",
    Icon = "📝",
    Nodes = new List<WorkflowNodeDefinition>
    {
        new() { Id = "search-prompts", NodeType = "SearchPromptsNode" },
        new() { Id = "show-actions", NodeType = "ShowActionsNode" },
        new() { Id = "check-placeholders", NodeType = "ConditionalNode", 
                Configuration = new() { ["condition"] = "hasPlaceholders" } },
        new() { Id = "fill-loop", NodeType = "LoopNode",
                Configuration = new() { ["itemsKey"] = "placeholders" } },
        new() { Id = "fill-placeholder", NodeType = "FillPlaceholderNode" },
        new() { Id = "apply-values", NodeType = "ApplyPlaceholdersNode" },
        new() { Id = "show-output-options", NodeType = "ShowActionsNode" },
        new() { Id = "execute-llm", NodeType = "ExecuteLLMNode" },
        new() { Id = "copy", NodeType = "CopyToClipboardNode" },
        new() { Id = "paste", NodeType = "PasteNode" },
        new() { Id = "close", NodeType = "CloseCommandPaletteNode" }
    },
    Connections = new Dictionary<string, string>
    {
        ["search-prompts"] = "show-actions",
        ["show-actions"] = "check-placeholders",
        ["check-placeholders"] = "fill-loop", // if true
        ["fill-loop"] = "fill-placeholder",
        ["fill-placeholder"] = "fill-loop", // loop back
        ["fill-loop-exit"] = "apply-values", // when loop completes
        ["apply-values"] = "show-output-options",
        ["show-output-options"] = "execute-llm", // if execute selected
        ["execute-llm"] = "copy", // or "paste"
        ["copy"] = "close",
        ["paste"] = "close"
    },
    EntryNodeId = "search-prompts"
};

5. Plugin System Example

public class BuiltInWorkflowsPlugin : IWorkflowPlugin
{
    public string PluginId => "promptarq.builtin";
    public string Name => "PromptArq Built-in Workflows";
    public Version Version => new Version(1, 0, 0);
    
    public IEnumerable<Workflow> GetWorkflows()
    {
        yield return CreateFillPlaceholdersWorkflow();
        yield return CreateOneTimePromptWorkflow();
        yield return CreateQuickPasteWorkflow();
        // ... more workflows
    }
    
    public IEnumerable<(string, Type)> GetNodes()
    {
        yield return ("SearchPromptsNode", typeof(SearchPromptsNode));
        yield return ("ShowActionsNode", typeof(ShowActionsNode));
        yield return ("FillPlaceholderNode", typeof(FillPlaceholderNode));
        yield return ("ExecuteLLMNode", typeof(ExecuteLLMNode));
        yield return ("CopyToClipboardNode", typeof(CopyToClipboardNode));
        yield return ("PasteNode", typeof(PasteNode));
        yield return ("ShowTextPanelNode", typeof(ShowTextPanelNode));
        // ... more nodes
    }
}

// Third-party plugin example
public class CalculatorPlugin : IWorkflowPlugin
{
    public string PluginId => "community.calculator";
    public string Name => "Calculator Workflow";
    public Version Version => new Version(1, 0, 0);
    
    public IEnumerable<Workflow> GetWorkflows()
    {
        yield return new Workflow
        {
            Id = "calculator",
            Name = "Calculator",
            Description = "Perform mathematical calculations",
            Icon = "🔢",
            Nodes = new List<WorkflowNodeDefinition>
            {
                new() { Id = "input", NodeType = "TextInputNode",
                        Configuration = new() { ["hint"] = "Enter calculation (e.g., 5 + 3 * 2)" } },
                new() { Id = "calculate", NodeType = "CalculateNode" },
                new() { Id = "show-result", NodeType = "ShowTextPanelNode" },
                new() { Id = "copy", NodeType = "CopyToClipboardNode" },
                new() { Id = "close", NodeType = "CloseCommandPaletteNode" }
            },
            Connections = new Dictionary<string, string>
            {
                ["input"] = "calculate",
                ["calculate"] = "show-result",
                ["show-result"] = "copy",
                ["copy"] = "close"
            },
            EntryNodeId = "input"
        };
    }
    
    public IEnumerable<(string, Type)> GetNodes()
    {
        yield return ("CalculateNode", typeof(CalculateNode));
    }
}

Benefits

Extensibility

  • Add new workflows without modifying CommandPaletteForm
  • Create workflows by composing existing nodes
  • Third-party plugins can add new workflows

Reusability

  • Nodes can be reused across multiple workflows
  • Built-in components (TextDisplayPanel, etc.) can be used in any workflow
  • Common patterns (filter, transform, loop) are reusable utilities

Testability

  • Nodes can be unit tested in isolation
  • Workflows can be tested without UI
  • Mock services can be injected via IServiceProvider

Maintainability

  • Clear separation of concerns
  • Smaller, focused classes
  • Easy to understand data flow (input -> transform -> output)

Flexibility

  • Workflows can be as simple (1 node) or complex (20+ nodes) as needed
  • Conditional branching, loops, and transformations supported
  • Dynamic workflow generation possible

Discoverability

  • All workflows registered in IWorkflowRegistry
  • Can show workflow browser/gallery
  • Metadata enables searching and filtering

Migration Strategy

Phase 1: Core Infrastructure

  1. Implement core interfaces (IWorkflowNode, INodeUIProvider, WorkflowContext, etc.)
  2. Implement WorkflowEngine and WorkflowNavigationStack
  3. Implement IWorkflowRegistry with basic registration
  4. Add service provider integration

Phase 2: Built-in Nodes

  1. Create base node categories (Input, Action, UI, Utility, Output)
  2. Implement common utility nodes (Conditional, Filter, Transform, Loop)
  3. Implement built-in component wrappers (ShowTextPanelNode, etc.)
  4. Create BuiltInComponentsPlugin

Phase 3: Refactor CommandPaletteForm

  1. Add workflow engine and registry fields
  2. Implement RenderNodeUI() for generic rendering
  3. Refactor event handlers to use ExecuteCurrentNode()
  4. Update navigation to use WorkflowNavigationStack

Phase 4: Migrate Existing Workflows

  1. Convert "Fill Placeholders" workflow to new system
  2. Convert "One Time Prompt" workflow
  3. Create BuiltInWorkflowsPlugin
  4. Remove old WorkflowState enum

Phase 5: Polish & Documentation

  1. Add workflow validation (detect cycles, missing nodes, etc.)
  2. Create workflow builder/editor UI (future enhancement)
  3. Write documentation for creating custom nodes
  4. Write documentation for creating workflows

Phase 6: Plugin System

  1. Implement plugin loading from assemblies
  2. Add plugin management UI
  3. Create example third-party plugins
  4. Publish plugin development guide

Acceptance Criteria

  • Core interfaces and classes implemented
  • WorkflowEngine can execute multi-step workflows
  • CommandPaletteForm refactored to generic shell
  • All existing workflows converted to new system
  • Navigation (ESC key) works with navigation stack
  • Built-in nodes for all current functionality
  • At least one example third-party plugin
  • Unit tests for core components
  • Integration tests for complete workflows
  • Documentation for creating custom nodes
  • Documentation for creating workflows

Future Enhancements

  1. Visual Workflow Editor - Drag-and-drop workflow builder (like Alfred's editor)
  2. Workflow Import/Export - Share workflows as JSON files
  3. Workflow Marketplace - Browse and install community workflows
  4. Advanced Triggers - Launch workflows via hotkeys, file system events, etc.
  5. Workflow Variables - Global variables shared across nodes
  6. Async Workflows - Long-running workflows that run in background
  7. Workflow Debugging - Step-through debugger for workflows
  8. Conditional UI - Nodes can dynamically change UI based on state

References


Estimated Effort: Large (8-12 weeks)
Priority: High
Labels: enhancement, architecture, command-palette, extensibility

Metadata

Metadata

Labels

No labels
No labels

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions