-
Notifications
You must be signed in to change notification settings - Fork 0
Description
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:
- Hardcoded workflows -
WorkflowStateenum with specific states (SelectingPrompt,SelectingAction,FillingPlaceholder, etc.) - Tight coupling - Actions are tightly coupled to
PromptActionTypeenum - Manual state management - Each workflow requires specific handler methods (
ShowActionsForPrompt(),StartFillPlaceholdersWorkflow(), etc.) - Limited extensibility - Adding new workflows requires modifying
CommandPaletteFormand adding new enum values - Built-in components hardcoded - Components like
TextDisplayPanelare hardcoded into specific workflows - 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 promptsTextInputNode- Single text inputMultiTextInputNode- Multiple text inputsSelectionNode- Select from predefined optionsFillPlaceholderNode- Fill template placeholders
Action Nodes
Perform operations and transformations.
ExecuteLLMNode- Execute prompt through LLMCopyToClipboardNode- Copy text to clipboardPasteNode- Paste text to active windowOpenInEditorNode- Open prompt in editorExportNode- Export data to fileHttpRequestNode- Make HTTP requests
UI Nodes
Display information to user.
ShowTextPanelNode- Display text inTextDisplayPanelShowActionsNode- Display action listShowNotificationNode- Show toast notificationShowConfirmationNode- Show confirmation dialog
Utility Nodes
Control flow and data transformation.
ConditionalNode- Branch based on conditionsFilterNode- Filter items based on predicateTransformNode- Transform dataLoopNode- Iterate through itemsDelayNode- Add delay between stepsAggregateNode- Aggregate multiple results
Output Nodes
Final output and side effects.
CloseCommandPaletteNode- Close the paletteRecordHistoryNode- Record action in historyLogNode- Log to console/fileChainWorkflowNode- 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
- Implement core interfaces (
IWorkflowNode,INodeUIProvider,WorkflowContext, etc.) - Implement
WorkflowEngineandWorkflowNavigationStack - Implement
IWorkflowRegistrywith basic registration - Add service provider integration
Phase 2: Built-in Nodes
- Create base node categories (Input, Action, UI, Utility, Output)
- Implement common utility nodes (Conditional, Filter, Transform, Loop)
- Implement built-in component wrappers (ShowTextPanelNode, etc.)
- Create BuiltInComponentsPlugin
Phase 3: Refactor CommandPaletteForm
- Add workflow engine and registry fields
- Implement
RenderNodeUI()for generic rendering - Refactor event handlers to use
ExecuteCurrentNode() - Update navigation to use
WorkflowNavigationStack
Phase 4: Migrate Existing Workflows
- Convert "Fill Placeholders" workflow to new system
- Convert "One Time Prompt" workflow
- Create BuiltInWorkflowsPlugin
- Remove old
WorkflowStateenum
Phase 5: Polish & Documentation
- Add workflow validation (detect cycles, missing nodes, etc.)
- Create workflow builder/editor UI (future enhancement)
- Write documentation for creating custom nodes
- Write documentation for creating workflows
Phase 6: Plugin System
- Implement plugin loading from assemblies
- Add plugin management UI
- Create example third-party plugins
- 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
- Visual Workflow Editor - Drag-and-drop workflow builder (like Alfred's editor)
- Workflow Import/Export - Share workflows as JSON files
- Workflow Marketplace - Browse and install community workflows
- Advanced Triggers - Launch workflows via hotkeys, file system events, etc.
- Workflow Variables - Global variables shared across nodes
- Async Workflows - Long-running workflows that run in background
- Workflow Debugging - Step-through debugger for workflows
- 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