mirror of
https://github.com/codewhale-hq/Codewhale.git
synced 2026-11-11 17:09:23 -10:00
fix(web): follow §46 and the site history for the OrcaRouter offer; derive home makers from facts (CW-7933)
A lessons review of the site history (codewhale-design vintage) flagged
that the offer strip above the hero repeated a reverted choice: on
2026-10-27 (35c5a62b7c) the offer left the top because the page opened on
a sponsored banner, and moved beside the models section it relates to.
The strip now sits inside the models section, still dated and labelled.
CURRENT_DECISIONS §46: English surfaces only, no featuring in zh-Hans, and
the disclosure scoped to the campaign link (founder amendment 2026-10-28:
the general "referral fee" line "isn't totally accurate"). The promo is
gated to locale en, and the disclosure now reads "This link is part of a
referral campaign; Codewhale may earn a referral reward on top-ups made
through it."
The home maker chips were a hand-typed list; they are now the distinct
makers in facts.models (gateways excluded), alphabetical, so new makers
appear without an edit and no provider is ranked (§46 neutral order).
Also removes components/whale-live.tsx, lib/content/release-highlights.ts
and the whale-live CSS, which nothing imported.
Checks in web/: tsc pass; eslint pass; vitest 793 passed; next build pass.
Browser: /en shows the offer card in the models section with the
disclosure; /zh shows no OrcaRouter text.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
a072208590
commit
cc0d42bf4a
6 files changed
+32
-333
No files matched your search
+25
-21
@@ -12,7 +12,7 @@ import { GitHubStar, FaceWall, MergedList, PulseStats } from "@/components/proje
|
||||
import { LiveStarLink } from "@/components/github-live";
|
||||
import { loadProjectPulse } from "@/lib/project-pulse";
|
||||
import { PROJECT_COPY } from "@/lib/content/project";
|
||||
import { HOME_MAKERS, HOME_OVERVIEW } from "@/lib/content/home-overview";
|
||||
import { HOME_OVERVIEW } from "@/lib/content/home-overview";
|
||||
import { PRODUCT_COPY } from "@/lib/content/product";
|
||||
import { PARTNER_PROMO } from "@/lib/content/partner-promo";
|
||||
import { fill, getChrome, getComputerUse, getHome, pickText, splitToken } from "@/lib/i18n/dictionaries";
|
||||
@@ -38,9 +38,13 @@ export default async function HomePage({ params }: { params: Promise<{ locale: s
|
||||
const sourceIsPublished = publishedRelease?.version === sourceVersion;
|
||||
const promoLeft = Date.parse(PARTNER_PROMO.endsAt) - Date.now();
|
||||
const promoDays = Math.max(1, Math.ceil(promoLeft / 86_400_000));
|
||||
const showPromo = promoLeft > 0 && PARTNER_PROMO.locales.includes(locale);
|
||||
const jsonLd = buildSoftwareApplicationJsonLd(publishedRelease);
|
||||
const ledeParts = splitToken(d.heroIntro, "brand");
|
||||
const firstParty = new Set(HOME_MAKERS);
|
||||
const gateways = new Set(["OpenRouter", "Together", "OpenAI Codex"]);
|
||||
const makers = [...new Set(facts.models.map((model) => model.provider).filter((name): name is string => !!name && !gateways.has(name)))]
|
||||
.sort((a, b) => a.localeCompare(b));
|
||||
const firstParty = new Set(makers);
|
||||
const newest = facts.models
|
||||
.filter((model) => model.addedAt && model.provider && firstParty.has(model.provider))
|
||||
.sort((a, b) => (b.addedAt ?? "").localeCompare(a.addedAt ?? ""))
|
||||
@@ -56,24 +60,6 @@ export default async function HomePage({ params }: { params: Promise<{ locale: s
|
||||
dangerouslySetInnerHTML={{ __html: serializeJsonLd(jsonLd) }}
|
||||
/>
|
||||
|
||||
{promoLeft > 0 && (
|
||||
<aside className="home-promo-bar" aria-label={PARTNER_PROMO.title}>
|
||||
<div className="home-promo-bar-inner">
|
||||
<Image src={PARTNER_PROMO.image.src} alt="" width={64} height={36} className="home-promo-bar-art" unoptimized />
|
||||
<p>
|
||||
<strong>{PARTNER_PROMO.title}</strong>
|
||||
<span>{pickText(PARTNER_PROMO.bar, locale)}</span>
|
||||
<span className="home-promo-bar-days">{fill(pickText(promoDays === 1 ? PARTNER_PROMO.dayLeft : PARTNER_PROMO.daysLeft, locale), { count: promoDays })}</span>
|
||||
</p>
|
||||
<span className="home-promo-bar-tag" title={pickText(PARTNER_PROMO.disclosure, locale)}>{pickText(PARTNER_PROMO.referralTag, locale)}</span>
|
||||
<a href={PARTNER_PROMO.href} rel="sponsored noopener" className="btn btn-primary">
|
||||
{pickText(PARTNER_PROMO.cta, locale)}
|
||||
<Icon name="external" className="icon" />
|
||||
</a>
|
||||
</div>
|
||||
</aside>
|
||||
)}
|
||||
|
||||
<div className="home-intro">
|
||||
<div className="home-water" aria-hidden="true"><Strata /></div>
|
||||
<section className="home-hero" aria-labelledby="home-title">
|
||||
@@ -149,7 +135,7 @@ export default async function HomePage({ params }: { params: Promise<{ locale: s
|
||||
link={<Link href={`/${locale}/models`} className="section-link">{d.modelsLink}<Icon name="arrow-right" className="icon icon-flip" /></Link>}
|
||||
>
|
||||
<ul className="home-makers" role="list">
|
||||
{HOME_MAKERS.map((maker) => <li key={maker} className="pill">{maker}</li>)}
|
||||
{makers.map((maker) => <li key={maker} className="pill">{maker}</li>)}
|
||||
<li className="pill pill-quiet">{pickText(HOME_OVERVIEW.modelsAny, locale)}</li>
|
||||
</ul>
|
||||
{newest.length > 0 && (
|
||||
@@ -165,6 +151,24 @@ export default async function HomePage({ params }: { params: Promise<{ locale: s
|
||||
</ol>
|
||||
</div>
|
||||
)}
|
||||
{showPromo && (
|
||||
<aside className="home-promo-bar home-promo-inline" aria-label={PARTNER_PROMO.title}>
|
||||
<div className="home-promo-bar-inner">
|
||||
<Image src={PARTNER_PROMO.image.src} alt="" width={64} height={36} className="home-promo-bar-art" unoptimized />
|
||||
<p>
|
||||
<strong>{PARTNER_PROMO.title}</strong>
|
||||
<span>{pickText(PARTNER_PROMO.bar, locale)}</span>
|
||||
<span className="home-promo-bar-days">{fill(pickText(promoDays === 1 ? PARTNER_PROMO.dayLeft : PARTNER_PROMO.daysLeft, locale), { count: promoDays })}</span>
|
||||
</p>
|
||||
<span className="home-promo-bar-tag">{pickText(PARTNER_PROMO.referralTag, locale)}</span>
|
||||
<p className="home-promo-bar-disclosure">{pickText(PARTNER_PROMO.disclosure, locale)}</p>
|
||||
<a href={PARTNER_PROMO.href} rel="sponsored noopener" className="btn btn-primary">
|
||||
{pickText(PARTNER_PROMO.cta, locale)}
|
||||
<Icon name="external" className="icon" />
|
||||
</a>
|
||||
</div>
|
||||
</aside>
|
||||
)}
|
||||
</Section>
|
||||
|
||||
<Section
|
||||
|
||||
+4
-55
@@ -138,60 +138,6 @@
|
||||
.living-whale-mark { display: none; transition: none; }
|
||||
}
|
||||
|
||||
/* ---------- the live whale ---------- */
|
||||
.whale-live {
|
||||
position: relative;
|
||||
aspect-ratio: 1;
|
||||
}
|
||||
.whale-live > .whale-live-poster,
|
||||
.whale-live > .whale-live-canvas {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
/* The phase, in a word: a small point of light and a label, above the
|
||||
whale's back. Light means live work (DIRECTION.md). */
|
||||
.whale-live-state {
|
||||
position: absolute;
|
||||
inset-inline-start: 50%;
|
||||
top: 8%;
|
||||
translate: -50% 0;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: var(--space-2);
|
||||
padding: 0.3rem 0.75rem;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: var(--radius-pill);
|
||||
background: var(--panel);
|
||||
color: var(--text);
|
||||
font-size: var(--text-caption);
|
||||
font-weight: 500;
|
||||
white-space: nowrap;
|
||||
box-shadow: 0 6px 18px -10px rgb(11 72 187 / 0.35);
|
||||
}
|
||||
.whale-live-dot {
|
||||
width: 0.45rem;
|
||||
height: 0.45rem;
|
||||
border-radius: var(--radius-pill);
|
||||
background: var(--brand-light);
|
||||
}
|
||||
.whale-live-state[data-state="done"] .whale-live-dot { background: var(--live); }
|
||||
.whale-live-state[data-state="rest"] .whale-live-dot { background: var(--muted); }
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
.whale-live-state:not([data-state="rest"]):not([data-state="done"]) .whale-live-dot {
|
||||
animation: whale-pulse 1.4s var(--ease-out) infinite;
|
||||
}
|
||||
}
|
||||
@keyframes whale-pulse {
|
||||
0% { box-shadow: 0 0 0 0 rgb(30 143 216 / 0.55); }
|
||||
100% { box-shadow: 0 0 0 0.5rem rgb(30 143 216 / 0); }
|
||||
}
|
||||
.whale-live-poster { --pose-size: 100%; padding: 14%; box-sizing: border-box; }
|
||||
.whale-live-canvas { opacity: 0; }
|
||||
.whale-live[data-ready] .whale-live-canvas { opacity: 1; }
|
||||
.whale-live[data-ready] .whale-live-poster { visibility: hidden; }
|
||||
|
||||
/* The install band ends the page in the sea: one line of light at its top,
|
||||
and water that reaches the floor colour by its own bottom edge, so it runs
|
||||
into the footer without a seam. */
|
||||
@@ -294,8 +240,11 @@
|
||||
.home-proof-stars { display: inline-flex; align-items: center; gap: var(--space-1); }
|
||||
.home-proof-date { color: var(--muted); }
|
||||
|
||||
/* ---------- partner offer: a dated strip above the hero ---------- */
|
||||
/* ---------- partner offer: a dated strip in the models section (English only, §46) ---------- */
|
||||
.home-promo-bar { border-bottom: 1px solid var(--line); background: var(--surface); }
|
||||
.home-promo-inline { margin-top: var(--space-7); border: 1px solid var(--line); border-radius: var(--radius-surface); }
|
||||
.home-promo-inline .home-promo-bar-inner { width: auto; padding: var(--space-4) var(--space-5); }
|
||||
.home-promo-bar-disclosure { flex-basis: 100%; order: 10; margin: 0; color: var(--muted); font-size: var(--text-caption); }
|
||||
.home-promo-bar-inner {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
|
||||
@@ -1,224 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { WhalePose } from "@/components/whale-pose";
|
||||
import { whaleStateLabels, type WhaleStateKey } from "@/lib/content/whale-states";
|
||||
import {
|
||||
cueWhale,
|
||||
loadWhaleReference,
|
||||
onWhaleCue,
|
||||
WHALE_PERFORMANCE,
|
||||
type WhaleCue,
|
||||
type WhaleDirector,
|
||||
type WhaleReference,
|
||||
} from "@/lib/whale-motion";
|
||||
|
||||
/**
|
||||
* The live v2 whale: the desktop app's own Director and rig, drawn on a
|
||||
* canvas. Until the reference loads (or if it fails) the static rest pose
|
||||
* stands in, so the hero never shows an empty box.
|
||||
*
|
||||
* Motion budget: 30 fps while visible, nothing while hidden or off screen.
|
||||
* Under prefers-reduced-motion the Director paints its poster pose once per
|
||||
* change and never runs a clock. The gaze follows the pointer only at rest,
|
||||
* and only with motion allowed.
|
||||
*
|
||||
* With `perform`, it acts out a short session on its own (WHALE_PERFORMANCE)
|
||||
* and names each phase in a word beneath it. A reader's choice elsewhere on
|
||||
* the page (a terminal view) takes over for a while, then the loop resumes.
|
||||
*
|
||||
* Known limits: one live whale per page (habitat gaze is module state in the
|
||||
* vendored reference), and cues are illustrative — this component observes
|
||||
* no real session.
|
||||
*/
|
||||
const CHOSEN_HOLD_MS = 15000;
|
||||
/** Share of the canvas the 124-unit rig box fills; the rest is room for props. */
|
||||
const INSET = 0.72;
|
||||
|
||||
export function WhaleLive({
|
||||
locale = "en",
|
||||
className = "",
|
||||
follow = true,
|
||||
perform = false,
|
||||
asleep = false,
|
||||
}: {
|
||||
locale?: string;
|
||||
className?: string;
|
||||
follow?: boolean;
|
||||
perform?: boolean;
|
||||
asleep?: boolean;
|
||||
}) {
|
||||
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||
const asleepRef = useRef(asleep);
|
||||
const wakeRef = useRef<() => void>(() => {});
|
||||
const [ready, setReady] = useState(false);
|
||||
const [state, setState] = useState<WhaleStateKey>("rest");
|
||||
const labels = whaleStateLabels(locale);
|
||||
|
||||
useEffect(() => {
|
||||
const canvas = canvasRef.current;
|
||||
if (!canvas) return;
|
||||
let cancelled = false;
|
||||
let frame = 0;
|
||||
let last = 0;
|
||||
let visible = true;
|
||||
let live: { reference: WhaleReference; director: WhaleDirector } | null = null;
|
||||
const motion = window.matchMedia("(prefers-reduced-motion: reduce)");
|
||||
const cleanups: (() => void)[] = [];
|
||||
|
||||
const paint = () => {
|
||||
if (!live) return;
|
||||
const box = canvas.getBoundingClientRect();
|
||||
if (!box.width || !box.height) return;
|
||||
const dpr = Math.min(window.devicePixelRatio || 1, 2);
|
||||
const width = Math.round(box.width * dpr), height = Math.round(box.height * dpr);
|
||||
if (canvas.width !== width) canvas.width = width;
|
||||
if (canvas.height !== height) canvas.height = height;
|
||||
const ctx = canvas.getContext("2d");
|
||||
if (!ctx) return;
|
||||
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
||||
ctx.clearRect(0, 0, box.width, box.height);
|
||||
// The rig's 124-unit box holds the body; props (page, pad, wrench,
|
||||
// calves) reach past it, so the whale draws at INSET of the canvas.
|
||||
const fit = (Math.min(box.width, box.height) / 124) * INSET;
|
||||
ctx.translate(box.width / 2, box.height / 2);
|
||||
ctx.scale(fit, fit);
|
||||
live.reference.acting.render(ctx, live.director, {
|
||||
theme: "paper",
|
||||
size: Math.min(box.width, box.height),
|
||||
dpr,
|
||||
lod: 0,
|
||||
pose: follow ? live.reference.habitat.pose(live.director) : undefined,
|
||||
});
|
||||
};
|
||||
|
||||
const tick = (now: number) => {
|
||||
frame = 0;
|
||||
if (!live || live.director.reduced || !visible || document.hidden || asleepRef.current) return;
|
||||
// Never replay time spent hidden: clamp the step like the app does.
|
||||
const dt = last ? Math.min((now - last) / 1000, 0.1) : 1 / 30;
|
||||
if (now - last >= 1000 / 31) {
|
||||
last = now;
|
||||
live.director.step(dt);
|
||||
paint();
|
||||
}
|
||||
frame = requestAnimationFrame(tick);
|
||||
};
|
||||
const schedule = () => {
|
||||
if (!frame && live && !live.director.reduced && visible && !document.hidden && !asleepRef.current) {
|
||||
last = 0;
|
||||
frame = requestAnimationFrame(tick);
|
||||
}
|
||||
};
|
||||
wakeRef.current = schedule;
|
||||
|
||||
loadWhaleReference().then((reference) => {
|
||||
if (cancelled) return;
|
||||
const director = new reference.acting.Director({ seed: 11, reduced: motion.matches });
|
||||
live = { reference, director };
|
||||
setReady(true);
|
||||
paint();
|
||||
schedule();
|
||||
|
||||
let turn = 0;
|
||||
let step = 0;
|
||||
let timer: ReturnType<typeof setTimeout> | undefined;
|
||||
const autoplay = (delay: number) => {
|
||||
clearTimeout(timer);
|
||||
if (!perform || motion.matches) return;
|
||||
timer = setTimeout(() => {
|
||||
const { cue, seconds } = WHALE_PERFORMANCE[step % WHALE_PERFORMANCE.length];
|
||||
step += 1;
|
||||
cueWhale(cue);
|
||||
autoplay(seconds * 1000);
|
||||
}, delay);
|
||||
};
|
||||
const apply = (cue: WhaleCue) => {
|
||||
if (cue.presence === "Done") turn += 1;
|
||||
director.set(
|
||||
cue.presence,
|
||||
cue.kind ? { kind: cue.kind, observed: true, parallel: cue.parallel } : null,
|
||||
cue.presence === "Done"
|
||||
? { freshness: "Live", turnId: `site-${turn}`, status: "completed" }
|
||||
: { freshness: "Live" },
|
||||
);
|
||||
setState(cue.label);
|
||||
paint();
|
||||
schedule();
|
||||
// A reader's choice holds the stage, then the session starts over.
|
||||
if (cue.chosen) { step = 0; autoplay(CHOSEN_HOLD_MS); }
|
||||
};
|
||||
cleanups.push(onWhaleCue(apply));
|
||||
autoplay(1200);
|
||||
cleanups.push(() => clearTimeout(timer));
|
||||
|
||||
const onMotion = () => { director.setReduced(motion.matches); paint(); schedule(); autoplay(1200); };
|
||||
motion.addEventListener("change", onMotion);
|
||||
cleanups.push(() => motion.removeEventListener("change", onMotion));
|
||||
}).catch(() => { /* The static pose stays in place. */ });
|
||||
|
||||
const onVisibility = () => schedule();
|
||||
document.addEventListener("visibilitychange", onVisibility);
|
||||
cleanups.push(() => document.removeEventListener("visibilitychange", onVisibility));
|
||||
|
||||
if (typeof IntersectionObserver !== "undefined") {
|
||||
const io = new IntersectionObserver((entries) => {
|
||||
visible = entries.some((entry) => entry.isIntersecting);
|
||||
schedule();
|
||||
});
|
||||
io.observe(canvas);
|
||||
cleanups.push(() => io.disconnect());
|
||||
}
|
||||
const resize = new ResizeObserver(() => paint());
|
||||
resize.observe(canvas);
|
||||
cleanups.push(() => resize.disconnect());
|
||||
|
||||
if (follow) {
|
||||
// The whale glances toward the pointer anywhere in the hero, not only
|
||||
// over its own box. Passive: scrolling and clicks are untouched.
|
||||
const move = (event: PointerEvent) => {
|
||||
if (!live || motion.matches) return;
|
||||
const box = canvas.getBoundingClientRect();
|
||||
const fit = (Math.min(box.width, box.height) / 124) * INSET;
|
||||
if (!fit) return;
|
||||
live.reference.habitat.observe(
|
||||
(event.clientX - box.left - box.width / 2) / fit,
|
||||
(event.clientY - box.top - box.height / 2) / fit,
|
||||
);
|
||||
};
|
||||
const leave = () => live?.reference.habitat.leave();
|
||||
window.addEventListener("pointermove", move, { passive: true });
|
||||
document.documentElement.addEventListener("pointerleave", leave, { passive: true });
|
||||
cleanups.push(() => {
|
||||
window.removeEventListener("pointermove", move);
|
||||
document.documentElement.removeEventListener("pointerleave", leave);
|
||||
});
|
||||
}
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
if (frame) cancelAnimationFrame(frame);
|
||||
cleanups.forEach((cleanup) => cleanup());
|
||||
};
|
||||
}, [follow, perform]);
|
||||
|
||||
useEffect(() => {
|
||||
asleepRef.current = asleep;
|
||||
if (!asleep) wakeRef.current();
|
||||
}, [asleep]);
|
||||
|
||||
return (
|
||||
<div className={`whale-live ${className}`.trim()} data-ready={ready || undefined}>
|
||||
<WhalePose pose="rest" priority className="whale-live-poster" />
|
||||
<canvas ref={canvasRef} className="whale-live-canvas" aria-hidden="true" />
|
||||
{perform && (
|
||||
// Decorative narration: hidden from assistive technology so the loop
|
||||
// never announces itself every few seconds.
|
||||
<p className="whale-live-state" data-state={state} aria-hidden="true">
|
||||
<span className="whale-live-dot" />
|
||||
{labels[state]}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,7 +1,5 @@
|
||||
import type { LocalizedText } from "./vocabulary";
|
||||
|
||||
export const HOME_MAKERS = ["DeepSeek", "Anthropic", "OpenAI", "Google", "xAI", "Moonshot/Kimi", "Qwen", "Z.ai", "MiniMax", "Mistral"];
|
||||
|
||||
export const HOME_OVERVIEW = {
|
||||
modelsCount: { en: "{providers} providers and {models} models in the current source.", zh: "当前源码包含 {providers} 个提供商和 {models} 个模型。" },
|
||||
modelsAny: { en: "Any OpenAI-compatible endpoint", zh: "任何 OpenAI 兼容端点" },
|
||||
|
||||
@@ -1,20 +1,13 @@
|
||||
export const PARTNER_PROMO = {
|
||||
locales: ["en"],
|
||||
href: "https://www.orcarouter.ai/redeem/1a6250545fdc4e0aaccb0a4470159e67",
|
||||
image: {
|
||||
src: "/codewhale-x-orcarouter.webp",
|
||||
width: 1440,
|
||||
height: 810,
|
||||
alt: {
|
||||
en: "Codewhale × OrcaRouter: The whales are teaming up. +30% DeepSeek v4.1 Flash usage.",
|
||||
zh: "Codewhale × OrcaRouter:双鲸携手,DeepSeek v4.1 Flash 用量增加 30%。",
|
||||
},
|
||||
},
|
||||
endsAt: "2026-11-21T12:29:57Z",
|
||||
title: "Codewhale × OrcaRouter",
|
||||
body: {
|
||||
en: "Get 30% extra DeepSeek V4.1 Flash credit on each OrcaRouter top-up. One claim per developer, open until November 21, 2026.",
|
||||
zh: "在 OrcaRouter 每笔充值可额外获得 30% 的 DeepSeek V4.1 Flash 额度。每位开发者限领 1 份,报名截至 2026 年 11 月 21 日。",
|
||||
},
|
||||
bar: {
|
||||
en: "30% extra DeepSeek V4.1 Flash credit on each OrcaRouter top-up, until November 21.",
|
||||
zh: "每笔 OrcaRouter 充值额外获得 30% 的 DeepSeek V4.1 Flash 额度,截至 11 月 21 日。",
|
||||
@@ -24,7 +17,7 @@ export const PARTNER_PROMO = {
|
||||
referralTag: { en: "Referral link", zh: "推荐链接" },
|
||||
cta: { en: "Claim on OrcaRouter", zh: "在 OrcaRouter 领取" },
|
||||
disclosure: {
|
||||
en: "Codewhale earns a referral fee from OrcaRouter.",
|
||||
zh: "Codewhale 会从 OrcaRouter 获得推荐佣金。",
|
||||
en: "This link is part of a referral campaign; Codewhale may earn a referral reward on top-ups made through it.",
|
||||
zh: "此链接属于推荐活动;通过此链接充值时,Codewhale 可能获得推荐奖励。",
|
||||
},
|
||||
};
|
||||
@@ -1,21 +0,0 @@
|
||||
export const RELEASE_HIGHLIGHTS = {
|
||||
version: "0.10.3",
|
||||
comingHeading: { en: "Coming in v{version}", zh: "v{version} 即将推出" },
|
||||
newHeading: { en: "New in v{version}", zh: "v{version} 新功能" },
|
||||
comingLede: { en: "Merged on main and not in a published release yet. Build main to try it now.", zh: "已合并到 main,尚未包含在已发布版本中。现在可从 main 构建试用。" },
|
||||
changelogLink: { en: "Read the changelog", zh: "查看更新日志" },
|
||||
items: [
|
||||
{ title: "Steer mid-turn", body: { en: "Ctrl+Enter closes the in-flight model request and continues the same turn with your message. Tab and Shift+Tab change mode and permissions while the agent works; the change applies at its next step.", zh: "Ctrl+Enter 会立即结束正在进行的模型请求,并带着你的消息继续同一轮。智能体工作时也可以用 Tab 和 Shift+Tab 切换模式和权限,改动会在它的下一步生效。" } },
|
||||
{ title: "MCP", body: { en: "MCP servers now connect through the official MCP SDK (2.4) behind Codewhale's Rust broker. Newer servers are discovered with server/discover; older stdio and HTTP servers fall back to the legacy handshake.", zh: "MCP 服务器现在通过官方 MCP SDK(2.4)连接,由 Codewhale 的 Rust 代理把关。较新的服务器通过 server/discover 发现;较旧的 stdio 和 HTTP 服务器回退到旧版握手。" } },
|
||||
{ title: "MCP fixes", body: { en: "Older servers that reject the discovery probe still connect. A server that refuses sign-in shows its real 401 and the /mcp login hint, and one whose command cannot start says why.", zh: "拒绝发现探测的旧服务器仍可连接。拒绝登录的服务器会显示真实的 401 和 /mcp login 提示;命令无法启动的服务器会说明原因。" } },
|
||||
{ title: "DeepSeek Harness plugins", body: { en: "Reviewed DSH plugins run in the Native extension host. Tool output is checked against its declared schema, and unsupported options are refused instead of ignored.", zh: "经过审核的 DSH 插件在 Native 扩展宿主中运行。工具输出按声明的 schema 校验,不支持的选项会被拒绝而不是被忽略。" } },
|
||||
{ title: "Computer Use", body: { en: "No longer bundled in the binary. Install it from the codewhale marketplace, which points at its own release; every decision still needs your approval.", zh: "不再内置于二进制文件中。可从 codewhale 插件市场安装,市场指向它的独立发布版本;每个决定仍需你批准。" } },
|
||||
{ title: "Opper and Runcrate", body: { en: "Two more bundled providers in /provider: Opper, an EU-hosted OpenAI-compatible gateway (OPPER_API_KEY), and Runcrate (RUNCRATE_API_KEY). Each provider's live model listing is the model authority.", zh: "/provider 新增两个内置提供商:Opper(欧盟托管的 OpenAI 兼容网关,OPPER_API_KEY)和 Runcrate(RUNCRATE_API_KEY)。模型列表以各提供商的实时列表为准。" } },
|
||||
{ title: "Recall after compaction", body: { en: "Two read-only tools, context_archive_search and context_archive_get, let the model search and read what compaction replaced, with bounded results and credential masking.", zh: "两个只读工具 context_archive_search 和 context_archive_get 让模型搜索并读取被压缩替换的内容,结果有上限,凭据会被遮蔽。" } },
|
||||
{ title: "Goals ask before guessing", body: { en: "When a /goal needs an answer only you can give, the model is told to yield and ask instead of inventing one. This is prompt guidance, not enforcement.", zh: "当 /goal 需要只有你才能给出的答案时,模型会被引导交还控制权并提问,而不是自行编造。这是提示引导,并非强制。" } },
|
||||
{ title: "Image-heavy sessions", body: { en: "Older images are left out of an outbound request with a receipt, and an HTTP 413 is retried once with a smaller body. Saved images stay intact.", zh: "较早的图片会从发出的请求中省略并留下记录;遇到 HTTP 413 时会用更小的请求体重试一次。已保存的图片保持不变。" } },
|
||||
{ title: "Instruction budgets", body: { en: "Global and project instructions get separate budgets, and every trim, skip or refusal shows in /context report.", zh: "全局指令和项目指令分别拥有各自的预算,每次截断、跳过或拒绝都会显示在 /context report 中。" } },
|
||||
{ title: "codewhale uninstall", body: { en: "Previews the release files the installer recorded, on Unix and Windows; --yes removes only those. Package-managed copies print their own removal command. Your data stays unless you add --purge-data. Windows removal is compile-checked but not yet exercised on a Windows host.", zh: "在 Unix 和 Windows 上预览安装程序记录的发布文件;--yes 只删除这些文件。包管理器安装的版本会给出各自的卸载命令。除非加上 --purge-data,否则你的数据会保留。Windows 上的删除已通过编译检查,但尚未在 Windows 主机上实际运行。" } },
|
||||
{ title: "/rc", body: { en: "codewhale rc attaches to the hosted control plane for web Chat again: it accepts the exact released runtime identity instead of only the retired 0.9.13 one.", zh: "codewhale rc 可以再次连接托管控制平面以使用网页 Chat:它接受确切的已发布运行时身份,而不再只接受已退役的 0.9.13。" } },
|
||||
],
|
||||
};
|
||||
Reference in new issue
Block a user