From 285f623d6f328bfcef192938841d3082fb23dd88 Mon Sep 17 00:00:00 2001 From: ekko <152005280+EKKOLearnAI@users.noreply.github.com> Date: Fri, 29 May 2026 19:06:54 +0800 Subject: [PATCH] =?UTF-8?q?[codex]=20=E4=BF=AE=E5=A4=8D=20Coding=20Agents?= =?UTF-8?q?=20=E7=9A=84=20Codex=20=E5=90=AF=E5=8A=A8=E5=92=8C=E4=BB=A3?= =?UTF-8?q?=E7=90=86=E9=9A=94=E7=A6=BB=20(#1123)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: add coding agent install status * chore: add latest claude opus model preset * feat: add coding agent config editing * Add scoped coding agent launch proxy * Add Codex proxy plan * fix coding agents codex launch proxy * fix codex catalog context test --------- Co-authored-by: Codex --- docs/codex-config-proxy-plan.md | 158 +++ .../public/coding-agents/claude-code.svg | 7 + .../public/coding-agents/codex-openai.png | Bin 0 -> 39863 bytes packages/client/src/api/coding-agents.ts | 133 +++ .../components/hermes/chat/TerminalPanel.vue | 55 +- .../src/components/layout/AppSidebar.vue | 8 + packages/client/src/i18n/locales/en.ts | 77 ++ packages/client/src/i18n/locales/zh.ts | 77 ++ packages/client/src/router/index.ts | 5 + .../src/views/hermes/CodingAgentsView.vue | 953 +++++++++++++++++ .../server/src/controllers/coding-agents.ts | 119 +++ .../server/src/routes/claude-code-proxy.ts | 7 + packages/server/src/routes/codex-proxy.ts | 7 + packages/server/src/routes/coding-agents.ts | 12 + packages/server/src/routes/index.ts | 6 + .../server/src/services/claude-code-proxy.ts | 995 ++++++++++++++++++ packages/server/src/services/codex-proxy.ts | 908 ++++++++++++++++ packages/server/src/services/coding-agents.ts | 958 +++++++++++++++++ packages/server/src/shared/providers.ts | 1 + tests/server/coding-agents-launch.test.ts | 676 ++++++++++++ 20 files changed, 5158 insertions(+), 4 deletions(-) create mode 100644 docs/codex-config-proxy-plan.md create mode 100644 packages/client/public/coding-agents/claude-code.svg create mode 100644 packages/client/public/coding-agents/codex-openai.png create mode 100644 packages/client/src/api/coding-agents.ts create mode 100644 packages/client/src/views/hermes/CodingAgentsView.vue create mode 100644 packages/server/src/controllers/coding-agents.ts create mode 100644 packages/server/src/routes/claude-code-proxy.ts create mode 100644 packages/server/src/routes/codex-proxy.ts create mode 100644 packages/server/src/routes/coding-agents.ts create mode 100644 packages/server/src/services/claude-code-proxy.ts create mode 100644 packages/server/src/services/codex-proxy.ts create mode 100644 packages/server/src/services/coding-agents.ts create mode 100644 tests/server/coding-agents-launch.test.ts diff --git a/docs/codex-config-proxy-plan.md b/docs/codex-config-proxy-plan.md new file mode 100644 index 0000000..d504b11 --- /dev/null +++ b/docs/codex-config-proxy-plan.md @@ -0,0 +1,158 @@ +# Codex Config And Proxy Plan + +## Goals + +- Launch Codex from Hermes without mutating the user's real `~/.codex`. +- Keep model/provider config isolated by Hermes profile and provider. +- Support OpenAI Responses providers directly. +- Add a local adapter for providers that only expose OpenAI Chat Completions. +- Keep Codex resume/history stable by using a consistent model provider id. + +## Directory Layout + +All generated Codex state should live under the Web UI home: + +```text +~/.hermes-web-ui/coding-agent/ + model/{profile}/{provider}/codex/ + config.toml + auth.json + AGENTS.md + workspace/{profile}/{provider}/ +``` + +Launch command shape: + +```bash +cd ~/.hermes-web-ui/coding-agent/workspace/{profile}/{provider} \ + && CODEX_HOME=~/.hermes-web-ui/coding-agent/model/{profile}/{provider}/codex \ + codex --model {model} +``` + +## Current MVP Config + +For Responses-compatible providers, generate: + +```toml +model_provider = "custom" +model = "provider-model-id" +disable_response_storage = true + +[model_providers.custom] +name = "provider-id" +base_url = "https://provider.example/v1" +wire_api = "responses" +requires_openai_auth = false +experimental_bearer_token = "provider-api-key" +``` + +Keep `auth.json` empty for third-party providers: + +```json +{} +``` + +Reason: avoid overwriting the user's official Codex / ChatGPT login cache. + +## Stable Provider Id + +Use `model_provider = "custom"` for third-party providers. + +Codex history and resume behavior can depend on provider identity. Keeping a stable provider id avoids making history appear to move between provider-specific ids. + +Provider identity remains visible in: + +- `[model_providers.custom].name` +- the generated directory path +- the UI launch result + +## Local Proxy Plan + +Some providers expose only OpenAI Chat Completions: + +```text +/v1/chat/completions +``` + +Codex prefers Responses: + +```text +/v1/responses +``` + +Add a local proxy endpoint: + +```text +/api/codex-proxy/{routeKey}/v1/responses +``` + +The `routeKey` should encode: + +```text +profile + "\0" + provider + "\0" + model +``` + +Authentication should use a generated `hwui_...` token, not the upstream provider key. + +## Responses To Chat Mapping + +When the upstream provider is Chat Completions only: + +- Convert Responses `input` items to Chat `messages`. +- Convert Responses `tools` to Chat `tools`. +- Convert `max_output_tokens` to `max_tokens`. +- Preserve `stream: true`. +- Map function calls and function outputs both ways. + +Response event mapping for streaming: + +```text +chat delta.content -> response.output_text.delta +chat delta.tool_calls -> response.function_call_arguments.delta +finish -> response.completed +``` + +Non-streaming mapping: + +```text +chat.choices[0].message.content -> output message/content +chat.choices[0].message.tool_calls -> output function_call items +chat.usage -> response.usage +``` + +## Config Generation With Proxy + +For Chat-only providers, generated Codex config should point at Hermes: + +```toml +model_provider = "custom" +model = "provider-model-id" +disable_response_storage = true + +[model_providers.custom] +name = "provider-id" +base_url = "http://127.0.0.1:{serverPort}/api/codex-proxy/{routeKey}/v1" +wire_api = "responses" +requires_openai_auth = false +experimental_bearer_token = "hwui_generated_route_token" +``` + +The proxy then forwards to the real provider with the real provider key. + +## Implementation Tasks + +1. Add a Codex proxy service parallel to the Claude Code proxy service. +2. Register route targets in memory for launch-time provider/model selection. +3. Add `/api/codex-proxy/:key/v1/responses`. +4. Implement Responses to Chat conversion. +5. Implement Chat to Responses conversion for streaming and non-streaming. +6. Add launch-time api mode selection for Codex providers. +7. Generate Codex `base_url` against the local proxy when api mode is Chat Completions. +8. Add server tests for config generation, auth rejection, streaming conversion, tool call conversion, and error passthrough. + +## Open Questions + +- Whether to expose Codex protocol selection in the UI immediately or infer it from provider preset metadata. +- Whether to persist proxy targets or require relaunch after server restart. +- Whether model catalog generation is needed for the first Codex MVP. +- Whether MCP and `AGENTS.md` should be copied from a template or edited only through the advanced config editor. diff --git a/packages/client/public/coding-agents/claude-code.svg b/packages/client/public/coding-agents/claude-code.svg new file mode 100644 index 0000000..b4a2131 --- /dev/null +++ b/packages/client/public/coding-agents/claude-code.svg @@ -0,0 +1,7 @@ + + Claude Code + + \ No newline at end of file diff --git a/packages/client/public/coding-agents/codex-openai.png b/packages/client/public/coding-agents/codex-openai.png new file mode 100644 index 0000000000000000000000000000000000000000..44fc39cafd24ce53609847f60eb5de75650cb51c GIT binary patch literal 39863 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 tg|Ae3xi4j#DL + shellCommand: string + files: Array<{ key: string; path: string; absolutePath: string }> +} + +export interface CodingAgentNativeLaunchResult extends CodingAgentLaunchResult { + nativeTerminal: true + terminal: string +} + +export async function fetchCodingAgentsStatus(): Promise { + return request('/api/coding-agents') +} + +export async function installCodingAgent(id: CodingAgentId): Promise { + return request(`/api/coding-agents/${id}/install`, { method: 'POST' }) +} + +export async function deleteCodingAgent(id: CodingAgentId): Promise { + return request(`/api/coding-agents/${id}`, { method: 'DELETE' }) +} + +export async function readCodingAgentConfigFile( + id: CodingAgentId, + key: string, + scope: CodingAgentConfigScope = {}, +): Promise { + const params = new URLSearchParams() + if (scope.profile) params.set('profile', scope.profile) + if (scope.provider) params.set('provider', scope.provider) + const query = params.toString() + return request( + `/api/coding-agents/${id}/config-files/${encodeURIComponent(key)}${query ? `?${query}` : ''}`, + ) +} + +export async function writeCodingAgentConfigFile( + id: CodingAgentId, + key: string, + content: string, + scope: CodingAgentConfigScope = {}, +): Promise { + return request(`/api/coding-agents/${id}/config-files/${encodeURIComponent(key)}`, { + method: 'PUT', + body: JSON.stringify({ content, profile: scope.profile, provider: scope.provider }), + }) +} + +export async function prepareCodingAgentLaunch( + id: CodingAgentId, + data: CodingAgentLaunchRequest, +): Promise { + return request(`/api/coding-agents/${id}/launch/prepare`, { + method: 'POST', + body: JSON.stringify(data), + }) +} + +export async function launchCodingAgentNativeTerminal( + id: CodingAgentId, + data: CodingAgentLaunchRequest, +): Promise { + return request(`/api/coding-agents/${id}/launch/native`, { + method: 'POST', + body: JSON.stringify(data), + }) +} diff --git a/packages/client/src/components/hermes/chat/TerminalPanel.vue b/packages/client/src/components/hermes/chat/TerminalPanel.vue index 9747c94..f2fdb80 100644 --- a/packages/client/src/components/hermes/chat/TerminalPanel.vue +++ b/packages/client/src/components/hermes/chat/TerminalPanel.vue @@ -12,7 +12,7 @@ import type { ITheme } from "@xterm/xterm"; const { t } = useI18n(); const message = useMessage(); -const props = defineProps<{ visible?: boolean }>(); +const props = defineProps<{ visible?: boolean; initialCommand?: string }>(); // ─── Terminal themes ──────────────────────────────────────────── @@ -106,6 +106,7 @@ const MAX_RECONNECT_ATTEMPTS = 3; let touchScrollLastY: number | null = null; let touchScrollRemainder = 0; const TOUCH_SCROLL_LINE_PX = 18; +const initialCommandSent = ref(false); // ─── Computed ────────────────────────────────────────────────── @@ -224,6 +225,7 @@ function handleControl(msg: any) { exited: false, }); switchSession(msg.id); + runInitialCommand(); break; case "exited": { @@ -251,6 +253,15 @@ function createSession() { send({ type: "create" }); } +function runInitialCommand() { + const command = props.initialCommand?.trim(); + if (!command || initialCommandSent.value) return; + initialCommandSent.value = true; + setTimeout(() => { + send(`${command}\r`); + }, 100); +} + function getOrCreateTerm(id: string): { term: Terminal; fitAddon: FitAddon } { let entry = termMap.get(id); if (!entry) { @@ -570,8 +581,11 @@ onUnmounted(() => { .terminal-panel-drawer { display: flex; height: 100%; + width: 100%; min-height: 0; + min-width: 0; position: relative; + overflow: hidden; } .sidebar-overlay { @@ -764,9 +778,11 @@ onUnmounted(() => { display: flex; align-items: center; justify-content: space-between; + gap: 10px; padding: 12px 16px; border-bottom: 1px solid $border-color; flex-shrink: 0; + min-width: 0; } .header-session-title { @@ -783,6 +799,7 @@ onUnmounted(() => { align-items: center; gap: 8px; flex-shrink: 0; + min-width: 0; } .theme-select { @@ -800,12 +817,15 @@ onUnmounted(() => { margin: 8px; overflow: hidden; min-height: 0; + min-width: 0; display: flex; flex-direction: column; } .terminal-xterm { flex: 1; + min-height: 0; + min-width: 0; border-radius: $radius-md; overflow: hidden; border: 1px solid $border-color; @@ -842,19 +862,46 @@ onUnmounted(() => { @media (max-width: $breakpoint-mobile) { .terminal-panel-drawer { - height: calc(100 * var(--vh)); - max-height: calc(100 * var(--vh)); + height: 100%; + max-height: 100%; } .terminal-main { min-height: 0; + min-width: 0; + } + + .terminal-header { + padding: 8px; + gap: 6px; + } + + .header-session-title { + display: none; + } + + .header-actions { + width: 100%; + justify-content: flex-end; + gap: 6px; + } + + .theme-select { + width: 96px; } .terminal-container { - margin-bottom: calc(8px + env(safe-area-inset-bottom, 0px)); + margin: 6px; + margin-bottom: calc(6px + env(safe-area-inset-bottom, 0px)); } .terminal-xterm { + border-radius: $radius-sm; + + :deep(.xterm) { + padding: 6px; + } + :deep(.xterm-viewport), :deep(.xterm-scrollable-element) { touch-action: pan-y; diff --git a/packages/client/src/components/layout/AppSidebar.vue b/packages/client/src/components/layout/AppSidebar.vue index 13e128a..27a8262 100644 --- a/packages/client/src/components/layout/AppSidebar.vue +++ b/packages/client/src/components/layout/AppSidebar.vue @@ -263,6 +263,14 @@ function openChangelog() {