Skip to content

feat(gui): desktop shell integration — shell detection, external links, open-in-browser, per-OS install card (desktop stack 3/5) - #5287

Closed
devin-ai-integration[bot] wants to merge 8 commits into
devin/1789879088-tauri-desktop-shellfrom
devin/1789890042-gui-desktop-shell
Closed

devin-ai-integration[bot] wants to merge 8 commits into
devin/1789879088-tauri-desktop-shellfrom
devin/1789890042-gui-desktop-shell

Conversation

@devin-ai-integration

@devin-ai-integration devin-ai-integration Bot commented Sep 20, 2026

Copy link
Copy Markdown

Summary

Desktop stack 3/5 (stacked on #5260). The dashboard learns it is running inside the Tauri shell and adapts — without granting the loopback page any Tauri IPC.

  • Shell marker (Rust): the main webview gets a platform-realistic UA ending in OpenCodexDesktop/<version> (window::webview_user_agent(), per cfg!(target_os)). The GUI detects the shell purely from navigator.userAgent; structure/desktop-shell.md records the invariant.
  • External links in the webview: gui/src/lib/desktop-shell.ts (isDesktopShell, desktopShellVersion, hostOs, isExternalLink) + a capture-phase click handler in App.tsx. Cross-origin http(s) anchors are turned into a top-level navigation, which the existing Rust on_navigation policy denies and hands to the system browser — so target="_blank" links (docs, GitHub) work in WKWebView/WebView2 without a bridge.
  • "Open in browser": new management route POST /api/companion/open-in-browser { path } — accepts only a dashboard path (/…, not //…, ≤512 chars) and opens http://127.0.0.1:<port><path> via openUrl(). Inside the shell the Usage install card becomes "You're in the OpenCodex desktop app vX · Open in browser"; Chrome access to the same loopback URL stays first-class.
  • Presence kind: companion presence now reports kind: "menuBar" | "desktop" (from the UA prefix), so the card says "Desktop app connected" vs "Menu bar app connected".
  • Install card per OS: segmented macOS / Windows / Linux selector (defaults to the detected host OS), per-OS steps (DMG + Gatekeeper, MSI + SmartScreen, AppImage/deb + chmod +x), per-OS command hint. Keys added to all 10 locales; old installStep1..3 removed.

Not in this PR: packaging artifacts themselves (stack 5), widget bundling (stack 4).

Verification

  • bun run typecheck, bun run structure:check, git diff --check
  • bun test tests/server/companion-settings.test.ts (presence kinds; open-in-browser 400s + mocked openUrl receives exactly http://127.0.0.1:10100/#/usage)
  • gui: bun test tests (incl. new tests/desktop-shell.test.ts), bun run lint, bun run lint:i18n, bun run build
  • desktop/src-tauri: cargo fmt --check, cargo clippy --all-targets -- -D warnings, cargo test (UA marker + platform token)
  • docs-site build (macOS menu-bar guide gains a desktop-app note)
  • Manual: proxy from this branch on :10100 with a 120k-row seeded usage log; presence set via curl -A OpenCodexDesktop/2.61.0; shell state reproduced in Chrome with a DevTools UA override.

Screenshots

State
Overview (120,400 req · 2.4B tokens, integer K/M/B) overview
Inside the desktop shell: "You're in the OpenCodex desktop app 2.61.0" + Open in browser shell
Shell card + 24h timeline + models-on-chart switches shell chart
Browser: "Desktop app connected" (presence kind) + timeline + controls connected
Install card — macOS (DMG, Gatekeeper, xattr) macos
Install card — Windows (MSI, SmartScreen) windows
Install card — Linux (AppImage/deb, chmod +x) linux
Providers + install section providers

Checklist

  • Scope stays focused and avoids unrelated cleanup.
  • Docs or release notes were updated when needed.
  • Security-sensitive changes were reviewed for secrets, auth, and unsafe defaults — new route is loopback-path-only, under the management auth boundary; no IPC exposure to the loopback page.

Review readiness checklist

This PR stays in draft until every box below is ticked. Tick all four boxes once the requirements are met:

  • All CI tests are green on my local testing.

  • I pushed my PR to the latest dev commit.

  • I resolved all correct Codex and CodeRabbit findings.

  • My PR is ready for review.

Link to Devin session: https://app.devin.ai/sessions/f2dce68b7ca349e3ba39dab55bc12300
Open in Devin Desktop: https://app.devin.ai/desktop/session/f2dce68b7ca349e3ba39dab55bc12300?variant=devin
Requested by: @lidge-jun

lidge-jun and others added 3 commits September 20, 2026 00:45
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
…token in shell UA

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
@devin-ai-integration

Copy link
Copy Markdown
Author

I'll fix CI failures and address comments from users with write access. I'll skip comments containing "(aside)".

  • Disable automatic comment, CI, and merge conflict monitoring

@coderabbitai

coderabbitai Bot commented Sep 20, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

Bot user detected.

To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository: lidge-jun/opencodex/.coderabbit.yaml

Review profile: ASSERTIVE

Plan: Advanced

Run ID: 0fc9fe1c-0109-4d58-81dc-4b84e9fdfd5c

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@devin-ai-integration
devin-ai-integration Bot added this pull request to stack #5262 September 20, 2026 07:48
@github-actions

Copy link
Copy Markdown
Contributor

Deterministic PR hygiene checks passed.

@github-actions github-actions Bot added the enhancement New feature or request label Sep 20, 2026
@github-actions

github-actions Bot commented Sep 20, 2026

Copy link
Copy Markdown
Contributor

⏳ DRAFT

  • review readiness checklist open (0/4 boxes ticked).

What to do

  • Tick all four boxes in the PR description once you're done (currently 0/4).

Review readiness checklist

  • ⬜ All CI tests are green on my local testing.
  • ⬜ I pushed my PR to the latest dev commit.
  • ⬜ I resolved all correct Codex and CodeRabbit findings.
  • ⬜ My PR is ready for review.

0/4 boxes ticked.

This PR stays in draft until every box above is ticked.

lidge-jun and others added 3 commits September 20, 2026 01:00
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
@lidge-jun

Copy link
Copy Markdown
Owner

리뷰 · 우선순위 54 / 80

이 PR은 데스크톱 앱 스택의 세 번째 장입니다. 베이스는 dev가 아니라 앞 장 브랜치 devin/1789879088-tauri-desktop-shell입니다. 대시보드가 Tauri 창 안에서 돌아가는지 알아채고, 그에 맞게 화면을 바꿉니다. 웹뷰 User-Agent 끝에 OpenCodexDesktop/<버전>을 붙이고, GUI는 그 글자만 보고 셸인지 판단합니다. 루프백 페이지에 Tauri IPC를 주지 않습니다.

바깥 사이트 링크는 App에서 클릭을 가로채 최상위 이동으로 바꿉니다. 그러면 이미 있는 Rust 네비게이션 규칙이 막고, 시스템 브라우저로 엽니다. Usage 설치 카드는 macOS / Windows / Linux를 고를 수 있게 바뀌었고, 셸 안에서는 “데스크톱 앱 안에서 보는 중”과 “브라우저에서 열기”만 보입니다. 브라우저에서 열기는 관리 API POST /api/companion/open-in-browser가 대시보드 경로만 받아 http://127.0.0.1:<포트><경로>로 엽니다. 동반 앱 접속 표시는 menuBardesktop을 나눕니다. 테스트와 10개 언어 키가 같이 들어왔습니다. 체크리스트는 아직 0/4이고, 이 글을 쓸 때 CI 일부는 대기·실패 상태였습니다.

enforce-target / 베이스 브랜치 - 저장소 기본은 dev를 원합니다. 이 PR은 스택 3/5라 앞 장 위에 올라가 있습니다. dev로 바로 합치면 앞 장 커밋이 빠지거나 순서가 꼬입니다.

gui/src/pages/usage-companion-panel.tsx · installAnother - OS 선택에 Windows·Linux가 생겼는데, “다른 Mac에 설치” / “Install on another Mac” 문구는 그대로입니다. Windows를 고른 뒤에도 Mac 설치처럼 읽힙니다. gui/src/i18n/en.ts, gui/src/i18n/ko.ts 등 10개 로케일이 같습니다.

gui/src/pages/usage-companion-panel.tsx · 셸 분기 - isDesktopShell()이면 설치 안내·다른 기기 설치 블록을 통째로 숨깁니다. 데스크톱 창 안에서는 다른 PC용 설치 단계를 볼 수 없습니다. 의도인지 확인이 필요합니다.

src/server/management/companion-routes.ts · open-in-browser - 경로는 /로 시작하고 //가 아니며 512자 이하만 받습니다. 외부 URL·프로토콜 상대 URL은 막습니다. 다만 연 뒤 실패해도 응답은 성공으로 나갑니다. route-registry에는 mutates: false로 적혀 있는데, OS 브라우저를 여는 부작용은 있습니다.

gui/src/App.tsx · 바깥 링크 - capture 단계의 일반 클릭만 가로챕니다. 휠 클릭·수정 키+클릭은 웹뷰 기본 동작으로 남을 수 있습니다. WKWebView/WebView2에서 target="_blank"가 약해서 넣은 우회로라, 그 범위면 충분할 수 있습니다.

메인테이너의 판단이 필요한 지점

스택을 앞 장(#5260 계열)부터 순서대로 합칠지, 이 장만 dev 위로 다시 올릴지. 셸 안에서 “다른 기기 설치”를 숨길지 남길지. open-in-browser를 CLI 면제·비변경으로 둘지, 부작용 있는 관리 동작으로 다시 분류할지.

너의 추천

앞 장이 먼저 들어간 뒤에 이 PR을 합치는 편이 맞습니다. installAnother 문구는 OS 공통 말(“다른 기기에 설치”)로 바꾸는 게 좋습니다. 셸 안에서도 details로 설치 안내를 남겨 두면 다른 PC 설치가 편합니다. open-in-browser의 경로 제한과 테스트는 유지해도 됩니다. 머지 전에 체크리스트와 CI를 초록으로 맞추세요.

이 댓글은 grok-bot이 작성했습니다

@devin-ai-integration

Copy link
Copy Markdown
Author

Re grok-bot 5748578867:

  • Base branch / enforce-target: intentional — this is layer 3 of stack #5262 ([WRONG BRANCH] feat(standalone): compiled npm-free ocx binary (desktop stack 1/5) #5256feat(desktop): Tauri v2 cross-platform tray + webview shell (desktop stack 2/5) #5260feat(gui): desktop shell integration — shell detection, external links, open-in-browser, per-OS install card (desktop stack 3/5) #5287). GitHub retargets each layer to dev as the one below merges; merge order is bottom-up.
  • installAnother copy: agreed — changing to "Install on another device" in all 10 locales (pushing shortly).
  • Shell hides install guidance: agreed — the in-shell card keeps "You're in the desktop app · Open in browser" and regains the collapsed "Install on another device" details (OS selector + steps). Pushing shortly.
  • open-in-browser response / mutates: false: openUrl() is fire-and-forget by design (src/lib/open-url.ts spawns detached and swallows the error event so a headless host cannot kill the proxy), so there is no failure signal to surface; the 200 means "accepted and validated", which is documented in the route comment. mutates in the registry expresses proxy-state mutation for the CLI-parity gate; opening the user's browser changes no proxy state, so it stays false with the session-only exemption. Happy to reclassify if you want a stricter reading.
  • Modifier/middle clicks: intentional scope — the capture handler only covers the plain left-click path where WKWebView/WebView2 drop target="_blank". Middle/modifier clicks fall through to the webview default, which the Rust on_navigation deny policy still routes to the system browser for cross-origin http(s).

lidge-jun and others added 2 commits September 20, 2026 01:10
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
@devin-ai-integration

Copy link
Copy Markdown
Author

Follow-up to my note above: on reflection the registry convention is that every POST with an OS-visible side effect is mutates: true (e.g. /api/stop), so 033dafad4 reclassifies open-in-browser as mutating while keeping the session-only CLI exemption. The copy/in-shell guidance changes landed in 854746156.

@lidge-jun

Copy link
Copy Markdown
Owner

추가 리뷰 · 우선순위 24 / 80

이전 리뷰(우선순위 54) 이후 tip이 033dafad로 바뀌었습니다. 반영된 커밋은 854746156(문구·셸 설치 안내)과 033dafad(라우트 mutates)입니다. installAnother는 10개 로케일 모두 “다른 기기 / another device”처럼 OS 공통 말로 바뀌었습니다. 셸 안에서도 “다른 기기에 설치” details(OS 선택·단계·명령)가 다시 보입니다. open-in-browserroute-registry에서 mutates: true로 바뀌었고, CLI session-only 면제는 그대로입니다. 이 tip 기준 CI는 초록입니다. 베이스는 여전히 스택 앞 장 devin/1789879088-tauri-desktop-shell이고, PR은 draft이며 체크리스트는 0/4입니다.

gui/src/pages/usage-companion-panel.tsx · 셸 설치 안내 - 이전 지적(셸에서 설치 블록 숨김)은 해소되었습니다. installGuidance를 셸·연결됨 분기에서 같이 씁니다.

src/server/management/route-registry.ts · open-in-browser - 이전 mutates: false 지적은 해소되었습니다. OS에 브라우저를 여는 POST를 변경 동작으로 분류한 선택이 /api/stop 등과 맞습니다.

src/server/management/companion-routes.ts · 응답 의미 - openUrl()이 fire-and-forget이라 실패를 응답에 못 실는 점은 그대로입니다. 주석대로 200은 “검증 후 수락”입니다. 새 버그는 아닙니다.

gui/src/App.tsx · 바깥 링크 - 일반 좌클릭만 가로채는 범위는 의도라고 답변이 왔고, 휠·수정 키 클릭은 Rust on_navigation이 시스템 브라우저로 넘긴다고 했습니다. 추가 코드 변경은 없어도 됩니다.

메인테이너의 판단이 필요한 지점

스택(#5256#5260 → 이 PR)을 아래부터 순서대로 합칠지. draft 체크리스트를 언제 채우고 Ready로 올릴지. open-in-browser의 “수락=200” 계약을 문서·테스트만으로 둘지.

너의 추천

앞 장(#5260)이 dev에 들어간 뒤 이 장을 합치세요. 이번 푸시로 이전 리뷰의 실질 지적(문구·셸 안내·mutates)은 처리됐습니다. 체크리스트만 채운 뒤 Ready로 올리면 됩니다.

이 댓글은 grok-bot이 작성했습니다

@lidge-jun
lidge-jun marked this pull request as ready for review September 20, 2026 11:05
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 20, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review Completed 2026-09-20T11:10:46.280797Z 033dafa Draft marked ready
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@github-actions
github-actions Bot marked this pull request as draft September 20, 2026 11:07

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 033dafad47

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

if (typeof path !== "string" || !path.startsWith("/") || path.startsWith("//") || path.length > 512) {
return jsonResponse({ error: "invalid path" }, 400, ctx.req, ctx.config);
}
const url = `http://127.0.0.1:${ctx.config.port}${path}`;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Build the browser URL from the active listener

When the proxy is started with the supported ocx start --port <port> override, or with config.port = 0, ctx.config.port is not the port serving this request; startServer keeps the selected/bound port separately. The desktop shell discovers and loads the real runtime port, but this button opens the configured port (often 10100 or 0), so it fails or can open a different proxy. Derive the loopback URL from the authenticated request's actual port or propagate the bound port through the management context.

AGENTS.md reference: src/AGENTS.md:L10-L10

Useful? React with 👍 / 👎.

Comment on lines +50 to +51
const kind = userAgent.startsWith("OpenCodexMenuBar/") ? "menuBar"
: userAgent.startsWith("OpenCodexDesktop/") ? "desktop"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Match the desktop marker anywhere in the user agent

The new Tauri UA produced by desktop/src-tauri/src/window.rs:12 starts with Mozilla/5.0 and appends OpenCodexDesktop/<version>, so startsWith("OpenCodexDesktop/") can never recognize requests from the actual desktop webview. Consequently the presence record is not refreshed as desktop, and a normal browser viewing the same proxy reports that no desktop app is connected. Parse the marker as a token in the full UA and test with webview_user_agent()'s complete format.

Useful? React with 👍 / 👎.

}

export async function handleCompanionRoutes(ctx: ManagementContext): Promise<Response | null> {
if (ctx.url.pathname === "/api/companion/open-in-browser" && ctx.req.method === "POST") {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Require the declared GUI session principal

The route registry declares this host-side action session-only, but this handler never checks ctx.principal; an authenticated admin-token caller can therefore trigger system-browser launches despite not holding a dashboard session. This is also demonstrated by the added test invoking the route with "admin-token". Reject callers other than gui-session, as the other session-only management routes do.

AGENTS.md reference: src/AGENTS.md:L20-L20

Useful? React with 👍 / 👎.

Comment thread gui/src/i18n/en.ts
"usage.companion.osLinux": "Linux",
"usage.companion.runningInDesktop": "You’re in the OpenCodex desktop app {version}",
"usage.companion.openInBrowser": "Open in browser",
"usage.companion.installMacStep1": "Download OpenCodex-<version>-macos.dmg from the latest release and drag OpenCodex.app to Applications.",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep install copy on artifacts the release publishes

This replaces the working macOS ZIP instruction with a DMG and also advertises MSI/AppImage packages, but a repo-wide search finds no release automation that publishes any of those names; .github/workflows/ci.yml:1180-1219 only performs Rust checks with placeholder resources, while the existing macOS release script still emits OpenCodex-<version>-macos-<arch>.zip. Users following the newly visible installation card therefore reach the latest release but cannot find the named downloads. Keep the shipped ZIP instructions or hide the new per-OS copy until the packaging workflow lands.

Useful? React with 👍 / 👎.

@lidge-jun

Copy link
Copy Markdown
Owner

Superseded by #5318, merged to dev as 2ff7f3385d83c05279093eb9c7bdd5d5963d9605, with attribution in the branch commit.

#5196 landed first as 38a5ab9fc4eaa223d3d58982b0bc097279138fca. Squashing the bottom of a native stack detaches every child from its base, so the rest of the chain could no longer be merged through the stack: the base branches stopped resolving and the recorded bases pointed at tips the chain had been rebased away from. Replaying the branches on top of dev reproduced the already-squashed commits and produced 48 conflicts, none of them real disagreements.

The consolidation applies the tree difference of the stack tip against the point the chain branched from. One file genuinely conflicted and both intents were kept. Everything from the standalone binary through the signed packaging is on dev.

Closing as superseded rather than stale.

@lidge-jun lidge-jun closed this Sep 20, 2026
@lidge-jun
lidge-jun deleted the devin/1789890042-gui-desktop-shell branch September 20, 2026 11:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant