Phase 003: remove the coding-agent and ekko-agent runtimes
Every chat session now runs through the Hermes bridge, so the three chat chains become feature-equivalent instead of three partially-maintained dispatch paths. Keeping the coding-agent and in-house agent runtimes alive meant every bridge change had to be mirrored into two more runtimes that had already drifted. Historical rows are read, not migrated: sessions stored with the retired source normalize to cli on load, workflow graphs naming the retired agents keep loading and run on the bridge, and the visible-source check plus SOURCE_LABELS still recognize the old value so those sessions stay listed. The public surface follows the code: the OpenAPI generator and the MCP chat-run tool schema drop the six retired entry fields so external callers are not told about runtimes that no longer exist. harness-check gains a RETIRED_RUNTIME scan so the residual check is a mechanical gate instead of a manual grep, with the two surviving read-side compat sites recorded with reasons. Refs: docs/phases/phase_003.md
This commit is contained in:
@@ -1,7 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { renameSession, setSessionWorkspace, batchDeleteSessions, exportSession } from "@/api/hermes/sessions";
|
||||
import type { AvailableModelGroup } from "@/api/hermes/system";
|
||||
import { fetchCodingAgentsStatus, inferCodingAgentApiMode, normalizeCodingAgentApiMode, type ChatCodingAgentId, type CodingAgentApiMode, type CodingAgentId } from "@/api/coding-agents";
|
||||
import { useChatStore, type Session } from "@/stores/hermes/chat";
|
||||
import { useAppStore } from "@/stores/hermes/app";
|
||||
import { useProfilesStore } from "@/stores/hermes/profiles";
|
||||
@@ -16,8 +14,6 @@ import {
|
||||
NSelect,
|
||||
NTooltip,
|
||||
NPopconfirm,
|
||||
NRadioButton,
|
||||
NRadioGroup,
|
||||
useMessage,
|
||||
type DropdownOption,
|
||||
} from "naive-ui";
|
||||
@@ -321,14 +317,9 @@ const headerTitle = computed(() =>
|
||||
);
|
||||
|
||||
const showNewChatModal = ref(false);
|
||||
const newChatAgent = ref<"hermes" | ChatCodingAgentId>("hermes");
|
||||
const newChatAgentMode = ref<"global" | "scoped">("scoped");
|
||||
const newChatProfile = ref<string>("default");
|
||||
const newChatProvider = ref<string>("");
|
||||
const newChatModel = ref<string>("");
|
||||
const newChatBaseUrl = ref<string>("");
|
||||
const newChatApiKey = ref<string>("");
|
||||
const newChatApiMode = ref<CodingAgentApiMode>("codex_responses");
|
||||
const newChatWorkspace = ref("");
|
||||
const newChatLoading = ref(false);
|
||||
|
||||
@@ -425,30 +416,6 @@ const hiddenDefaultWorkspaces = computed(() => {
|
||||
return defaultWorkspaces.value.filter(ws => !visible.has(ws));
|
||||
});
|
||||
|
||||
const CODING_AGENT_AUTH_PROVIDER_KEYS = new Set(["openai-codex", "copilot", "xai-oauth", "nous", "google-gemini-cli", "claude-oauth"]);
|
||||
|
||||
const showEkkoAgentEntry = import.meta.env.DEV;
|
||||
const newChatAgentOptions = computed(() => {
|
||||
const options = [
|
||||
{ label: "Hermes", value: "hermes" },
|
||||
{ label: "Claude Code", value: "claude-code" },
|
||||
{ label: "Codex", value: "codex" },
|
||||
];
|
||||
if (showEkkoAgentEntry) options.push({ label: "Ekko Agent", value: "ekko-agent" });
|
||||
return options;
|
||||
});
|
||||
|
||||
const newChatApiModeOptions = computed(() => [
|
||||
{ label: t("codingAgents.protocolOpenAiChat"), value: "chat_completions" },
|
||||
{ label: t("codingAgents.protocolOpenAiResponses"), value: "codex_responses" },
|
||||
{ label: t("codingAgents.protocolAnthropicMessages"), value: "anthropic_messages" },
|
||||
]);
|
||||
|
||||
const newChatAgentModeOptions = computed(() => [
|
||||
{ label: t("codingAgents.launchModeGlobal"), value: "global" },
|
||||
{ label: t("codingAgents.launchModeScoped"), value: "scoped" },
|
||||
]);
|
||||
|
||||
function getModelGroupsForProfile(profile: string) {
|
||||
const profileModels = appStore.profileModelGroups.find(
|
||||
(entry) => entry.profile === profile,
|
||||
@@ -456,18 +423,8 @@ function getModelGroupsForProfile(profile: string) {
|
||||
return profileModels?.groups || [];
|
||||
}
|
||||
|
||||
function isCodingAgentAuthProvider(provider?: string) {
|
||||
return CODING_AGENT_AUTH_PROVIDER_KEYS.has(String(provider || "").toLowerCase());
|
||||
}
|
||||
|
||||
function isNewChatProviderAllowed(group: AvailableModelGroup) {
|
||||
const mode = newChatAgent.value === "ekko-agent" ? "scoped" : newChatAgentMode.value;
|
||||
if (!(newChatAgent.value !== "hermes" && mode === "scoped")) return true;
|
||||
return !isCodingAgentAuthProvider(group.provider);
|
||||
}
|
||||
|
||||
function getSelectableModelGroupsForProfile(profile: string) {
|
||||
return getModelGroupsForProfile(profile).filter(isNewChatProviderAllowed);
|
||||
return getModelGroupsForProfile(profile);
|
||||
}
|
||||
|
||||
function getDefaultModelForProfile(profile: string) {
|
||||
@@ -536,66 +493,28 @@ const selectedNewChatProviderGroup = computed(() =>
|
||||
newChatModelGroups.value.find((item) => item.provider === newChatProvider.value),
|
||||
);
|
||||
|
||||
const isNewChatCodingAgent = computed(() => newChatAgent.value !== "hermes");
|
||||
const isNewChatExternalCodingAgent = computed(() => newChatAgent.value === "claude-code" || newChatAgent.value === "codex");
|
||||
const effectiveNewChatAgentMode = computed(() =>
|
||||
newChatAgent.value === "ekko-agent" ? "scoped" : newChatAgentMode.value,
|
||||
);
|
||||
const isNewChatGlobalCodingAgent = computed(() =>
|
||||
isNewChatCodingAgent.value && effectiveNewChatAgentMode.value === "global",
|
||||
);
|
||||
const newChatUsesProviderModel = computed(() => !isNewChatGlobalCodingAgent.value);
|
||||
const newChatNeedsBaseUrl = computed(() =>
|
||||
isNewChatCodingAgent.value && effectiveNewChatAgentMode.value === "scoped" && !selectedNewChatProviderGroup.value?.base_url,
|
||||
);
|
||||
const newChatNeedsApiKey = computed(() =>
|
||||
isNewChatCodingAgent.value && effectiveNewChatAgentMode.value === "scoped" && !selectedNewChatProviderGroup.value?.api_key,
|
||||
);
|
||||
const canConfirmNewChat = computed(() => {
|
||||
if (!newChatProfile.value) return false;
|
||||
if (!newChatUsesProviderModel.value) return true;
|
||||
if (!newChatProvider.value || !newChatModel.value) return false;
|
||||
if (!isNewChatCodingAgent.value) return true;
|
||||
if (isNewChatExternalCodingAgent.value && !newChatApiMode.value) return false;
|
||||
if (newChatNeedsBaseUrl.value && !newChatBaseUrl.value.trim()) return false;
|
||||
if (newChatNeedsApiKey.value && !newChatApiKey.value.trim()) return false;
|
||||
return true;
|
||||
});
|
||||
|
||||
function defaultNewChatApiMode(group?: AvailableModelGroup): CodingAgentApiMode {
|
||||
const providerKey = String(group?.provider || newChatProvider.value || "").toLowerCase();
|
||||
const baseUrl = String(group?.base_url || newChatBaseUrl.value || "").toLowerCase();
|
||||
return normalizeCodingAgentApiMode(
|
||||
group?.api_mode,
|
||||
inferCodingAgentApiMode(providerKey, baseUrl),
|
||||
);
|
||||
}
|
||||
|
||||
function syncNewChatApiMode() {
|
||||
newChatApiMode.value = defaultNewChatApiMode(selectedNewChatProviderGroup.value);
|
||||
}
|
||||
|
||||
function syncNewChatModelSelection() {
|
||||
const defaults = getDefaultModelForProfile(newChatProfile.value);
|
||||
newChatProvider.value = defaults.provider;
|
||||
newChatModel.value = defaults.model;
|
||||
newChatBaseUrl.value = "";
|
||||
newChatApiKey.value = "";
|
||||
syncNewChatApiMode();
|
||||
}
|
||||
|
||||
function ensureNewChatProviderSelection() {
|
||||
if (!newChatUsesProviderModel.value) return;
|
||||
const currentGroup = selectedNewChatProviderGroup.value;
|
||||
if (currentGroup && currentGroup.models.includes(newChatModel.value)) {
|
||||
syncNewChatApiMode();
|
||||
return;
|
||||
}
|
||||
syncNewChatModelSelection();
|
||||
}
|
||||
|
||||
watch(
|
||||
() => [newChatAgent.value, newChatAgentMode.value, newChatProfile.value],
|
||||
() => [newChatProfile.value],
|
||||
() => {
|
||||
ensureNewChatProviderSelection();
|
||||
// Reload workspace data when profile changes
|
||||
@@ -646,56 +565,16 @@ function handleNewChatProfileChange(value: string) {
|
||||
function handleNewChatProviderChange(value: string) {
|
||||
newChatProvider.value = value;
|
||||
newChatModel.value = newChatModelOptions.value[0]?.value || "";
|
||||
newChatBaseUrl.value = "";
|
||||
newChatApiKey.value = "";
|
||||
syncNewChatApiMode();
|
||||
}
|
||||
|
||||
async function confirmNewChat() {
|
||||
if (isNewChatExternalCodingAgent.value) {
|
||||
newChatLoading.value = true;
|
||||
try {
|
||||
const agentId = newChatAgent.value as CodingAgentId;
|
||||
const status = await fetchCodingAgentsStatus();
|
||||
const tool = status.tools.find((item) => item.id === agentId);
|
||||
if (!tool?.installed) {
|
||||
const fallbackName = agentId === "codex" ? "Codex" : "Claude Code";
|
||||
message.warning(t("codingAgents.installRequired", { agent: tool?.name || fallbackName }));
|
||||
showNewChatModal.value = false;
|
||||
await router.push({ name: "hermes.codingAgents" });
|
||||
return;
|
||||
}
|
||||
} catch {
|
||||
message.error(t("codingAgents.loadFailed"));
|
||||
return;
|
||||
} finally {
|
||||
newChatLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
const group = selectedNewChatProviderGroup.value;
|
||||
const source = newChatAgent.value === "hermes" ? "cli" : "coding_agent";
|
||||
const codingAgentMode = effectiveNewChatAgentMode.value;
|
||||
const isGlobalCodingAgent = source === "coding_agent" && codingAgentMode === "global";
|
||||
const agent = newChatAgent.value === "codex"
|
||||
? "codex"
|
||||
: newChatAgent.value === "claude-code"
|
||||
? "claude"
|
||||
: newChatAgent.value === "ekko-agent"
|
||||
? "ekko-agent"
|
||||
: "hermes";
|
||||
const session = chatStore.newChat({
|
||||
profile: newChatProfile.value,
|
||||
provider: isGlobalCodingAgent ? undefined : newChatProvider.value,
|
||||
model: isGlobalCodingAgent ? undefined : newChatModel.value,
|
||||
source,
|
||||
agent,
|
||||
codingAgentId: newChatAgent.value === "hermes" ? undefined : newChatAgent.value,
|
||||
codingAgentMode: source === "coding_agent" ? codingAgentMode : undefined,
|
||||
provider: newChatProvider.value,
|
||||
model: newChatModel.value,
|
||||
source: "cli",
|
||||
agent: "hermes",
|
||||
workspace: newChatWorkspace.value || null,
|
||||
baseUrl: source === "coding_agent" && !isGlobalCodingAgent ? group?.base_url || newChatBaseUrl.value.trim() || undefined : undefined,
|
||||
apiKey: source === "coding_agent" && !isGlobalCodingAgent ? group?.api_key || newChatApiKey.value.trim() || undefined : undefined,
|
||||
apiMode: isNewChatExternalCodingAgent.value && !isGlobalCodingAgent ? newChatApiMode.value : undefined,
|
||||
});
|
||||
// Record workspace to recent list
|
||||
if (newChatWorkspace.value && workspaceComposable) {
|
||||
@@ -867,7 +746,7 @@ const contextMenuOptions = computed(() => {
|
||||
|
||||
options.push({ label: t("chat.setWorkspace"), key: "workspace" })
|
||||
|
||||
if (contextSession.value?.source === "cli" || contextSession.value?.source === "coding_agent") {
|
||||
if (contextSession.value?.source === "cli") {
|
||||
options.push({ label: t("chat.setModel"), key: "model" })
|
||||
}
|
||||
|
||||
@@ -1041,7 +920,6 @@ async function handleWorkspaceConfirm() {
|
||||
}
|
||||
|
||||
const showSessionModelModal = ref(false);
|
||||
const showSessionModelModeModal = ref(false);
|
||||
const sessionModelSessionId = ref<string | null>(null);
|
||||
const sessionModelSearch = ref("");
|
||||
const sessionModelCollapsedGroups = ref<Record<string, boolean>>({});
|
||||
@@ -1049,44 +927,14 @@ const sessionModelValue = ref("");
|
||||
const sessionModelProvider = ref("");
|
||||
const sessionModelCustomInput = ref("");
|
||||
const sessionModelCustomProvider = ref("");
|
||||
const sessionModelApiMode = ref<CodingAgentApiMode>("codex_responses");
|
||||
const pendingSessionModelSwitch = ref<{ model: string; provider: string } | null>(null);
|
||||
|
||||
const sessionModelProfile = computed<string | null>(() => {
|
||||
const session = chatStore.sessions.find((s) => s.id === sessionModelSessionId.value);
|
||||
return session?.profile || null;
|
||||
});
|
||||
|
||||
const sessionModelSession = computed(() =>
|
||||
chatStore.sessions.find((s) => s.id === sessionModelSessionId.value) ||
|
||||
(chatStore.activeSession?.id === sessionModelSessionId.value ? chatStore.activeSession : undefined),
|
||||
);
|
||||
|
||||
const isSessionModelScopedCodingAgent = computed(() =>
|
||||
sessionModelSession.value?.source === "coding_agent" &&
|
||||
sessionModelSession.value?.codingAgentMode !== "global",
|
||||
);
|
||||
const sessionModelCodingAgentId = computed<ChatCodingAgentId | undefined>(() =>
|
||||
sessionModelSession.value?.codingAgentId ||
|
||||
(sessionModelSession.value?.agent === "claude"
|
||||
? "claude-code"
|
||||
: sessionModelSession.value?.agent === "codex"
|
||||
? "codex"
|
||||
: sessionModelSession.value?.agent === "ekko-agent"
|
||||
? "ekko-agent"
|
||||
: undefined),
|
||||
);
|
||||
const isSessionModelExternalCodingAgent = computed(() =>
|
||||
isSessionModelScopedCodingAgent.value &&
|
||||
(sessionModelCodingAgentId.value === "claude-code" || sessionModelCodingAgentId.value === "codex"),
|
||||
);
|
||||
|
||||
const sessionModelBaseGroups = computed(() =>
|
||||
sessionModelProfile.value
|
||||
? getModelGroupsForProfile(sessionModelProfile.value).filter((group) => (
|
||||
!isSessionModelScopedCodingAgent.value || !isCodingAgentAuthProvider(group.provider)
|
||||
))
|
||||
: [],
|
||||
sessionModelProfile.value ? getModelGroupsForProfile(sessionModelProfile.value) : [],
|
||||
);
|
||||
|
||||
const sessionModelProviderOptions = computed(() =>
|
||||
@@ -1176,25 +1024,12 @@ function sessionModelAlias(model: string, provider: string) {
|
||||
return appStore.getModelAlias(model, provider);
|
||||
}
|
||||
|
||||
function defaultSessionModelApiMode(provider: string): CodingAgentApiMode {
|
||||
const group = sessionModelBaseGroups.value.find((item) => item.provider === provider);
|
||||
const providerKey = String(group?.provider || provider || "").toLowerCase();
|
||||
const baseUrl = String(group?.base_url || "").toLowerCase();
|
||||
return normalizeCodingAgentApiMode(
|
||||
group?.api_mode,
|
||||
inferCodingAgentApiMode(providerKey, baseUrl),
|
||||
);
|
||||
}
|
||||
|
||||
async function applySessionModelSwitch(model: string, provider: string, apiMode?: CodingAgentApiMode) {
|
||||
async function applySessionModelSwitch(model: string, provider: string) {
|
||||
if (!sessionModelSessionId.value) return;
|
||||
const ok = await chatStore.switchSessionModel(model, provider, sessionModelSessionId.value, apiMode);
|
||||
const ok = await chatStore.switchSessionModel(model, provider, sessionModelSessionId.value);
|
||||
if (ok) {
|
||||
sessionModelValue.value = model;
|
||||
sessionModelProvider.value = provider;
|
||||
if (apiMode) sessionModelApiMode.value = apiMode;
|
||||
pendingSessionModelSwitch.value = null;
|
||||
showSessionModelModeModal.value = false;
|
||||
showSessionModelModal.value = false;
|
||||
message.success(t("chat.modelSet"));
|
||||
} else {
|
||||
@@ -1205,28 +1040,9 @@ async function applySessionModelSwitch(model: string, provider: string, apiMode?
|
||||
async function selectSessionModel(model: string, provider: string) {
|
||||
const meta = sessionModelBaseGroups.value.find((group) => group.provider === provider)?.model_meta?.[model];
|
||||
if (meta?.disabled || !sessionModelSessionId.value) return;
|
||||
if (isSessionModelExternalCodingAgent.value) {
|
||||
pendingSessionModelSwitch.value = { model, provider };
|
||||
sessionModelApiMode.value = sessionModelSession.value?.provider === provider && sessionModelSession.value.apiMode
|
||||
? normalizeCodingAgentApiMode(sessionModelSession.value.apiMode, defaultSessionModelApiMode(provider))
|
||||
: defaultSessionModelApiMode(provider);
|
||||
showSessionModelModeModal.value = true;
|
||||
return;
|
||||
}
|
||||
await applySessionModelSwitch(model, provider);
|
||||
}
|
||||
|
||||
async function confirmSessionModelMode() {
|
||||
const pending = pendingSessionModelSwitch.value;
|
||||
if (!pending) return;
|
||||
await applySessionModelSwitch(pending.model, pending.provider, sessionModelApiMode.value);
|
||||
}
|
||||
|
||||
function cancelSessionModelMode() {
|
||||
pendingSessionModelSwitch.value = null;
|
||||
showSessionModelModeModal.value = false;
|
||||
}
|
||||
|
||||
async function handleSessionModelCustomSubmit() {
|
||||
const model = sessionModelCustomInput.value.trim();
|
||||
const provider = sessionModelCustomProvider.value;
|
||||
@@ -1589,27 +1405,6 @@ async function handleSessionModelCustomSubmit() {
|
||||
</div>
|
||||
</NModal>
|
||||
|
||||
<NModal
|
||||
v-model:show="showSessionModelModeModal"
|
||||
preset="dialog"
|
||||
:title="t('codingAgents.protocolScope')"
|
||||
:mask-closable="true"
|
||||
style="width: min(420px, calc(100vw - 32px))"
|
||||
>
|
||||
<NSelect
|
||||
v-model:value="sessionModelApiMode"
|
||||
:options="newChatApiModeOptions"
|
||||
/>
|
||||
<template #action>
|
||||
<NButton size="small" @click="cancelSessionModelMode">
|
||||
{{ t('common.cancel') }}
|
||||
</NButton>
|
||||
<NButton size="small" type="primary" @click="confirmSessionModelMode">
|
||||
{{ t('common.confirm') }}
|
||||
</NButton>
|
||||
</template>
|
||||
</NModal>
|
||||
|
||||
<NDrawer
|
||||
v-model:show="showNewChatModal"
|
||||
class="new-chat-drawer"
|
||||
@@ -1619,26 +1414,6 @@ async function handleSessionModelCustomSubmit() {
|
||||
>
|
||||
<NDrawerContent :title="t('chat.newChat')" closable>
|
||||
<div class="new-chat-form">
|
||||
<label class="new-chat-field">
|
||||
<span class="new-chat-label">{{ t("chat.agent") }}</span>
|
||||
<NSelect
|
||||
v-model:value="newChatAgent"
|
||||
:options="newChatAgentOptions"
|
||||
:disabled="newChatLoading"
|
||||
/>
|
||||
</label>
|
||||
<label v-if="isNewChatExternalCodingAgent" class="new-chat-field">
|
||||
<span class="new-chat-label">{{ t("codingAgents.launchModeScope") }}</span>
|
||||
<NRadioGroup v-model:value="newChatAgentMode" name="new-chat-coding-agent-mode">
|
||||
<NRadioButton
|
||||
v-for="option in newChatAgentModeOptions"
|
||||
:key="option.value"
|
||||
:value="option.value"
|
||||
>
|
||||
{{ option.label }}
|
||||
</NRadioButton>
|
||||
</NRadioGroup>
|
||||
</label>
|
||||
<label class="new-chat-field">
|
||||
<span class="new-chat-label">{{ t("sidebar.profiles") }}</span>
|
||||
<NSelect
|
||||
@@ -1648,7 +1423,7 @@ async function handleSessionModelCustomSubmit() {
|
||||
@update:value="handleNewChatProfileChange"
|
||||
/>
|
||||
</label>
|
||||
<label v-if="newChatUsesProviderModel" class="new-chat-field">
|
||||
<label class="new-chat-field">
|
||||
<span class="new-chat-label">{{ t("models.provider") }}</span>
|
||||
<NSelect
|
||||
:value="newChatProvider"
|
||||
@@ -1657,7 +1432,7 @@ async function handleSessionModelCustomSubmit() {
|
||||
@update:value="handleNewChatProviderChange"
|
||||
/>
|
||||
</label>
|
||||
<label v-if="newChatUsesProviderModel" class="new-chat-field">
|
||||
<label class="new-chat-field">
|
||||
<span class="new-chat-label">{{ t("models.models") }}</span>
|
||||
<NSelect
|
||||
v-model:value="newChatModel"
|
||||
@@ -1666,30 +1441,6 @@ async function handleSessionModelCustomSubmit() {
|
||||
filterable
|
||||
/>
|
||||
</label>
|
||||
<label v-if="isNewChatExternalCodingAgent && effectiveNewChatAgentMode === 'scoped'" class="new-chat-field">
|
||||
<span class="new-chat-label">{{ t("codingAgents.protocolScope") }}</span>
|
||||
<NSelect
|
||||
v-model:value="newChatApiMode"
|
||||
:options="newChatApiModeOptions"
|
||||
:disabled="newChatLoading"
|
||||
/>
|
||||
</label>
|
||||
<label v-if="newChatNeedsBaseUrl" class="new-chat-field">
|
||||
<span class="new-chat-label">{{ t("models.baseUrl") }}</span>
|
||||
<NInput
|
||||
v-model:value="newChatBaseUrl"
|
||||
:placeholder="t('models.baseUrlPlaceholder')"
|
||||
/>
|
||||
</label>
|
||||
<label v-if="newChatNeedsApiKey" class="new-chat-field">
|
||||
<span class="new-chat-label">{{ t("models.apiKey") }}</span>
|
||||
<NInput
|
||||
v-model:value="newChatApiKey"
|
||||
type="password"
|
||||
show-password-on="click"
|
||||
:placeholder="t('models.apiKeyPlaceholder')"
|
||||
/>
|
||||
</label>
|
||||
<div class="new-chat-field">
|
||||
<span class="new-chat-label">
|
||||
{{ t("chat.workspace") }}
|
||||
|
||||
Reference in New Issue
Block a user