From c5d578c5b1e526597e41e90c3711adf6437b7be4 Mon Sep 17 00:00:00 2001 From: leokun Date: Thu, 27 Aug 2026 21:45:28 +0800 Subject: [PATCH] feat(docs): add documentation site and demo features - Introduced a new documentation site for Cursor BYOK using Next.js and Fumadocs. - Added a product demo page with a corresponding Vite configuration. - Implemented a demo API to simulate LLM calls and responses. - Enhanced the Makefile to include new build and development commands for the documentation. - Updated package.json scripts for building and running the documentation site. - Created various components and layouts for the documentation structure, including blog and user documentation sections. - Added styling for the new components and layouts to ensure a cohesive design. - Included a README and other necessary files for local development and deployment. --- Makefile | 9 +- apps/desktop/demo/index.html | 23 + apps/desktop/package.json | 1 + apps/desktop/src/demo/api.ts | 287 + apps/desktop/src/demo/index.tsx | 8 + apps/desktop/src/layouts/AppHeader.tsx | 3 + .../src/layouts/MacTrafficLights.module.scss | 30 + apps/desktop/src/layouts/MacTrafficLights.tsx | 9 + apps/desktop/src/native/platform.ts | 5 + apps/desktop/tsconfig.node.json | 2 +- apps/desktop/vite.demo.config.ts | 22 + apps/docs/.gitignore | 29 + apps/docs/AGENTS.md | 9 + apps/docs/CLAUDE.md | 1 + apps/docs/README.md | 48 + apps/docs/app/(home)/blog/[slug]/page.tsx | 61 + apps/docs/app/(home)/blog/page.tsx | 44 + apps/docs/app/(home)/layout.tsx | 6 + apps/docs/app/(home)/page.tsx | 135 + apps/docs/app/api/search/route.ts | 4 + apps/docs/app/docs/[[...slug]]/page.tsx | 63 + apps/docs/app/docs/layout.tsx | 11 + apps/docs/app/global.css | 16 + apps/docs/app/layout.tsx | 26 + apps/docs/app/llms-full.txt/route.ts | 10 + .../app/llms.mdx/docs/[[...slug]]/route.ts | 23 + apps/docs/app/llms.txt/route.ts | 8 + apps/docs/app/og/docs/[...slug]/route.tsx | 28 + .../components/hero/DesktopDemo.module.css | 45 + apps/docs/components/hero/DesktopDemo.tsx | 18 + apps/docs/components/mdx.tsx | 18 + .../content/blog/2026-08-27-documentation.mdx | 37 + apps/docs/content/docs/index.mdx | 64 + apps/docs/content/docs/installation.mdx | 68 + apps/docs/content/docs/meta.json | 5 + .../docs/content/docs/model-configuration.mdx | 67 + apps/docs/content/docs/troubleshooting.mdx | 62 + apps/docs/eslint.config.mjs | 16 + apps/docs/lib/blog.ts | 35 + apps/docs/lib/cn.ts | 1 + apps/docs/lib/layout.shared.tsx | 31 + apps/docs/lib/shared.ts | 13 + apps/docs/lib/source.ts | 51 + apps/docs/next.config.mjs | 10 + apps/docs/package-lock.json | 10518 ++++++++++++++++ apps/docs/package.json | 39 + apps/docs/postcss.config.mjs | 7 + apps/docs/proxy.ts | 32 + apps/docs/scripts/build-product-demo.mjs | 18 + apps/docs/tsconfig.json | 34 + server/prompt/cursor/modes/agent.json | 2 +- server/prompt/cursor/modes/subagent.json | 2 +- server/prompt/cursor/tools.json | 31 +- server/src/cursor/interaction/render.rs | 12 - server/src/cursor/projection/encode.rs | 1 - server/src/cursor/request/context.rs | 15 +- server/src/cursor/tools/codec/mod.rs | 4 +- server/src/cursor/tools/codec/request.rs | 26 - server/src/cursor/tools/codec/response.rs | 78 +- .../src/cursor/tools/dispatch/await_shell.rs | 54 - server/src/cursor/tools/dispatch/mod.rs | 2 - server/src/cursor/tools/result/await_shell.rs | 145 - server/src/cursor/tools/result/gate.rs | 867 +- server/src/cursor/tools/result/mod.rs | 9 +- server/src/cursor/tools/runtime.rs | 63 - server/src/model/configuration.rs | 78 + server/src/model/mod.rs | 2 + server/src/model/tool_result_replay.rs | 226 + server/tests/prefix_stability.rs | 56 +- server/tests/tool_loop.rs | 83 - 70 files changed, 13299 insertions(+), 567 deletions(-) create mode 100644 apps/desktop/demo/index.html create mode 100644 apps/desktop/src/demo/api.ts create mode 100644 apps/desktop/src/demo/index.tsx create mode 100644 apps/desktop/src/layouts/MacTrafficLights.module.scss create mode 100644 apps/desktop/src/layouts/MacTrafficLights.tsx create mode 100644 apps/desktop/vite.demo.config.ts create mode 100644 apps/docs/.gitignore create mode 100644 apps/docs/AGENTS.md create mode 100644 apps/docs/CLAUDE.md create mode 100644 apps/docs/README.md create mode 100644 apps/docs/app/(home)/blog/[slug]/page.tsx create mode 100644 apps/docs/app/(home)/blog/page.tsx create mode 100644 apps/docs/app/(home)/layout.tsx create mode 100644 apps/docs/app/(home)/page.tsx create mode 100644 apps/docs/app/api/search/route.ts create mode 100644 apps/docs/app/docs/[[...slug]]/page.tsx create mode 100644 apps/docs/app/docs/layout.tsx create mode 100644 apps/docs/app/global.css create mode 100644 apps/docs/app/layout.tsx create mode 100644 apps/docs/app/llms-full.txt/route.ts create mode 100644 apps/docs/app/llms.mdx/docs/[[...slug]]/route.ts create mode 100644 apps/docs/app/llms.txt/route.ts create mode 100644 apps/docs/app/og/docs/[...slug]/route.tsx create mode 100644 apps/docs/components/hero/DesktopDemo.module.css create mode 100644 apps/docs/components/hero/DesktopDemo.tsx create mode 100644 apps/docs/components/mdx.tsx create mode 100644 apps/docs/content/blog/2026-08-27-documentation.mdx create mode 100644 apps/docs/content/docs/index.mdx create mode 100644 apps/docs/content/docs/installation.mdx create mode 100644 apps/docs/content/docs/meta.json create mode 100644 apps/docs/content/docs/model-configuration.mdx create mode 100644 apps/docs/content/docs/troubleshooting.mdx create mode 100644 apps/docs/eslint.config.mjs create mode 100644 apps/docs/lib/blog.ts create mode 100644 apps/docs/lib/cn.ts create mode 100644 apps/docs/lib/layout.shared.tsx create mode 100644 apps/docs/lib/shared.ts create mode 100644 apps/docs/lib/source.ts create mode 100644 apps/docs/next.config.mjs create mode 100644 apps/docs/package-lock.json create mode 100644 apps/docs/package.json create mode 100644 apps/docs/postcss.config.mjs create mode 100644 apps/docs/proxy.ts create mode 100644 apps/docs/scripts/build-product-demo.mjs create mode 100644 apps/docs/tsconfig.json delete mode 100644 server/src/cursor/tools/dispatch/await_shell.rs delete mode 100644 server/src/cursor/tools/result/await_shell.rs create mode 100644 server/src/model/tool_result_replay.rs diff --git a/Makefile b/Makefile index b23ddc2..f937d23 100644 --- a/Makefile +++ b/Makefile @@ -1,16 +1,20 @@ LOCAL_TAURI_SIGNING_KEY := $(CURDIR)/.tauri/cursor-byok.local.key -.PHONY: check dev-web dev-server dev-desktop build-web build-server build-desktop build-docker +.PHONY: check dev-web dev-docs dev-server dev-desktop build-web build-docs build-server build-desktop build-docker check: cargo fmt --all -- --check cargo clippy --workspace --all-targets -- -D warnings cargo test --workspace --all-targets npm --prefix apps/desktop run check + npm --prefix apps/docs run check dev-web: npm --prefix apps/desktop run dev:web +dev-docs: + npm --prefix apps/docs run dev + dev-server: CURSOR_CONSOLE_DIR=apps/desktop/dist cargo run --package cursor-server --bin cursor-server @@ -20,6 +24,9 @@ dev-desktop: build-web: npm --prefix apps/desktop run build +build-docs: + npm --prefix apps/docs run build + build-server: cargo build --release --package cursor-server --bin cursor-server diff --git a/apps/desktop/demo/index.html b/apps/desktop/demo/index.html new file mode 100644 index 0000000..7d430f0 --- /dev/null +++ b/apps/desktop/demo/index.html @@ -0,0 +1,23 @@ + + + + + + + + Cursor BYOK 产品演示 + + +
+ + + diff --git a/apps/desktop/package.json b/apps/desktop/package.json index f4658bc..985bcb5 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -10,6 +10,7 @@ "typecheck:node": "tsc --noEmit -p tsconfig.node.json", "i18n:scan": "STATIC_I18N_SCAN=true vite build", "build": "vite build", + "build:demo": "npm run typecheck && npm run typecheck:node && vite build --config vite.demo.config.ts", "check": "npm run typecheck && npm run typecheck:node && npm run build", "dev:web": "concurrently --kill-others --success first --names server,web \"cross-env CURSOR_CONSOLE_PROXY=http://127.0.0.1:1420 cargo run --manifest-path ../../server/Cargo.toml --bin cursor-server\" \"wait-on http-get://127.0.0.1:3000/__byok-api__/healthz && vite\"", "serve": "vite preview", diff --git a/apps/desktop/src/demo/api.ts b/apps/desktop/src/demo/api.ts new file mode 100644 index 0000000..06ed380 --- /dev/null +++ b/apps/desktop/src/demo/api.ts @@ -0,0 +1,287 @@ +import type { + CallDetail, + CursorHarnessStatus, + LlmCall, + Model, + Overview, + OverviewTokenUsageBucket, + ProxySettings, + StatisticsStorage, + TabSettings, +} from "../api"; + +const API_ROOT = "/__byok-api__/api"; +const FIXED_NOW = Date.UTC(2026, 7, 27, 8, 0, 0); + +const models: Model[] = [ + createModel({ hash: "mock-claude-sonnet", order: 1, name: "Claude Sonnet 4", type: "anthropic", url: "https://api.anthropic.com", modelId: "claude-sonnet-4-20250514" }), + createModel({ hash: "mock-claude-opus", order: 2, name: "Claude Opus 4", type: "anthropic", url: "https://api.anthropic.com", modelId: "claude-opus-4-20250514" }), + createModel({ hash: "mock-gpt", order: 3, name: "GPT-5.2", type: "openai", url: "https://api.openai.com", modelId: "gpt-5.2" }), + createModel({ hash: "mock-o3", order: 4, name: "o3", type: "openai", url: "https://api.openai.com", modelId: "o3" }), + createModel({ hash: "mock-deepseek-v3", order: 5, name: "DeepSeek V3.2", type: "openai", url: "https://api.deepseek.com", modelId: "deepseek-chat", endpoint: "/v1/chat/completions" }), + createModel({ hash: "mock-deepseek-r1", order: 6, name: "DeepSeek R1", type: "openai", url: "https://api.deepseek.com", modelId: "deepseek-reasoner", endpoint: "/v1/chat/completions" }), + createModel({ hash: "mock-gemini-pro", order: 7, name: "Gemini 2.5 Pro", type: "openai", url: "https://generativelanguage.googleapis.com", modelId: "gemini-2.5-pro", endpoint: "/v1beta/openai/chat/completions" }), + createModel({ hash: "mock-gemini-flash", order: 8, name: "Gemini 2.5 Flash", type: "openai", url: "https://generativelanguage.googleapis.com", modelId: "gemini-2.5-flash", endpoint: "/v1beta/openai/chat/completions" }), + createModel({ hash: "mock-qwen-max", order: 9, name: "Qwen3 Max", type: "openai", url: "https://dashscope.aliyuncs.com/compatible-mode", modelId: "qwen3-max", endpoint: "/v1/chat/completions" }), + createModel({ hash: "mock-qwen-plus", order: 10, name: "Qwen Plus", type: "openai", url: "https://dashscope.aliyuncs.com/compatible-mode", modelId: "qwen-plus", endpoint: "/v1/chat/completions" }), + createModel({ hash: "mock-kimi-k2", order: 11, name: "Kimi K2", type: "openai", url: "https://api.moonshot.cn", modelId: "kimi-k2-0711-preview", endpoint: "/v1/chat/completions" }), + createModel({ hash: "mock-kimi-128k", order: 12, name: "Moonshot V1 128K", type: "openai", url: "https://api.moonshot.cn", modelId: "moonshot-v1-128k", endpoint: "/v1/chat/completions" }), + createModel({ hash: "mock-glm-45", order: 13, name: "GLM-4.5", type: "openai", url: "https://open.bigmodel.cn", modelId: "glm-4.5", endpoint: "/api/paas/v4/chat/completions" }), + createModel({ hash: "mock-glm-air", order: 14, name: "GLM-4.5-Air", type: "openai", url: "https://open.bigmodel.cn", modelId: "glm-4.5-air", endpoint: "/api/paas/v4/chat/completions" }), + createModel({ hash: "mock-mistral-large", order: 15, name: "Mistral Large", type: "openai", url: "https://api.mistral.ai", modelId: "mistral-large-latest", endpoint: "/v1/chat/completions" }), + createModel({ hash: "mock-mistral-small", order: 16, name: "Mistral Small", type: "openai", url: "https://api.mistral.ai", modelId: "mistral-small-latest", endpoint: "/v1/chat/completions" }), +]; + +const calls: LlmCall[] = Array.from({ length: 24 }, (_, index) => { + const model = models[index % models.length]; + const failed = index === 7 || index === 19; + return { + call_kind: "provider_llm", + route: "local_byok", + call_id: `mock-call-${String(index + 1).padStart(3, "0")}`, + run_id: `mock-run-${Math.floor(index / 3) + 1}`, + conversation_id: `mock-conversation-${Math.floor(index / 4) + 1}`, + provider_call_index: index + 1, + model_hash: model.model_hash, + provider_type: model.type, + provider_url: model.base_url, + request_type: model.type === "anthropic" ? "messages" : "responses", + request_url: model.type === "anthropic" ? `${model.base_url}/v1/messages` : `${model.base_url}${model.openai_endpoint}`, + model_id: model.model_id, + display_name: model.display_name, + reasoning_effort: index % 2 === 0 ? "high" : null, + fast: index % 3 === 0, + status: failed ? "failed" : "completed", + finish_reason: failed ? null : "stop", + created_at_ms: FIXED_NOW - index * 3 * 60_000, + ttfb_ms: 210 + index * 13, + ttft_ms: 370 + index * 17, + duration_ms: failed ? 812 : 1_420 + index * 71, + input_tokens: 4_800 + index * 337, + output_tokens: failed ? 0 : 820 + index * 43, + total_tokens: failed ? 4_800 + index * 337 : 5_620 + index * 380, + cache_read_tokens: 3_100 + index * 251, + cache_write_tokens: 320 + index * 19, + reasoning_tokens: index % 2 === 0 ? 420 + index * 11 : null, + message_count: 14 + (index % 8), + tool_count: 3 + (index % 5), + http_status: failed ? 429 : 200, + error_kind: failed ? "provider_rate_limit" : null, + error_message: failed ? "Mock provider rate limit" : null, + detailed: true, + }; +}); + +const harnessStatus: CursorHarnessStatus = { + platform: "macos", + ca: "ready", + configured_models: models.length, + enabled_models: models.length, + integration: "enabled", + proxy_url: "http://127.0.0.1:54321", + ca_install_command: null, +}; + +let detailed = true; +let portSettings = { proxy_port: 0, service_port: 0 }; +let proxySettings: ProxySettings = { + mode: "system", + address: "", + auth_enabled: false, + username: "", + has_password: false, +}; +let tabSettings: TabSettings = { mode: "public", address: "" }; +let storage: StatisticsStorage = { bytes: 26_004_480, call_count: calls.length, trace_count: calls.length }; + +export function installDemoApi() { + const nativeFetch = window.fetch.bind(window); + + window.fetch = async (input, init) => { + const requestUrl = input instanceof Request ? input.url : input instanceof URL ? input.href : input; + const url = new URL(requestUrl, window.location.href); + if (!url.pathname.startsWith(API_ROOT)) return nativeFetch(input, init); + + const path = url.pathname.slice(API_ROOT.length) || "/"; + const method = (init?.method ?? (input instanceof Request ? input.method : "GET")).toUpperCase(); + const body = await readBody(input, init); + + if (path === "/ads") return json({ slots: [] }); + if (path === "/models" && method === "GET") return json(models); + if (path === "/models" && method === "POST") return json(models); + if (path === "/models/order") return json(models); + if (path === "/models/discover") return json({ models: models.map((model) => model.model_id) }); + if (path === "/models/import-v0049" && method === "GET") { + return json({ source: "demo", total: 0, new_models: 0, existing_models: 0, models: [] }); + } + if (path === "/models/import-v0049") return json({ imported: 0, skipped: 0, total: 0 }); + if (/^\/models\/[^/]+\/test\/[^/]+$/.test(path) && method === "POST") { + return json({ duration_ms: 1_284, first_text_ms: 418, output_tokens: 42, tokens_per_second: 38.6, tokens_estimated: false, output: "Mock connectivity test passed." }); + } + if (/^\/models\/[^/]+\/test\/[^/]+$/.test(path) || /^\/models\/[^/]+$/.test(path)) { + return method === "DELETE" ? empty() : json(models[0]); + } + if (path === "/overview") return json(createOverview(url.searchParams)); + if (path === "/llm-calls") return json(calls); + if (path.startsWith("/llm-calls/")) return json(createCallDetail(path.slice("/llm-calls/".length))); + if (path === "/harness/cursor/status") return json(harnessStatus); + if (path === "/harness/cursor/ca/initialize" || path === "/harness/cursor/enabled") return json(harnessStatus); + if (path === "/settings/observability" && method === "GET") return json({ detailed }); + if (path === "/settings/observability") { + detailed = Boolean((body as { detailed?: unknown } | null)?.detailed); + return json({ detailed }); + } + if (path === "/settings/ports" && method === "GET") return json(portSettings); + if (path === "/settings/ports") { + portSettings = body as typeof portSettings; + return json(portSettings); + } + if (path === "/settings/storage/statistics" && method === "GET") return json(storage); + if (path === "/settings/storage/statistics") { + storage = { bytes: 0, call_count: 0, trace_count: 0 }; + return json(storage); + } + if (path === "/settings/proxy" && method === "GET") return json(proxySettings); + if (path === "/settings/proxy") { + const next = body as Partial; + proxySettings = { ...proxySettings, ...next, has_password: Boolean(next.has_password) }; + return json(proxySettings); + } + if (path === "/settings/tab" && method === "GET") return json(tabSettings); + if (path === "/settings/tab") { + tabSettings = body as TabSettings; + return json(tabSettings); + } + if (path === "/settings/desktop" && method === "GET") return json({ silent_start: false, show_dock_icon: true }); + if (path === "/settings/desktop") return json(body); + if (path === "/desktop/open-external-url") { + const target = (body as { url?: string } | null)?.url; + if (target) { + const next = new URL(target, window.location.href); + if (next.origin === window.location.origin && next.hash) window.location.hash = next.hash; + } + return empty(); + } + if (path.endsWith("/dismissals")) return empty(); + + return json({ message: `Unhandled demo endpoint: ${method} ${path}` }, 404); + }; +} + +function createModel({ hash, order, name, type, url, modelId, endpoint = "/v1/responses" }: { + hash: string; + order: number; + name: string; + type: Model["type"]; + url: string; + modelId: string; + endpoint?: string; +}): Model { + return { + model_hash: hash, + sort_order: order, + display_name: name, + type, + base_url: url, + use_full_url: false, + api_key: "demo-key", + tooltip_data: `${name} Mock 通道`, + model_id: modelId, + reasoning_effort: type === "openai" ? "high" : null, + openai_endpoint: type === "openai" ? endpoint : "", + openai_extra_params_enabled: false, + openai_extra_params: {}, + custom_headers_enabled: false, + custom_headers: {}, + anthropic_extra_params_enabled: false, + anthropic_extra_params: {}, + context_window_tokens: 200_000, + max_completion_tokens: type === "openai" ? 32_000 : null, + anthropic_max_tokens: type === "anthropic" ? 32_000 : null, + anthropic_thinking_effort: type === "anthropic" ? "high" : null, + thinking_budget_tokens: null, + created_at_ms: FIXED_NOW - order * 86_400_000, + updated_at_ms: FIXED_NOW, + }; +} + +function createOverview(params: URLSearchParams): Overview { + const start = Number(params.get("start_ms")); + const end = Number(params.get("end_ms")); + const duration = Number.isFinite(start) && Number.isFinite(end) && end > start ? end - start : 365 * 86_400_000; + const granularity = duration <= 2 * 60 * 60_000 ? "minute" : duration <= 2 * 86_400_000 ? "hour" : "day"; + const step = granularity === "minute" ? 60_000 : granularity === "hour" ? 3_600_000 : 86_400_000; + const count = granularity === "minute" ? Math.min(60, Math.max(10, Math.ceil(duration / step))) : granularity === "hour" ? Math.min(24, Math.max(8, Math.ceil(duration / step))) : Math.min(365, Math.max(7, Math.ceil(duration / step))); + const series = createSeries(count, step, Number.isFinite(end) && end > 0 ? end : FIXED_NOW); + const totals = series.reduce((sum, bucket) => ({ + input: sum.input + bucket.input_tokens, + cacheRead: sum.cacheRead + bucket.cache_read_tokens, + cacheWrite: sum.cacheWrite + bucket.cache_write_tokens, + output: sum.output + bucket.output_tokens, + }), { input: 0, cacheRead: 0, cacheWrite: 0, output: 0 }); + const llmCalls = Math.max(12, Math.round(count * 5.4)); + + return { + metrics: { + llm_calls: llmCalls, + successful_calls: llmCalls - Math.max(1, Math.floor(llmCalls * 0.008)), + failed_calls: Math.max(1, Math.floor(llmCalls * 0.008)), + token_usage: totals.input + totals.cacheRead + totals.cacheWrite + totals.output, + prompt_tokens: totals.input + totals.cacheRead + totals.cacheWrite, + input_tokens: totals.input, + cache_read_tokens: totals.cacheRead, + cache_write_tokens: totals.cacheWrite, + output_tokens: totals.output, + }, + token_usage_granularity: granularity, + token_usage_series: series, + }; +} + +function createSeries(count: number, step: number, end: number): OverviewTokenUsageBucket[] { + return Array.from({ length: count }, (_, index) => { + const wave = 0.72 + ((index * 17) % 31) / 50; + return { + bucket_start_ms: end - (count - index) * step, + input_tokens: Math.round(18_000 * wave), + cache_read_tokens: Math.round(62_000 * wave), + cache_write_tokens: Math.round(7_500 * wave), + output_tokens: Math.round(12_500 * wave), + }; + }); +} + +function createCallDetail(id: string): CallDetail { + const call = calls.find((item) => item.call_id === decodeURIComponent(id)) ?? calls[0]; + return { + call, + request: { + headers: { authorization: "Bearer sk-demo-••••", "content-type": "application/json" }, + body: { model: call.model_id, stream: true, messages: [{ role: "user", content: "Mock Agent request" }] }, + byte_count: 8_426, + }, + response_chunks: [ + { seq: 1, received_offset_ms: 418, data: "event: response.created", byte_count: 128 }, + { seq: 2, received_offset_ms: 512, data: "event: response.output_text.delta", byte_count: 256 }, + ], + cursor_trace: null, + }; +} + +async function readBody(input: RequestInfo | URL, init?: RequestInit): Promise { + const raw = init?.body ?? (input instanceof Request ? await input.clone().text() : null); + if (typeof raw !== "string" || raw.length === 0) return null; + try { return JSON.parse(raw) as unknown; } + catch { return raw; } +} + +function json(value: unknown, status = 200) { + return new Response(JSON.stringify(value), { + status, + headers: { "content-type": "application/json" }, + }); +} + +function empty() { + return new Response(null, { status: 204 }); +} diff --git a/apps/desktop/src/demo/index.tsx b/apps/desktop/src/demo/index.tsx new file mode 100644 index 0000000..3a4164a --- /dev/null +++ b/apps/desktop/src/demo/index.tsx @@ -0,0 +1,8 @@ +import { installDemoApi } from "./api"; + +installDemoApi(); +document.documentElement.dataset.platform = "macos"; +localStorage.setItem("cursor-byok.locale", "zh-CN"); +localStorage.setItem("cursor-byok.theme", "default-dark"); + +void import("../index"); diff --git a/apps/desktop/src/layouts/AppHeader.tsx b/apps/desktop/src/layouts/AppHeader.tsx index 7932fe6..2af714a 100644 --- a/apps/desktop/src/layouts/AppHeader.tsx +++ b/apps/desktop/src/layouts/AppHeader.tsx @@ -3,6 +3,7 @@ import appIcon from "../../src-tauri/icons/32x32.png"; import { currentAppVersion } from "../native/appLifecycle"; import type { DesktopPlatform } from "../native/platform"; import { WindowControls } from "./WindowControls"; +import { MacTrafficLights } from "./MacTrafficLights"; import styles from "./AppHeader.module.scss"; type AppHeaderProps = { @@ -11,6 +12,7 @@ type AppHeaderProps = { }; export function AppHeader({ platform, nativeDesktop }: AppHeaderProps) { + const showMacTrafficLights = !nativeDesktop && platform === "macos"; const showNativeUi = nativeDesktop && platform !== "macos"; const [version, setVersion] = useState("…"); @@ -25,6 +27,7 @@ export function AppHeader({ platform, nativeDesktop }: AppHeaderProps) { return