Skip to content

Latest commit

 

History

History
573 lines (403 loc) · 20.8 KB

File metadata and controls

573 lines (403 loc) · 20.8 KB

Handoff: Cursor Extension / Codex Proxy UX Research

Generated: 2026-05-24

Workspace Prepared

This handoff workspace is:

/home/myadmin/cursor-extension-handoff

It contains:

  • HANDOFF.md — this document.
  • .gitignore — ignores .sources/.
  • .sources/cursor-azure-proxy — clone of this project at committed main only.
  • .sources/ungate — shallow clone of Orchid's Ungate extension.

Important: .sources/cursor-azure-proxy is a clean git clone and does not include the current active working-tree edits in /home/myadmin/cursor-azure-proxy. Those edits are described below and should be copied/applied intentionally if needed.

Required by the handoff skill, an empty tempfile was created and read before writing this real handoff:

/tmp/handoff-salLCs.md

Source Metadata

Current active proxy repo:

  • Path: /home/myadmin/cursor-azure-proxy
  • Remote: https://github.com/gabrii/Cursor-Azure-GPT-5.git
  • Branch: main
  • HEAD: 236d2e98071d281f1b3199fb49aa7a90599e6c84
  • Dirty: yes, substantial uncommitted work.

Prepared clean proxy clone:

  • Path: /home/myadmin/cursor-extension-handoff/.sources/cursor-azure-proxy
  • Source: local clone from /home/myadmin/cursor-azure-proxy
  • Does not include active dirty changes.

Ungate clone:

  • Path: /home/myadmin/cursor-extension-handoff/.sources/ungate
  • Remote: https://github.com/orchidfiles/ungate.git
  • Branch: main
  • HEAD: f53e2ed704e711a5732ca58b96eb2b82e302d7d4

User's Latest Direction

The user asked:

what if we took all superiour aspects of em all, and created from 0 a new cursor extension, for top UX but functionality also

A mode switch to Plan was attempted and rejected by the user. Do not try to switch modes again in this conversation context. The next session should continue in the current mode and can discuss/plan inline.

Then user requested:

/handoff with most details possible, leave the handoff in a new dir already redy for the work (../(newdir)/...) and also clone our project and the extension on in the new dir under .sources/ (git ignored)+

This document fulfils that request.

Project Context

The active project is an LLM proxy for Cursor. Cursor requests in this environment are themselves routed through this proxy, so Docker restarts can interrupt the active agent.

Repository purpose:

  • Accept Cursor-compatible OpenAI requests.
  • Adapt to Azure Responses API or ChatGPT/Codex backend.
  • Forward to Azure/Codex.
  • Adapt streamed responses back to Cursor-compatible Chat Completions SSE.

Main user goal during this thread:

  • Investigate and fix GitHub issue #96: reasoning/thinking appears incorrectly or not at all in Cursor.
  • User wanted native Cursor built-in thinking blocks, ideally 1:1 with Cursor BYOK/native model UX.
  • Multiple attempts showed Cursor's BYOK/custom OpenAI path ignores native reasoning fields.
  • Current practical result: configurable reasoning display mode, with user's local config set to hide thinking (none).

Issue:

  • https://github.com/gabrii/Cursor-Azure-GPT-5/issues/96
  • Current state: open.
  • Comment was added documenting the limitation and display modes: https://github.com/gabrii/Cursor-Azure-GPT-5/issues/96#issuecomment-4529274787
  • User explicitly said not to write anything else in the issue when researching Orchid/Ungate.

Important Current Dirty Worktree State

Active repo /home/myadmin/cursor-azure-proxy has uncommitted changes. Latest git status --short showed:

 M .env.example
 M README.md
 M app/azure/response_adapter.py
 M app/codex/adapter.py
 M app/codex/request_adapter.py
 M app/codex/response_adapter.py
 M app/codex/settings.py
 M app/settings.py
 M tests/recordings/default_recording/downstream_response.sse
 M tests/recordings/empty_tools/downstream_response.sse
 M tests/recordings/multiple_ping_pongs/downstream_response.sse
 M tests/recordings/one_ping_pong/downstream_response.sse
 M tests/recordings/reply_parallel_tool_call/downstream_response.sse
 M tests/recordings/reply_single_tool_call/downstream_response.sse
 M tests/recordings/sse_without_closing_new_lines/downstream_response.sse
 M tests/recordings/verbosity_level/downstream_response.sse
 M tests/settings.py
 M tests/test_codex_request_adapter.py
 M tests/test_codex_response_adapter.py
 M tests/test_response_adapter.py
?? app/reasoning_display.py
?? env

Notes:

  • .env is modified but not shown by git status; it is likely ignored. It contains secrets. Do not copy or quote secrets.
  • Untracked env exists and predates or is unrelated; do not assume it should be committed.
  • app/codex/request_adapter.py and tests/test_codex_request_adapter.py had pre-existing/unrelated changes retained per user instruction. They strip unsupported chat controls from Codex Responses-shaped payloads and add tests. Do not revert.

Current diff stat from active repo:

 .env.example                                       |    6 +
 README.md                                          |   12 +-
 app/azure/response_adapter.py                      |   83 +-
 app/codex/adapter.py                               |    4 +-
 app/codex/request_adapter.py                       |   26 +
 app/codex/response_adapter.py                      |   51 +-
 app/codex/settings.py                              |    5 +
 app/settings.py                                    |    7 +
 tests/recordings/default_recording/downstream_response.sse      |    8 +-
 tests/recordings/empty_tools/downstream_response.sse |    8 +-
 tests/recordings/multiple_ping_pongs/downstream_response.sse    |    8 +-
 tests/recordings/one_ping_pong/downstream_response.sse          |    8 +-
 tests/recordings/reply_parallel_tool_call/downstream_response.sse | 114 +-
 tests/recordings/reply_single_tool_call/downstream_response.sse | 1414 ++++++++++----------
 tests/recordings/sse_without_closing_new_lines/downstream_response.sse | 8 +-
 tests/recordings/verbosity_level/downstream_response.sse | 142 +-
 tests/settings.py                                  |    1 +
 tests/test_codex_request_adapter.py                |   29 +
 tests/test_codex_response_adapter.py               |   85 +-
 tests/test_response_adapter.py                     |  118 +-
 20 files changed, 1207 insertions(+), 930 deletions(-)

Reasoning / Thinking Work Completed

Initial Behavior

Original adapters flattened reasoning into visible <think> text in delta.content. Cursor displayed that as normal chat content, which was wrong.

Native Field Attempts

Tried emitting native fields:

  • delta.reasoning_content
  • delta.reasoning
  • delta.reasoning_details
  • delta.thinking_blocks
  • delta.provider_specific_fields.thinking_blocks
  • content: null on reasoning chunks

User visually confirmed no native Cursor thinking block appeared. The response showed no thinking blocks at all.

Markdown Fallback

Implemented Markdown <details> fallback. User confirmed it worked visually but looked bad and sometimes unreliable. User explicitly said markdown dropdowns are ugly and not 1:1 Cursor UX.

Current Configurable Display Mode

Approved design and implemented a configurable env var:

REASONING_DISPLAY_MODE

Modes:

  • none: hide thinking text from normal chat output, preserve native reasoning metadata only. This is now the user's local config.
  • mdthinkblocks: Markdown <details> fallback. Documented as experimental/new/unstable, looks bad, may fail to show thinking consistently.
  • thinkblocks: legacy visible <think>...</think> chat content.

New helper file:

app/reasoning_display.py

Functions:

  • parse_reasoning_display_mode(value)
  • reasoning_start_delta(mode)
  • reasoning_end_delta(mode)
  • reasoning_content_delta(text, mode)

The helper always preserves native metadata:

  • reasoning
  • reasoning_content
  • reasoning_details
  • thinking_blocks
  • provider_specific_fields.thinking_blocks

Codex wiring:

  • app/codex/adapter.py passes self.settings.reasoning_display_mode into adapt_responses_sse_to_chat_sse.
  • app/codex/settings.py exposes reasoning_display_mode from Flask config.
  • app/codex/response_adapter.py uses shared display helper.

Azure wiring:

  • app/azure/response_adapter.py uses shared display helper and reads Flask config per stream.

Docs/config:

  • .env.example documents the three modes and explicitly warns md blocks are unstable/ugly/unreliable.
  • README.md has a “Reasoning Display Mode” section.
  • Active local .env was set to REASONING_DISPLAY_MODE=none and Docker was rebuilt/restarted.

Verification after this work:

  • source .venv/bin/activate && pytest -k "" -> 112 passed, 1 warning
  • source .venv/bin/activate && flask lint -> passed; black reformatted app/reasoning_display.py
  • Docker restarted with docker compose up -d --build && sleep 10
  • Container was healthy.

GitHub Issue Update

A comment was posted to issue #96:

https://github.com/gabrii/Cursor-Azure-GPT-5/issues/96#issuecomment-4529274787

Content summary:

  • Native Cursor thinking blocks do not appear for BYOK/proxy Chat Completions even with common reasoning metadata fields.
  • Markdown <details> exists but experimental/unstable/ugly.
  • REASONING_DISPLAY_MODE added with none, mdthinkblocks, thinkblocks.
  • Issue intentionally left open in case future Cursor supports native reasoning metadata for BYOK/custom providers.

Later user asked to inspect Orchid/Ungate but explicitly: “dont write anything in the issue”. Do not post further comments unless explicitly asked.

Orchid / Ungate Research

User asked about Orchid's issue comment:

@flowtyone Try Ungate extension: https://github.com/orchidfiles/ungate

We cloned:

/tmp/ungate-inspect

and for handoff:

/home/myadmin/cursor-extension-handoff/.sources/ungate

High-Level Ungate Architecture

Ungate is a Cursor/VS Code extension plus local proxy plus Cloudflare Quick Tunnel.

Components:

  • apps/extension: VS Code/Cursor extension host.
  • apps/api: local Fastify OpenAI-compatible proxy.
  • apps/web: Svelte dashboard.
  • packages/shared: shared types/schemas/constants.

README says Cursor backend cannot call localhost, so a tunnel is required:

Cursor chat -> Cursor backend -> Cloudflare tunnel -> Ungate proxy -> Provider API

It does not patch Cursor's renderer or private chat UI. It uses Cursor’s custom OpenAI Base URL.

Ungate Tunnel Generation

Relevant file:

apps/extension/src/tunnel-manager.ts

Mechanism:

  1. Local API server starts and exposes a port.
  2. User clicks dashboard “Start tunnel”.
  3. Extension calls TunnelManager.start(port).
  4. TunnelManager.ensureBinary() checks cloudflared:
    • dev package binary (bin from cloudflared npm package)
    • ~/.ungate/bin/cloudflared
    • otherwise downloads via install(CLOUDFLARED_BIN_PATH).
  5. It starts Cloudflare Quick Tunnel:
const t = Tunnel.quick(`http://localhost:${port}`, {
  '--config': '/dev/null',
  '--edge-ip-version': '4'
});
  1. Listens for url event:
t.on('url', (url) => {
  this.setState({ status: 'running', url, error: null });
});
  1. Dashboard displays ${url}/v1 and copies that into Cursor OpenAI Base URL.

Important properties:

  • Uses Cloudflare Quick Tunnel; no Cloudflare login/config required.
  • URL is ephemeral and changes on restart.
  • Stores tunnel state in runtime file so multiple Cursor windows coordinate.
  • Auto-stops tunnel when no live clients remain.

Ungate Codex/OpenAI Subscription Path

