This document defines how modelman handles URLs, state persistence, and deep linking across the application.
modelman uses a three-tier state management strategy:
- URL Paths - Core navigation and view modes
- URL Search Params - Shareable, linkable state
- LocalStorage - User preferences and settings
http://localhost:8009/ → Test mode (default)
http://localhost:8009/test → Test mode (explicit)
http://localhost:8009/chat → Chat mode
http://localhost:8009/oauth/callback → OAuth callback (special route)
| Parameter | Type | Description | Example |
|---|---|---|---|
server |
string | Server ID to display | ?server=abc-123 |
tool |
string | Tool name to select (requires server) | ?tool=get_weather |
search |
string | Search/filter query | ?search=api |
params |
base64 | Pre-filled tool parameters | ?params=eyJ1c2VySWQiOiIxMjMifQ== |
execution |
string | Execution history ID | ?execution=def-456 |
try |
base64 | Try in modelman config | ?try=eyJuYW1lIjoiV2Vh... |
# Default view
http://localhost:8009/
# Test mode with specific server and tool
http://localhost:8009/test?server=weather-api&tool=get_forecast
# Chat mode with selected server
http://localhost:8009/chat?server=notion-mcp
# Deep link to execution result
http://localhost:8009/test?server=api-server&execution=abc123
# Search for tools
http://localhost:8009/test?server=github-mcp&search=issue
# Pre-filled tool execution
http://localhost:8009/test?server=api&tool=fetch¶ms=eyJ1c2VySWQiOiIxMjMifQ==
# Try in modelman (existing feature)
http://localhost:8009/?try=eyJuYW1lIjoiV2VhdGhlciJ9
# OAuth callback (existing feature)
http://localhost:8009/oauth/callback?code=xyz&state=abc| Feature | Storage Method | Rationale |
|---|---|---|
| View Mode (test/chat) | URL Path | Core navigation, back/forward support |
| Selected Server | URL Param | Shareable, deep linkable |
| Selected Tool | URL Param | Shareable, deep linkable |
| Search Query | URL Param | Shareable for documentation |
| Tool Parameters | URL Param (optional) | Share pre-filled executions |
| Execution History | URL Param | Share specific results |
| Try in modelman | URL Param | Existing feature |
| OAuth Callback | URL Path | OAuth spec requirement |
| Input Mode | LocalStorage | User preference |
| Theme | LocalStorage | User preference |
| Sidebar Collapsed | LocalStorage | UI preference |
| Auto-reconnect | LocalStorage | Technical preference |
| Server Sort Order | LocalStorage | Personal organization |
| History Filter | LocalStorage | Ephemeral UI state |
| LLM Settings | LocalStorage | Sensitive data |
| Connection Status | Zustand Only | Transient, re-checked on load |
| Tool Executing | Zustand Only | Transient state |
| Toasts | Zustand Only | Ephemeral notifications |
- ✅ Major navigation change
- ✅ Fundamentally different view
- ✅ Back/forward should navigate
- ✅ Distinct "page" concept
Examples: /test, /chat, /oauth/callback
- ✅ Users need to share this state
- ✅ Filters or focuses current view
- ✅ Deep linking is valuable
- ✅ Affects displayed data
- ❌ NOT for sensitive data
Examples: ?server=abc, ?tool=xyz, ?search=query
- ✅ Personal preference
- ✅ Should persist across sessions
- ✅ Not meant to be shared
- ✅ UI/UX settings
- ✅ Sensitive data (API keys)
Examples: Input mode, theme, sidebar state, API keys
- ✅ Completely transient
- ✅ Should reset on page load
- ✅ Loading/processing state
- ✅ Derived from other state
Examples: Connection status, executing tools, toast notifications
We provide a useURLState hook to manage URL synchronization:
import { useURLState } from './hooks/useURLState';
function MyComponent() {
const { updateURL, readURL } = useURLState();
// Update URL when state changes
const handleServerSelect = (serverId: string) => {
setSelectedServer(serverId);
updateURL({ server: serverId });
};
// Read URL on mount
useEffect(() => {
const urlState = readURL();
if (urlState.server) {
setSelectedServer(urlState.server);
}
}, []);
}Standard localStorage keys used throughout the app:
// Zustand persistence (existing)
'modelman-storage' // Server configs, cached tools, history
// User preferences (new)
'modelman-input-mode' // 'form' | 'json'
'modelman-sidebar-collapsed' // boolean
'modelman-auto-reconnect' // boolean (may already exist)
'modelman-history-filter' // 'all' | 'success' | 'error'
'modelman-server-sort' // 'recent' | 'alphabetical' | 'manual'Access via utility functions:
import { getPreference, setPreference } from './lib/preferences';
const inputMode = getPreference('input-mode', 'form'); // with default
setPreference('input-mode', 'json');Use the navigation helpers to ensure URLs are updated correctly:
import { navigateToView, navigateToServer, navigateToTool } from './lib/navigation';
// Navigate to different view
navigateToView('chat');
// → http://localhost:8009/chat
// Navigate to server
navigateToServer('server-id');
// → http://localhost:8009/test?server=server-id
// Navigate to tool
navigateToTool('server-id', 'tool-name');
// → http://localhost:8009/test?server=server-id&tool=tool-nameWhen redirecting to OAuth provider, preserve the current app state:
// Before redirect
const currentState = {
returnPath: '/test',
server: currentServerId,
tool: currentToolName,
view: currentViewMode
};
// Encode in OAuth state parameter
const oauthState = btoa(JSON.stringify({
serverId: serverId,
returnState: currentState
}));
// After callback
const decoded = JSON.parse(atob(state));
const { returnState } = decoded;
// Restore user's context
window.location.href = `${returnState.returnPath}?server=${returnState.server}&tool=${returnState.tool}`;Use standard URL encoding for simple strings:
const url = `/?server=${encodeURIComponent(serverId)}`;Use base64 for complex data structures:
const params = { userId: '123', filters: ['active'] };
const encoded = btoa(JSON.stringify(params));
const url = `/?params=${encoded}`;- ❌ API keys
- ❌ Auth tokens
- ❌ Passwords
- ❌ Personal information
URL changes should support browser back/forward:
- Use
pushStatefor user-initiated actions (clicking, selecting) - Use
replaceStatefor automated/transient changes (auto-select first tool) - Listen to
popstateto handle back/forward
// User clicks server → pushState
window.history.pushState({}, '', newURL);
// Auto-select first server → replaceState
window.history.replaceState({}, '', newURL);
// Handle back/forward
window.addEventListener('popstate', () => {
const urlState = readURL();
restoreStateFromURL(urlState);
});http://localhost:8009/test?server=weather-api&tool=get_forecast
User opens link → App loads → Selects weather-api server → Selects get_forecast tool
http://localhost:8009/test?server=github-api&search=repository
User opens link → App loads → Selects github-api → Filters tools containing "repository"
http://localhost:8009/test?server=api&execution=abc123
User opens link → App loads → Selects api server → Scrolls to and highlights execution abc123
http://localhost:8009/test?server=api&tool=fetch_user¶ms=eyJ1c2VySWQiOiIxMjMifQ==
User opens link → App loads → Selects server and tool → Pre-fills form with decoded params
# Test path routing
open http://localhost:8009/test
open http://localhost:8009/chat
# Test deep linking
open "http://localhost:8009/test?server=my-server&tool=my-tool"
# Test back/forward
# Navigate around the app, then use browser back/forward buttonsdescribe('URL State Management', () => {
it('should restore server from URL on mount', () => {
const serverId = 'test-server';
window.history.pushState({}, '', `/?server=${serverId}`);
render(<App />);
expect(screen.getByText('test-server')).toBeInTheDocument();
});
it('should update URL when selecting tool', () => {
// ... test implementation
});
});- View Mode State - Change from state to path-based routing
- const [viewMode, setViewMode] = useState<ViewMode>('test');
+ const viewMode = window.location.pathname.includes('/chat') ? 'chat' : 'test';- Server Selection - Add URL sync
const setSelectedServer = (serverId: string | null) => {
useAppStore.setState({ selectedServerId: serverId });
+ updateURL({ server: serverId });
};- Input Mode - Move to localStorage
- inputMode: 'form', // in Zustand state
+ const inputMode = getPreference('input-mode', 'form');✅ Keep URLs readable and shareable ✅ Use descriptive parameter names ✅ Handle missing/invalid URL params gracefully ✅ Clear irrelevant params when context changes ✅ Debounce rapid URL updates (search typing) ✅ Test back/forward navigation ✅ Document all URL parameters
❌ Put sensitive data in URLs ❌ Make URLs excessively long ❌ Update URL on every keystroke (debounce!) ❌ Break back button behavior ❌ Use URL params for transient UI state (modal open) ❌ Assume URL params are valid without validation
Potential future URL features:
- QR Code Generation - For mobile sharing
- Short URLs - Compress long deep links
- URL Templates - Pre-defined link patterns for docs
- Link Analytics - Track shared link usage (opt-in)
- Workspace Sharing - Share entire server configurations
- Execution Replay - Re-run past executions from link
- Check if
useURLStatehook is imported - Verify URL is being updated in browser
- Check browser console for errors
- Ensure
popstatelistener is registered
- Verify using
pushStatenotreplaceStatefor user actions - Check
popstatehandler is restoring state - Ensure URL params are being read on navigation
- Check if it should be in URL (shareable) or localStorage (preference)
- Verify localStorage key is correct
- Check browser localStorage quota not exceeded
- Verify state parameter is being encoded
- Check callback handler is decoding state
- Ensure return URL is being constructed correctly
- Try in modelman - Deep linking server configurations
- Authentication - OAuth flow details
- Storage - LocalStorage and data persistence
- Architecture - Overall app structure
Last Updated: November 2, 2025 Version: 1.0.0