You're working on a proposal. You need to find a specific contract, extract some numbers, add them to a spreadsheet, and generate a summary. In most tools, that's four separate apps and four context switches. In AiFiler, you press Ctrl+Shift+A and describe what you need. The Universal Command figures out what you're asking for, routes it to the right system, and executes it.
That simplicity hides a lot of complexity. When you type a query into Universal Command, AiFiler isn't just searching—it's parsing intent, routing to specialized handlers, managing context, and executing across multiple subsystems. And it does this for 50+ different types of requests without slowing down or getting confused.
Here's how we built it.
The Problem We Solved
Before Universal Command existed, users had to know which tool to use. Want to search documents? Use search. Want to create something? Use the document editor. Want to analyze your knowledge base? Use the analytics dashboard. Want to tag things? Click the tag button. This fractured the experience. Users had to build mental models of where each feature lived.
The real insight came from watching how people actually worked: they thought in intents, not features. "Find contracts from Q4," "summarize this document," "create a meeting note," "tag everything related to product roadmap." They didn't think "I need to use the search feature." They thought "I need this thing done."
Universal Command flips the model: you describe the intent, and the system figures out how to fulfill it.
The Architecture: Intent → Router → Handler → Executor
The flow looks like this:
User Input → Intent Detection → Universal Router → Intent Handler → Action Executor → Result
Let's break each layer:
Intent Detection
When you type into Universal Command, the first step is understanding what you're asking for. This isn't simple keyword matching. We run the input through an AI model that classifies it into one of 50+ intent categories. The model sees patterns like:
- "Find all documents about X" →
SEARCH_DOCUMENTS - "Create a note about X" →
CREATE_DOCUMENT - "What's the summary of X" →
ANALYZE_DOCUMENT - "Tag everything with X" →
BATCH_TAG - "Show me documents I created last week" →
FILTER_BY_METADATA
The intent detection happens in lib/intelligence/universalRouter.ts. We send the query and current context (what document you're viewing, what workspace you're in, recent actions) to the model, and it returns a structured response with:
{
intent: "SEARCH_DOCUMENTS",
confidence: 0.94,
parameters: {
query: "Q4 contracts",
filters: { type: "contract" }
},
alternativeIntents: [
{ intent: "FILTER_BY_METADATA", confidence: 0.32 }
]
}
The confidence score matters. If we're below a threshold (typically 0.7), we ask the user to clarify. If there are competing intents, we might show suggestions.
The Universal Router
Once we know the intent, the router decides where to send it. This is where the architecture gets interesting.
In lib/intelligence/intentHandlers.ts, we have 50+ handler functions, each specialized for a specific intent. The router is a simple dispatcher:
const handlers = {
SEARCH_DOCUMENTS: searchDocumentsHandler,
CREATE_DOCUMENT: createDocumentHandler,
ANALYZE_DOCUMENT: analyzeDocumentHandler,
BATCH_TAG: batchTagHandler,
FILTER_BY_METADATA: filterByMetadataHandler,
// ... 45 more
};
const handler = handlers[intent];
if (!handler) {
return { error: "Intent not recognized" };
}
return handler(parameters, context);
This is deliberately simple. We're not building a complex state machine. Each handler is a pure function that takes parameters and context and returns an action.
Intent Handlers: Specialized Logic
Each handler knows how to do one thing well. The searchDocumentsHandler doesn't need to know how to create documents. The batchTagHandler doesn't need to know how to analyze. This separation lets us:
- Test independently: Each handler has unit tests. We test search in isolation from tagging.
- Update safely: If we change how search works, it doesn't affect tagging.
- Add new intents quickly: New intent? Write a handler, add it to the dispatcher, done.
Here's what a handler looks like (simplified):
async function searchDocumentsHandler(params, context) {
const { query, filters } = params;
const { userId, workspaceId } = context;
// Validate input
if (!query || query.length < 2) {
return { error: "Query too short" };
}
// Build the search
const results = await supabase
.from('documents')
.select('*')
.eq('workspace_id', workspaceId)
.textSearch('content', query)
.match(filters);
// Return structured action
return {
type: 'SEARCH_RESULTS',
data: results,
metadata: { count: results.length, query }
};
}
The handler returns an action, not a UI update. This is crucial. The handler describes what should happen; the executor makes it happen.
The Action Executor
Once we have an action, the executor carries it out. This is in lib/intelligence/actionExecutor.ts. The executor:
- Validates the action: Is it safe? Does the user have permission?
- Executes it: Calls APIs, updates state, triggers side effects.
- Handles errors: If something fails, it returns a clear error message.
- Tracks it: Logs the action for analytics and debugging.
The executor also handles action chaining. If a user asks "find contracts and tag them as urgent," that's two intents:
- Find contracts
- Tag the results
The executor sees this and chains them: execute the search, then pass the results to the tag handler.
async function executeAction(action, context) {
// Validate
if (!action.type) return { error: "Invalid action" };
// Execute
const result = await executors[action.type](action.data, context);
// Handle chaining
if (action.chain) {
return executeAction(action.chain(result), context);
}
return result;
}
Why This Matters for You
As a user, you don't see any of this. You press Ctrl+Shift+A, type "find all contracts from Q4 and tag them as reviewed," and it happens. But the architecture underneath means:
Speed: Each intent handler is optimized for its specific job. Search doesn't carry the overhead of document creation. Tagging doesn't carry the overhead of analysis.
Reliability: When we fix a bug in one intent, we don't risk breaking others. Our test suite covers 50+ intents independently.
Extensibility: When we add new features—like the recent Memory and Skills sections—we just add new intent handlers. The router doesn't change.
Context awareness: Because every handler receives context (current workspace, current document, user preferences), intents can be smart. "Create a note about this" knows which document you're looking at.
The Real Complexity: Context Management
The trickiest part isn't the routing. It's the context manager in lib/intelligence/contextManager.tsx.
When you use Universal Command, you're not in a vacuum. You might be:
- Viewing a specific document
- Inside a workspace
- In the middle of a workflow
- Collaborating with teammates
- Using AiFiler on mobile vs. desktop
The context manager tracks all of this and makes it available to handlers. A handler can ask "what document is the user currently viewing?" and get an answer. It can ask "what filters are active right now?" and adjust accordingly.
This is why "tag this" works when you're viewing a document, but "tag everything about X" works when you're in search results. The handler sees different contexts and adapts.
Performance: The Circuit Breaker Pattern
With 50+ intents, we needed to prevent one slow handler from blocking others. We use a circuit breaker pattern for expensive operations like knowledge graph queries and session intelligence.
If a handler starts timing out, the circuit breaker opens:
async function executeWithCircuitBreaker(handler, timeout = 5000) {
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), timeout);
try {
return await handler(controller.signal);
} catch (error) {
if (error.name === 'AbortError') {
return { error: "Request timed out", fallback: true };
}
throw error;
} finally {
clearTimeout(timer);
}
}
If a handler is slow, we return a fallback result and move on. The user still gets something useful, and the app stays responsive.
What's Next
The architecture we've built handles 50+ intents today, but it's designed to scale. When we add new features—like the recent MCP (Model Context Protocol) server integration—we just add new handlers. The router doesn't change. The executor doesn't change.
The real power of Universal Command isn't that it handles 50 intents. It's that it can handle 500 intents with the same architecture, the same reliability, and the same speed.
That's what happens when you separate concerns: intent detection, routing, handling, and execution. Each layer does one thing. Each layer does it well. And together, they create something that feels simple to use but is sophisticated underneath.
Enjoyed this article?
Get more articles like this delivered to your inbox. No spam, unsubscribe anytime.

