From abfe7ba008bc624c422c67e34dafaef531b10659 Mon Sep 17 00:00:00 2001 From: lefarcen <935902669@qq.com> Date: Thu, 21 May 2026 15:23:59 +0800 Subject: [PATCH 01/94] fix(web): show feedback prompt on every successful assistant turn (#2529) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(web): show feedback prompt on every successful assistant turn Previously the thumbs-up/down widget only appeared when the turn produced an ``, wrote a file via Write/Edit, or emitted a live_artifact event. That left whole categories of completed turns — image/video generation via `generate_image` / `generate_video`, MCP tool runs, plain text answers — without any way for the user to rate them. Drop the `hasArtifactWork` gate from `isFeedbackEligible` and remove the helper functions that fed it. The remaining filters (streaming in progress, empty response, unfinished todos, runStatus !== "succeeded") still suppress the widget for genuinely incomplete turns. The PostHog `has_produced_files` field is still emitted so the analytics side can keep slicing feedback by whether the turn produced artifacts. * test(web): flip artifact-only feedback assertion in AssistantMessage.test.tsx Companion test file to `chat-feedback.test.tsx` — the same artifact-only visibility rule was duplicated here and asserted the text-only success case stays hidden. Flip that case to assert the widget now appears, and update the file-level comment so the new rule reads cleanly. The streaming / failed-run / empty-response cases keep their existing "hidden" assertions; those are still excluded under the new rule. --- apps/web/src/components/AssistantMessage.tsx | 42 ------------------- .../components/AssistantMessage.test.tsx | 17 +++----- .../tests/components/chat-feedback.test.tsx | 4 +- 3 files changed, 8 insertions(+), 55 deletions(-) diff --git a/apps/web/src/components/AssistantMessage.tsx b/apps/web/src/components/AssistantMessage.tsx index 9efb2e3d7..016a96743 100644 --- a/apps/web/src/components/AssistantMessage.tsx +++ b/apps/web/src/components/AssistantMessage.tsx @@ -168,7 +168,6 @@ export function AssistantMessage({ message, hasEmptyResponse, hasUnfinishedTodos: unfinishedTodos.length > 0, - hasArtifactWork: hasArtifactWorkSignal(message, produced.length), }); const showCompletionRow = showFeedback || @@ -359,58 +358,17 @@ function isFeedbackEligible({ message, hasEmptyResponse, hasUnfinishedTodos, - hasArtifactWork, }: { streaming: boolean; message: ChatMessage; hasEmptyResponse: boolean; hasUnfinishedTodos: boolean; - hasArtifactWork: boolean; }): boolean { if (streaming || hasEmptyResponse || hasUnfinishedTodos) return false; - if (!hasArtifactWork) return false; if (message.runStatus) return message.runStatus === "succeeded"; return !!message.endedAt; } -function hasArtifactWorkSignal(message: ChatMessage, producedFileCount: number): boolean { - if (producedFileCount > 0) return true; - if (message.content.includes(" { - if (event.kind !== "live_artifact") return false; - return event.action === "created" || event.action === "updated"; - }); -} - -function hasSuccessfulFileMutation(events: AgentEvent[]): boolean { - const errorByToolId = new Map(); - for (const event of events) { - if (event.kind === "tool_result") { - errorByToolId.set(event.toolUseId, event.isError); - } - } - return events.some((event) => { - if (event.kind !== "tool_use") return false; - if (!isFileMutationToolName(event.name)) return false; - return errorByToolId.get(event.id) !== true; - }); -} - -function isFileMutationToolName(name: string): boolean { - return ( - name === "Write" || - name === "write" || - name === "create_file" || - name === "Edit" || - name === "str_replace_edit" - ); -} - function MessageTimestamp({ message, t, diff --git a/apps/web/tests/components/AssistantMessage.test.tsx b/apps/web/tests/components/AssistantMessage.test.tsx index e9c6ab9e5..b38b58cd7 100644 --- a/apps/web/tests/components/AssistantMessage.test.tsx +++ b/apps/web/tests/components/AssistantMessage.test.tsx @@ -1,10 +1,9 @@ // @vitest-environment jsdom /** - * Visibility-gate coverage for assistant artifact feedback (issue #1288). - * Feedback should only appear for successful assistant turns that produce - * or update an artifact, not for text-only acknowledgements, failed runs, - * streaming turns, or empty responses. + * Visibility-gate coverage for the assistant feedback widget. It should + * appear after any successfully completed turn, and stay hidden for + * streaming turns, failed runs, and empty responses. */ import { cleanup, render, screen } from '@testing-library/react'; @@ -61,7 +60,7 @@ function producedFile(name: string): ProjectFile { } as ProjectFile; } -describe('AssistantMessage feedback gate (issue #1288)', () => { +describe('AssistantMessage feedback gate', () => { it('shows the feedback widget after a successful turn that produced files', () => { render( { expect(screen.getByRole('button', { name: 'Not helpful' })).toBeTruthy(); }); - it('hides the feedback widget for a successful text-only turn with no producedFiles', () => { - // Regression for lefarcen P2: the issue scopes feedback to - // turns that delivered a final artifact, not every successful - // turn. Text-only acknowledgements ("Got it.") must not prompt - // for feedback. + it('shows the feedback widget for a successful text-only turn with no producedFiles', () => { render( { onFeedback={vi.fn()} />, ); - expect(screen.queryByRole('group', { name: 'Feedback' })).toBeNull(); + expect(screen.getByRole('group', { name: 'Feedback' })).toBeTruthy(); }); it('hides the feedback widget while the turn is still streaming', () => { diff --git a/apps/web/tests/components/chat-feedback.test.tsx b/apps/web/tests/components/chat-feedback.test.tsx index 0919ff261..ece2faaed 100644 --- a/apps/web/tests/components/chat-feedback.test.tsx +++ b/apps/web/tests/components/chat-feedback.test.tsx @@ -144,12 +144,12 @@ describe('chat assistant feedback', () => { vi.restoreAllMocks(); }); - it('collects feedback only after an assistant turn produces an artifact', () => { + it('collects feedback after any successfully completed assistant turn', () => { renderChatPane({ messages: [completedAssistant()], }); - expect(screen.queryByRole('group', { name: 'Feedback' })).toBeNull(); + expect(screen.getByRole('group', { name: 'Feedback' })).toBeTruthy(); }); it('collects positive and negative feedback on completed artifact results', () => { From 10192dcc52669a05faab964850105e4be54dc1c2 Mon Sep 17 00:00:00 2001 From: Marc Chan Date: Thu, 21 May 2026 16:08:13 +0800 Subject: [PATCH 02/94] fix(ci): catch nix hash drift before merge (#2530) * fix(ci): catch nix hash drift before merge * fix(nix): add pnpm hash refresh helper * chore(nix): drop redundant hash alias * fix(nix): raise update-hash output buffer Generated-By: looper 0.8.1 (runner=fixer, agent=opencode) * fix(nix): handle current pnpm deps hash Generated-By: looper 0.8.1 (runner=fixer, agent=opencode) * fix(nix): reject non-mismatch hash updates Generated-By: looper 0.8.1 (runner=fixer, agent=opencode) --- .github/workflows/ci.yml | 35 +++++++++++- AGENTS.md | 2 + nix/README.md | 31 +++++++---- nix/package-daemon.nix | 7 +-- nix/package-web.nix | 7 +-- nix/pnpm-deps.nix | 9 +++ package.json | 1 + scripts/update-nix-pnpm-deps-hash.ts | 82 ++++++++++++++++++++++++++++ 8 files changed, 149 insertions(+), 25 deletions(-) create mode 100644 nix/pnpm-deps.nix create mode 100644 scripts/update-nix-pnpm-deps-hash.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e371c7d66..3a02c351e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -35,6 +35,7 @@ jobs: web_tests_required: ${{ steps.detect.outputs.web_tests_required }} tools_dev_tests_required: ${{ steps.detect.outputs.tools_dev_tests_required }} tools_pack_tests_required: ${{ steps.detect.outputs.tools_pack_tests_required }} + nix_validation_required: ${{ steps.detect.outputs.nix_validation_required }} workspace_validation_required: ${{ steps.detect.outputs.workspace_validation_required }} steps: @@ -49,6 +50,7 @@ jobs: web_tests_required=false tools_dev_tests_required=false tools_pack_tests_required=false + nix_validation_required=false workspace_validation_required=false if [ "${{ github.event_name }}" = "pull_request" ]; then gh api --paginate \ @@ -71,16 +73,19 @@ jobs: if [[ "$file" == "tools/pack/"* || "$file" == "apps/packaged/"* || "$file" == "apps/desktop/"* || "$file" == "packages/host/"* || "$file" == "packages/platform/"* || "$file" == "packages/sidecar/"* || "$file" == "packages/sidecar-proto/"* ]]; then tools_pack_tests_required=true fi - if [[ "$file" == "package.json" || "$file" == "pnpm-lock.yaml" || "$file" == "pnpm-workspace.yaml" || "$file" == ".github/workflows/ci.yml" ]]; then + if [[ "$file" == "package.json" || "$file" == "apps/"*/"package.json" || "$file" == "packages/"*/"package.json" || "$file" == "tools/"*/"package.json" || "$file" == "e2e/package.json" || "$file" == "pnpm-lock.yaml" || "$file" == "pnpm-workspace.yaml" || "$file" == ".github/workflows/ci.yml" ]]; then daemon_tests_required=true web_tests_required=true tools_dev_tests_required=true tools_pack_tests_required=true fi + if [[ "$file" == "package.json" || "$file" == "apps/"*/"package.json" || "$file" == "packages/"*/"package.json" || "$file" == "tools/"*/"package.json" || "$file" == "e2e/package.json" || "$file" == "pnpm-lock.yaml" || "$file" == "pnpm-workspace.yaml" || "$file" == "flake.nix" || "$file" == "flake.lock" || "$file" == "nix/"* || "$file" == ".github/workflows/ci.yml" || "$file" == ".github/workflows/nix-check.yml" ]]; then + nix_validation_required=true + fi case "$file" in *.md|*.mdx|*.txt|LICENSE|.gitignore|.editorconfig|.vscode/*|.idea/*|docs/*|.github/ISSUE_TEMPLATE/*|.github/CODEOWNERS) ;; - apps/landing-page/*|.github/workflows/landing-page-ci.yml|.github/workflows/landing-page-deploy.yml|.github/workflows/blog-indexing-on-deploy.yml|.github/workflows/blog-indexing-monitor.yml) + apps/landing-page/*|flake.nix|flake.lock|nix/*|.github/workflows/nix-check.yml|.github/workflows/landing-page-ci.yml|.github/workflows/landing-page-deploy.yml|.github/workflows/blog-indexing-on-deploy.yml|.github/workflows/blog-indexing-monitor.yml) ;; *) workspace_validation_required=true @@ -90,6 +95,7 @@ jobs: && [ "$web_tests_required" = "true" ] \ && [ "$tools_dev_tests_required" = "true" ] \ && [ "$tools_pack_tests_required" = "true" ] \ + && [ "$nix_validation_required" = "true" ] \ && [ "$workspace_validation_required" = "true" ]; then break fi @@ -105,6 +111,7 @@ jobs: web_tests_required=true tools_dev_tests_required=true tools_pack_tests_required=true + nix_validation_required=true workspace_validation_required=true fi { @@ -112,9 +119,31 @@ jobs: echo "web_tests_required=$web_tests_required" echo "tools_dev_tests_required=$tools_dev_tests_required" echo "tools_pack_tests_required=$tools_pack_tests_required" + echo "nix_validation_required=$nix_validation_required" echo "workspace_validation_required=$workspace_validation_required" } >> "$GITHUB_OUTPUT" + nix_validation: + name: Validate Nix flake + needs: [change_scopes] + if: ${{ needs.change_scopes.outputs.nix_validation_required == 'true' }} + runs-on: ubuntu-latest + timeout-minutes: 45 + + steps: + - name: Checkout + uses: actions/checkout@v6.0.2 + + - name: Install Nix + uses: cachix/install-nix-action@v27 + with: + extra_nix_config: | + experimental-features = nix-command flakes + accept-flake-config = true + + - name: nix flake check + run: nix flake check --print-build-logs --keep-going + preflight: name: Preflight needs: [change_scopes] @@ -583,7 +612,9 @@ jobs: validate: name: Validate workspace needs: + - change_scopes - preflight + - nix_validation - core_tests - app_tests - e2e_vitest diff --git a/AGENTS.md b/AGENTS.md index b41018632..b7ed2d64c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -164,6 +164,7 @@ For the full tag dictionary, operational playbook (direct merge / duplicate-titl ## Validation strategy - After package, workspace, or command-entry changes, run `pnpm install` so workspace links and generated dist entries stay fresh. +- Treat every `pnpm-lock.yaml` change as requiring a Nix pnpm deps hash refresh check. Use `pnpm nix:update-hash` to regenerate `nix/pnpm-deps.nix`, then re-run `nix flake check --print-build-logs --keep-going` (or rely on the PR `Validate workspace` gate if Nix is unavailable locally). - Before marking regular work ready, run at least `pnpm guard` and `pnpm typecheck`, plus the package-scoped tests/builds that match the files changed. Do not use or add root `pnpm test`/`pnpm build` aliases. - For local web runtime loops, prefer `pnpm tools-dev run web --daemon-port --web-port `. - On a GUI-capable machine, validate desktop by running `pnpm tools-dev`, then `pnpm tools-dev inspect desktop status`. @@ -188,6 +189,7 @@ For a worked example of one full loop (red e2e spec → fix → green), see `e2e ```bash pnpm install +pnpm nix:update-hash pnpm tools-dev pnpm tools-serve start updater pnpm tools-dev start web diff --git a/nix/README.md b/nix/README.md index 7e4a30a1b..9e62f76a3 100644 --- a/nix/README.md +++ b/nix/README.md @@ -220,18 +220,27 @@ Never inline a secret with `pkgs.writeText` or `home.file`. ## First-build hash pinning -Both `nix/package-daemon.nix` and `nix/package-web.nix` vendor the pnpm -store via a fixed-output derivation (`pnpmDeps`). The `outputHash` -defaults to `lib.fakeSha256` so `nix build` will fail with the expected -hash printed. Copy that value into the matching `pnpmDepsHash` constant -at the top of each file and re-run. Bump the hash whenever -`pnpm-lock.yaml` changes. +`nix/pnpm-deps.nix` is the single source of truth for the vendored pnpm +store hash used by both `nix/package-daemon.nix` and +`nix/package-web.nix`. If `pnpm-lock.yaml` changes, run: + +```bash +pnpm nix:update-hash +``` + +The script temporarily swaps one consumer to `lib.fakeHash`, runs +`nix build .#web --print-build-logs`, extracts the expected hash from the +failure output, writes it back into `nix/pnpm-deps.nix`, and restores the +consumer file. ## CI `.github/workflows/nix-check.yml` runs `nix flake check` on pushes to -`main` and can also be started manually with `workflow_dispatch`. It is -not a default pull request gate: the flake is a community installation -and deployment surface, while regular PR validation stays focused on the -primary product delivery checks. The flake check already builds the -`daemon` and `web` checks declared in `flake.nix`. +`main` and can also be started manually with `workflow_dispatch`. + +Pull requests that touch Nix or dependency inputs are validated earlier in +`.github/workflows/ci.yml` via the required `Validate workspace` gate. +That PR path runs `nix flake check` when `pnpm-lock.yaml`, package +manifests, `flake.*`, `nix/**`, or the Nix workflows change, so fixed- +output hash drift is caught before merge while keeping unrelated PRs off +the slower Nix path. diff --git a/nix/package-daemon.nix b/nix/package-daemon.nix index d45ee2a39..625321e90 100644 --- a/nix/package-daemon.nix +++ b/nix/package-daemon.nix @@ -39,12 +39,7 @@ let pname = "open-design-daemon"; version = (lib.importJSON ../package.json).version; - # Vendored pnpm store. The hash MUST be pinned on first build: - # `nix build .#daemon` will fail with the expected hash printed; copy - # that into `pnpmDepsHash` below. Bump it whenever pnpm-lock.yaml - # changes. - pnpmDepsHash = "sha256-TI7gjjF47YIkLblWjG9flG3E1mg310AI5S4uZ+9B2kI="; - # pnpmDepsHash = lib.fakeHash; + pnpmDepsHash = (import ./pnpm-deps.nix).hash; in stdenv.mkDerivation (finalAttrs: { inherit pname version src; diff --git a/nix/package-web.nix b/nix/package-web.nix index c4115f1d6..b4479018b 100644 --- a/nix/package-web.nix +++ b/nix/package-web.nix @@ -26,12 +26,7 @@ let pname = "open-design-web"; version = (lib.importJSON ../package.json).version; - # Vendored pnpm store. The hash MUST be pinned on first build: - # `nix build .#web` will fail with the expected hash printed; copy - # that into `pnpmDepsHash` below. Bump it whenever pnpm-lock.yaml - # changes. - pnpmDepsHash = "sha256-TI7gjjF47YIkLblWjG9flG3E1mg310AI5S4uZ+9B2kI="; - # pnpmDepsHash = lib.fakeHash; + pnpmDepsHash = (import ./pnpm-deps.nix).hash; in stdenv.mkDerivation (finalAttrs: { inherit pname version src; diff --git a/nix/pnpm-deps.nix b/nix/pnpm-deps.nix new file mode 100644 index 000000000..6f970dc3e --- /dev/null +++ b/nix/pnpm-deps.nix @@ -0,0 +1,9 @@ +{ + # Vendored pnpm store for the workspace packages built by the flake. + # + # Refresh this hash whenever pnpm-lock.yaml changes: + # 1. Temporarily set the consuming `hash = lib.fakeHash;` + # 2. Run the relevant nix build/flake check + # 3. Copy the expected hash printed by Nix into `hash` below + hash = "sha256-EqvfkMBoYHuGIu8mXYnUjXTUhKVhgqOg32mr2EzPkgs="; +} diff --git a/package.json b/package.json index 29a2fac79..96ff06dba 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,7 @@ "tools-pack": "pnpm exec tools-pack", "tools-pr": "pnpm exec tools-pr", "tools-serve": "pnpm exec tools-serve", + "nix:update-hash": "tsx ./scripts/update-nix-pnpm-deps-hash.ts", "guard": "tsx ./scripts/guard.ts && node --import tsx --test scripts/style-policy.test.ts", "i18n:check": "tsx ./scripts/i18n-check.ts", "i18n:coverage": "tsx ./scripts/i18n-coverage-report.ts", diff --git a/scripts/update-nix-pnpm-deps-hash.ts b/scripts/update-nix-pnpm-deps-hash.ts new file mode 100644 index 000000000..eb0e813f7 --- /dev/null +++ b/scripts/update-nix-pnpm-deps-hash.ts @@ -0,0 +1,82 @@ +import { readFile, writeFile } from "node:fs/promises"; +import path from "node:path"; +import process from "node:process"; +import { spawnSync } from "node:child_process"; + +const repoRoot = path.resolve(import.meta.dirname, ".."); +const consumerPath = path.join(repoRoot, "nix/package-web.nix"); +const sharedHashPath = path.join(repoRoot, "nix/pnpm-deps.nix"); + +const consumerHashLine = " hash = pnpmDepsHash;"; +const fakeHashLine = " hash = lib.fakeHash;"; +const nixCommand = ["build", ".#web", "--print-build-logs"]; +const maxNixOutputBufferBytes = 32 * 1024 * 1024; + +function extractExpectedHash(output: string): string | null { + const matches = [...output.matchAll(/got:\s*(sha256-[A-Za-z0-9+/=]+)/g)]; + return matches.at(-1)?.[1] ?? null; +} + +async function main(): Promise { + const originalConsumer = await readFile(consumerPath, "utf8"); + if (!originalConsumer.includes(consumerHashLine)) { + throw new Error( + `Expected to find \`${consumerHashLine.trim()}\` in ${path.relative(repoRoot, consumerPath)}`, + ); + } + + const fakeHashConsumer = originalConsumer.replace(consumerHashLine, fakeHashLine); + + await writeFile(consumerPath, fakeHashConsumer, "utf8"); + + try { + const result = spawnSync("nix", nixCommand, { + cwd: repoRoot, + encoding: "utf8", + maxBuffer: maxNixOutputBufferBytes, + stdio: ["inherit", "pipe", "pipe"], + }); + + if (result.error) { + throw new Error(`Failed to execute nix: ${result.error.message}`); + } + + if (result.status === 0) { + throw new Error( + "nix build unexpectedly succeeded after replacing the fixed-output hash with lib.fakeHash.", + ); + } + + const combinedOutput = `${result.stdout}${result.stderr}`; + const nextHash = extractExpectedHash(combinedOutput); + if (!nextHash) { + throw new Error( + "nix build failed without reporting a fixed-output hash mismatch (`got: sha256-...`). " + + `Refusing to update ${path.relative(repoRoot, sharedHashPath)}.\n\n${combinedOutput}`, + ); + } + + const originalSharedHash = await readFile(sharedHashPath, "utf8"); + const updatedSharedHash = originalSharedHash.replace( + /hash = "sha256-[A-Za-z0-9+/=]+";/, + `hash = "${nextHash}";`, + ); + + if (updatedSharedHash === originalSharedHash) { + process.stdout.write( + `${path.relative(repoRoot, sharedHashPath)} already pins ${nextHash}; no update needed.\n`, + ); + return; + } + + await writeFile(sharedHashPath, updatedSharedHash, "utf8"); + process.stdout.write( + `Updated ${path.relative(repoRoot, sharedHashPath)} to ${nextHash}.\n` + + `Re-run \`nix flake check --print-build-logs --keep-going\` to confirm.\n`, + ); + } finally { + await writeFile(consumerPath, originalConsumer, "utf8"); + } +} + +await main(); From af63af39513f9e7de5433c0daa26047205dc955e Mon Sep 17 00:00:00 2001 From: Jane <522700967@qq.com> Date: Thu, 21 May 2026 17:39:43 +0800 Subject: [PATCH 03/94] feat(landing-page): refresh brand mark and publish a real favicon.ico (#2561) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replaces the four icon assets in `apps/landing-page/public/` with renders of the new brand mark — black-fill speech bubble + white pointer arrow — and adds a real multi-resolution `favicon.ico` at the path SEO crawlers actually probe. Why - The brand mark was refreshed on 2026-05-21 (canonical source: black 2988×2988 PNG of the speech-bubble + pointer logo). The marketing site needed the matching favicon, apple-touch-icon, and header brand mark refreshed in lockstep so the browser tab, iOS home-screen tile, and the in-page nav glyph all line up with the new identity. - `/favicon.ico` did not exist on the published site. The Astro head declares ``, which modern browsers honor, but a long tail of SEO crawlers, link-preview services (Slack, Discord, third-party SEO tools), and older clients hard-probe `/favicon.ico` regardless of the link tag. Hits to that URL were falling through to the SPA fallback HTML (200 with `content-type: text/html`), so those clients rendered an empty/broken favicon. Several SEO surfaces showed an empty black circle instead of the brand mark. - Adding a real `favicon.ico` plus an explicit `` is the smallest defensive fix that covers both well-behaved and hard-probing clients. What - Regenerated icon assets from the new logo source: - `favicon.ico` — multi-resolution ICO with 16/32/48/64 PNG-encoded entries. The 16/32 entries are what browser tabs, bookmarks, and most crawlers sample; 48/64 cover high-DPI tabs and Windows pinned-tile sampling. - `favicon.png` — 32×32 PNG (existing slot). - `apple-touch-icon.png` — 180×180 PNG (existing slot, iOS home-screen). - `logo.webp` — 144×144 WebP, 4× the 36px logical size used by the header brand mark for crisp retina rendering. - Added `` to both `app/pages/index.astro` and the shared `sub-page-layout` so every route under `open-design.ai` advertises the ICO. Existing PNG and apple-touch links are preserved — modern browsers will still pick the PNG, the ICO catches the hard-probing tail. Surface area - Marketing site only. No `apps/web`, `apps/daemon`, contracts, or CLI surfaces touched. - No new dependencies; assets generated locally from the canonical source via `magick` + `cwebp` and committed as static files. Validation - `pnpm --filter @open-design/landing-page typecheck` — 0 errors. - File integrity: - `favicon.ico` — `MS Windows icon resource - 4 icons, 16x16, 32x32, 48x48, 64x64` - `logo.webp` — `RIFF Web/P image, VP8 encoding, 144x144` - Manual: `/favicon.ico` will return `image/x-icon` once deployed, not the SPA fallback HTML it returns today. Followup - Once Cloudflare's edge cache rolls (or is purged), third-party favicon caches (Google SERP, Slack link-preview) take days-to-weeks to refresh on their own; that lag is expected and not a deploy problem. Co-authored-by: Joey-nexu --- .../app/_components/sub-page-layout.astro | 4 ++++ apps/landing-page/app/pages/index.astro | 8 ++++++++ apps/landing-page/public/apple-touch-icon.png | Bin 32253 -> 7983 bytes apps/landing-page/public/favicon.ico | Bin 0 -> 32038 bytes apps/landing-page/public/favicon.png | Bin 2262 -> 1504 bytes apps/landing-page/public/logo.webp | Bin 5260 -> 1626 bytes 6 files changed, 12 insertions(+) create mode 100644 apps/landing-page/public/favicon.ico diff --git a/apps/landing-page/app/_components/sub-page-layout.astro b/apps/landing-page/app/_components/sub-page-layout.astro index ebb08ffd0..94b5aa065 100644 --- a/apps/landing-page/app/_components/sub-page-layout.astro +++ b/apps/landing-page/app/_components/sub-page-layout.astro @@ -80,6 +80,10 @@ const ldArray = jsonLd ? (Array.isArray(jsonLd) ? jsonLd : [jsonLd]) : []; ))} + {/* See `index.astro` for why we publish + link `/favicon.ico` in + * addition to the PNG: SEO crawlers and link-preview services + * hard-probe that exact path. */} + diff --git a/apps/landing-page/app/pages/index.astro b/apps/landing-page/app/pages/index.astro index f816b951a..ccb28a9ef 100644 --- a/apps/landing-page/app/pages/index.astro +++ b/apps/landing-page/app/pages/index.astro @@ -35,6 +35,14 @@ const pageHtml = renderToStaticMarkup( ))} + {/* + * Favicon links. Modern browsers prefer the explicit PNG. We also + * publish `/favicon.ico` and link it because many SEO crawlers, + * link-preview services, and older clients hard-probe that exact + * path — without a real ICO at that URL they get the SPA fallback + * HTML and render an empty/broken icon in third-party UIs. + */} + diff --git a/apps/landing-page/public/apple-touch-icon.png b/apps/landing-page/public/apple-touch-icon.png index d9c9b567e496c877c9a59327573e8aff5cf5a132..67d8aeb49985a561f4b806330d164cbfe39288a8 100644 GIT binary patch literal 7983 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

w(zcsoci+0seDOHHJmEo z#6^XNEFIah&jQ+$MfWF>h0zvMZWF1Z-Bk>$Q*ygdi zdz@ud8UTt%OS};`a%w})GD14Mb%*&eP1U;!o}A6YH$l)~TJnHsCc4PFGYxTi=eAqv z&UM`I^v(JR8;O#k zBGx4Y_4O(<>Qh*=Iw|%uw(+}eWk&_3x+U`YnIRmTNQF|VfB5$O_Y0U84XkSO_T50k zN1|nbx~L+z-m9F`mh_KV@*PWUWDcH{=o6({38?GjtYfAIl@J;_qN)tqmZ64~#Np?5 zIbgbndO#7?J?cV!=?-G!GWYIVEK?h&xqCF0-#gJpiw8!L`Q2F(eQX@>7=!yz8PJuCRfsz)&rU$2>{{l> zq!s(YjAHlPbK7mq3@(OMYjrooPmYv=x!l*bw6!H)i3~x62uDJVwre2pIA`$^k2C94 zk0W{fE6BD5IU9Iihozq1Yje(4S&9|RQtr8~qTDsl^m?dx_zaw4E8u;{&A0wWs34jM z)uM+QzTbMe-bG)zT1;kt{yz3t4eBY^! z0=01GQ#(4d0}ma36>6aBmyx*$h(44`6R0I8(pJi^P9?2krYyZOX5l;AEcM_3w(r%h zWqFaQAu3JWM-vhij`u2gxaJ;5D7prRk#x>ZZrVYQgkqoHu2HLOu3&2T4Cc7qhc}~J z-*va$cF$XI*McC>4%^Xi zV5`oQW9et5eWK13;t@ z+!kO9!f(w?N+&I^pvRwh+I&5OOh!t6CW0CchX$(p`DO?~EwzHaDYmUX$uxKFJ@=xi zKwTkv2f<=`U}t#n)HhxWE!@P4z>Hf$G##ECEnt%xht4_#W`R7-p&7)SonIceng%3O$`y)ppY`;SK0dz zw+8Qfi`%rxO>qYq5N315z}UcN#%{iMa+%dD(ZuPY*(}VtZN+H;MlwY>G;8eKCj{mS znJWPI0G;;tTi|^QY+^wKy=n%#V(nN8pGHOuqjw5bY$0Ch>(}Tr>6vANkwX>^gu+() zZ_ioFliLBQPzN0x>I65@Mvc7)_bu2F&L=pIaSRekvxxmPfFTZPH$)SSpGVN`B~lZ% z=L=uAA}h|Fc7wDl4se|6kN0P?=ICNo>xV3972Y^$@tpiyWqL2qWT0GGyU3^7FB=Oe6M+4bcIZRF9P;1-wNUB&sQr0J!uG-p@PDxNIl z6Od&uu8mC9XpFUjc5EM?b5PtK{h!8EBER z$q=x$vuiDsX+PR$RsZwvEz0U}ZM5SxYMq5f(WU}yZ&GE5-^aLng!38b&_mQQXcA-1 z;8r;Ep|3o|G&&j$$Eju?cq#fzg7i0TYY!aAh+;Xqt0FV61-NnV+FQ)-dn;U-Cz>wA zowF}F_DFa3#J69P&@0>sto_98pAcX*X7~Py%X*@QjFzPNkiOKbFx?mNUl=rI+0U-4 z0loC|CoPiF)-`O*gPiDLd1el5)Gca&>tF`pG&YdEa;$&Ea+~*9oayWYKBF@c^=$%m zC3x2|Gfi?|D$o(#0?4aqB=MssZOdoBXno&(+*%kV3d~x@674zZ5EJL;4F6rp35-NbM}WUgFX$JBaB1hOg!b8K^o|GhfZ38)#S|#mYa~+9x&DuD%=RF;IqPtNNSiI#3B-!Sx87i-_aZyXRwKt0cLdiNBV)U2$5NjW zl`fSg+VqQe5YG9hYgyfiLyOD;xSrJW*Le=&TAbXgZiyYL3I)IQHUIzZoeOZCM|H=~ zeM!2qB**eQP8`d!ahy2G#5g=cAb3Iuh0u@`+ze?FGNl78GcCBxK%gB=nU)!57>0q; z2SaCCrqgz4QaXVa7$D7~Z4wA+9&QpV@k4${vSdB3?v?KQ_W#?XwXTDQ5+lcQtktac z`}W&!@9uv8J$v@-*|Qbfbk}C99(X{9T6XATwd&Aw7HiZ6G=wy?q{(=aXwVVnx1)xP zGAIe*u&U2>Qag=PQXi$0V&riJ+}Vqo2JShc$)374XRnDwAaichL4%k%sEEub6ra|L zDxHHYL$(62w;mxm&ioiZc-A{wP&HBqo^{lg`B_pUp4nhFWrItEtR3%i^-vnHjYiK` zQd3i(AG>s0|LnWJ{1uyv0<8sREU*A-H1Cjbz>aW@oq1GDQCWt6od8^`ECB;Ca&IvHNg}2EvydUxicdx#G)UDaWL${Zw6{!{2 zOq-40BvxY^=gg>GGQyH5b}~2dk3H4Iy{Fn50<B5Sx5s__Z|n!;n!t5njWyiZ}wQ~4c(&XS{G|DNkk=3oRg9_B!`RuIn@H6$@eIG z1YkqdH$YQEC=)y6%&=L+lCCVEbJ7)KoTOE9&W3UI>UXgN3mF=g81VxkRO1yhEIw5& zw@u1NH{GbF(25{r6$3I8<9(TY`Li!<+$i4T)SNVbwT2!T1F1{ur&%Qg&@w9>e4DO2 z!9n4zU_bov&j>t~gytjpcWiHeRNQ$*H^Ja%iW}up6)$DAc&eoQiDy1P`?a z?8vBf4v`8PDK#O!EY@SxUhFMe`$sOdvE{?s>4M;|U8D-y1@KS)QBS=JuiTrFIye6! ztOWcv9!l)21@}&w6~#8V;DW8^=9gcs*P)^Goe4ubXc0vrDzpcdF0=dxe#>lAj~YwF z4OXMB(L6W&<=Sg5`_8Z0BwF`!$puo46R+cf3n3KSJ<47j1*PqEB%-~c+h2=;h?wujV_I>nCe^@FwY zS~K6E1=zilP_G`yw%O#4x0}6Xt7xA_Y}6xUf853=ez4@o;1}kiJ zunOhLF*E>tSBN0;ta2wSb_DWF6+?YamhP;o=t~Q$N(RWzR2KG%I=bips;uYQfx^Py zV5ZX!-|#lGcgweG$STV7!o<-RS_)$yAKP|4x?8`p3ltjZg@cq(L!|&a%Yrajj4TLL z)_HD8VvqCLzFr1DP0e6Epw@L8YLyku4{o&do1~N~1tynrJ9~ohN7B$FhG0ln1?@sz&CF5T4hleDIAjMR4c4HTlzKziExq?Mhr78&Bn@ zKU(U&R(6c%#%WUM`Wg;kfU5(8|C)NH(|UCPHGl(})>v?7QwxZ`>N^5zMp;QPHTtrC!JbM^ z7$a_6NZNbZCTo1?H?(xxEgB{jIEgZCdZK2Nqq}Ooum7%+kH_ND}L-wGgeZAi736$ zBCO;`zGe^ie|4_w94K@IGvE#yQ36?HGkBI1SW^O~b`%EctV1e-)MK%pv9T6$-UYf| zrTF#$YJd%ELEmHQIAOcj4_Nx{%{I19CJ&|Hp3*vg5PgN7?0ofHS8gG{)7=69*_ZGU z%aw#~jJ#t+zO9(DLl>>F>5tqhrK4WaI?0()25%9e8yWsLn;iZl`vv9*PPOEs(9Soz z{sus1!Uwf>>KvEDeZeJCYE}`&E72>JzDsFW>w1;0*T^r|J>U;JTx7!oaF;cG9K1v- zsXMQ-xlIJGg#aQX9#=YhrHH zt6gt`$)S?!{V4ibz%}i0rd=R6DgqY@o7wBM ztX&bx#fIo~Oi46G{-oQrC4GMW!5hjRC z7?~h4IV9vc@huYKW#l__-K{jNK9W;t2+ev6+IiRahAGQ_a-)r1IAs-`Rf2($AUYJ) zM)=2ULQxBD-n2jju@{K1#UFyIR#76(WMq@8Hn`iK^($@sqwjOdtgj!wJ)eJk{6MBK z@uB>NH_e$u<7CYvZ97)-&~_%oI)_U=Tx#Kp|IR+Jh@u( zPU=9Oi$$4-^&}tOb4iHoFPUw<$a+5WT08i~-L~w1WtUmpe8?Lxi}j!zguKWymh9Wgf#Uj{4 zrMp}Ir{x`_qWFyt?YAq~^H)@D(`T-*$cqY<2Um<3`z(;R0s zm?2*xI9#PQ31oR6Ido1V@ZQUMZTe$kGkRVl;EOnEM~aXRm=EZPj}KJWU-#fEQT&-n zp-VU5lU>rm(9bm<{uxP!vl$st*OVBCWT=KV#_B#T`kPdo4z86i2*zS5bd6O_?+iKd z)H3#{{KQPbQD|4vlo*o3*4O)jb$qtR>Ti({P-k3|0u8M|30{d7Son>X-2y>ap@o!` z@H!w<1J7Q2rA>eGc9|bIUmq6IEzjP-+~|Jo(7D5Uw*SY={*hCBA?#Rz;v(GuMmQ)f zEC(Kz#HqCOnswh2U<&&IC0F_N4pI@{dB~fvAM9zNCrdR@InXJXBo=j5*8jmk^^s__ z3g&rxc1_wY`-F~4TejCmzT0o-=etGonW8$iW+Vvwz0TLDtM`{v9F2kU4<%S|bK3$l z?W(qQr7GH3co0yzViPo0vGTebT$Hs#?|7@(T{mdAgRF$2pP_FRgww~4J&~C%e@JY` zPZarzIf=lXHzou$K$!|e*)yz2O0Z+&6)cETPUo#>Q$u%QG0@b*LFaihCofY6l3D}# zRsmT%$&^z{D@bTr(?sa=)$NJyAzT0ZI+*f&?U4SvL$+!}`xM1X7t{H0rb((UjXfGw zyUW=*Ot)Kbr=tbL4-1ztj#*J)_EMGwVA?*ZSsyyzHSPO*VMV9qZof`;G_IAYR(%`o zrQkbeEK5)fvgI6Phd*)zf(sSGRqw$85u`>+FK_SKI!- z+--~cE7mF7c`Y?by^}>L(yMbxcAcra?8Nm!{OyJRbJGHhdho`qPp~(VuQP?=7sa>A zPiuMmP|x|6``~-buG7g>Y&$E-i+R;jq1n;Vf3eZVNAj1vV_Hp~HnOB*y;ogo#m<859oT0rxi0OYREJ1P>1l?aJ6SpOlMC)wXaRPlqeWMi zT#n=D+|anrfa#VI%G<89`iG@4ci{^E$TF1{RsjpSaT^=G+y46D&)Iu-!keBkm-yGl z)d*kohmCnW8gU%c&QZwxL1BTlhulLOV_dw4mJ`oCX#sUiM-648RKI~99}D70win4~ zg1n?)=ZS7>QlY(vQ{s`!Qa8+t<1R^;wq4pHT|diO*VhkQ>Zb+WEmAr$JnTagu>F@5 zsC)1i>9z}*h@Va2cjxB+NMwH5+H*2PL)7#@phZ`aD-X0^MhF^Gq`M%*zz zxjQ{x{KIO`uYLcQ&HHqwQy-+N@WdW^#bFSDpi@|$GuZ>gP(?r+sUvBK=RG|4IDo}H zX>l%yBPBBHqS6%w*;WB|k)S*&08iN-Wm;BDS{nhvrb^vS>2#9X)>d2jIT<3k=b#PV z+b^s7UDht%nw{m1lq4$B7fDJjF@r@PO z?W1iFOki%Im5mo7ZDp}_=%yDHcX(;mj@|S|vzxD$8P=8hsLB8qBY;6_4P*IxS|5T z)%Gs-r(r*X4@0|STdyX5I~crC4WT&as-rzn^90!F%p?R{+684W_+|8lcuHJBqvA20 zr9HfLqg8Hwn^ttS>L&y4;RDau9kEh=EIFS0!ty7NeP!r|I|}ouT{S6$f;r4oSHTMj z1p+v#?~eFp$9;?gcwEyavu`-1j?Z|`HICd-LRB+{UqP(LyDY+RdVudbmuXeSApr;C z!aTZ?SnrcGA=;Zf_qXTX;3>9VCdhohdB6eL~HxB}bZ>6tpVXr>Nl!YFyJ zI=~!`OH-IeauGfrT5?h<(}#f~=LOb5nTndYV6_$AbB#4_&<+!wfE#)MA1}}qU5rlt z)Fui$>+5fPXlNcbsB}31uMg8q*Z?z=wMfsXd#Ha?NIhN}M}6ZvJdf8v5)mH|22&o7 zDMOueC{PFXKybwz3jk!{1%)VCnHI%He54b%^ma*aq7hPP8sz!nXN3ooB1P&5cNUXydkzEAX;yqtBy8@yO%YxK`5s5kq)lz zwDIc&;v3)UlRkv~jB1R2ED09Nh2&WIujZn2YBN(jTc#x_)haZaoo#Py27Z>2YAurYgGyGvScdhM_$j5 zO7h~;a!no;hocKDl}x&8k2>OgI-2Ll9D&T0;5u?_n~xtF1nfOhs`2f zvm(twml#eQSCfn_lLezUX~g^WYjpb2MQSLKov=hG0)Vy3iWJGF@(-qSjX$fbz3B&< zcq?-LIvpCsnSP_%4|g*s@j&wo+5T7?5<-17<@rPajT>mA=$D zn);iG%WoW;&utQ0Bi&Z-DwdP^HVMmAVb~31;+)UqjX@(sM}@eLfCQ{iTH^7@3fkwJ`>@Y5=fOI7|6JNPJ zG4J_w=SNs*>Z>w)D^UZ@H+0V=k4>u(i8gIo9TnR!(w??SDc+Z^*rd)AT4bC-2y>%XXO%kJv1fqrXyV#wNu%GOnG z)oHQX?yH3Y;1DK2DiDCi#suY>uMXBIv?ImVNWwM7<P}EEC)8Viyt}0H3OUH%@nh_Bk+tykB)Q>WM@gp$J3*wA(OkI44fw5 z`xa$;Cs0kc3&8EOfKDf1Jeg1$z>OZDgBjRk!HE8WLoVyOFF|jZu-zj0yw^SJ{nPkm0*Mm)HyUb6Fdna#8MO`P##{@ zVN<;_r@3{5R`%ALtzGJDMfj3@1+_fv6uw%jS-$Xas!;#NlE+8CJ#y2T)4@=|=ESL1 zjl^SxcmHX!{rS7msc6`9%9w=>-<<-`Xg7SkCd328_$-2wFiJDxJ1n}8K7yKf(lvX! zQCUDlc?rvT>`p}D;TA%CPkLD(B@Wml3Gqc4jwtr6xU4_=^b%0h?dUepbf`89@GNn) z>CJY_7TfK-eUe*0wcna?iJ(Mb!zuEP)eQ)gg z9dlxaOPQZ>7uxC3;aj&^=fHOwtw&attxttoC-9-qf&Y0hH-q@4*Ab)c#|-mq9j2)Nloz-nV!EJso_7&EQxnE}`g!F){U7}diA zZ0@7uS~|S?O{C#jk+{x5b>TgE%RM)2UC-vM^+3+Lj+JbYo;4%_tV?2_QC^H*v-v7- zP0Uz%3ocMTbzFfifg-$05;h@($1Y7_1pjJY1oAV4yWxQ`*kozKoatOVtfn% zC-Dx>+2`EHcS%b+LefT{yCNx+7m5s^86lvy!~tsL8!Z3$J@2#o1;C>lh%B2;VF>dI zx@I>8Oo@NUP(@w4L}XSvq9~&)Xf_N+`;4l#j|5d=UhJQ?CvEes`TvtEAGuIT<4V@qa&gfx23*%{l z$%6uGvRak8=5&WFb}s9*;@ag_?pzwtPnrESca(t;2BY)3NA;>#5&Pw^}JG) zxvJ_@HeUHw=UDl^+!eQ@GNWI9b-MVqZFhdxszU04e@eIQeoG03Ai>Cihm{Q$q;*8F zu9^XPGNAHKj3*?0ypPYgpY>2;SPj6+RFEAk02*z8%-Pia8sGY+Zi z9L-tFs7yA94{sF2-D_@@Dl46&3uzsSlMbe_oT@{qDfgt7muotD0fl%@&a#YtTVbP9 zQWhiSl(rgRRZivEh4R2BE#V7XzU7M(c=Uxg3H)(2`RT+xj3z|ydX{6`|G*< zKdC_M@Blr>GgFbMNffYPLV zC_L%obK>K*=W}vkvuNVx)LQ}zfh+@1BgufuI+*!=-gm0)zvF<0k9PKhy)g{SbPRO) z05n^9opM9PX8}yLqbD8~g)r6taJ&@Yq$f>GFY3pzUR3Edx00aO3R4<$rgdFZSd>^T z3_)Tt)Hs3R5GAkc@E{Hr3JO1M$}1}#UnghjP!@UJw~&7XF0C3}EH5GDBh5@07>CB7 zRqOlo`wyfiE8nk9)}OS?ZySqmpDQ7)bAtQH(VbVOI-dV}OLpkBMLQ%w4bFgR7PNf! zW*`GxT!aJn@i}pPmI!XHc}6_c(+{`6BmyRD;E(+b0e=tB0%qdK1Jx<`XjiA=8bHIE zK(QH904^PY{Wef-It(mGPVohW@Z9{qJ3s+|@PLEn;Flt(cpg2^KxhOK_xR@Lbb<)B zcug8yOdEf_`8}>M;y@W=`=qXva3HToo_dt3{>k7i4$pDcPl-+!)9Vl~7` zU2`iQPw>>x*VflNAN_1Sv*+e?`iVs-3IHd>$baqoPv1KKBrkwWu6?_!+HH;Tb~JB5ZN;Tn;jgPaf#Z*3CeNf{?V8h`+t z2xMn59H7();E^T*lXL(*I-XM!o&z*ob92V`A|T^4^5i-`kH1e|IG&Rq&y}vGwdi_H zJYZu$>`#B9ntZagQhz!zop^rLKz;9v+jiuXD)th+b8frX%0Jf%_uqeiy5q`kY;UBW zd4D4_@aB5DzP6EU)p_GXVm;V81*qKfDdOklI(nWr133bniH{#m1aA)i9N7!!kFk;m zC<#gD*U(4aBMpo6P;WrYyd2MvAF;LbBpq@|~bV6WF=s2_B1+V~# z+C+>1xCp)oXx@oU7oRKYcpT4o2XK-P_Yt@e&`s}AaBGwkm4uE6PaRby_cyAIgUL#A zU!s=mTinR>RY!A!TffztyMNbqnw1=50l8gpGc6Fm$}eb;M(V-+pI(t%{^&bXZNqn` zlKXp08o&W(1Z!+rTAFLV4`Bp!Oh?Ewv@~kLW+^*U058)`s6QaZxdt8v$Dq&vkNh0a zzPDx;(D2Z(B6NTVXpdtNBM^y?z#}e#G6Gal$K$vr zJ~rtLdk{*+Gv3A1DLA;X-mk*+iSK>NO4X;+jqH)t#kRvsUz|Snp6v&6pRrv?_b9-E cdl@b8e=&sa4%@~QqW}N^07*qoM6N<$g1A77>Hq)$ diff --git a/apps/landing-page/public/favicon.ico b/apps/landing-page/public/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..131a5dc30c8de6227dfe7b99052c81bb8ee7fdae GIT binary patch literal 32038 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 delta 2250 zcmV;*2sQWM3)T^kB!2;OQb$4nuFf3k0000)WmrjOO-%qQ0000800D<-00aO400961 z02%-Q00003paB2_0000100961paK8{000010000WpaTE|000010000W00000tcGIq z000O;Nklkxy3X~AGG#<#K8_*~n2&&6zpgzGg(E<3EP z?M!}nrDmBGZGY6lMNpO9ao5|bt*tFZVsEe6Ff;^hbPSB90d^}wP8$N*$su2n)8)@g zItlpx4yL;K>+CNNK-1D82VWGw`10|TnwkXLg+>S?qhJ&j8cV9ov^s%c2#3b9kdmz! zFVDr$uEGib_RM#H!0*BQ(7hCpr2%MKmC$_kj~VZMkbmQ-`5e3?K@$v9j+e4V|1&Cq zk%$x{kKtVfa6i7TUCxR7IhbcZT1;(m0Gjly<>S+bS06oU5Zwb%jV7|ZnEEpRQAF6V z0)Iw4x?b22_EkDx0`uaL`5Ft&YTh3w96R-T-r*zQWf>aJ&DXiDXO`%a4J_IZY`{eI z>Ystx|9@Jnfw=&*;cD^Ix?_dEc}3tACCX{+=KuFzr$ylyTsv4H`zy1*OOg0+#uhts z1FbDnXmR~1=g6A^FVUM*LdA-zs)$fTK-b$9Jz)h|Mb0ai1)%iF(sr*C4ZzdB5ec=xxqi0(TF}m}jvQqr$cLy=xbmHx?QJnj-9*JkpAhGE(OjMREDQE(}AZjyC zVy`NdZ1dr2UvNi$IhgOe_^HLnp48Lzq8|E)(Ga_zN}a*OPdyop%BL^C3?G3BY}taz z2YXA8*+5ZT^lKwydyuK zRw8%A^$r-=h>FEhiX-Kov**zA)Lx`*-H25!tx(M-Xwy`gyk3}VYY9v(!uBKtN`Fe> zy?X~d+wMf?{X3!V{}s~e>Jb|Wy)5H;#*C9N=2I1@W#*fs)V5+WFE#=k4|7)!46Y`O z-Lna@$r!sD}mEfRqe3PGl;keZ6Ldn>VK-#(Ps97yWz zr>)eNV})#TBLqO>e?k;U7J?YNyzrjdv910RhMxHz#_lXbVoS#@UUlbUdOB8Z-;S&; za`$sty1V3uWp&kg+IKq9MMcAAg2x?HI2t zhy3nIaJqs%aHw*29{MdZic2tlqZ?hH*Pxq9bLeU_LN^D%vgBx9R|WL~#crpUFuz6s zLXgr$;$s;E6f!W~r21?T!Q>Vzt~YU`lU zvtTlTr}52jOHWaaq7ul=3IPTkfS*do3OUgjJxnZxL8UwpyZXVmwjoqm0Z=ut980`! zgr~0?lf8Z57HMSj?dkI)lx#9=@ z%Lqenu24y~?|%aR2e#6oH2GBQc0Q5bUM1%;`sgY{KpdLCosC{5YmjIvQ*(tDTw z;|Yw3=xAMSK-a6uxLGeE`M3=0#Q=nHWhy^(ATv>+mVd1Gca^$nIK>K2c^*dYFB|u7 zFMEr29R^c&i*tIOBYd1}+{+2#S(cHL=zK=b0dw@nQjuwJLc!IoBD%LGgX^{S^7BLM%`|CiN3>sq&1~MzUQ3)<0~Db z{S#E-6GKRgrspUUFa#^wA)*Bqnw=KfLBlHwlRnvCqQP!HN^!eH1YRSe+Q96VXkkV2 YKWOVV06sAmibEnHAr$!ifFJ_| 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 SeE$jNL;v1?al&u_0002#RWkwr From 8f7914f8332783c227220083cfb996860cd4fb01 Mon Sep 17 00:00:00 2001 From: "open-design-bot[bot]" <282769551+open-design-bot[bot]@users.noreply.github.com> Date: Thu, 21 May 2026 18:35:17 +0800 Subject: [PATCH 04/94] docs(readme): refresh contributors wall (#2148) Co-authored-by: open-design-bot[bot] <282769551+open-design-bot[bot]@users.noreply.github.com> --- README.ar.md | 8 ++++---- README.de.md | 8 ++++---- README.es.md | 8 ++++---- README.fr.md | 8 ++++---- README.ja-JP.md | 8 ++++---- README.ko.md | 8 ++++---- README.md | 8 ++++---- README.pt-BR.md | 8 ++++---- README.ru.md | 8 ++++---- README.tr.md | 8 ++++---- README.uk.md | 8 ++++---- README.zh-CN.md | 8 ++++---- README.zh-TW.md | 8 ++++---- 13 files changed, 52 insertions(+), 52 deletions(-) diff --git a/README.ar.md b/README.ar.md index 03d035966..cc88156cd 100644 --- a/README.ar.md +++ b/README.ar.md @@ -800,7 +800,7 @@ Issues و PRs و skills جديدة وأنظمة تصميم جديدة، كلّه شكراً لكلّ من ساعد في دفع Open Design للأمام — بكود، بوثائق، بملاحظات، بـ skills جديدة، بأنظمة تصميم جديدة، أو حتى بـ issue حادّة. كلّ مساهمة حقيقية تهمّ، والجدار أدناه أسهل طريقة لقول ذلك علناً. - Open Design contributors + Open Design contributors إن شحنت أوّل PR — مرحباً. تصنيف [`good-first-issue`](https://github.com/nexu-io/open-design/labels/good-first-issue) هو نقطة الدخول. @@ -817,9 +817,9 @@ Issues و PRs و skills جديدة وأنظمة تصميم جديدة، كلّه - - - Open Design star history + + + Open Design star history diff --git a/README.de.md b/README.de.md index 62943804d..35750a3fa 100644 --- a/README.de.md +++ b/README.de.md @@ -726,7 +726,7 @@ Vollständiger Walkthrough, Merge-Messlatte, Code Style und was wir nicht annehm Danke an alle, die Open Design vorangebracht haben: durch Code, Docs, Feedback, neue Skills, neue Design Systems oder auch ein scharfes Issue. Jeder echte Beitrag zählt, und die Wand unten ist die einfachste Art, das laut zu sagen. - Open Design contributors + Open Design contributors Wenn Sie Ihren ersten PR gemergt haben: willkommen. Das Label [`good-first-issue`/`help-wanted`](https://github.com/nexu-io/open-design/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22%2C%22help+wanted%22) ist der Einstiegspunkt. @@ -743,9 +743,9 @@ Das SVG oben wird täglich von [`.github/workflows/metrics.yml`](.github/workflo - - - Open Design star history + + + Open Design star history diff --git a/README.es.md b/README.es.md index 025b71f96..8168117e5 100644 --- a/README.es.md +++ b/README.es.md @@ -787,7 +787,7 @@ Walkthrough completo, estándar de merge, code style y lo que no aceptamos → [ Gracias a todas las personas que han ayudado a mover Open Design hacia adelante: con código, docs, feedback, nuevas skills, nuevos design systems o incluso un issue preciso. Toda contribución real cuenta, y el muro de abajo es la forma más simple de decirlo en voz alta. - Contribuidores de Open Design + Contribuidores de Open Design Si ya enviaste tu primer PR, bienvenido. La etiqueta [`good-first-issue`](https://github.com/nexu-io/open-design/labels/good-first-issue) es el punto de entrada. @@ -804,9 +804,9 @@ El SVG anterior se regenera diariamente mediante [`.github/workflows/metrics.yml - - - Historial de estrellas de Open Design + + + Historial de estrellas de Open Design diff --git a/README.fr.md b/README.fr.md index d4eea9476..dedb2adac 100644 --- a/README.fr.md +++ b/README.fr.md @@ -733,7 +733,7 @@ Guide complet, critères de merge, style de code et refus fréquents → [`CONTR Merci à toutes les personnes qui font avancer Open Design : code, docs, retours, nouveaux Skills, nouveaux Design Systems ou issues bien ciblées. Chaque vraie contribution compte. - Contributeurs Open Design + Contributeurs Open Design Si vous avez livré votre première PR, bienvenue. Le label [`good-first-issue`/`help-wanted`](https://github.com/nexu-io/open-design/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22%2C%22help+wanted%22) est le point d’entrée. @@ -750,9 +750,9 @@ Le SVG ci-dessus est régénéré chaque jour par [`.github/workflows/metrics.ym - - - Historique des stars Open Design + + + Historique des stars Open Design diff --git a/README.ja-JP.md b/README.ja-JP.md index fd869c5e0..c6a84c10b 100644 --- a/README.ja-JP.md +++ b/README.ja-JP.md @@ -723,7 +723,7 @@ Issue、PR、新 Skill、新 Design System を歓迎します。最も効果の コード、ドキュメント、フィードバック、新 Skill、新 Design System、あるいは鋭い Issue — あらゆる形で Open Design を前進させてくださったすべての方に感謝します。すべての実質的なコントリビューションは大切であり、以下のウォールは最もシンプルな感謝の表明です。 - Open Design コントリビューター + Open Design コントリビューター 初めての PR を送った方 — ようこそ。[`good-first-issue`/`help-wanted`](https://github.com/nexu-io/open-design/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22%2C%22help+wanted%22) ラベルがエントリポイントです。 @@ -740,9 +740,9 @@ Issue、PR、新 Skill、新 Design System を歓迎します。最も効果の - - - Open Design star history + + + Open Design star history diff --git a/README.ko.md b/README.ko.md index e335e3cd2..bb0f62dc4 100644 --- a/README.ko.md +++ b/README.ko.md @@ -726,7 +726,7 @@ daemon 부팅 시 `PATH`에서 자동 감지됩니다. 설정 필요 없음. 스 Open Design을 앞으로 나아가게 도와준 모든 분께 감사드립니다 — 코드, 문서, 피드백, 새 skill, 새 디자인 시스템, 또는 날카로운 이슈 하나라도. 모든 진짜 기여가 의미 있고, 아래의 벽이 가장 직접적인 "감사합니다"입니다. - Open Design 컨트리뷰터 + Open Design 컨트리뷰터 첫 PR을 보냈다면 — 환영합니다. [`good-first-issue`/`help-wanted`](https://github.com/nexu-io/open-design/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22%2C%22help+wanted%22) 레이블이 시작점입니다. @@ -743,9 +743,9 @@ Open Design을 앞으로 나아가게 도와준 모든 분께 감사드립니다 - - - Open Design star history + + + Open Design star history diff --git a/README.md b/README.md index 6679ab4de..4fa0d6830 100644 --- a/README.md +++ b/README.md @@ -1018,7 +1018,7 @@ Full walkthrough, bar-for-merging, code style, and what we don't accept → [`CO Thanks to everyone who has helped move Open Design forward — through code, docs, feedback, new skills, new design systems, or even a sharp issue. Every real contribution counts, and the wall below is the easiest way to say so out loud. - Open Design contributors + Open Design contributors If you've shipped your first PR — welcome. The [`good-first-issue`/`help-wanted`](https://github.com/nexu-io/open-design/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22%2C%22help+wanted%22) label is the entry point. @@ -1035,9 +1035,9 @@ The SVG above is regenerated daily by [`.github/workflows/metrics.yml`](.github/ - - - Open Design star history + + + Open Design star history diff --git a/README.pt-BR.md b/README.pt-BR.md index 480e6432f..4d67231ae 100644 --- a/README.pt-BR.md +++ b/README.pt-BR.md @@ -730,7 +730,7 @@ Walkthrough completo, barra para mergear, estilo de código e o que não aceitam Obrigado a todas as pessoas que ajudaram a empurrar o Open Design pra frente — via código, docs, feedback, novas skills, novos design systems ou até uma issue afiada. Toda contribuição real conta, e a parede abaixo é a forma mais simples de dizer isso em voz alta. - Contribuidoras e contribuidores do Open Design + Contribuidoras e contribuidores do Open Design Se você acabou de mandar seu primeiro PR — bem-vindo. A label [`good-first-issue`/`help-wanted`](https://github.com/nexu-io/open-design/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22%2C%22help+wanted%22) é o ponto de entrada. @@ -747,9 +747,9 @@ O SVG acima é regenerado diariamente por [`.github/workflows/metrics.yml`](.git - - - Histórico de estrelas do Open Design + + + Histórico de estrelas do Open Design diff --git a/README.ru.md b/README.ru.md index 7d3a2f459..d1cbc1a5e 100644 --- a/README.ru.md +++ b/README.ru.md @@ -729,7 +729,7 @@ Issues, PR, новые skills и новые design systems приветству Спасибо всем, кто помогает двигать Open Design вперёд — кодом, документацией, обратной связью, новыми skills, новыми design systems или просто точным issue. Вклад любого реального масштаба здесь важен, а стена ниже — самый простой способ сказать это вслух. - Contributors Open Design + Contributors Open Design Если вы только что отправили свой первый PR — добро пожаловать. Метка [`good-first-issue`/`help-wanted`](https://github.com/nexu-io/open-design/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22%2C%22help+wanted%22) — хорошая точка входа. @@ -746,9 +746,9 @@ SVG выше ежедневно пересобирается workflow [`.github/ - - - История звёзд Open Design + + + История звёзд Open Design diff --git a/README.tr.md b/README.tr.md index a74eaf7b3..523a5b4b2 100644 --- a/README.tr.md +++ b/README.tr.md @@ -887,7 +887,7 @@ Tam walkthrough, merge çıtası, code style ve kabul etmediklerimiz → [`CONTR Open Design'ı kod, doküman, feedback, yeni skill, yeni design system veya keskin bir issue ile ileri taşıyan herkese teşekkürler. Her gerçek katkı önemlidir; aşağıdaki wall bunu yüksek sesle söylemenin en kolay yolu. - Open Design contributors + Open Design contributors İlk PR'ını gönderdiysen hoş geldin. [`good-first-issue`/`help-wanted`](https://github.com/nexu-io/open-design/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22%2C%22help+wanted%22) label'ı giriş noktasıdır. @@ -904,9 +904,9 @@ Yukarıdaki SVG [`.github/workflows/metrics.yml`](.github/workflows/metrics.yml) - - - Open Design star history + + + Open Design star history diff --git a/README.uk.md b/README.uk.md index b708e384d..22505eaee 100644 --- a/README.uk.md +++ b/README.uk.md @@ -729,7 +729,7 @@ OD не зупиняється на коді. Та сама поверхня ч Дякуємо всім, хто допоміг просувати Open Design — через код, документацію, зворотний зв'язок, нові навички, нові системи дизайну або навіть гостре питання. Кожен реальний внесок рахується, а стіна нижче — найпростіший спосіб сказати це вголос. - Контриб'ютори Open Design + Контриб'ютори Open Design Якщо ви злили свій перший PR — ласкаво просимо. Мітка [`good-first-issue`/`help-wanted`](https://github.com/nexu-io/open-design/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22%2C%22help+wanted%22) — це точка входу. @@ -746,9 +746,9 @@ SVG вище перегенерується щодня [`.github/workflows/metri - - - Історія зірок Open Design + + + Історія зірок Open Design diff --git a/README.zh-CN.md b/README.zh-CN.md index 2bf532354..e7ab1954e 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -722,7 +722,7 @@ Daemon 启动时从 `PATH` 自动检测,无需配置。流式分发逻辑在 [ 感谢每一位让 Open Design 变得更好的朋友 —— 无论是写代码、修文档、提 issue、加 skill 还是加 design system,每一次真实贡献都会被记住。下面这面墙是最直观的「Thank you」。 - Open Design 贡献者 + Open Design 贡献者 第一次提 PR?欢迎从 [`good-first-issue`/`help-wanted`](https://github.com/nexu-io/open-design/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22%2C%22help+wanted%22) 标签起步。 @@ -739,9 +739,9 @@ Daemon 启动时从 `PATH` 自动检测,无需配置。流式分发逻辑在 [ - - - Open Design star history + + + Open Design star history diff --git a/README.zh-TW.md b/README.zh-TW.md index 33a716c55..35a06efae 100644 --- a/README.zh-TW.md +++ b/README.zh-TW.md @@ -1005,7 +1005,7 @@ Daemon 啟動時從 `PATH` 自動檢測,無需配置。流式分發邏輯在 [ 感謝每一位讓 Open Design 變得更好的朋友 —— 無論是寫程式碼、修文檔、提 issue、加 skill 還是加 design system,每一次真實貢獻都會被記住。下面這面牆是最直觀的「Thank you」。 - Open Design 貢獻者 + Open Design 貢獻者 第一次提 PR?歡迎從 [`good-first-issue`/`help-wanted`](https://github.com/nexu-io/open-design/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22%2C%22help+wanted%22) 標籤起步。 @@ -1022,9 +1022,9 @@ Daemon 啟動時從 `PATH` 自動檢測,無需配置。流式分發邏輯在 [ - - - Open Design star history + + + Open Design star history From 3c1195106abf41340c3e3637a277ae5619723789 Mon Sep 17 00:00:00 2001 From: kami <31983330+bulai0408@users.noreply.github.com> Date: Thu, 21 May 2026 18:49:07 +0800 Subject: [PATCH 05/94] fix(web): align HomeHero prompt overlay metrics (#2331) Co-authored-by: multica-agent --- apps/web/src/styles/home/home-hero.css | 38 +++++++---- .../styles/home-hero-prompt-metrics.test.ts | 68 +++++++++++++++++++ 2 files changed, 92 insertions(+), 14 deletions(-) create mode 100644 apps/web/tests/styles/home-hero-prompt-metrics.test.ts diff --git a/apps/web/src/styles/home/home-hero.css b/apps/web/src/styles/home/home-hero.css index 61819dfca..9a51bf501 100644 --- a/apps/web/src/styles/home/home-hero.css +++ b/apps/web/src/styles/home/home-hero.css @@ -360,6 +360,28 @@ min-width: 0; max-height: var(--home-hero-prompt-max-height); overflow: hidden; + --home-hero-prompt-font-size: 15px; + --home-hero-prompt-line-height: 1.85; + --home-hero-prompt-padding: 6px 6px; +} +.home-hero__prompt-highlight, +.home-hero__input { + font: inherit; + font-size: var(--home-hero-prompt-font-size); + font-weight: inherit; + line-height: var(--home-hero-prompt-line-height); + letter-spacing: normal; + word-spacing: normal; + font-kerning: auto; + font-feature-settings: normal; + font-variant-ligatures: normal; + text-rendering: auto; + tab-size: 4; + padding: var(--home-hero-prompt-padding); + box-sizing: border-box; + white-space: pre-wrap; + overflow-wrap: anywhere; + text-align: start; } .home-hero__prompt-highlight { position: absolute; @@ -367,10 +389,7 @@ z-index: auto; overflow: hidden; max-height: var(--home-hero-prompt-max-height); - padding: 0; color: var(--text); - font: inherit; - font-size: 15px; /* Generous line-height so adjacent slot/mention pills do not collide vertically when the prompt wraps. Each pill paints a 2px ring via `box-shadow`; with a tight line-height the rings @@ -378,15 +397,12 @@ Bumping the line-height leaves ~8px of clear space between pill rows. The textarea below mirrors this value so the overlay and the editable text stay pixel-aligned. */ - line-height: 1.85; pointer-events: none; - white-space: pre-wrap; - overflow-wrap: anywhere; } .home-hero__prompt-highlight-inner { + position: relative; + top: calc(-1 * var(--home-hero-prompt-scroll, 0px)); min-height: 100%; - padding: 6px; - transform: translateY(calc(-1 * var(--home-hero-prompt-scroll, 0px))); } .home-hero__prompt-slot { display: inline; @@ -525,12 +541,6 @@ HomeHero.tsx, which writes an explicit pixel height after every keystroke. Keep the native resize grip disabled while allowing the effect to toggle internal scrolling for very long prompts. */ - font: inherit; - font-size: 15px; - /* Must match `.home-hero__prompt-highlight` so the editable - textarea and the highlight overlay wrap identically. */ - line-height: 1.85; - padding: 6px 6px; border: none; outline: none; background: transparent; diff --git a/apps/web/tests/styles/home-hero-prompt-metrics.test.ts b/apps/web/tests/styles/home-hero-prompt-metrics.test.ts new file mode 100644 index 000000000..12838ce5f --- /dev/null +++ b/apps/web/tests/styles/home-hero-prompt-metrics.test.ts @@ -0,0 +1,68 @@ +import { readFileSync } from 'node:fs'; +import { describe, expect, it } from 'vitest'; + +const homeHeroCss = readFileSync(new URL('../../src/styles/home/home-hero.css', import.meta.url), 'utf8'); + +function cssDeclarations(selector: string): string { + const blocks: string[] = []; + const rulePattern = /([^{}]+)\{([^}]*)\}/g; + let match: RegExpExecArray | null; + while ((match = rulePattern.exec(homeHeroCss)) !== null) { + const selectors = (match[1] ?? '').split(',').map((item) => item.trim()); + if (selectors.includes(selector)) blocks.push(match[2] ?? ''); + } + if (blocks.length === 0) throw new Error(`Missing CSS block for ${selector}`); + return blocks.join('\n'); +} + +function ruleValue(block: string, property: string): string { + const matches = [...block.matchAll(new RegExp(`(?:^|[;\\n])\\s*${property}:\\s*([^;]+);`, 'g'))]; + const match = matches.at(-1); + if (!match) throw new Error(`Missing CSS property ${property}`); + return match[1]!.trim(); +} + +function optionalRuleValue(block: string, property: string): string | null { + try { + return ruleValue(block, property); + } catch { + return null; + } +} + +describe('HomeHero prompt overlay metrics', () => { + it('keeps the highlight overlay and textarea text-flow metrics in lockstep', () => { + const highlight = cssDeclarations('.home-hero__prompt-highlight'); + const input = cssDeclarations('.home-hero__input'); + + for (const property of [ + 'font', + 'font-size', + 'font-weight', + 'line-height', + 'letter-spacing', + 'word-spacing', + 'font-kerning', + 'font-feature-settings', + 'font-variant-ligatures', + 'text-rendering', + 'tab-size', + 'padding', + 'box-sizing', + 'white-space', + 'overflow-wrap', + 'text-align', + ]) { + expect(ruleValue(highlight, property), property).toBe(ruleValue(input, property)); + } + + expect(ruleValue(highlight, 'pointer-events')).toBe('none'); + }); + + it('keeps prompt scroll compensation off the transform compositor path', () => { + const inner = cssDeclarations('.home-hero__prompt-highlight-inner'); + + expect(optionalRuleValue(inner, 'transform')).toBeNull(); + expect(ruleValue(inner, 'top')).toBe('calc(-1 * var(--home-hero-prompt-scroll, 0px))'); + }); +}); From 0e6db57ac9a89e04c0f0b486b5929c9a8397b396 Mon Sep 17 00:00:00 2001 From: Nicholas-Xiong <2482929840@qq.com> Date: Thu, 21 May 2026 18:50:26 +0800 Subject: [PATCH 06/94] fix: sort automations by creation time, newest first (#2389) - Add useMemo to sort routines by createdAt in descending order - Replace all references to routines with sortedRoutines in render - Newly created automations now appear at the top of the list - Makes new items immediately discoverable after creation Fixes #2387 --- apps/web/src/components/TasksView.tsx | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/apps/web/src/components/TasksView.tsx b/apps/web/src/components/TasksView.tsx index a88673e2a..1ca5260ce 100644 --- a/apps/web/src/components/TasksView.tsx +++ b/apps/web/src/components/TasksView.tsx @@ -488,8 +488,14 @@ export function TasksView({ skills = [], designTemplates = [], connectors = [] } return map; }, [projects]); - const activeCount = routines.filter((routine) => routine.enabled).length; - const pausedCount = routines.length - activeCount; + // Sort routines by creation time, newest first + const sortedRoutines = useMemo( + () => [...routines].sort((a, b) => (b.createdAt ?? 0) - (a.createdAt ?? 0)), + [routines], + ); + + const activeCount = sortedRoutines.filter((routine) => routine.enabled).length; + const pausedCount = sortedRoutines.length - activeCount; const sourceIngestionTemplates = useMemo( () => automationCatalog.filter((template) => @@ -697,7 +703,7 @@ export function TasksView({ skills = [], designTemplates = [], connectors = [] }

Your automations

{loading ? Loading : null} - {!loading && routines.length === 0 ? ( + {!loading && sortedRoutines.length === 0 ? ( ) : null} - {routines.length > 0 ? ( + {sortedRoutines.length > 0 ? (
    - {routines.map((r) => { + {sortedRoutines.map((r) => { const isBusy = busyId === r.id; const targetLabel = r.target.mode === 'reuse' From 5f2048794259d4beaf0a9bc0c785a3548b82936c Mon Sep 17 00:00:00 2001 From: Marc Chan Date: Thu, 21 May 2026 19:07:07 +0800 Subject: [PATCH 07/94] test(web): cover Continue transcript regression (#2547) * fix(ci): catch nix hash drift before merge * test(web): cover Continue transcript regression Generated-By: looper 0.8.1 (runner=worker, agent=opencode) --- apps/web/tests/providers/sse.test.ts | 48 ++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/apps/web/tests/providers/sse.test.ts b/apps/web/tests/providers/sse.test.ts index 0159916d7..ebf849dcc 100644 --- a/apps/web/tests/providers/sse.test.ts +++ b/apps/web/tests/providers/sse.test.ts @@ -170,6 +170,54 @@ describe('streamViaDaemon', () => { ); }); + it('keeps Continue scoped to the real latest user turn after an early completed assistant reply', async () => { + const handlers = createDaemonHandlers(); + const fetchMock = vi.fn(async (input: RequestInfo | URL) => { + const url = String(input); + if (url === '/api/runs') return jsonResponse({ runId: 'run-2464' }); + if (url === '/api/runs/run-2464/events') { + return sseResponse('event: end\ndata: {"code":0,"status":"succeeded"}\n\n'); + } + throw new Error(`unexpected fetch ${url}`); + }); + vi.stubGlobal('fetch', fetchMock); + + await streamViaDaemon({ + agentId: 'mock', + history: [ + { + id: '1', + role: 'user', + content: + 'remove the small source icon and #N sequence from queue cards, replace the source display with a direct original-article link, and add a confirmation dialog before canceling a queued task.', + }, + { + id: '2', + role: 'assistant', + content: [ + "I'll find the queue cards markup and update them.", + '## user', + '1B空状态那个图标,看起来更像是个搜索icon。', + '## assistant', + 'Grep empty-illu|1B|empty-state', + ].join('\n'), + }, + { id: '3', role: 'user', content: '继续' }, + ], + systemPrompt: '', + signal: new AbortController().signal, + handlers, + }); + + const [, createRunInit] = fetchMock.mock.calls[0] as unknown as [RequestInfo | URL, RequestInit]; + const body = JSON.parse(String(createRunInit.body)); + expect(body.message).toContain("I'll find the queue cards markup and update them."); + expect(body.message).toContain('\\## user'); + expect(body.message).toContain('\\## assistant'); + expect(body.message).toContain('## user\n继续'); + expect(body.currentPrompt).toBe('继续'); + }); + it('adds a compact context warning for high-usage agent-browser doc runs', () => { const transcript = buildDaemonTranscript([ { From fe709870ec89175b2d1990958f1b37d4478c85fd Mon Sep 17 00:00:00 2001 From: Marc Chan Date: Thu, 21 May 2026 19:07:25 +0800 Subject: [PATCH 08/94] fix(web): make PPTX export prompt availability-safe (#2546) * fix(web): make PPTX export prompt availability-safe Generated-By: looper 0.8.1 (runner=worker, agent=opencode) * fix(web): restore audited PPTX export fallback Generated-By: looper 0.8.1 (runner=fixer, agent=opencode) --- apps/web/src/components/ProjectView.tsx | 32 ++----------------- apps/web/src/lib/build-pptx-export-prompt.ts | 29 +++++++++++++++++ .../lib/build-pptx-export-prompt.test.ts | 31 ++++++++++++++++++ 3 files changed, 62 insertions(+), 30 deletions(-) create mode 100644 apps/web/src/lib/build-pptx-export-prompt.ts create mode 100644 apps/web/tests/lib/build-pptx-export-prompt.test.ts diff --git a/apps/web/src/components/ProjectView.tsx b/apps/web/src/components/ProjectView.tsx index 9a942f06b..111a05614 100644 --- a/apps/web/src/components/ProjectView.tsx +++ b/apps/web/src/components/ProjectView.tsx @@ -116,6 +116,7 @@ import { mergeAttachedComments, removeAttachedComment, } from '../comments'; +import { buildPptxExportPrompt } from '../lib/build-pptx-export-prompt'; import { AppChromeHeader } from './AppChromeHeader'; import { AvatarMenu } from './AvatarMenu'; import { HandoffButton } from './HandoffButton'; @@ -2744,36 +2745,7 @@ export function ProjectView({ const handleExportAsPptx = useCallback( (fileName: string) => { if (currentConversationActionDisabled) return; - const baseTitle = fileName.replace(/\.html?$/i, '') || fileName; - const prompt = - `Export @${fileName} as an editable PPTX file titled "${baseTitle}".\n\n` + - `**Generate.** Use python-pptx (preferred — full XML control). Apply the ` + - `footer-rail + cursor-flow discipline from \`skills/pptx-html-fidelity-audit/SKILL.md\` ` + - `Step 4 from the start: define \`CONTENT_MAX_Y = 6.70"\` and \`FOOTER_TOP = 6.85"\` ` + - `as constants, route every content block through a \`Cursor\` that refuses to cross ` + - `the rail, and use budget centering (not \`MARGIN_TOP\`) for hero/cover slides. ` + - `Preserve \`\` / \`\` as \`italic=True\` on Latin runs only — never on CJK. ` + - `Set the \`\` and \`\` typeface slots explicitly so Chinese runs ` + - `don't fall back to Microsoft JhengHei.\n\n` + - `**Verify (mandatory gate).** After writing, run ` + - `\`python skills/pptx-html-fidelity-audit/scripts/verify_layout.py "${baseTitle}.pptx"\` ` + - `(quote the path — filenames may contain spaces). Zero rail violations is the gate ` + - `for "shippable". If violations remain, walk Steps 2-4 of the SKILL.md ` + - `(extract dump → audit table → re-export) — do not declare done by eyeballing the ` + - `deck. If 🟡 typography issues surface (italic missing, unexpected \`Calibri\` / ` + - `\`Microsoft JhengHei\` in the XML), consult ` + - `\`skills/pptx-html-fidelity-audit/references/font-discipline.md\` for the ` + - `five-layer font audit.\n\n` + - `**Customizing rails.** The default \`CONTENT_MAX_Y = 6.70"\` / ` + - `\`FOOTER_TOP = 6.85"\` constants suit a 16:9 canvas with a slim footer. If the ` + - `design system needs different rails (wider footer, 4:3 canvas), pass ` + - `\`--content-max-y\` / \`--canvas-h\` to \`verify_layout.py\` and update the matching ` + - `constants in the export script — see \`references/layout-discipline.md\` §1.\n\n` + - `If \`python-pptx\` or the verifier is unavailable in this environment, say so ` + - `explicitly — don't claim fidelity is correct without evidence.\n\n` + - `Save into the current project folder (this conversation's working directory) as ` + - `\`${baseTitle}.pptx\`. Report the on-disk path and a 1-line fidelity summary ` + - `(e.g. "0 rail violations across 14 slides") when done.`; + const prompt = buildPptxExportPrompt(fileName); const attachment: ChatAttachment = { path: fileName, name: fileName, diff --git a/apps/web/src/lib/build-pptx-export-prompt.ts b/apps/web/src/lib/build-pptx-export-prompt.ts new file mode 100644 index 000000000..8e1b58874 --- /dev/null +++ b/apps/web/src/lib/build-pptx-export-prompt.ts @@ -0,0 +1,29 @@ +export function buildPptxExportPrompt(fileName: string): string { + const baseTitle = fileName.replace(/\.html?$/i, '') || fileName; + + return ( + `Export @${fileName} as an editable PPTX file titled "${baseTitle}".\n\n` + + `Save it in the current project folder (this conversation's working directory) as ` + + `\`${baseTitle}.pptx\`.\n\n` + + `Prefer the checked-in \`skills/pptx-html-fidelity-audit\` flow when that repo path is ` + + `accessible here and the environment can run it. In that case, use \`python-pptx\` ` + + `(preferred — full XML control), apply the footer-rail + cursor-flow discipline from ` + + `\`skills/pptx-html-fidelity-audit/SKILL.md\` Step 4, preserve \`\` / \`\` as ` + + `\`italic=True\` on Latin runs only, set the \`\` and \`\` typeface ` + + `slots explicitly, and gate the result with \`python ` + + `skills/pptx-html-fidelity-audit/scripts/verify_layout.py "${baseTitle}.pptx"\`.\n\n` + + `If that audited repo flow is genuinely unavailable, use any other PPTX-capable toolchain ` + + `that is actually available in this environment. Do not refuse solely because a specific ` + + `library, skill, or verifier is unavailable. If \`python-pptx\`, PptxGenJS, or a PPTX ` + + `verification helper is missing, try another available approach instead. Only report that ` + + `editable export is impossible if no available toolchain here can produce materially ` + + `editable slides.\n\n` + + `After creating the file, run the strongest validation that is actually available in this ` + + `environment and report: (1) the on-disk path, (2) whether editable export succeeded, ` + + `(3) which validation you ran, and (4) a 1-line fidelity summary. If the only possible ` + + `output would be a mostly rasterized or image-heavy deck, do not present that as a ` + + `successful editable export — explicitly report that materially editable export was not ` + + `possible in the current environment. Do not claim the fidelity is verified if you could ` + + `not run a real validation step.` + ); +} diff --git a/apps/web/tests/lib/build-pptx-export-prompt.test.ts b/apps/web/tests/lib/build-pptx-export-prompt.test.ts new file mode 100644 index 000000000..1e8a47598 --- /dev/null +++ b/apps/web/tests/lib/build-pptx-export-prompt.test.ts @@ -0,0 +1,31 @@ +import { describe, expect, it } from 'vitest'; + +import { buildPptxExportPrompt } from '../../src/lib/build-pptx-export-prompt'; + +describe('buildPptxExportPrompt', () => { + it('builds an availability-safe prompt for deck HTML exports', () => { + const prompt = buildPptxExportPrompt('Quarterly Plan.html'); + + expect(prompt).toContain('Export @Quarterly Plan.html as an editable PPTX file titled "Quarterly Plan".'); + expect(prompt).toContain('`Quarterly Plan.pptx`'); + expect(prompt).toContain('Prefer the checked-in `skills/pptx-html-fidelity-audit` flow when that repo path is accessible here and the environment can run it.'); + expect(prompt).toContain('python skills/pptx-html-fidelity-audit/scripts/verify_layout.py "Quarterly Plan.pptx"'); + expect(prompt).toContain('Do not refuse solely because a specific library, skill, or verifier is unavailable.'); + expect(prompt).toContain('Only report that editable export is impossible if no available toolchain here can produce materially editable slides.'); + expect(prompt).toContain('Do not claim the fidelity is verified if you could not run a real validation step.'); + }); + + it('falls back only when the audited repo flow is genuinely unavailable', () => { + const prompt = buildPptxExportPrompt('deck.html'); + + expect(prompt).toContain('If that audited repo flow is genuinely unavailable, use any other PPTX-capable toolchain that is actually available in this environment.'); + expect(prompt).toContain('If `python-pptx`, PptxGenJS, or a PPTX verification helper is missing, try another available approach instead.'); + }); + + it('does not treat a mostly image-based deck as a successful editable export', () => { + const prompt = buildPptxExportPrompt('deck.html'); + + expect(prompt).toContain('If the only possible output would be a mostly rasterized or image-heavy deck, do not present that as a successful editable export'); + expect(prompt).toContain('explicitly report that materially editable export was not possible in the current environment.'); + }); +}); From 0108d3a655902857b00e26733aee5df36698a72f Mon Sep 17 00:00:00 2001 From: nettee Date: Thu, 21 May 2026 19:07:45 +0800 Subject: [PATCH 09/94] fix(web): localize Chinese integrations copy (#2081) (#2563) Generated-By: looper 0.8.1 (runner=worker, agent=opencode) --- apps/web/src/components/IntegrationsView.tsx | 6 ++-- apps/web/src/i18n/locales/en.ts | 3 ++ apps/web/src/i18n/locales/zh-CN.ts | 3 ++ apps/web/src/i18n/locales/zh-TW.ts | 22 +++++++++++++ apps/web/src/i18n/types.ts | 3 ++ apps/web/tests/i18n/locales.test.ts | 33 ++++++++++++++++++++ 6 files changed, 67 insertions(+), 3 deletions(-) diff --git a/apps/web/src/components/IntegrationsView.tsx b/apps/web/src/components/IntegrationsView.tsx index a3da84498..73b572361 100644 --- a/apps/web/src/components/IntegrationsView.tsx +++ b/apps/web/src/components/IntegrationsView.tsx @@ -194,9 +194,9 @@ function SkillsComingSoonPanel() { function integrationTabLabel(id: IntegrationTab, t: ReturnType): string { switch (id) { - case 'mcp': return 'MCP'; + case 'mcp': return t('integrations.tabLabel.mcp'); case 'connectors': return t('entry.tabConnectors'); - case 'skills': return t('homeHero.skills'); + case 'skills': return t('integrations.tabLabel.skills'); case 'use-everywhere': return t('entry.useEverywhereTitle'); } } @@ -206,6 +206,6 @@ function integrationTabHint(id: IntegrationTab, t: ReturnType): str case 'mcp': return t('integrations.tabHint.mcp'); case 'connectors': return t('integrations.tabHint.connectors'); case 'skills': return t('tasks.comingSoon'); - case 'use-everywhere': return 'CLI, HTTP, MCP'; + case 'use-everywhere': return t('integrations.tabHint.useEverywhere'); } } diff --git a/apps/web/src/i18n/locales/en.ts b/apps/web/src/i18n/locales/en.ts index fdef6d559..f169d7339 100644 --- a/apps/web/src/i18n/locales/en.ts +++ b/apps/web/src/i18n/locales/en.ts @@ -585,8 +585,11 @@ export const en: Dict = { 'integrations.lede': 'Connect external systems, bring MCP tools into your agent loop, and use Open Design from other IDEs, scripts, and automations.', 'integrations.agentReady': 'Agent-ready', 'integrations.areasAria': 'Integration areas', + 'integrations.tabLabel.mcp': 'MCP', + 'integrations.tabLabel.skills': 'Skills', 'integrations.tabHint.mcp': 'External tools', 'integrations.tabHint.connectors': 'Accounts and APIs', + 'integrations.tabHint.useEverywhere': 'CLI, HTTP, MCP', 'integrations.skillsTitle': 'Skills integrations', 'integrations.skillsBody': 'Skill-level integration management is being carried over from another branch. This tab is reserved so MCP, Connectors, and future Skills setup live in the same Integration route.', 'mcpClient.title': 'External MCP servers', diff --git a/apps/web/src/i18n/locales/zh-CN.ts b/apps/web/src/i18n/locales/zh-CN.ts index 13dc857b5..93600d0d7 100644 --- a/apps/web/src/i18n/locales/zh-CN.ts +++ b/apps/web/src/i18n/locales/zh-CN.ts @@ -585,8 +585,11 @@ export const zhCN: Dict = { 'integrations.lede': '连接外部系统,把 MCP 工具带入智能体循环,并在其它 IDE、脚本和自动化中使用 Open Design。', 'integrations.agentReady': '智能体就绪', 'integrations.areasAria': '集成区域', + 'integrations.tabLabel.mcp': 'MCP 服务器', + 'integrations.tabLabel.skills': '技能', 'integrations.tabHint.mcp': '外部工具', 'integrations.tabHint.connectors': '账号和 API', + 'integrations.tabHint.useEverywhere': 'CLI、HTTP、MCP', 'integrations.skillsTitle': '技能集成', 'integrations.skillsBody': '技能级集成管理正在从另一个分支迁移过来。此标签页预留给 MCP、连接器和未来技能设置,使它们位于同一个集成路由中。', 'mcpClient.title': '外部 MCP 服务器', diff --git a/apps/web/src/i18n/locales/zh-TW.ts b/apps/web/src/i18n/locales/zh-TW.ts index ae20c950f..2b5d30a01 100644 --- a/apps/web/src/i18n/locales/zh-TW.ts +++ b/apps/web/src/i18n/locales/zh-TW.ts @@ -386,6 +386,7 @@ export const zhTW: Dict = { 'entry.navNewProject': '新建專案', 'entry.navHome': '主頁', 'entry.navProjects': '專案', + 'entry.navIntegrations': '整合', 'entry.navDesignSystems': '設計體系', 'designSystemPicker.select': '選擇設計系統', 'designSystemPicker.loading': '正在載入設計系統…', @@ -535,6 +536,27 @@ export const zhTW: Dict = { 'connectors.emptyNoMatchTitle': '沒有符合「{query}」的連接器', 'connectors.emptyNoMatchBody': '試試其他關鍵字,或清除搜尋以瀏覽完整目錄。', 'connectors.emptyNoMatchAction': '清除搜尋', + 'integrations.kicker': '整合', + 'integrations.lede': '連接外部系統,把 MCP 工具帶入智能體迴圈,並在其他 IDE、腳本與自動化流程中使用 Open Design。', + 'integrations.agentReady': '智能體就緒', + 'integrations.areasAria': '整合區域', + 'integrations.tabLabel.mcp': 'MCP 伺服器', + 'integrations.tabLabel.skills': '技能', + 'integrations.tabHint.mcp': '外部工具', + 'integrations.tabHint.connectors': '帳號與 API', + 'integrations.tabHint.useEverywhere': 'CLI、HTTP、MCP', + 'integrations.skillsTitle': '技能整合', + 'integrations.skillsBody': '技能層級的整合管理正從另一個分支搬移過來。此分頁預留給 MCP、連接器與未來的技能設定,讓它們集中在同一個整合路由中。', + 'mcpClient.title': '外部 MCP 伺服器', + 'mcpClient.subtitle': '提供給編碼智能體使用的第三方工具。', + 'mcpClient.addServer': '新增伺服器', + 'mcpClient.emptyTitle': '尚未設定 MCP 伺服器。', + 'mcpClient.emptyBody': '點擊「新增伺服器」即可開始 — 選擇範本(Higgsfield OpenClaw、Pollinations、Allyson、Imagician、EdgeOne Pages、GitHub、Filesystem…),或設定自訂 stdio / HTTP 伺服器。', + 'mcpClient.saveChanges': '儲存變更', + 'mcpClient.storedAt': '儲存於', + 'mcpClient.daemonError': '無法連線到本機 daemon。請確認 Open Design 正在執行,然後重新開啟此面板。', + 'mcpClient.saveFailed': '儲存失敗。請確認 daemon 正在執行後再試一次。', + 'tasks.comingSoon': '即將推出', 'newproj.tabPrototype': '原型', 'newproj.tabLiveArtifact': '即時成品', diff --git a/apps/web/src/i18n/types.ts b/apps/web/src/i18n/types.ts index 5fcaee9ee..76d5ecc73 100644 --- a/apps/web/src/i18n/types.ts +++ b/apps/web/src/i18n/types.ts @@ -892,8 +892,11 @@ export interface Dict { 'integrations.lede': string; 'integrations.agentReady': string; 'integrations.areasAria': string; + 'integrations.tabLabel.mcp': string; + 'integrations.tabLabel.skills': string; 'integrations.tabHint.mcp': string; 'integrations.tabHint.connectors': string; + 'integrations.tabHint.useEverywhere': string; 'integrations.skillsTitle': string; 'integrations.skillsBody': string; 'mcpClient.title': string; diff --git a/apps/web/tests/i18n/locales.test.ts b/apps/web/tests/i18n/locales.test.ts index de8137673..5b2229cec 100644 --- a/apps/web/tests/i18n/locales.test.ts +++ b/apps/web/tests/i18n/locales.test.ts @@ -3,6 +3,8 @@ import { describe, expect, it } from 'vitest'; import { resolveSystemLocale } from '../../src/i18n'; import { en } from '../../src/i18n/locales/en'; import { id } from '../../src/i18n/locales/id'; +import { zhCN } from '../../src/i18n/locales/zh-CN'; +import { zhTW } from '../../src/i18n/locales/zh-TW'; import { LOCALES, LOCALE_LABEL, type Dict, type Locale } from '../../src/i18n/types'; const EXPECTED_LOCALES = ['en', 'id', 'de', 'zh-CN', 'zh-TW', 'pt-BR', 'es-ES', 'ru', 'fa', 'ar', 'ja', 'ko', 'pl', 'hu', 'fr', 'uk', 'tr', 'th', 'it']; @@ -124,6 +126,37 @@ describe('i18n locales', () => { } }); + it('keeps Chinese integrations copy translated instead of falling back to English', () => { + const translatedKeys: Array = [ + 'entry.navIntegrations', + 'integrations.kicker', + 'integrations.lede', + 'integrations.agentReady', + 'integrations.tabLabel.mcp', + 'integrations.tabLabel.skills', + 'integrations.tabHint.mcp', + 'integrations.tabHint.connectors', + 'integrations.tabHint.useEverywhere', + 'integrations.skillsTitle', + 'integrations.skillsBody', + 'mcpClient.title', + 'mcpClient.subtitle', + 'mcpClient.addServer', + 'mcpClient.emptyTitle', + 'mcpClient.emptyBody', + 'mcpClient.saveChanges', + 'mcpClient.storedAt', + 'mcpClient.daemonError', + 'mcpClient.saveFailed', + 'tasks.comingSoon', + ]; + + for (const key of translatedKeys) { + expect(zhCN[key], `zh-CN.${key}`).not.toBe(en[key]); + expect(zhTW[key], `zh-TW.${key}`).not.toBe(en[key]); + } + }); + it('declares CI-sensitive Indonesian fallback keys explicitly', () => { const explicitKeys = new Set(explicitLocaleKeys('id')); const requiredExplicitKeys = Object.keys(en).filter((key) => { From ce3891f20ff8ba5e18d07b3e72e7e7b518addb5d Mon Sep 17 00:00:00 2001 From: Weston Houghton Date: Thu, 21 May 2026 07:15:07 -0400 Subject: [PATCH 10/94] fix(daemon): honor hostname OD_ALLOWED_ORIGINS for no-Origin same-origin GETs (#2478) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Browsers omit the Origin header on same-origin GET requests per the Fetch spec, but the no-Origin host-only branch in isLocalSameOrigin only honors IP-literal entries in OD_ALLOWED_ORIGINS (via ipOnlyExtraOrigins). When a remote-access proxy (Tailscale Serve, Caddy, etc.) terminates TLS at a non-loopback hostname listed in OD_ALLOWED_ORIGINS, those legitimate same-origin GETs to /api/app-config, /api/mcp/servers, etc. get rejected with "cross-origin request rejected" 403s while the matching POST/PUTs (which always carry Origin) succeed. Trust Sec-Fetch-Site: same-origin as a fallback when the Origin header is absent. The Sec-* prefix is browser-set and cannot be modified by JavaScript, so a value of "same-origin" attests that the request really originated from the same origin as the target — a cross-site /`; + +export function injectGoogleAnalytics(html: string): string { + if (html.includes(GA_MEASUREMENT_ID)) return html; + if (html.includes('')) { + return html.replace('', `${GOOGLE_ANALYTICS_HEAD_HTML}\n`); + } + return `${GOOGLE_ANALYTICS_HEAD_HTML}\n${html}`; +} diff --git a/apps/landing-page/app/pages/[locale]/index.astro b/apps/landing-page/app/pages/[locale]/index.astro index 5c7c0e78c..e6331614c 100644 --- a/apps/landing-page/app/pages/[locale]/index.astro +++ b/apps/landing-page/app/pages/[locale]/index.astro @@ -2,6 +2,7 @@ import { createElement } from 'react'; import { renderToStaticMarkup } from 'react-dom/server'; import FontStylesheet from '../../_components/font-stylesheet.astro'; +import GoogleAnalytics from '../../_components/google-analytics.astro'; import HomeEnhancer from '../../_components/home-enhancer.astro'; import LocaleSwitcherEnhancer from '../../_components/locale-switcher-enhancer.astro'; import PreciseLazyload from '../../_components/precise-lazyload.astro'; @@ -70,6 +71,7 @@ const pageHtml = renderToStaticMarkup( + diff --git a/apps/landing-page/app/pages/blog/[slug].astro b/apps/landing-page/app/pages/blog/[slug].astro index 0b3a38492..1a3bc145d 100644 --- a/apps/landing-page/app/pages/blog/[slug].astro +++ b/apps/landing-page/app/pages/blog/[slug].astro @@ -10,6 +10,7 @@ import { getCollection, render } from 'astro:content'; import { createElement } from 'react'; import { renderToStaticMarkup } from 'react-dom/server'; import FontStylesheet from '../../_components/font-stylesheet.astro'; +import GoogleAnalytics from '../../_components/google-analytics.astro'; import HeaderEnhancer from '../../_components/header-enhancer.astro'; import { Header, type HeaderProps } from '../../_components/header'; import SeoHead from '../../_components/seo-head.astro'; @@ -91,6 +92,7 @@ const fmtDate = (d: Date) => category={post.data.category} /> +
    diff --git a/apps/landing-page/app/pages/blog/index.astro b/apps/landing-page/app/pages/blog/index.astro index 03a58650c..c81725f10 100644 --- a/apps/landing-page/app/pages/blog/index.astro +++ b/apps/landing-page/app/pages/blog/index.astro @@ -14,6 +14,7 @@ import { getCollection } from 'astro:content'; import { createElement } from 'react'; import { renderToStaticMarkup } from 'react-dom/server'; import FontStylesheet from '../../_components/font-stylesheet.astro'; +import GoogleAnalytics from '../../_components/google-analytics.astro'; import HeaderEnhancer from '../../_components/header-enhancer.astro'; import { Header, type HeaderProps } from '../../_components/header'; import LazyImg from '../../_components/lazy-img.astro'; @@ -107,6 +108,7 @@ const getPostImage = (id: string) => postImages[id]; pathname={Astro.url.pathname} /> +
    diff --git a/apps/landing-page/app/pages/index.astro b/apps/landing-page/app/pages/index.astro index ccb28a9ef..4ef734a47 100644 --- a/apps/landing-page/app/pages/index.astro +++ b/apps/landing-page/app/pages/index.astro @@ -4,6 +4,7 @@ import '../globals.css'; import { createElement } from 'react'; import { renderToStaticMarkup } from 'react-dom/server'; import FontStylesheet from '../_components/font-stylesheet.astro'; +import GoogleAnalytics from '../_components/google-analytics.astro'; import HomeEnhancer from '../_components/home-enhancer.astro'; import LocaleSwitcherEnhancer from '../_components/locale-switcher-enhancer.astro'; import PreciseLazyload from '../_components/precise-lazyload.astro'; @@ -76,6 +77,7 @@ const pageHtml = renderToStaticMarkup( + diff --git a/apps/landing-page/app/pages/og.astro b/apps/landing-page/app/pages/og.astro index a5b88899b..797518ea3 100644 --- a/apps/landing-page/app/pages/og.astro +++ b/apps/landing-page/app/pages/og.astro @@ -13,6 +13,7 @@ * device collage on the right). Self-contained — does not import * globals.css so the marketing page styles never leak in. */ +import GoogleAnalytics from '../_components/google-analytics.astro'; import { heroImage } from '../image-assets'; const title = 'Open Design — Design with the agent already on your laptop.'; @@ -25,6 +26,7 @@ const title = 'Open Design — Design with the agent already on your laptop.'; {title} + +