feat: Add intelligent auto-router and enhanced integrations
- Add intelligent-router.sh hook for automatic agent routing - Add AUTO-TRIGGER-SUMMARY.md documentation - Add FINAL-INTEGRATION-SUMMARY.md documentation - Complete Prometheus integration (6 commands + 4 tools) - Complete Dexto integration (12 commands + 5 tools) - Enhanced Ralph with access to all agents - Fix /clawd command (removed disable-model-invocation) - Update hooks.json to v5 with intelligent routing - 291 total skills now available - All 21 commands with automatic routing 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
552
dexto/packages/webui/lib/stores/chatStore.ts
Normal file
552
dexto/packages/webui/lib/stores/chatStore.ts
Normal file
@@ -0,0 +1,552 @@
|
||||
/**
|
||||
* Chat Store
|
||||
*
|
||||
* Manages message state per session using Zustand.
|
||||
* Each session has isolated message state to support multi-session scenarios.
|
||||
*/
|
||||
|
||||
import { create } from 'zustand';
|
||||
import type { InternalMessage, Issue, SanitizedToolResult, LLMProvider } from '@dexto/core';
|
||||
import type { TextPart, ImagePart, AudioPart, FilePart, FileData, UIResourcePart } from '@/types';
|
||||
|
||||
// =============================================================================
|
||||
// Types
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* UI Message role - excludes 'system' which is filtered out before reaching UI
|
||||
*/
|
||||
export type UIMessageRole = 'user' | 'assistant' | 'tool';
|
||||
|
||||
/**
|
||||
* Tool result type for UI messages
|
||||
* Broader than SanitizedToolResult to handle legacy formats and edge cases
|
||||
*/
|
||||
export type ToolResult =
|
||||
| SanitizedToolResult
|
||||
| { error: string | Record<string, unknown> }
|
||||
| string
|
||||
| Record<string, unknown>;
|
||||
|
||||
/**
|
||||
* Sub-agent progress data for spawn_agent tool calls
|
||||
*/
|
||||
export interface SubAgentProgress {
|
||||
/** Short task description */
|
||||
task: string;
|
||||
/** Agent ID (e.g., 'explore-agent') */
|
||||
agentId: string;
|
||||
/** Number of tools called by the sub-agent */
|
||||
toolsCalled: number;
|
||||
/** Current tool being executed */
|
||||
currentTool: string;
|
||||
/** Current tool arguments (optional) */
|
||||
currentArgs?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Message in the chat UI
|
||||
* Extends core InternalMessage with UI-specific fields
|
||||
* Note: Excludes 'system' role as system messages are not displayed in UI
|
||||
*/
|
||||
export interface Message extends Omit<InternalMessage, 'content' | 'role'> {
|
||||
id: string;
|
||||
role: UIMessageRole;
|
||||
createdAt: number;
|
||||
content: string | null | Array<TextPart | ImagePart | AudioPart | FilePart | UIResourcePart>;
|
||||
|
||||
// User attachments
|
||||
imageData?: { image: string; mimeType: string };
|
||||
fileData?: FileData;
|
||||
|
||||
// Tool-related fields
|
||||
toolName?: string;
|
||||
toolArgs?: Record<string, unknown>;
|
||||
toolCallId?: string;
|
||||
toolResult?: ToolResult;
|
||||
toolResultMeta?: SanitizedToolResult['meta'];
|
||||
toolResultSuccess?: boolean;
|
||||
/** Sub-agent progress data (for spawn_agent tool calls) */
|
||||
subAgentProgress?: SubAgentProgress;
|
||||
|
||||
// Approval fields
|
||||
requireApproval?: boolean;
|
||||
approvalStatus?: 'pending' | 'approved' | 'rejected';
|
||||
|
||||
// LLM metadata
|
||||
tokenUsage?: {
|
||||
inputTokens?: number;
|
||||
outputTokens?: number;
|
||||
reasoningTokens?: number;
|
||||
totalTokens?: number;
|
||||
};
|
||||
reasoning?: string;
|
||||
model?: string;
|
||||
provider?: LLMProvider;
|
||||
|
||||
// Session reference
|
||||
sessionId?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Error state for a session
|
||||
*/
|
||||
export interface ErrorMessage {
|
||||
id: string;
|
||||
message: string;
|
||||
timestamp: number;
|
||||
context?: string;
|
||||
recoverable?: boolean;
|
||||
sessionId?: string;
|
||||
anchorMessageId?: string;
|
||||
detailedIssues?: Issue[];
|
||||
}
|
||||
|
||||
/**
|
||||
* State for a single session
|
||||
*/
|
||||
export interface SessionChatState {
|
||||
messages: Message[];
|
||||
streamingMessage: Message | null;
|
||||
processing: boolean;
|
||||
error: ErrorMessage | null;
|
||||
loadingHistory: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Default state for a new session
|
||||
*/
|
||||
const defaultSessionState: SessionChatState = {
|
||||
messages: [],
|
||||
streamingMessage: null,
|
||||
processing: false,
|
||||
error: null,
|
||||
loadingHistory: false,
|
||||
};
|
||||
|
||||
// =============================================================================
|
||||
// Store Interface
|
||||
// =============================================================================
|
||||
|
||||
interface ChatStore {
|
||||
/**
|
||||
* Session states keyed by session ID
|
||||
*/
|
||||
sessions: Map<string, SessionChatState>;
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Message Actions
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Add a message to a session
|
||||
*/
|
||||
addMessage: (sessionId: string, message: Message) => void;
|
||||
|
||||
/**
|
||||
* Update an existing message
|
||||
*/
|
||||
updateMessage: (sessionId: string, messageId: string, updates: Partial<Message>) => void;
|
||||
|
||||
/**
|
||||
* Remove a message from a session
|
||||
*/
|
||||
removeMessage: (sessionId: string, messageId: string) => void;
|
||||
|
||||
/**
|
||||
* Clear all messages in a session
|
||||
*/
|
||||
clearMessages: (sessionId: string) => void;
|
||||
|
||||
/**
|
||||
* Set all messages for a session at once
|
||||
*/
|
||||
setMessages: (sessionId: string, messages: Message[]) => void;
|
||||
|
||||
/**
|
||||
* Initialize or replace session state with history
|
||||
*/
|
||||
initFromHistory: (sessionId: string, messages: Message[]) => void;
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Streaming Actions
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Set the current streaming message for a session
|
||||
*/
|
||||
setStreamingMessage: (sessionId: string, message: Message | null) => void;
|
||||
|
||||
/**
|
||||
* Append content to the streaming message
|
||||
*/
|
||||
appendToStreamingMessage: (
|
||||
sessionId: string,
|
||||
content: string,
|
||||
chunkType?: 'text' | 'reasoning'
|
||||
) => void;
|
||||
|
||||
/**
|
||||
* Finalize streaming message (move to messages array)
|
||||
*/
|
||||
finalizeStreamingMessage: (sessionId: string, updates?: Partial<Message>) => void;
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// State Actions
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Set processing state for a session
|
||||
*/
|
||||
setProcessing: (sessionId: string, processing: boolean) => void;
|
||||
|
||||
/**
|
||||
* Set error state for a session
|
||||
*/
|
||||
setError: (sessionId: string, error: ErrorMessage | null) => void;
|
||||
|
||||
/**
|
||||
* Set loading history state for a session
|
||||
*/
|
||||
setLoadingHistory: (sessionId: string, loading: boolean) => void;
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Session Actions
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Initialize a session with default state
|
||||
*/
|
||||
initSession: (sessionId: string) => void;
|
||||
|
||||
/**
|
||||
* Remove a session completely
|
||||
*/
|
||||
removeSession: (sessionId: string) => void;
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Selectors
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Get state for a session (creates default if not exists)
|
||||
*/
|
||||
getSessionState: (sessionId: string) => SessionChatState;
|
||||
|
||||
/**
|
||||
* Get messages for a session
|
||||
*/
|
||||
getMessages: (sessionId: string) => Message[];
|
||||
|
||||
/**
|
||||
* Get a specific message by ID
|
||||
*/
|
||||
getMessage: (sessionId: string, messageId: string) => Message | undefined;
|
||||
|
||||
/**
|
||||
* Find message by tool call ID
|
||||
*/
|
||||
getMessageByToolCallId: (sessionId: string, toolCallId: string) => Message | undefined;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Helper Functions
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Get or create session state
|
||||
*/
|
||||
function getOrCreateSession(
|
||||
sessions: Map<string, SessionChatState>,
|
||||
sessionId: string
|
||||
): SessionChatState {
|
||||
const existing = sessions.get(sessionId);
|
||||
if (existing) return existing;
|
||||
return { ...defaultSessionState };
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate unique message ID
|
||||
*/
|
||||
export function generateMessageId(): string {
|
||||
return `msg-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Store Implementation
|
||||
// =============================================================================
|
||||
|
||||
export const useChatStore = create<ChatStore>()((set, get) => ({
|
||||
sessions: new Map(),
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Message Actions
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
addMessage: (sessionId, message) => {
|
||||
set((state) => {
|
||||
const newSessions = new Map(state.sessions);
|
||||
const sessionState = getOrCreateSession(newSessions, sessionId);
|
||||
|
||||
newSessions.set(sessionId, {
|
||||
...sessionState,
|
||||
messages: [...sessionState.messages, message],
|
||||
});
|
||||
|
||||
return { sessions: newSessions };
|
||||
});
|
||||
},
|
||||
|
||||
updateMessage: (sessionId, messageId, updates) => {
|
||||
set((state) => {
|
||||
const newSessions = new Map(state.sessions);
|
||||
const sessionState = newSessions.get(sessionId);
|
||||
if (!sessionState) return state;
|
||||
|
||||
const messageIndex = sessionState.messages.findIndex((m) => m.id === messageId);
|
||||
if (messageIndex === -1) return state;
|
||||
|
||||
const newMessages = [...sessionState.messages];
|
||||
newMessages[messageIndex] = { ...newMessages[messageIndex], ...updates };
|
||||
|
||||
newSessions.set(sessionId, {
|
||||
...sessionState,
|
||||
messages: newMessages,
|
||||
});
|
||||
|
||||
return { sessions: newSessions };
|
||||
});
|
||||
},
|
||||
|
||||
removeMessage: (sessionId, messageId) => {
|
||||
set((state) => {
|
||||
const newSessions = new Map(state.sessions);
|
||||
const sessionState = newSessions.get(sessionId);
|
||||
if (!sessionState) return state;
|
||||
|
||||
newSessions.set(sessionId, {
|
||||
...sessionState,
|
||||
messages: sessionState.messages.filter((m) => m.id !== messageId),
|
||||
});
|
||||
|
||||
return { sessions: newSessions };
|
||||
});
|
||||
},
|
||||
|
||||
clearMessages: (sessionId) => {
|
||||
set((state) => {
|
||||
const newSessions = new Map(state.sessions);
|
||||
const sessionState = newSessions.get(sessionId);
|
||||
if (!sessionState) return state;
|
||||
|
||||
newSessions.set(sessionId, {
|
||||
...sessionState,
|
||||
messages: [],
|
||||
streamingMessage: null,
|
||||
});
|
||||
|
||||
return { sessions: newSessions };
|
||||
});
|
||||
},
|
||||
|
||||
setMessages: (sessionId, messages) => {
|
||||
set((state) => {
|
||||
const newSessions = new Map(state.sessions);
|
||||
const sessionState = getOrCreateSession(newSessions, sessionId);
|
||||
|
||||
newSessions.set(sessionId, {
|
||||
...sessionState,
|
||||
messages,
|
||||
});
|
||||
|
||||
return { sessions: newSessions };
|
||||
});
|
||||
},
|
||||
|
||||
initFromHistory: (sessionId, messages) => {
|
||||
set((state) => {
|
||||
const newSessions = new Map(state.sessions);
|
||||
const sessionState = getOrCreateSession(newSessions, sessionId);
|
||||
|
||||
newSessions.set(sessionId, {
|
||||
...sessionState,
|
||||
messages,
|
||||
processing: false,
|
||||
error: null,
|
||||
streamingMessage: null,
|
||||
});
|
||||
|
||||
return { sessions: newSessions };
|
||||
});
|
||||
},
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Streaming Actions
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
setStreamingMessage: (sessionId, message) => {
|
||||
set((state) => {
|
||||
const newSessions = new Map(state.sessions);
|
||||
const sessionState = getOrCreateSession(newSessions, sessionId);
|
||||
|
||||
newSessions.set(sessionId, {
|
||||
...sessionState,
|
||||
streamingMessage: message,
|
||||
});
|
||||
|
||||
return { sessions: newSessions };
|
||||
});
|
||||
},
|
||||
|
||||
appendToStreamingMessage: (sessionId, content, chunkType = 'text') => {
|
||||
set((state) => {
|
||||
const newSessions = new Map(state.sessions);
|
||||
const sessionState = getOrCreateSession(newSessions, sessionId);
|
||||
if (!sessionState.streamingMessage) return state;
|
||||
|
||||
const currentMessage = sessionState.streamingMessage;
|
||||
let updatedMessage: Message;
|
||||
|
||||
if (chunkType === 'reasoning') {
|
||||
// Append to reasoning field
|
||||
updatedMessage = {
|
||||
...currentMessage,
|
||||
reasoning: (currentMessage.reasoning || '') + content,
|
||||
};
|
||||
} else {
|
||||
// Append to content
|
||||
const currentContent =
|
||||
typeof currentMessage.content === 'string' ? currentMessage.content : '';
|
||||
updatedMessage = {
|
||||
...currentMessage,
|
||||
content: currentContent + content,
|
||||
};
|
||||
}
|
||||
|
||||
newSessions.set(sessionId, {
|
||||
...sessionState,
|
||||
streamingMessage: updatedMessage,
|
||||
});
|
||||
|
||||
return { sessions: newSessions };
|
||||
});
|
||||
},
|
||||
|
||||
finalizeStreamingMessage: (sessionId, updates = {}) => {
|
||||
set((state) => {
|
||||
const newSessions = new Map(state.sessions);
|
||||
const sessionState = getOrCreateSession(newSessions, sessionId);
|
||||
if (!sessionState.streamingMessage) return state;
|
||||
|
||||
const finalizedMessage: Message = {
|
||||
...sessionState.streamingMessage,
|
||||
...updates,
|
||||
};
|
||||
|
||||
// Ensure messages array exists (defensive)
|
||||
const existingMessages = sessionState.messages ?? [];
|
||||
|
||||
newSessions.set(sessionId, {
|
||||
...sessionState,
|
||||
messages: [...existingMessages, finalizedMessage],
|
||||
streamingMessage: null,
|
||||
});
|
||||
|
||||
return { sessions: newSessions };
|
||||
});
|
||||
},
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// State Actions
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
setProcessing: (sessionId, processing) => {
|
||||
set((state) => {
|
||||
const newSessions = new Map(state.sessions);
|
||||
const sessionState = getOrCreateSession(newSessions, sessionId);
|
||||
|
||||
newSessions.set(sessionId, {
|
||||
...sessionState,
|
||||
processing,
|
||||
});
|
||||
|
||||
return { sessions: newSessions };
|
||||
});
|
||||
},
|
||||
|
||||
setError: (sessionId, error) => {
|
||||
set((state) => {
|
||||
const newSessions = new Map(state.sessions);
|
||||
const sessionState = getOrCreateSession(newSessions, sessionId);
|
||||
|
||||
newSessions.set(sessionId, {
|
||||
...sessionState,
|
||||
error,
|
||||
});
|
||||
|
||||
return { sessions: newSessions };
|
||||
});
|
||||
},
|
||||
|
||||
setLoadingHistory: (sessionId, loading) => {
|
||||
set((state) => {
|
||||
const newSessions = new Map(state.sessions);
|
||||
const sessionState = getOrCreateSession(newSessions, sessionId);
|
||||
|
||||
newSessions.set(sessionId, {
|
||||
...sessionState,
|
||||
loadingHistory: loading,
|
||||
});
|
||||
|
||||
return { sessions: newSessions };
|
||||
});
|
||||
},
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Session Actions
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
initSession: (sessionId) => {
|
||||
set((state) => {
|
||||
if (state.sessions.has(sessionId)) return state;
|
||||
|
||||
const newSessions = new Map(state.sessions);
|
||||
newSessions.set(sessionId, { ...defaultSessionState });
|
||||
|
||||
return { sessions: newSessions };
|
||||
});
|
||||
},
|
||||
|
||||
removeSession: (sessionId) => {
|
||||
set((state) => {
|
||||
const newSessions = new Map(state.sessions);
|
||||
newSessions.delete(sessionId);
|
||||
return { sessions: newSessions };
|
||||
});
|
||||
},
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Selectors
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
getSessionState: (sessionId) => {
|
||||
const state = get().sessions.get(sessionId);
|
||||
return state ?? { ...defaultSessionState };
|
||||
},
|
||||
|
||||
getMessages: (sessionId) => {
|
||||
return get().getSessionState(sessionId).messages;
|
||||
},
|
||||
|
||||
getMessage: (sessionId, messageId) => {
|
||||
return get()
|
||||
.getMessages(sessionId)
|
||||
.find((m) => m.id === messageId);
|
||||
},
|
||||
|
||||
getMessageByToolCallId: (sessionId, toolCallId) => {
|
||||
return get()
|
||||
.getMessages(sessionId)
|
||||
.find((m) => m.toolCallId === toolCallId);
|
||||
},
|
||||
}));
|
||||
Reference in New Issue
Block a user