mirror of
https://github.com/Hmbown/DeepSeek-TUI.git
synced 2026-09-03 06:50:13 +08:00
perf(web): serve one canonical host and a 61%-smaller product screenshot
Two things the site was paying for on every visit.
**www served the whole site.** `web/wrangler.jsonc` binds both `codewhale.net`
and `www.codewhale.net` to this worker as custom domains, and nothing
redirected between them — so every page had a second reachable, indexable URL
and the two hosts split whatever authority they earned. `middleware.ts` now
301s `www` to the apex before the locale and static-asset branches, so a
document that moves does not keep pulling subresources from the host it left.
The redirect keeps path and query, leaves the apex, localhost, and preview
hosts alone, and still carries the security headers.
Doing it in middleware rather than moving the route to the existing
`web/redirect` worker is deliberate: reassigning a custom domain between
workers has a window where `www` answers with nothing, and this ships
atomically with the next deploy.
**The LCP element was a 440 KB PNG.** `app/[locale]/page.tsx` marks the product
screenshot `priority`, so it is the largest contentful paint, and it was served
raw — `next/image` is wired up but the Cloudflare transform binding is not
enabled, so nothing resized or re-encoded it. Re-encoded as lossless WebP:
440,507 -> 172,380 bytes (-60.9%)
Verified pixel-identical before swapping, not assumed — decoded both and
compared: difference bbox `None`, max channel delta 0 across RGBA. A lossy
q88 encode was 41 KB but showed a max channel delta of 67, which is visible
artifacting on terminal text, so it was rejected.
`docs/public-surface-facts.json` holds the README and website screenshots to
one byte-identical canonical asset, so both move together — `assets/screenshot.webp`
is the same file, and the 19 READMEs and their source stamps follow. That
contract is why this is one commit and not two.
The contract test read dimensions from the PNG IHDR; it now parses the packed
14-bit fields of the lossless VP8L header, and still accepts PNG so the asset
can be swapped back without touching the test.
Enabling the Cloudflare Images binding would let `next/image` negotiate AVIF
and per-viewport widths on top of this, but it needs Images enabled on the
zone, so it is left for a deliberate infra change rather than smuggled in here.
Verified: `npx vitest run` — 31 files, 276 tests passing (5 new middleware
tests, confirmed failing without the redirect); `tsc --noEmit` clean;
`check-readme-translations.py` reports 18 in sync; `prepare-release.sh 0.9.10`
clean.
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
<!-- source: README.md sha256:79856d0c10be -->
|
||||
<!-- source: README.md sha256:acee66e62ab0 -->
|
||||
# Codewhale
|
||||
|
||||
وكيل برمجة مفتوح المصدر لطرفيّتك — أحضر نموذجك بنفسك.
|
||||
@@ -29,7 +29,7 @@ reviewer على Kimi. اكتب أدوارك ودستور constitution خاصًا
|
||||
[](https://www.npmjs.com/package/codewhale)
|
||||
[](https://discord.gg/37gfS3ksug)
|
||||
|
||||

|
||||

|
||||
|
||||
## التثبيت
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<!-- source: README.md sha256:79856d0c10be -->
|
||||
<!-- source: README.md sha256:acee66e62ab0 -->
|
||||
# Codewhale
|
||||
|
||||
Un agent de programació de codi obert per al teu terminal — porta el teu propi model.
|
||||
@@ -32,7 +32,7 @@ coses més útils que pots fer — mira [Contribuir](#contribuir).
|
||||
[](https://www.npmjs.com/package/codewhale)
|
||||
[](https://discord.gg/37gfS3ksug)
|
||||
|
||||

|
||||

|
||||
|
||||
## Instal·lació
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<!-- source: README.md sha256:79856d0c10be -->
|
||||
<!-- source: README.md sha256:acee66e62ab0 -->
|
||||
# Codewhale
|
||||
|
||||
Ein Open-Source-Programmieragent für Ihr Terminal — bringen Sie Ihr eigenes Modell mit.
|
||||
@@ -36,7 +36,7 @@ eines der nützlichsten Dinge, die Sie tun können — siehe
|
||||
[](https://www.npmjs.com/package/codewhale)
|
||||
[](https://discord.gg/37gfS3ksug)
|
||||
|
||||

|
||||

|
||||
|
||||
## Installation
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<!-- source: README.md sha256:79856d0c10be -->
|
||||
<!-- source: README.md sha256:acee66e62ab0 -->
|
||||
# Codewhale
|
||||
|
||||
Un agente de programación de código abierto para tu terminal — trae tu propio modelo.
|
||||
@@ -33,7 +33,7 @@ más útiles que puedes hacer — mira [Contribuir](#contribuir).
|
||||
[](https://www.npmjs.com/package/codewhale)
|
||||
[](https://discord.gg/37gfS3ksug)
|
||||
|
||||

|
||||

|
||||
|
||||
## Instalación
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<!-- source: README.md sha256:79856d0c10be -->
|
||||
<!-- source: README.md sha256:acee66e62ab0 -->
|
||||
# Codewhale
|
||||
|
||||
Un agent de programmation open source pour votre terminal — apportez votre propre modèle.
|
||||
@@ -35,7 +35,7 @@ voir [Contribuer](#contribuer).
|
||||
[](https://www.npmjs.com/package/codewhale)
|
||||
[](https://discord.gg/37gfS3ksug)
|
||||
|
||||

|
||||

|
||||
|
||||
## Installation
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<!-- source: README.md sha256:79856d0c10be -->
|
||||
<!-- source: README.md sha256:acee66e62ab0 -->
|
||||
# Codewhale
|
||||
|
||||
आपके टर्मिनल के लिए एक ओपन सोर्स कोडिंग एजेंट — मॉडल आप लाएँ।
|
||||
@@ -32,7 +32,7 @@ Codewhale की शुरुआत DeepSeek के लिए एक नेट
|
||||
[](https://www.npmjs.com/package/codewhale)
|
||||
[](https://discord.gg/37gfS3ksug)
|
||||
|
||||

|
||||

|
||||
|
||||
## इंस्टॉल
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<!-- source: README.md sha256:79856d0c10be -->
|
||||
<!-- source: README.md sha256:acee66e62ab0 -->
|
||||
# Codewhale
|
||||
|
||||
Sebuah coding agent sumber terbuka untuk terminal Anda — bawa model pilihan Anda sendiri.
|
||||
@@ -18,7 +18,7 @@ Kami selalu membuka kesempatan bagi para kontributor dan cara untuk terus berkem
|
||||
[](https://www.npmjs.com/package/codewhale)
|
||||
[](https://discord.gg/37gfS3ksug)
|
||||
|
||||

|
||||

|
||||
|
||||
## Instalasi
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<!-- source: README.md sha256:79856d0c10be -->
|
||||
<!-- source: README.md sha256:acee66e62ab0 -->
|
||||
# Codewhale
|
||||
|
||||
Un agente di programmazione open source per il tuo terminale — porta il tuo modello.
|
||||
@@ -33,7 +33,7 @@ tu possa fare — vedi [Contribuire](#contribuire).
|
||||
[](https://www.npmjs.com/package/codewhale)
|
||||
[](https://discord.gg/37gfS3ksug)
|
||||
|
||||

|
||||

|
||||
|
||||
## Installazione
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<!-- source: README.md sha256:79856d0c10be -->
|
||||
<!-- source: README.md sha256:acee66e62ab0 -->
|
||||
# Codewhale
|
||||
|
||||
ターミナルで動くオープンソースのコーディングエージェント — モデルはあなたが持ち込む。
|
||||
@@ -18,7 +18,7 @@ Codewhale は DeepSeek のためのネイティブ体験として始まりまし
|
||||
[](https://www.npmjs.com/package/codewhale)
|
||||
[](https://discord.gg/37gfS3ksug)
|
||||
|
||||

|
||||

|
||||
|
||||
## インストール
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<!-- source: README.md sha256:79856d0c10be -->
|
||||
<!-- source: README.md sha256:acee66e62ab0 -->
|
||||
# Codewhale
|
||||
|
||||
터미널에서 쓰는 오픈소스 코딩 에이전트 — 모델은 당신이 가져옵니다.
|
||||
@@ -18,7 +18,7 @@ Codewhale은 DeepSeek을 위한 네이티브 경험으로 시작했습니다.
|
||||
[](https://www.npmjs.com/package/codewhale)
|
||||
[](https://discord.gg/37gfS3ksug)
|
||||
|
||||

|
||||

|
||||
|
||||
## 설치
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@ useful things you can do — see [Contributing](#contributing).
|
||||
[](https://www.npmjs.com/package/codewhale)
|
||||
[](https://discord.gg/37gfS3ksug)
|
||||
|
||||

|
||||

|
||||
|
||||
## Install
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<!-- source: README.md sha256:79856d0c10be -->
|
||||
<!-- source: README.md sha256:acee66e62ab0 -->
|
||||
# Codewhale
|
||||
|
||||
Otwartoźródłowy agent programistyczny do Twojego terminala — model przynosisz sam.
|
||||
@@ -33,7 +33,7 @@ zrobić — zobacz [Współtworzenie](#współtworzenie).
|
||||
[](https://www.npmjs.com/package/codewhale)
|
||||
[](https://discord.gg/37gfS3ksug)
|
||||
|
||||

|
||||

|
||||
|
||||
## Instalacja
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<!-- source: README.md sha256:79856d0c10be -->
|
||||
<!-- source: README.md sha256:acee66e62ab0 -->
|
||||
# Codewhale
|
||||
|
||||
Um agente de programação de código aberto para o seu terminal — traga o seu próprio modelo.
|
||||
@@ -33,7 +33,7 @@ uma das coisas mais úteis que você pode fazer — veja [Contribuindo](#contrib
|
||||
[](https://www.npmjs.com/package/codewhale)
|
||||
[](https://discord.gg/37gfS3ksug)
|
||||
|
||||

|
||||

|
||||
|
||||
## Instalação
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<!-- source: README.md sha256:79856d0c10be -->
|
||||
<!-- source: README.md sha256:acee66e62ab0 -->
|
||||
# Codewhale
|
||||
|
||||
Открытый агент для программирования в вашем терминале — модель приносите с собой.
|
||||
@@ -35,7 +35,7 @@ harness станет вашим, а не нашим.
|
||||
[](https://www.npmjs.com/package/codewhale)
|
||||
[](https://discord.gg/37gfS3ksug)
|
||||
|
||||

|
||||

|
||||
|
||||
## Установка
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<!-- source: README.md sha256:79856d0c10be -->
|
||||
<!-- source: README.md sha256:acee66e62ab0 -->
|
||||
# Codewhale
|
||||
|
||||
Terminaliniz için açık kaynak bir kodlama ajanı — modeli siz getirin.
|
||||
@@ -34,7 +34,7 @@ yapabileceğiniz en yararlı şeylerden biridir — bkz.
|
||||
[](https://www.npmjs.com/package/codewhale)
|
||||
[](https://discord.gg/37gfS3ksug)
|
||||
|
||||

|
||||

|
||||
|
||||
## Kurulum
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<!-- source: README.md sha256:79856d0c10be -->
|
||||
<!-- source: README.md sha256:acee66e62ab0 -->
|
||||
# Codewhale
|
||||
|
||||
Агент для програмування з відкритим кодом у вашому терміналі — модель приносите ви.
|
||||
@@ -35,7 +35,7 @@ Codewhale починався як нативний інструмент для D
|
||||
[](https://www.npmjs.com/package/codewhale)
|
||||
[](https://discord.gg/37gfS3ksug)
|
||||
|
||||

|
||||

|
||||
|
||||
## Встановлення
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<!-- source: README.md sha256:79856d0c10be -->
|
||||
<!-- source: README.md sha256:acee66e62ab0 -->
|
||||
# Codewhale
|
||||
|
||||
Một coding agent mã nguồn mở cho terminal của bạn — mang theo model của riêng bạn.
|
||||
@@ -33,7 +33,7 @@ trong những điều hữu ích nhất bạn có thể làm — xem [Đóng gó
|
||||
[](https://www.npmjs.com/package/codewhale)
|
||||
[](https://discord.gg/37gfS3ksug)
|
||||
|
||||

|
||||

|
||||
|
||||
## Cài đặt
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<!-- source: README.md sha256:79856d0c10be -->
|
||||
<!-- source: README.md sha256:acee66e62ab0 -->
|
||||
# Codewhale
|
||||
|
||||
一个面向终端的开源编程智能体——模型由你自带。
|
||||
@@ -18,7 +18,7 @@ Codewhale 最初是为 DeepSeek 打造的原生体验,如今已成长为一个
|
||||
[](https://www.npmjs.com/package/codewhale)
|
||||
[](https://discord.gg/37gfS3ksug)
|
||||
|
||||

|
||||

|
||||
|
||||
## 安装
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<!-- source: README.md sha256:79856d0c10be -->
|
||||
<!-- source: README.md sha256:acee66e62ab0 -->
|
||||
# Codewhale
|
||||
|
||||
給終端機用的開源程式設計智能體——模型由你自備。
|
||||
@@ -18,7 +18,7 @@ Codewhale 起初是為 DeepSeek 打造的原生體驗,如今已成長為社群
|
||||
[](https://www.npmjs.com/package/codewhale)
|
||||
[](https://discord.gg/37gfS3ksug)
|
||||
|
||||

|
||||

|
||||
|
||||
## 安裝
|
||||
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 430 KiB |
BIN
assets/screenshot.webp
Normal file
BIN
assets/screenshot.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 168 KiB |
@@ -230,8 +230,8 @@
|
||||
]
|
||||
},
|
||||
"screenshot": {
|
||||
"readme": "assets/screenshot.png",
|
||||
"website": "web/public/codewhale-tui.png",
|
||||
"readme": "assets/screenshot.webp",
|
||||
"website": "web/public/codewhale-tui.webp",
|
||||
"sourceVersion": null,
|
||||
"sourceCommit": null,
|
||||
"terminal": "unrecorded",
|
||||
|
||||
@@ -159,7 +159,7 @@ export default async function HomePage({ params }: { params: Promise<{ locale: s
|
||||
<span>{d.shotSession}</span>
|
||||
</div>
|
||||
<Image
|
||||
src="/codewhale-tui.png"
|
||||
src="/codewhale-tui.webp"
|
||||
alt={d.screenshotAlt}
|
||||
width={1562}
|
||||
height={1256}
|
||||
|
||||
@@ -255,7 +255,7 @@ describe("homepage integration", () => {
|
||||
// (plain "Unreleased", per docs/design/WEB_VOICE.md).
|
||||
expect(homepage).toContain("d.sourceCandidate");
|
||||
expect(getHome("en").sourceCandidate).toBe("Unreleased");
|
||||
expect(homepage).toContain('src="/codewhale-tui.png"');
|
||||
expect(homepage).toContain('src="/codewhale-tui.webp"');
|
||||
for (const label of ["Plan", "Act", "Operate", "Ask", "Auto-Review", "Full Access"]) {
|
||||
expect(homepage).toContain(label);
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
* "Plan · Work · Operate", "Ask · Auto-Review · Full Access",
|
||||
* "TUI · exec · web · API", "Codewhale", "GitHub", "Issues",
|
||||
* `npm install -g codewhale`, `cargo test --locked`, `codewhale exec`,
|
||||
* package-manager proper nouns, mirror names, and `/codewhale-tui.png`.
|
||||
* package-manager proper nouns, mirror names, and `/codewhale-tui.webp`.
|
||||
*/
|
||||
|
||||
export interface ChromeDict {
|
||||
@@ -214,7 +214,7 @@ export interface HomeDict {
|
||||
|
||||
/** Screenshot toolbar label, e.g. "Current session". */
|
||||
shotSession: string;
|
||||
/** Screenshot alt text for /codewhale-tui.png. */
|
||||
/** Screenshot alt text for /codewhale-tui.webp. */
|
||||
screenshotAlt: string;
|
||||
/** Screenshot figcaption. */
|
||||
figcaption: string;
|
||||
|
||||
50
web/lib/middleware.test.ts
Normal file
50
web/lib/middleware.test.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { NextRequest } from "next/server";
|
||||
import { middleware } from "../middleware";
|
||||
|
||||
/**
|
||||
* `www.codewhale.net` and `codewhale.net` are both bound to this worker as
|
||||
* custom domains, so without a canonical-host redirect the whole site is
|
||||
* reachable — and indexable — twice.
|
||||
*/
|
||||
function request(url: string, host: string, headers: Record<string, string> = {}) {
|
||||
return new NextRequest(new URL(url), {
|
||||
headers: new Headers({ host, ...headers }),
|
||||
});
|
||||
}
|
||||
|
||||
describe("canonical host", () => {
|
||||
it("301s www to the apex, preserving path and query", () => {
|
||||
const res = middleware(
|
||||
request("https://www.codewhale.net/en/docs/hooks?x=1", "www.codewhale.net"),
|
||||
);
|
||||
expect(res.status).toBe(301);
|
||||
expect(res.headers.get("location")).toBe("https://codewhale.net/en/docs/hooks?x=1");
|
||||
});
|
||||
|
||||
it("redirects assets and API routes too, so a moved document stops pulling from www", () => {
|
||||
for (const path of ["/_next/static/chunk.js", "/api/curated", "/opengraph-image"]) {
|
||||
const res = middleware(request(`https://www.codewhale.net${path}`, "www.codewhale.net"));
|
||||
expect(res.status, path).toBe(301);
|
||||
expect(res.headers.get("location"), path).toBe(`https://codewhale.net${path}`);
|
||||
}
|
||||
});
|
||||
|
||||
it("leaves the apex host alone", () => {
|
||||
const res = middleware(request("https://codewhale.net/en", "codewhale.net"));
|
||||
expect(res.status).not.toBe(301);
|
||||
});
|
||||
|
||||
it("leaves localhost and preview hosts alone", () => {
|
||||
for (const host of ["localhost:3000", "codewhale-web.pages.dev"]) {
|
||||
const res = middleware(request("https://example.test/en", host));
|
||||
expect(res.status, host).not.toBe(301);
|
||||
}
|
||||
});
|
||||
|
||||
it("still applies security headers to the redirect", () => {
|
||||
const res = middleware(request("https://www.codewhale.net/en", "www.codewhale.net"));
|
||||
expect(res.headers.get("X-Content-Type-Options")).toBe("nosniff");
|
||||
expect(res.headers.get("Strict-Transport-Security")).toContain("max-age=");
|
||||
});
|
||||
});
|
||||
@@ -113,6 +113,22 @@ function pngDimensions(image: Buffer): [number, number] {
|
||||
return [image.readUInt32BE(16), image.readUInt32BE(20)];
|
||||
}
|
||||
|
||||
/**
|
||||
* Dimensions of the canonical product screenshot. It is a lossless VP8L WebP,
|
||||
* so the size lives in the 14-bit-packed VP8L header rather than a PNG IHDR;
|
||||
* PNG is still accepted so this keeps working if the asset is ever swapped back.
|
||||
*/
|
||||
function imageDimensions(image: Buffer): [number, number] {
|
||||
if (image.subarray(1, 4).toString("ascii") === "PNG") return pngDimensions(image);
|
||||
expect(image.subarray(0, 4).toString("ascii")).toBe("RIFF");
|
||||
expect(image.subarray(8, 12).toString("ascii")).toBe("WEBP");
|
||||
const chunk = image.subarray(12, 16).toString("ascii");
|
||||
expect(chunk, "expected a lossless VP8L screenshot").toBe("VP8L");
|
||||
// VP8L: 1 signature byte, then width-1 and height-1 as 14-bit LE fields.
|
||||
const bits = image.readUInt32LE(21);
|
||||
return [(bits & 0x3fff) + 1, ((bits >> 14) & 0x3fff) + 1];
|
||||
}
|
||||
|
||||
function comparableVersion(value: string): number {
|
||||
const [major = 0, minor = 0, patch = 0] = value.split(".").map((n) => Number.parseInt(n, 10) || 0);
|
||||
return major * 1_000_000 + minor * 1_000 + patch;
|
||||
@@ -585,14 +601,14 @@ done
|
||||
const digest = (image: Buffer) => createHash("sha256").update(image).digest("hex");
|
||||
|
||||
expect(digest(readmeImage)).toBe(digest(websiteImage));
|
||||
expect(pngDimensions(readmeImage)).toEqual([1562, 1256]);
|
||||
expect(imageDimensions(readmeImage)).toEqual([1562, 1256]);
|
||||
expect(statSync(new URL(matrix.screenshot.readme, root)).size).toBeLessThan(500_000);
|
||||
expect(matrix.screenshot.terminal).toBe("unrecorded");
|
||||
|
||||
const readme = text("README.md");
|
||||
const homepage = text("web/app/[locale]/page.tsx");
|
||||
expect(readme).toContain("assets/screenshot.png");
|
||||
expect(homepage).toContain('src="/codewhale-tui.png"');
|
||||
expect(readme).toContain("assets/screenshot.webp");
|
||||
expect(homepage).toContain('src="/codewhale-tui.webp"');
|
||||
// Alt text and figcaption are dictionary-backed (#4934); the screenshot
|
||||
// contract now runs through the EN reference value and the page's use of
|
||||
// it, and every routed locale must caption the same session honestly.
|
||||
|
||||
@@ -17,9 +17,35 @@ function applySecurityHeaders(res: NextResponse): NextResponse {
|
||||
return res;
|
||||
}
|
||||
|
||||
/**
|
||||
* The one host this site is indexed under. `www` is also bound to this worker
|
||||
* as a custom domain, so without this guard the entire site answers on two
|
||||
* hosts and every page has a duplicate URL a crawler can reach.
|
||||
*
|
||||
* This runs before the locale and static-asset branches on purpose: the
|
||||
* canonical host has to win for assets and API routes too, or a `www` page
|
||||
* keeps pulling subresources from `www` after the document moved.
|
||||
*/
|
||||
const CANONICAL_HOST = "codewhale.net";
|
||||
|
||||
function canonicalHostRedirect(req: NextRequest): NextResponse | null {
|
||||
const host = req.headers.get("host");
|
||||
if (!host) return null;
|
||||
// Compare without the port so local and preview hosts are untouched.
|
||||
const bare = host.split(":")[0].toLowerCase();
|
||||
if (bare !== `www.${CANONICAL_HOST}`) return null;
|
||||
const url = req.nextUrl.clone();
|
||||
url.host = CANONICAL_HOST;
|
||||
url.port = "";
|
||||
return NextResponse.redirect(url, 301);
|
||||
}
|
||||
|
||||
export function middleware(req: NextRequest) {
|
||||
const { pathname } = req.nextUrl;
|
||||
|
||||
const canonical = canonicalHostRedirect(req);
|
||||
if (canonical) return applySecurityHeaders(canonical);
|
||||
|
||||
// Skip API routes, static files, _next, and the dot-less metadata route
|
||||
// for the shared OG image (but still apply security headers).
|
||||
if (
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 430 KiB |
BIN
web/public/codewhale-tui.webp
Normal file
BIN
web/public/codewhale-tui.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 168 KiB |
Reference in New Issue
Block a user