Changelog, dev startup, README, AgentHub 0.4.0, model catalog, and landing site (#7)

Branch-length batch covering tooling, the model layer, the Web App and the public
surfaces. Highlights:

- Changelog: a per-release `changelog/<version>/` tree, grouped by the surface each
  change touches, with a root CHANGELOG.md holding one line per release.
- Dev startup: `scripts/dev-prebuild.mjs` serializes the skills+core prebuild behind a
  lock and keeps `pnpm install` current; `pnpm dev` runs server+web together.
- AgentHub 0.3.3 -> 0.4.0: OmniMessage complete payloads carry one opaque `fidelity`
  object in place of item-level `signature`/`phase`, threaded verbatim through Trace,
  replay and resume; malformed classification adapted to the new error types.
- Model layer: a model is always referenced by an explicit `(provider, model_id)` pair.
  The provider is never inferred, guessed or defaulted -- both the catalog inference and
  the unique-match config resolution are gone, and CLI, SDK, server routes and
  run_subagent all require the complete pair. Catalog gains the Qwen Token Plan, Qwen
  Pay-As-You-Go and Fireworks AI gateways, plus an expanded OpenRouter group.
- Web App: catalog preset sync and per-group speed test on the Models page, positional
  slash commands, a markdown renderer, skill-library update reminders, and a vertically
  centred draft page whose upward menus size themselves to the room available.
- Public surfaces: restructured READMEs, the penguin.ooo landing site and blog, refreshed
  benchmark results for both suites, and the demo videos playing on the landing page.

Includes the fixes from a full review of the branch: 23 confirmed findings, among them a
provider-inference bug that could send one vendor's API key to another vendor's endpoint,
and an Escape handler that destroyed the composer's contents unrecoverably.

Verified on the branch head: pnpm test (1127 passing, 7 packages), pnpm typecheck and
pnpm format:check clean, Playwright e2e 14/14.
This commit is contained in:
Yaowei Zheng authored and GitHub committed 2026-07-21 17:43:31 +08:00
1 parent abf0a2f248
commit d4faee3a1e
214 files changed
+9406 -1688

No files matched your search

@@ -0,0 +1,122 @@
/**
* Rotating announcement bar above the sticky nav (it scrolls away with the page).
* Light brand-tinted background; announcements auto-advance by SLIDING in one
* direction only (a clone of the first slide follows the last one, and the track
* snaps back without animation once the clone is fully in view). No manual
* switch controls; hovering or focusing the bar pauses the rotation. Every
* announcement is a link into the blog, with a small arrow marking it as
* click-through.
*/
import { useEffect, useState } from "react";
import { Link } from "react-router";
import { S } from "../lib/strings";
import { ArrowRightIcon } from "./icons";
const ROTATE_MS = 6000;
/** Both announcements point at blog posts (content/blog/<slug>.*.md). */
const ITEMS = [
{ key: "models", to: "/blog/introducing-penguinharness" },
{ key: "fireworks", to: "/blog/fireworks-credits-amd" },
] as const;
const REDUCED_MOTION_QUERY = "(prefers-reduced-motion: reduce)";
function prefersReducedMotion(): boolean {
return window.matchMedia(REDUCED_MOTION_QUERY).matches;
}
export function AnnouncementBar() {
const texts: Record<(typeof ITEMS)[number]["key"], string> = {
models: S.announcement.models,
fireworks: S.announcement.fireworks,
};
// pos runs 0..ITEMS.length where ITEMS.length is the clone of slide 0.
const [pos, setPos] = useState(0);
const [animate, setAnimate] = useState(true);
// Focus pauses like hover does: tabIndex={-1} never blurs an already focused
// link, so advancing past it would strand document focus inside an aria-hidden
// slide translated out of the viewport, and WCAG 2.2.2 wants the rotation
// stoppable without a pointer. The two are tracked apart so the pointer
// leaving the bar cannot restart it while focus is still inside.
const [hovered, setHovered] = useState(false);
const [focused, setFocused] = useState(false);
const paused = hovered || focused;
const [reduced, setReduced] = useState(prefersReducedMotion);
// Tracked live rather than read once, because the preference can be toggled
// while the page is open and it decides how the rotation wraps around.
useEffect(() => {
const mq = window.matchMedia(REDUCED_MOTION_QUERY);
const onChange = (e: MediaQueryListEvent) => setReduced(e.matches);
setReduced(mq.matches);
mq.addEventListener("change", onChange);
return () => mq.removeEventListener("change", onChange);
}, []);
useEffect(() => {
if (paused) return;
// styles.css kills every transition under reduced motion, so the clone's
// transitionend never arrives and the snap-back below never runs: those
// users wrap around the real slides instead and never land on the clone.
const advance = (p: number) => {
if (reduced) return (p + 1) % ITEMS.length;
return p >= ITEMS.length ? p : p + 1;
};
const timer = setInterval(() => setPos(advance), ROTATE_MS);
return () => clearInterval(timer);
}, [paused, reduced]);
// After snapping back to the real first slide, re-enable the transition on the
// next frame pair so the jump itself never animates.
useEffect(() => {
if (animate) return;
const raf = requestAnimationFrame(() => requestAnimationFrame(() => setAnimate(true)));
return () => cancelAnimationFrame(raf);
}, [animate]);
const slides = [...ITEMS, ITEMS[0]!];
return (
<div
role="region"
aria-label={S.announcement.label}
onMouseEnter={() => setHovered(true)}
onMouseLeave={() => setHovered(false)}
onFocusCapture={() => setFocused(true)}
onBlurCapture={() => setFocused(false)}
className="border-b border-brand-100 bg-brand-50 dark:border-brand-950 dark:bg-brand-950/50"
>
<div className="mx-auto h-9 max-w-6xl overflow-hidden px-4 sm:px-6">
<div
className={`flex h-full ${animate ? "transition-transform duration-500 ease-out" : ""}`}
style={{ transform: `translateX(-${pos * 100}%)` }}
onTransitionEnd={(e) => {
if (e.target !== e.currentTarget || e.propertyName !== "transform") return;
if (pos === ITEMS.length) {
setAnimate(false);
setPos(0);
}
}}
>
{slides.map((item, i) => (
<div
key={`${item.key}-${i}`}
aria-hidden={i !== pos}
className="flex w-full shrink-0 items-center justify-center"
>
<Link
to={item.to}
tabIndex={i === pos ? 0 : -1}
className="inline-flex min-w-0 items-center gap-1.5 text-[13px] font-medium text-brand-800 underline-offset-2 hover:underline dark:text-brand-200"
>
<span className="truncate">{texts[item.key]}</span>
<ArrowRightIcon className="h-3.5 w-3.5 shrink-0" aria-hidden="true" />
</Link>
</div>
))}
</div>
</div>
</div>
);
}
@@ -0,0 +1,45 @@
/**
* Demo video embed. The file itself is hosted in the sibling community repo (see
* `demoVideoUrl` in lib/links.ts for why), so the important part here is that nothing
* is fetched until the visitor asks: `preload="none"` plus a poster means a page view
* costs one ~37 KB still, not a ~9 MB download, and the section still looks finished
* before anyone presses play.
*
* Deliberately NOT wrapped in BrowserFrame: that chrome (with its localhost address bar)
* says "this is the product's UI", which is true of a screenshot and of the case demos,
* but not of a narrated slide deck. A plain framed surface instead.
*/
export function DemoVideo({
src,
poster,
label,
caption,
}: {
src: string;
poster: string;
/** Accessible name — the surrounding copy is decorative to a screen reader. */
label: string;
caption?: string;
}) {
return (
<figure className="mx-auto w-full max-w-4xl">
<div className="overflow-hidden rounded-xl border border-gray-200 bg-gray-950 shadow-sm dark:border-gray-800">
<video
src={src}
poster={poster}
controls
preload="none"
playsInline
aria-label={label}
className="block h-auto w-full"
/>
</div>
{caption && (
<figcaption className="mt-3 text-center text-xs text-gray-500 dark:text-gray-400">
{caption}
</figcaption>
)}
</figure>
);
}
+16
View File
@@ -315,6 +315,22 @@ export function ChevronDownIcon(props: IconProps) {
);
}
export function ChevronLeftIcon(props: IconProps) {
return (
<Icon {...props}>
<path d="m15 18-6-6 6-6" />
</Icon>
);
}
export function ChevronRightIcon(props: IconProps) {
return (
<Icon {...props}>
<path d="m9 18 6-6-6-6" />
</Icon>
);
}
export function ExternalLinkIcon(props: IconProps) {
return (
<Icon {...props}>
+43 -18
View File
@@ -1,30 +1,34 @@
/**
* Sticky top navigation: logo + section anchors + blog link + language/theme toggles +
* GitHub. Desktop links share a sliding hover pill, while the selected section or route
* keeps its own active background; section links route through "/#id" so the Router
* and URL hash stay in sync while native smooth scrolling remains enabled. A disclosure
* menu covers small screens.
* keeps its own active background. On the home page the active section tracks the
* LIVE scroll position (scroll-spy), so the highlight follows as you scroll; on other
* routes it falls back to route state (e.g. Blog). Section links route through "/#id"
* so the URL hash stays in sync on click. A disclosure menu covers small screens.
*/
import { useState } from "react";
import { useRef, useState } from "react";
import type { MouseEvent } from "react";
import { Link, useLocation } from "react-router";
import { S } from "../lib/strings";
import { DOCS_URL, REPO_URL } from "../lib/links";
import { getActiveNavItem, SECTION_IDS } from "../lib/nav-state";
import type { ActiveNavItem, SectionId } from "../lib/nav-state";
import { useScrollSpy } from "../lib/use-scroll-spy";
import { GitHubIcon, MenuIcon, XIcon } from "./icons";
import { ThemeToggle } from "./theme-toggle";
import { LangToggle } from "./lang-toggle";
interface Indicator {
left: number;
width: number;
}
/** Stable empty list: keeps the spy idle away from the home page. */
const NO_IDS: readonly string[] = [];
export function Nav() {
const { pathname, hash } = useLocation();
const activeItem = getActiveNavItem(pathname, hash);
const onHome = pathname === "/";
const spied = useScrollSpy(onHome ? SECTION_IDS : NO_IDS) as SectionId | null;
const activeItem: ActiveNavItem = onHome ? spied : getActiveNavItem(pathname, hash);
const [open, setOpen] = useState(false);
const [indicator, setIndicator] = useState<Indicator | null>(null);
const pillRef = useRef<HTMLSpanElement | null>(null);
const pillVisible = useRef(false);
const sectionLabel: Record<(typeof SECTION_IDS)[number], string> = {
highlights: S.nav.highlights,
@@ -45,9 +49,33 @@ export function Nav() {
const deskLinkCls = (active: boolean) =>
`relative z-10 rounded-md px-2.5 py-1.5 text-sm transition-colors ${active ? activeLinkCls : deskInactiveLinkCls}`;
/**
* The hover pill appears IN PLACE under the first link it lands on (position
* jumps with only the fade animating), slides while moving between links, and
* fades out where it is on leave — never sweeping in from the nav's edge.
*/
const slideTo = (e: MouseEvent<HTMLElement>) => {
const el = e.currentTarget;
setIndicator({ left: el.offsetLeft, width: el.offsetWidth });
const pill = pillRef.current;
if (!pill) return;
if (!pillVisible.current) {
pill.style.transitionProperty = "opacity";
pill.style.left = `${el.offsetLeft}px`;
pill.style.width = `${el.offsetWidth}px`;
void pill.offsetWidth; // flush the jump before restoring the full transition
pill.style.transitionProperty = "";
pillVisible.current = true;
} else {
pill.style.left = `${el.offsetLeft}px`;
pill.style.width = `${el.offsetWidth}px`;
}
pill.style.opacity = "1";
};
const hidePill = () => {
const pill = pillRef.current;
if (pill) pill.style.opacity = "0";
pillVisible.current = false;
};
const desktopLinks = (
@@ -122,17 +150,14 @@ export function Nav() {
<nav
className="relative ml-4 hidden items-center gap-0.5 md:flex"
aria-label="Primary"
onMouseLeave={() => setIndicator(null)}
onMouseLeave={hidePill}
>
{/* Sliding hover pill: moves between links; active links retain their own background. */}
{/* Sliding hover pill: appears in place, slides between links, fades out in place; active links retain their own background. */}
<span
ref={pillRef}
aria-hidden="true"
className="absolute top-1/2 h-8 -translate-y-1/2 rounded-md bg-gray-100 transition-[left,width,opacity] duration-200 ease-out dark:bg-gray-800"
style={{
left: indicator?.left ?? 0,
width: indicator?.width ?? 0,
opacity: indicator ? 1 : 0,
}}
style={{ left: 0, width: 0, opacity: 0 }}
/>
{desktopLinks}
</nav>