mirror of
https://github.com/siteboon/claudecodeui.git
synced 2026-07-04 20:42:57 +08:00
feat: add Hermes provider
This commit is contained in:
@@ -39,6 +39,7 @@ interface UseChatComposerStateArgs {
|
||||
codexModel: string;
|
||||
geminiModel: string;
|
||||
opencodeModel: string;
|
||||
hermesModel: string;
|
||||
isLoading: boolean;
|
||||
canAbortSession: boolean;
|
||||
tokenBudget: Record<string, unknown> | null;
|
||||
@@ -173,6 +174,7 @@ export function useChatComposerState({
|
||||
codexModel,
|
||||
geminiModel,
|
||||
opencodeModel,
|
||||
hermesModel,
|
||||
isLoading,
|
||||
canAbortSession,
|
||||
tokenBudget,
|
||||
@@ -336,6 +338,8 @@ export function useChatComposerState({
|
||||
? geminiModel
|
||||
: provider === 'opencode'
|
||||
? opencodeModel
|
||||
: provider === 'hermes'
|
||||
? (hermesModel === '__hermes_configured_model__' ? undefined : hermesModel)
|
||||
: claudeModel,
|
||||
tokenUsage: tokenBudget,
|
||||
};
|
||||
@@ -391,6 +395,7 @@ export function useChatComposerState({
|
||||
cursorModel,
|
||||
geminiModel,
|
||||
opencodeModel,
|
||||
hermesModel,
|
||||
handleBuiltInCommand,
|
||||
handleCustomCommand,
|
||||
input,
|
||||
@@ -703,6 +708,8 @@ export function useChatComposerState({
|
||||
? 'gemini-settings'
|
||||
: provider === 'opencode'
|
||||
? 'opencode-settings'
|
||||
: provider === 'hermes'
|
||||
? 'hermes-settings'
|
||||
: 'claude-settings';
|
||||
const savedSettings = safeLocalStorage.getItem(settingsKey);
|
||||
if (savedSettings) {
|
||||
@@ -729,6 +736,8 @@ export function useChatComposerState({
|
||||
? geminiModel
|
||||
: provider === 'opencode'
|
||||
? opencodeModel
|
||||
: provider === 'hermes'
|
||||
? (hermesModel === '__hermes_configured_model__' ? undefined : hermesModel)
|
||||
: claudeModel;
|
||||
|
||||
// One message shape for every provider. The backend resolves the
|
||||
@@ -774,6 +783,7 @@ export function useChatComposerState({
|
||||
executeCommand,
|
||||
geminiModel,
|
||||
opencodeModel,
|
||||
hermesModel,
|
||||
isLoading,
|
||||
onSessionProcessing,
|
||||
onSessionEstablished,
|
||||
|
||||
@@ -15,6 +15,7 @@ const FALLBACK_DEFAULT_MODEL: Record<LLMProvider, string> = {
|
||||
codex: 'gpt-5.4',
|
||||
gemini: 'gemini-3.1-pro-preview',
|
||||
opencode: 'anthropic/claude-sonnet-4-5',
|
||||
hermes: '__hermes_configured_model__',
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -29,6 +30,7 @@ const FALLBACK_PERMISSION_MODES: Record<LLMProvider, PermissionMode[]> = {
|
||||
codex: ['default', 'acceptEdits', 'bypassPermissions'],
|
||||
gemini: ['default', 'acceptEdits', 'bypassPermissions', 'plan'],
|
||||
opencode: ['default'],
|
||||
hermes: ['default'],
|
||||
};
|
||||
|
||||
type ProviderCapabilities = {
|
||||
@@ -93,6 +95,9 @@ export function useChatProviderState({ selectedSession, selectedProject }: UseCh
|
||||
const [opencodeModel, setOpenCodeModel] = useState<string>(() => {
|
||||
return localStorage.getItem('opencode-model') || FALLBACK_DEFAULT_MODEL.opencode;
|
||||
});
|
||||
const [hermesModel, setHermesModel] = useState<string>(() => {
|
||||
return localStorage.getItem('hermes-model') || FALLBACK_DEFAULT_MODEL.hermes;
|
||||
});
|
||||
|
||||
/**
|
||||
* Backend-owned capability matrix keyed by provider. Drives the permission
|
||||
@@ -141,12 +146,20 @@ export function useChatProviderState({ selectedSession, selectedProject }: UseCh
|
||||
return;
|
||||
}
|
||||
|
||||
setOpenCodeModel(model);
|
||||
localStorage.setItem('opencode-model', model);
|
||||
if (targetProvider === 'opencode') {
|
||||
setOpenCodeModel(model);
|
||||
localStorage.setItem('opencode-model', model);
|
||||
return;
|
||||
}
|
||||
|
||||
if (targetProvider === 'hermes') {
|
||||
setHermesModel(model);
|
||||
localStorage.setItem('hermes-model', model);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const loadProviderModels = useCallback(async (options: { bypassCache?: boolean } = {}) => {
|
||||
const providers: LLMProvider[] = ['claude', 'cursor', 'codex', 'gemini', 'opencode'];
|
||||
const providers: LLMProvider[] = ['claude', 'cursor', 'codex', 'gemini', 'opencode', 'hermes'];
|
||||
const requestId = providerModelsRequestIdRef.current + 1;
|
||||
providerModelsRequestIdRef.current = requestId;
|
||||
const isHardRefresh = options.bypassCache === true;
|
||||
@@ -324,6 +337,19 @@ export function useChatProviderState({ selectedSession, selectedProject }: UseCh
|
||||
}
|
||||
}, [providerModelCatalog.opencode, opencodeModel]);
|
||||
|
||||
useEffect(() => {
|
||||
const hermes = providerModelCatalog.hermes;
|
||||
if (hermes) {
|
||||
const next = pickStoredOrCurrent('hermes-model', hermesModel, hermes);
|
||||
if (next !== hermesModel) {
|
||||
setHermesModel(next);
|
||||
}
|
||||
if (localStorage.getItem('hermes-model') !== next) {
|
||||
localStorage.setItem('hermes-model', next);
|
||||
}
|
||||
}
|
||||
}, [providerModelCatalog.hermes, hermesModel]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!selectedSession?.id) {
|
||||
return;
|
||||
@@ -434,6 +460,8 @@ export function useChatProviderState({ selectedSession, selectedProject }: UseCh
|
||||
setGeminiModel,
|
||||
opencodeModel,
|
||||
setOpenCodeModel,
|
||||
hermesModel,
|
||||
setHermesModel,
|
||||
permissionMode,
|
||||
setPermissionMode,
|
||||
pendingPermissionRequests,
|
||||
|
||||
@@ -75,6 +75,8 @@ function ChatInterface({
|
||||
setGeminiModel,
|
||||
opencodeModel,
|
||||
setOpenCodeModel,
|
||||
hermesModel,
|
||||
setHermesModel,
|
||||
permissionMode,
|
||||
pendingPermissionRequests,
|
||||
setPendingPermissionRequests,
|
||||
@@ -201,6 +203,7 @@ function ChatInterface({
|
||||
codexModel,
|
||||
geminiModel,
|
||||
opencodeModel,
|
||||
hermesModel,
|
||||
isLoading: isProcessing,
|
||||
canAbortSession,
|
||||
tokenBudget,
|
||||
@@ -293,7 +296,9 @@ function ChatInterface({
|
||||
? t('messageTypes.gemini')
|
||||
: provider === 'opencode'
|
||||
? t('messageTypes.opencode', { defaultValue: 'OpenCode' })
|
||||
: t('messageTypes.claude');
|
||||
: provider === 'hermes'
|
||||
? t('messageTypes.hermes', { defaultValue: 'Hermes' })
|
||||
: t('messageTypes.claude');
|
||||
|
||||
return (
|
||||
<div className="flex h-full items-center justify-center">
|
||||
@@ -334,6 +339,8 @@ function ChatInterface({
|
||||
setGeminiModel={setGeminiModel}
|
||||
opencodeModel={opencodeModel}
|
||||
setOpenCodeModel={setOpenCodeModel}
|
||||
hermesModel={hermesModel}
|
||||
setHermesModel={setHermesModel}
|
||||
providerModelCatalog={providerModelCatalog}
|
||||
providerModelsLoading={providerModelsLoading}
|
||||
tasksEnabled={tasksEnabled}
|
||||
@@ -425,7 +432,9 @@ function ChatInterface({
|
||||
? t('messageTypes.gemini')
|
||||
: provider === 'opencode'
|
||||
? t('messageTypes.opencode', { defaultValue: 'OpenCode' })
|
||||
: t('messageTypes.claude'),
|
||||
: provider === 'hermes'
|
||||
? t('messageTypes.hermes', { defaultValue: 'Hermes' })
|
||||
: t('messageTypes.claude'),
|
||||
})}
|
||||
isTextareaExpanded={isTextareaExpanded}
|
||||
sendByCtrlEnter={sendByCtrlEnter}
|
||||
|
||||
@@ -39,6 +39,8 @@ interface ChatMessagesPaneProps {
|
||||
setGeminiModel: (model: string) => void;
|
||||
opencodeModel: string;
|
||||
setOpenCodeModel: (model: string) => void;
|
||||
hermesModel: string;
|
||||
setHermesModel: (model: string) => void;
|
||||
providerModelCatalog: Partial<Record<LLMProvider, ProviderModelsDefinition>>;
|
||||
providerModelsLoading: boolean;
|
||||
tasksEnabled: boolean;
|
||||
@@ -89,6 +91,8 @@ function ChatMessagesPane({
|
||||
setGeminiModel,
|
||||
opencodeModel,
|
||||
setOpenCodeModel,
|
||||
hermesModel,
|
||||
setHermesModel,
|
||||
providerModelCatalog,
|
||||
providerModelsLoading,
|
||||
tasksEnabled,
|
||||
@@ -177,6 +181,8 @@ function ChatMessagesPane({
|
||||
setGeminiModel={setGeminiModel}
|
||||
opencodeModel={opencodeModel}
|
||||
setOpenCodeModel={setOpenCodeModel}
|
||||
hermesModel={hermesModel}
|
||||
setHermesModel={setHermesModel}
|
||||
providerModelCatalog={providerModelCatalog}
|
||||
providerModelsLoading={providerModelsLoading}
|
||||
tasksEnabled={tasksEnabled}
|
||||
|
||||
@@ -63,6 +63,7 @@ const PROVIDER_LABELS: Record<string, string> = {
|
||||
codex: 'Codex',
|
||||
gemini: 'Gemini',
|
||||
opencode: 'OpenCode',
|
||||
hermes: 'Hermes',
|
||||
};
|
||||
|
||||
const FALLBACK_COMMANDS: CommandEntry[] = [
|
||||
|
||||
@@ -183,6 +183,8 @@ const MessageComponent = memo(({ message, prevMessage, createDiff, onFileOpen, a
|
||||
? t('messageTypes.gemini')
|
||||
: provider === 'opencode'
|
||||
? t('messageTypes.opencode', { defaultValue: 'OpenCode' })
|
||||
: provider === 'hermes'
|
||||
? t('messageTypes.hermes', { defaultValue: 'Hermes' })
|
||||
: t('messageTypes.claude'))}
|
||||
</div>
|
||||
</div>
|
||||
@@ -430,4 +432,3 @@ const MessageComponent = memo(({ message, prevMessage, createDiff, onFileOpen, a
|
||||
});
|
||||
|
||||
export default MessageComponent;
|
||||
|
||||
|
||||
@@ -29,6 +29,7 @@ const PROVIDER_META: { id: LLMProvider; name: string }[] = [
|
||||
{ id: "gemini", name: "Google" },
|
||||
{ id: "cursor", name: "Cursor" },
|
||||
{ id: "opencode", name: "OpenCode" },
|
||||
{ id: "hermes", name: "Hermes" },
|
||||
];
|
||||
|
||||
const MOD_KEY =
|
||||
@@ -50,6 +51,8 @@ type ProviderSelectionEmptyStateProps = {
|
||||
setGeminiModel: (model: string) => void;
|
||||
opencodeModel: string;
|
||||
setOpenCodeModel: (model: string) => void;
|
||||
hermesModel: string;
|
||||
setHermesModel: (model: string) => void;
|
||||
providerModelCatalog: Partial<Record<LLMProvider, ProviderModelsDefinition>>;
|
||||
providerModelsLoading: boolean;
|
||||
tasksEnabled: boolean;
|
||||
@@ -79,11 +82,13 @@ function getCurrentModel(
|
||||
co: string,
|
||||
g: string,
|
||||
o: string,
|
||||
h: string,
|
||||
) {
|
||||
if (p === "claude") return c;
|
||||
if (p === "codex") return co;
|
||||
if (p === "gemini") return g;
|
||||
if (p === "opencode") return o;
|
||||
if (p === "hermes") return h;
|
||||
return cu;
|
||||
}
|
||||
|
||||
@@ -92,6 +97,7 @@ function getProviderDisplayName(p: LLMProvider) {
|
||||
if (p === "cursor") return "Cursor";
|
||||
if (p === "codex") return "Codex";
|
||||
if (p === "opencode") return "OpenCode";
|
||||
if (p === "hermes") return "Hermes";
|
||||
return "Gemini";
|
||||
}
|
||||
|
||||
@@ -111,6 +117,8 @@ export default function ProviderSelectionEmptyState({
|
||||
setGeminiModel,
|
||||
opencodeModel,
|
||||
setOpenCodeModel,
|
||||
hermesModel,
|
||||
setHermesModel,
|
||||
providerModelCatalog,
|
||||
providerModelsLoading,
|
||||
tasksEnabled,
|
||||
@@ -140,6 +148,7 @@ export default function ProviderSelectionEmptyState({
|
||||
codexModel,
|
||||
geminiModel,
|
||||
opencodeModel,
|
||||
hermesModel,
|
||||
);
|
||||
|
||||
const currentModelLabel = useMemo(() => {
|
||||
@@ -164,12 +173,15 @@ export default function ProviderSelectionEmptyState({
|
||||
} else if (providerId === "opencode") {
|
||||
setOpenCodeModel(modelValue);
|
||||
localStorage.setItem("opencode-model", modelValue);
|
||||
} else if (providerId === "hermes") {
|
||||
setHermesModel(modelValue);
|
||||
localStorage.setItem("hermes-model", modelValue);
|
||||
} else {
|
||||
setCursorModel(modelValue);
|
||||
localStorage.setItem("cursor-model", modelValue);
|
||||
}
|
||||
},
|
||||
[setClaudeModel, setCursorModel, setCodexModel, setGeminiModel, setOpenCodeModel],
|
||||
[setClaudeModel, setCursorModel, setCodexModel, setGeminiModel, setOpenCodeModel, setHermesModel],
|
||||
);
|
||||
|
||||
const handleModelSelect = useCallback(
|
||||
@@ -319,6 +331,10 @@ export default function ProviderSelectionEmptyState({
|
||||
model: opencodeModel,
|
||||
defaultValue: "Ready with OpenCode {{model}}",
|
||||
}),
|
||||
hermes: t("providerSelection.readyPrompt.hermes", {
|
||||
model: hermesModel,
|
||||
defaultValue: "Ready with Hermes {{model}}",
|
||||
}),
|
||||
}[provider]
|
||||
}
|
||||
</p>
|
||||
|
||||
Reference in New Issue
Block a user