Relevant files:

  • apps/api/src/routes/openai.ts
  • apps/api/src/orchestration/openai/provider-handlers/openai-mapped-chat-handler.ts
  • apps/api/src/proxy/proxy-client.ts
  • apps/api/src/proxy/openai-client.ts
  • apps/api/src/proxy/responses-input-normalizer/*
  • apps/api/src/proxy/responses-stream-mapper/*

Ungate has one /v1/chat/completions route. It resolves a model mapping and routes:

  1. MiniMax if mapping/prefix says MiniMax.
  2. OpenAI/Codex mapped provider if mapping provider is openai.
  3. Claude as default.

Codex/OpenAI subscription path is OpenAiMappedChatHandler -> proxyOpenAIRequest -> OpenAiClient.proxy.

Upstream:

${config.openai.codexUrl}/responses

Headers include:

  • authorization: Bearer <access token>
  • chatgpt-account-id
  • originator: codex_cli_rs
  • accept: text/event-stream
  • content-type: application/json

We did not see equivalent session/thread propagation to our session_id, thread_id, x-client-request-id handling.

Codex-Only Comparison: Our Proxy vs Ungate

Our strengths:

  • Explicit /codex route; no provider inference.
  • Uses existing codex login auth file (~/.codex/auth.json).
  • Robust token refresh with lock and known refresh error codes.
  • Preserves Cursor conversation/session identity headers upstream.
  • Handles Codex reasoning SSE events and preserves metadata.
  • Configurable reasoning display modes.
  • Explicit model allowlist and rewrites.
  • Simpler for server/Docker deployment.

Ungate strengths:

  • Better onboarding UX: extension/dashboard/status bar.
  • Cloudflare tunnel manager.
  • Model mapping UI.
  • OAuth flow inside dashboard.
  • More decomposed input normalizer (CodexInputUtils, ResponsesInputShape, ResponsesInputText).
  • More robust stream state for tool calls: pending map by output index, processed item IDs, deferred names, diagnostics counts.

Ungate weaknesses for Codex:

  • Its Responses stream mapper did not handle reasoning events (response.reasoning_text.delta, response.reasoning_summary_text.delta, etc.). It maps text/tools/completed only.
  • It likely drops Codex reasoning.
  • No evidence of native Cursor thinking UI support.
  • Larger security/breakage surface: OAuth DB, tunnel, extension, binary downloads, Cursor key-fix private DB hack.

Best things to borrow:

  1. Optional tunnel helper / extension dashboard.
  2. Stream state machine for tool calls keyed by output_index.
  3. Input normalizer decomposition.
  4. Model suffix reasoning aliases (gpt-5.4-high -> gpt-5.4 + reasoning.effort=high).
  5. Dashboard/status UX eventually.

Do not borrow:

  • Their Codex reasoning mapper as-is.
  • Assumption that reasoning_content produces native Cursor UI.
  • Auth-disabled tunnel behavior.
  • Private Cursor DB/command hack unless explicitly desired.

Proposed Future Direction: New Extension From Scratch

The user asked about creating a new Cursor extension “from 0” combining superior aspects of both repos.

Best concept:

A Cursor extension wrapping the current proxy’s stronger Codex protocol implementation with Ungate-like UX.

Possible name TBD.

Product Goal

A top-UX Cursor extension for ChatGPT/Codex subscription usage in Cursor, with robust protocol behavior:

  • Extension starts local proxy automatically.
  • Extension manages public tunnel automatically.
  • Dashboard shows status, model list, base URL, proxy API key, logs.
  • User authenticates via existing Codex CLI auth initially; possibly later via built-in OAuth.
  • Codex request/response handling reuses or ports this repo’s stronger logic.
  • Reasoning display mode selectable in dashboard.
  • Native Cursor thinking support remains explicitly unresolved until Cursor supports BYOK metadata.

MVP Scope Recommendation

MVP should be Codex-only. Do not include Azure, Claude, MiniMax, or multi-provider abstraction yet.

MVP pieces:

  1. VS Code/Cursor extension shell.
  2. Local API process embedded or spawned.
  3. Cloudflare Quick Tunnel helper.
  4. Dashboard with:
    • API status
    • tunnel status and copy URL
    • auth status (codex login file found/valid)
    • supported models and rewrites
    • reasoning display mode
    • logs
  5. Local proxy route:
    • /v1/models
    • /v1/chat/completions
    • maybe /ready
  6. Codex auth from ~/.codex/auth.json first.
  7. Reuse this repo’s Codex request adapter and response adapter behavior.
  8. Preserve conversation identity headers.
  9. Optional model rewrite UI.

Defer:

  • Built-in OpenAI OAuth.
  • Azure support.
  • Multi-provider support.
  • Cursor OpenAI-key auto-fix hack.
  • Native Cursor UI patching.
  • Analytics beyond simple request logs.

Architecture Options

Option A: TypeScript all-in-one extension, port Python proxy logic to TS.

Pros:

  • Single extension package.
  • Easier distribution via Open VSX.
  • Better integration with webview/tunnel/status.

Cons:

  • Need to reimplement tested Python protocol behavior.
  • Higher risk of subtle stream bugs.

Option B: Extension launches Python/Flask proxy from this repo.

Pros:

  • Reuse existing tested code.
  • Faster MVP.
  • Lower protocol regression risk.

Cons:

  • Packaging Python, venv, dependencies, Docker/no-Docker complexity.
  • Less elegant marketplace install.

Option C: Extract a protocol core first, then extension.

Pros:

  • Best long-term architecture.
  • Codex proxy logic becomes reusable/testable.

Cons:

  • More work before user-visible UX.

Recommendation: Option A for a clean greenfield product if enough time; Option B for fastest proof-of-concept. Since user said “from 0” and “top UX but functionality also,” Option A with careful test porting is probably ideal.

Suggested Implementation Plan for Next Session

  1. Confirm whether MVP is Codex-only and TypeScript extension-first.
  2. Decide new repo path under /home/myadmin/cursor-extension-handoff or sibling.
  3. Draft architecture/spec in docs/ before coding if user wants thorough design.
  4. Build minimal extension scaffold:
    • package.json
    • extension activation
    • status bar
    • webview dashboard
  5. Add local Fastify server:
    • /health
    • /v1/models
    • /v1/chat/completions
  6. Port Codex auth reader/refresh from Python to TS.
  7. Port request adapter with tests from current repo.
  8. Port response SSE adapter with tests from current repo.
  9. Add Cloudflare Quick Tunnel manager from Ungate pattern.
  10. Add settings persistence and UI.
  11. Test in Cursor manually.

Suggested Skills for Next Session

Use these skills if available/relevant:

  • brainstorming — the user is asking for a new product/extension; design needs approval.
  • prototype — useful for quickly proving a VS Code/Cursor extension shell + local server + tunnel UX.
  • test-driven-development — important when porting Codex request/response adapters.
  • systematic-debugging — for Cursor/tunnel/runtime integration issues.
  • verification-before-completion — before claiming extension/server/tunnel works.
  • requesting-code-review — if a major extension MVP is implemented.
  • create-skill / write-a-skill not needed unless user asks to package this workflow.

The manually attached handoff skill was used for this document.

Verification Commands Already Run In Active Proxy Repo

After reasoning-mode work:

source .venv/bin/activate && pytest -k ""
# 112 passed, 1 warning

source .venv/bin/activate && flask lint
# passed; black reformatted app/reasoning_display.py

docker compose up -d --build && sleep 10
# container healthy

Latest Docker status after setting local REASONING_DISPLAY_MODE=none:

  • cursor-azure-proxy-flask-1 healthy
  • 127.0.0.1:5000->5000/tcp

Cautions for Next Agent

  • Do not post to GitHub issue #96 unless explicitly requested.
  • Do not expose .env secrets. The active .env contains real keys.
  • Do not revert unrelated dirty changes in app/codex/request_adapter.py or tests/test_codex_request_adapter.py.
  • Remember the user dislikes Markdown reasoning blocks and currently prefers none locally.
  • Native Cursor thinking blocks are not solved; avoid claiming they are.
  • If restarting Docker, include && sleep 10 to reduce agent interruption risk, per prior user request.
  • If creating a new extension repo, ensure .sources/ remains gitignored and do not vendor/clobber source clones unless intended.

Key Files To Read First Next Session

Active proxy:

  • /home/myadmin/cursor-azure-proxy/app/codex/adapter.py
  • /home/myadmin/cursor-azure-proxy/app/codex/request_adapter.py
  • /home/myadmin/cursor-azure-proxy/app/codex/response_adapter.py
  • /home/myadmin/cursor-azure-proxy/app/codex/auth_state.py
  • /home/myadmin/cursor-azure-proxy/app/codex/upstream.py
  • /home/myadmin/cursor-azure-proxy/app/reasoning_display.py
  • /home/myadmin/cursor-azure-proxy/tests/test_codex_response_adapter.py
  • /home/myadmin/cursor-azure-proxy/tests/test_codex_request_adapter.py

Ungate clone:

  • /home/myadmin/cursor-extension-handoff/.sources/ungate/apps/extension/src/tunnel-manager.ts
  • /home/myadmin/cursor-extension-handoff/.sources/ungate/apps/extension/src/extension-controller.ts
  • /home/myadmin/cursor-extension-handoff/.sources/ungate/apps/api/src/routes/openai.ts
  • /home/myadmin/cursor-extension-handoff/.sources/ungate/apps/api/src/proxy/openai-client.ts
  • /home/myadmin/cursor-extension-handoff/.sources/ungate/apps/api/src/proxy/responses-input-normalizer/build-body.ts
  • /home/myadmin/cursor-extension-handoff/.sources/ungate/apps/api/src/proxy/codex-input-utils.ts
  • /home/myadmin/cursor-extension-handoff/.sources/ungate/apps/api/src/proxy/responses-stream-mapper/responses-event-router.ts
  • /home/myadmin/cursor-extension-handoff/.sources/ungate/apps/web/src/features/tunnel/TunnelPanel.svelte