@nanocollective/nanocoder
Version:
A local-first CLI coding agent that brings the power of agentic coding tools like Claude Code and Gemini CLI to local models or controlled APIs like OpenRouter
266 lines • 11 kB
JavaScript
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { loadPreferences } from '../config/preferences.js';
import { defaultTheme } from '../config/themes.js';
import { createTokenizer } from '../tokenization/index.js';
import { BoundedMap } from '../utils/bounded-map.js';
export function useAppState() {
// Initialize theme and title shape from preferences
const preferences = loadPreferences();
const initialTheme = preferences.selectedTheme || defaultTheme;
const initialTitleShape = preferences.titleShape || 'pill';
const [client, setClient] = useState(null);
const [messages, setMessages] = useState([]);
const [displayMessages, setDisplayMessages] = useState([]);
const [messageTokenCache, setMessageTokenCache] = useState(new BoundedMap({
maxSize: 1000,
// No TTL - cache is session-based and cleared on app restart
}));
const [currentModel, setCurrentModel] = useState('');
const [currentProvider, setCurrentProvider] = useState('openai-compatible');
const [currentTheme, setCurrentTheme] = useState(initialTheme);
const [currentTitleShape, setCurrentTitleShape] = useState(initialTitleShape);
const [toolManager, setToolManager] = useState(null);
const [customCommandLoader, setCustomCommandLoader] = useState(null);
const [customCommandExecutor, setCustomCommandExecutor] = useState(null);
const [customCommandCache, setCustomCommandCache] = useState(new Map());
const [startChat, setStartChat] = useState(false);
const [mcpInitialized, setMcpInitialized] = useState(false);
const [updateInfo, setUpdateInfo] = useState(null);
// Connection status states
const [mcpServersStatus, setMcpServersStatus] = useState([]);
const [lspServersStatus, setLspServersStatus] = useState([]);
// Initialization status states
const [preferencesLoaded, setPreferencesLoaded] = useState(false);
const [customCommandsCount, setCustomCommandsCount] = useState(0);
// Cancelling indicator state
const [isCancelling, setIsCancelling] = useState(false);
const [isConversationComplete, setIsConversationComplete] = useState(false);
const [isSettingsMode, setIsSettingsMode] = useState(false);
// Cancellation state
const [abortController, setAbortController] = useState(null);
// Unified modal/mode state - replaces 11 individual boolean states
const [activeMode, setActiveMode] = useState(null);
const [isVscodeEnabled, setIsVscodeEnabled] = useState(false);
const [checkpointLoadData, setCheckpointLoadData] = useState(null);
const [showAllSessions, setShowAllSessions] = useState(false);
const [currentSessionId, setCurrentSessionId] = useState(null);
const [isToolConfirmationMode, setIsToolConfirmationMode] = useState(false);
const [isToolExecuting, setIsToolExecuting] = useState(false);
// Compact tool display state
const [compactToolDisplay, setCompactToolDisplay] = useState(true);
// Ref keeps current value accessible to long-running async loops
const compactToolDisplayRef = useRef(true);
compactToolDisplayRef.current = compactToolDisplay;
const [compactToolCounts, setCompactToolCounts] = useState(null);
// Mutable ref for the compact counts accumulator - shared between
// the async conversation loop and the toggle handler
const compactToolCountsRef = useRef({});
// Question mode state (ask_question tool)
const [isQuestionMode, setIsQuestionMode] = useState(false);
const [pendingQuestion, setPendingQuestion] = useState(null);
// Development mode state
const [developmentMode, setDevelopmentMode] = useState('normal');
// Context usage state
const [contextPercentUsed, setContextPercentUsed] = useState(null);
const [contextLimit, setContextLimit] = useState(null);
// Tool confirmation state
const [pendingToolCalls, setPendingToolCalls] = useState([]);
const [currentToolIndex, setCurrentToolIndex] = useState(0);
const [completedToolResults, setCompletedToolResults] = useState([]);
const [currentConversationContext, setCurrentConversationContext] = useState(null);
// Chat queue for components
const [chatComponents, setChatComponents] = useState([]);
// Live component that renders outside Static for real-time updates (e.g., BashProgress)
const [liveComponent, setLiveComponent] = useState(null);
// Use ref for component key counter to avoid stale closure issues
// State updates are async/batched, but ref updates are synchronous
// This prevents duplicate keys when addToChatQueue is called rapidly
const componentKeyCounterRef = useRef(0);
// Get the next unique component key - synchronous to prevent duplicates
const getNextComponentKey = useCallback(() => {
componentKeyCounterRef.current += 1;
return componentKeyCounterRef.current;
}, []);
// Helper function to add components to the chat queue with stable keys
const addToChatQueue = useCallback((component) => {
const newCounter = getNextComponentKey();
let componentWithKey = component;
if (React.isValidElement(component) && !component.key) {
componentWithKey = React.cloneElement(component, {
key: `chat-component-${newCounter}`,
});
}
setChatComponents(prevComponents => [
...prevComponents,
componentWithKey,
]);
}, [getNextComponentKey]);
// Create tokenizer based on current provider and model
const tokenizer = useMemo(() => {
if (currentProvider && currentModel) {
return createTokenizer(currentProvider, currentModel);
}
// Fallback to simple char/4 heuristic if provider/model not set
return createTokenizer('', '');
}, [currentProvider, currentModel]);
// Cleanup tokenizer resources when it changes
useEffect(() => {
return () => {
if (tokenizer.free) {
tokenizer.free();
}
};
}, [tokenizer]);
// Helper function for token calculation with caching
const getMessageTokens = useCallback((message) => {
const cacheKey = (message.content || '') + message.role + currentModel;
const cachedTokens = messageTokenCache.get(cacheKey);
if (cachedTokens !== undefined) {
return cachedTokens;
}
const tokens = tokenizer.countTokens(message);
// Defer cache update to avoid "Cannot update a component while rendering" error
// This can happen when components call getMessageTokens during their render
queueMicrotask(() => {
setMessageTokenCache(prev => {
const newCache = new BoundedMap({
maxSize: 1000,
});
// Copy existing entries
for (const [k, v] of prev.entries()) {
newCache.set(k, v);
}
// Add new entry
newCache.set(cacheKey, tokens);
return newCache;
});
});
return tokens;
}, [messageTokenCache, tokenizer, currentModel]);
// Message updater - no limits, display all messages
const updateMessages = useCallback((newMessages) => {
setMessages(newMessages);
setDisplayMessages(newMessages);
}, []);
// Reset tool confirmation state
const resetToolConfirmationState = () => {
setIsToolConfirmationMode(false);
setIsToolExecuting(false);
setPendingToolCalls([]);
setCurrentToolIndex(0);
setCompletedToolResults([]);
setCurrentConversationContext(null);
};
return {
// State
client,
messages,
displayMessages,
messageTokenCache,
currentModel,
currentProvider,
currentTheme,
currentTitleShape,
toolManager,
customCommandLoader,
customCommandExecutor,
customCommandCache,
startChat,
mcpInitialized,
updateInfo,
mcpServersStatus,
lspServersStatus,
preferencesLoaded,
customCommandsCount,
isCancelling,
isConversationComplete,
isSettingsMode,
abortController,
// Unified mode state
activeMode,
setActiveMode,
// Derived mode booleans (read-only convenience)
isModelSelectionMode: activeMode === 'model',
isProviderSelectionMode: activeMode === 'provider',
isModelDatabaseMode: activeMode === 'modelDatabase',
isConfigWizardMode: activeMode === 'configWizard',
isMcpWizardMode: activeMode === 'mcpWizard',
isCheckpointLoadMode: activeMode === 'checkpointLoad',
isExplorerMode: activeMode === 'explorer',
isIdeSelectionMode: activeMode === 'ideSelection',
isSchedulerMode: activeMode === 'scheduler',
isSessionSelectorMode: activeMode === 'sessionSelector',
isVscodeEnabled,
checkpointLoadData,
showAllSessions,
currentSessionId,
isToolConfirmationMode,
isToolExecuting,
compactToolDisplay,
compactToolDisplayRef,
compactToolCounts,
compactToolCountsRef,
isQuestionMode,
pendingQuestion,
developmentMode,
contextPercentUsed,
contextLimit,
pendingToolCalls,
currentToolIndex,
completedToolResults,
currentConversationContext,
chatComponents,
getNextComponentKey,
tokenizer,
// Setters
setClient,
setMessages,
setDisplayMessages,
setMessageTokenCache,
setCurrentModel,
setCurrentProvider,
setCurrentTheme,
setCurrentTitleShape,
setToolManager,
setCustomCommandLoader,
setCustomCommandExecutor,
setCustomCommandCache,
setStartChat,
setMcpInitialized,
setUpdateInfo,
setMcpServersStatus,
setLspServersStatus,
setPreferencesLoaded,
setCustomCommandsCount,
setIsCancelling,
setIsConversationComplete,
setIsSettingsMode,
setAbortController,
setIsVscodeEnabled,
setCheckpointLoadData,
setShowAllSessions,
setCurrentSessionId,
setIsToolConfirmationMode,
setIsToolExecuting,
setCompactToolDisplay,
setCompactToolCounts,
setIsQuestionMode,
setPendingQuestion,
setDevelopmentMode,
setContextPercentUsed,
setContextLimit,
setPendingToolCalls,
setCurrentToolIndex,
setCompletedToolResults,
setCurrentConversationContext,
setChatComponents,
liveComponent,
setLiveComponent,
// Utilities
addToChatQueue,
getMessageTokens,
updateMessages,
resetToolConfirmationState,
};
}
//# sourceMappingURL=useAppState.js.map