release: 0.1.5 (#134)

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Yaowei ZhengandClaude Fable 5 authored and GitHub committed 2026-07-30 19:23:41 +08:00
1 parent ba0860f8d1
commit 6664edf890
44 files changed
+596 -151

No files matched your search

+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@prismshadow/penguin-cli",
"version": "0.1.4",
"version": "0.1.5",
"type": "module",
"description": "PenguinHarness CLI: interactive REPL and single-task runner over @prismshadow/penguin-core.",
"license": "Apache-2.0",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@prismshadow/penguin-core",
"version": "0.1.4",
"version": "0.1.5",
"type": "module",
"description": "PenguinHarness core SDK: context_engine, OmniMessage protocol, LLM/Environment interfaces.",
"license": "Apache-2.0",
+1 -1
View File
@@ -56,7 +56,7 @@ export { Agent, createAgent } from "./agent.js";
export type { CreateAgentOptions, CreateSessionOptions, ResumeSessionOptions } from "./agent.js";
/** SDK version number. */
export const VERSION = "0.1.4";
export const VERSION = "0.1.5";
/** Release build date (UTC yyyy-mm-dd), stamped by the release workflow next to VERSION; null in a dev/source build. */
export const BUILD_DATE: string | null = null;
// Version-string helpers (shared by the CLI's `penguin update` and the server's update check).
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@prismshadow/penguin-docs",
"version": "0.1.4",
"version": "0.1.5",
"private": true,
"type": "module",
"description": "PenguinHarness documentation site (React + Vite + Tailwind CSS): bilingual zh/en Markdown pages with light/dark themes and per-page Copy Markdown, deployed to GitHub Pages under /docs/ next to the landing page.",
@@ -0,0 +1,73 @@
---
title: "PenguinHarness 0.1.5: offline installs, richer input, and runs that recover"
date: 2026-07-30
category: news
excerpt: 0.1.5 widens where PenguinHarness runs and what it can take in. Five self-contained offline bundles install it on machines with no network at all; the composer attaches any file and images now reach steering and goal objectives; nearly every LLM failure recovers inside the run instead of killing the turn; and the built-in skills learned a second visual language plus the thinking and image message kinds. Here is what shipped.
---
PenguinHarness 0.1.5 is out. The release widens both ends of the pipe: where the harness can be installed — including machines that never touch the internet — and what you can hand it once it is running. In between, runs got sturdier: almost every LLM failure now recovers inside the run, and a mid-request Stop can no longer strand a Session. Feature by feature:
## Install with no network at all
Every GitHub Release now attaches **five self-contained offline bundles** — Linux and macOS in x64 and arm64, Windows in x64. Each bundle carries the program archive, its SHA256 checksum and the platform's own installer, so the whole flow is: download on any networked machine, copy to the target, run one command.
```bash
mkdir penguin-offline
tar -xzf penguin-linux-x64-offline.tar.gz -C penguin-offline
./penguin-offline/install.sh
```
On Windows, unzip `penguin-win32-x64-offline.zip` and double-click `install.cmd` — or run `.\install.ps1` in PowerShell. Offline installs verify the SHA256 unconditionally (there is no network to re-download from, so a corrupted archive must be caught, not tolerated), the POSIX bundles pass their payload to the installer explicitly rather than letting a script scan its directory, and a renamed archive still installs because platform packages now carry a target manifest inside.
The Windows package also grew a floor to stand on: it bundles MinGit under `git/`, so `exec_command` has a real bash even on a machine with no Git for Windows installed. Your own Git for Windows still wins when present — its MSYS userland is the fuller one — and the GPLv2 obligations are recorded in the new root `THIRD-PARTY-NOTICES.md`.
The install story around the bundles is redone to match: the README now spells out every method — Linux, macOS, Windows, npm, and the offline packages — each as a complete copy-paste block, and the landing page's install sections switch by OS and method instead of listing everything at once.
## Attach anything, steer with images
The Web composer can now attach **files of any type**, not just images. Attachments are written into the Session scratchpad and handed to the model as `[attached file: <path>]` lines — non-ASCII filenames preserved — so the model reads them with its normal file tools, and the same attachment flow works mid-run.
Images, meanwhile, now reach **every** input. Mid-run steering carries them (an image with no caption is a complete steering message by itself), and a goal objective accepts images as scratchpad paths — re-injected as text every round, they work on every model, vision or not.
The composer's `@` mention grew up into an `/agent` command, and both switch commands (`/agent`, `/model`) now stage their pick as a chip beside the text — cached with the draft, applied only when Enter sends, and session-only.
## Runs that recover instead of dying
The classifier separating transient from permanent LLM failures used to be an allowlist: a gateway that worded a transient fault its own way killed the turn. 0.1.5 inverts it — **every failure except a rejected credential now retries inside the run**, with the retry visible in both frontends as a live countdown, compaction retrying under its own shorter budget, and a recovered failure no longer reported to the operator as an incident.
Two companions to that: pressing Stop mid-request can no longer leave a Session running forever when a provider's stream neither yields nor rejects after the abort; and the Cost center's error table now pages back through the whole history, stops recording an ordinary non-zero exit (`grep` finding nothing) as an error, and labels environment-sourced entries `[env]`.
## Skills that design — and build — better apps
The built-in skill library took two big steps this release:
- **web-design** now carries a second complete visual language — an opt-in *paper editorial* theme with warm paper tones, system-serif display headings and mono micro-labels — next to the default GitHub-style simplicity, plus a "ship complete" contract: a one-line request is the whole spec, and every delivered page includes dark mode, loading/empty/error states, a working keyboard path and zero external requests, unasked. Chat interfaces gained recipes for a collapsible reasoning block and composer image attachments.
- **penguin-sdk** documents the message kinds modern models actually emit and accept: stream `partial_thinking` into its own collapsed channel, build image input with `imageUrlMessage` (with the config `vision` flag degrading gracefully through the project's `vision_model`), fix the output format in the persona instead of shipping a Markdown renderer, and bridge cross-language BM25 retrieval with an ingest-time bilingual keyword map.
On top of both, the Web App's draft page gained an end-to-end **agent tuning example** — create, benchmark and optimize an Agent through isolated CLI sessions — and its example prompts got shorter, because the skills now carry the knowledge the prompts used to spell out.
## Also in 0.1.5
- The default system prompt is about a tenth shorter (1087 → 969 words), pins replies to the user's language, and directs shared tooling into a per-Agent `shared_env/` directory. Existing Agents keep their own prompt.
- Navigation entries settle on one name each; the Workspace and Agents panels share one width and one open/closed lifetime; Project display names are editable; the draft page's examples became a fixed-height folder shelf.
- The chat header's elapsed chip survives reloads and counts in-flight events — anchored to the server's clock, so live and replayed views agree.
- Pasting CJK or emoji into `penguin chat` no longer corrupts characters torn across stdin chunks.
- Duration and byte formatting carry into the next unit instead of printing `1m60s` or `1024KB`.
- `PORT` / `HOST` no longer leak into commands the Agent runs, and the development backend moved to port 7368, out of the installed `penguin web`'s way.
- Docs catch up in three reference blocks: `run_subagent`'s `provider` argument, the gateway credential table, and the Project model entry's `max_tokens`.
## Install or upgrade
```bash
curl -fsSL https://penguin.ooo/install.sh | sh
penguin web
```
Windows (PowerShell):
```powershell
irm https://penguin.ooo/install.ps1 | iex
penguin web
```
Or `npm install -g @prismshadow/penguin-cli` with Node >= 24 — and from this release, fully offline from the [Release assets](https://github.com/Prism-Shadow/penguin-harness/releases). Full detail per change: [changelog/0.1.5](https://github.com/Prism-Shadow/penguin-harness/tree/main/changelog/0.1.5).
@@ -0,0 +1,73 @@
---
title: "PenguinHarness 0.1.5:离线安装、更丰富的输入与更稳的运行"
date: 2026-07-30
category: news
excerpt: 0.1.5 同时拓宽了 PenguinHarness 的两端:五个自包含离线安装包让它装进完全没有网络的机器;composer 可以附加任意类型的文件,图像也进入了中途引导与目标模式;几乎所有 LLM 故障都在运行内自动恢复而不再终止任务;内置技能学会了第二套视觉语言,并写清了 thinking 与图像这两类消息的用法。逐项说明如下。
---
PenguinHarness 0.1.5 发布了。这个版本把管道的两端同时拓宽:harness 能装在哪里——包括从不联网的机器;装好之后你能递给它什么。中间那段也更结实了:几乎所有 LLM 故障都在运行内自动恢复,请求中途按下 Stop 也不再可能让 Session 悬死。逐项来看:
## 完全离线也能安装
每个 GitHub Release 现在附带**五个自包含离线安装包**——Linux 与 macOS 各有 x64、arm64 两种架构,Windows 为 x64。包内自带程序压缩包、SHA256 校验文件与对应平台的安装器,完整流程就是:在任意有网机器下载、拷贝到目标机器、跑一条命令。
```bash
mkdir penguin-offline
tar -xzf penguin-linux-x64-offline.tar.gz -C penguin-offline
./penguin-offline/install.sh
```
Windows 上解压 `penguin-win32-x64-offline.zip` 后双击 `install.cmd` 即可——或在 PowerShell 里运行 `.\install.ps1`。离线安装无条件校验 SHA256(没有网络可以重新下载,损坏的包必须被拦下而不是被容忍);POSIX 离线包把安装载荷显式传给安装器,而不是让脚本扫描所在目录;平台包内置目标清单,改过名的压缩包也能正常安装。
Windows 安装包还多了一块「地板」:内置 MinGit(位于 `git/`),即使机器上没装 Git for Windows,`exec_command` 也有真正的 bash 可用。装了 Git for Windows 时仍然优先用你自己的——它的 MSYS 用户层更完整;GPLv2 义务记录在新增的根目录 `THIRD-PARTY-NOTICES.md` 中。
围绕离线包,安装文档也整体重做:README 把每种方式——Linux、macOS、Windows、npm、离线安装包——都写成可整段复制的完整代码块;官网落地页的安装区改为按系统与方式切换展示,不再全部平铺。
## 附加任意文件,用图像引导
Web composer 现在可以附加**任意类型的文件**,不再局限于图像。附件写入 Session 暂存目录,以 `[attached file: <path>]` 行交给模型——非 ASCII 文件名原样保留——模型用常规文件工具读取,运行中途同样可以附加。
图像则进入了**所有**输入通道:中途引导可以携带图像(不带文字说明的图像本身就是一条完整的引导消息);目标模式的 objective 以暂存目录路径的形式接受图像——每轮以文本重新注入,因此无论模型是否具备视觉能力都可用。
composer 的 `@` 提及升级为 `/agent` 命令,两个切换命令(`/agent`、`/model`)的选择都以纸片形式暂存在输入框旁——与草稿一同缓存,按下 Enter 发送时才生效,且仅作用于当前会话。
## 运行不再死于一次故障
区分暂时性与永久性 LLM 故障的分类器过去是一张允许名单:网关用自己的措辞描述一次暂时故障,任务就被判死。0.1.5 把它反了过来——**除凭证被拒外的所有故障都在运行内重试**,重试在两个前端都以倒计时可见,压缩请求在自己更短的预算内重试,恢复成功的故障也不再作为事故报给管理员。
两项配套:请求中途按 Stop,即使供应商的流在中止后既不产出也不报错,Session 也不再可能永远悬着;成本中心的错误表可以翻页回看全部历史,普通的非零退出(`grep` 没搜到)不再被记为错误,环境来源的条目标注为 `[env]`。
## 会设计、也会构建的技能库
内置技能库这个版本迈了两大步:
- **web-design** 在默认的 GitHub 风格简洁语言之外,新增第二套完整视觉语言——可选的「纸质编辑风」主题:暖纸色调、系统衬线大标题、等宽小标签;并立下「交付即成品」的约定:一句话请求就是完整规格,交付页面默认包含暗色模式、加载/空态/错误状态、完整键盘路径与零外部请求。聊天界面新增了可折叠思考块与 composer 图像附件的配方。
- **penguin-sdk** 写清了现代模型真正收发的消息类型:把 `partial_thinking` 流入独立的折叠通道,用 `imageUrlMessage` 构造图像输入(配置的 `vision` 标志经项目 `vision_model` 平滑降级),在 persona 里约定输出格式而不是在 UI 里搭 Markdown 渲染管线,并用 ingest 时生成的双语关键词映射弥合跨语言 BM25 检索。
在这之上,Web 应用的草稿页新增了端到端的 **Agent 调优示例**——通过相互隔离的 CLI 会话创建、评测并优化一个 Agent;示例提示词也变短了,因为过去要在提示词里写明的知识,现在由技能承载。
## 0.1.5 还有这些
- 默认系统提示词缩短约十分之一(1087 → 969 词),回复语言锁定用户语言,共享工具装入 Agent 级 `shared_env/` 目录。已有 Agent 保持自己的提示词不变。
- 导航条目名称统一;Workspace 与 Agents 面板共用宽度与开合状态;Project 显示名可编辑;草稿页示例改为定高折叠书架。
- 会话头部的耗时纸片在刷新后不再归零,并统计在途事件——锚定服务器时钟,直播与回放一致。
- 向 `penguin chat` 粘贴中文或 emoji 不再因 stdin 分块撕裂字符而出现乱码。
- 时长与字节的缩写会正确进位,不再出现 `1m60s` 或 `1024KB`。
- `PORT` / `HOST` 不再泄漏进 Agent 运行的命令;开发后端移至 7368 端口,与已安装的 `penguin web` 互不干扰。
- 文档补齐三处参考:`run_subagent` 的 `provider` 参数、网关凭证表、Project 模型条目的 `max_tokens`。
## 安装或升级
```bash
curl -fsSL https://penguin.ooo/install.sh | sh
penguin web
```
Windows(PowerShell):
```powershell
irm https://penguin.ooo/install.ps1 | iex
penguin web
```
也可以在 Node >= 24 环境 `npm install -g @prismshadow/penguin-cli`——从这个版本起,还可以用 [Release 附件](https://github.com/Prism-Shadow/penguin-harness/releases)完全离线安装。每项改动的完整细节见 [changelog/0.1.5](https://github.com/Prism-Shadow/penguin-harness/tree/main/changelog/0.1.5)。
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@prismshadow/penguin-landing",
"version": "0.1.4",
"version": "0.1.5",
"private": true,
"type": "module",
"description": "PenguinHarness landing page (React + Vite + Tailwind CSS): multilingual, light/dark themes, benchmark showcase, and a local Markdown blog, deployed to GitHub Pages via GitHub Actions.",
@@ -20,9 +20,7 @@ const ROTATE_MS = 6000;
* freshest news goes at the front.
*/
const ITEMS = [
{ key: "freeModels", to: "/blog/free-models-in-penguin-harness" },
{ key: "gemini", to: "/blog/gemini-3-6-in-penguinharness" },
{ key: "models", to: "/blog/introducing-penguinharness" },
{ key: "k3AndFree", to: "/blog/free-models-in-penguin-harness" },
{ key: "fireworks", to: "/blog/fireworks-credits-amd" },
] as const;
@@ -34,9 +32,7 @@ function prefersReducedMotion(): boolean {
export function AnnouncementBar() {
const texts: Record<(typeof ITEMS)[number]["key"], string> = {
freeModels: S.announcement.freeModels,
gemini: S.announcement.gemini,
models: S.announcement.models,
k3AndFree: S.announcement.k3AndFree,
fireworks: S.announcement.fireworks,
};
// pos runs 0..ITEMS.length where ITEMS.length is the clone of slide 0.
+18
View File
@@ -24,6 +24,24 @@ export const INSTALL_CMD = "curl -fsSL https://penguin.ooo/install.sh | sh";
*/
export const INSTALL_CMD_WINDOWS = "irm https://penguin.ooo/install.ps1 | iex";
/**
* Offline-bundle install commands, per OS tab of the install switcher. Each Release
* attaches five self-contained bundles (see scripts/package-offline-bundles.sh); the
* commands show the most common architecture — the localized hint strings name the
* alternative archive. Language-neutral, like the one-liners above.
*/
export const OFFLINE_INSTALL_CMDS: Record<"linux" | "macos" | "windows", string> = {
linux: `mkdir penguin-offline
tar -xzf penguin-linux-x64-offline.tar.gz -C penguin-offline
./penguin-offline/install.sh`,
macos: `mkdir penguin-offline
tar -xzf penguin-darwin-arm64-offline.tar.gz -C penguin-offline
./penguin-offline/install.sh`,
windows: `Expand-Archive penguin-win32-x64-offline.zip -DestinationPath penguin-offline
cd penguin-offline
.\\install.cmd`,
};
/**
* Heavy marketing media lives in the sibling `penguin-harness-community` repo rather than
* in this one, so that assets only the landing site ever renders stay out of the clone of
+25 -16
View File
@@ -12,9 +12,7 @@ export const en: Strings = {
label: "Announcements",
prev: "Previous announcement",
next: "Next announcement",
freeModels: "Free models Ling 3.0 Flash and the Free Models Router are now in PenguinHarness",
gemini: "Gemini 3.6 Flash is now available in PenguinHarness",
models: "Kimi K3 and Qwen 3.8 Max are now available in PenguinHarness",
k3AndFree: "Kimi K3 and free models like Ling 3.0 Flash are now available in PenguinHarness",
fireworks: "Claim $50 in Fireworks API credits with the AMD Developer Program",
},
@@ -48,20 +46,16 @@ export const en: Strings = {
},
hero: {
badge: "Agents building agents",
// The trailing space lives in the PREFIX (a breakable space before the nowrap
// span) — inside titleNoWrap it would glue "Builder" to the unbreakable chunk.
titlePrefix: "Your Automated Agent Builder ",
titleNoWrap: "Lives on Your ",
// span) — inside titleNoWrap it would glue "Builder," to the unbreakable chunk.
titlePrefix: "Your Automated Agent Builder, ",
titleNoWrap: "Right on Your ",
titleWords: ["Desktop", "Server"],
titleSuffix: "",
keywords: ["Lightweight", "Efficient", "Open Source"],
subtitle: "Create Self-Evolving Agents in One Click",
ctaPrimary: "Get started",
ctaGithub: "GitHub",
installHint:
"One-line install (Linux / macOS / Windows, bundled Node runtime — unpack and run)",
installLabelPosix: "Linux / macOS",
installLabelWindows: "Windows",
installHint: "One-line install (bundled Node runtime — unpack and run)",
stats: [
{ value: "1000+", label: "supported models" },
{ value: "1×CPU", label: "minimum footprint" },
@@ -70,6 +64,23 @@ export const en: Strings = {
],
},
install: {
linux: "Linux",
macos: "macOS",
windows: "Windows",
online: "Online install",
offline: "Offline package",
offlineNote:
"Every GitHub Release attaches five self-contained offline bundles (Linux / macOS in x64 and arm64, Windows in x64), each carrying the program archive, its SHA256 checksum and the installer: download on a networked machine, copy to the target, and install with no network at all.",
offlineHints: {
linux: "On arm64 machines, use penguin-linux-arm64-offline.tar.gz.",
macos:
"Apple silicon uses the arm64 bundle; on Intel, use penguin-darwin-x64-offline.tar.gz.",
windows: "After unzipping you can also just double-click install.cmd.",
},
offlineRelease: "Download offline packages from GitHub Releases",
},
copy: {
copy: "Copy",
copied: "Copied",
@@ -105,7 +116,7 @@ export const en: Strings = {
compare: {
eyebrow: "vs. LangChain",
title: "The gap between 1× and 100×",
title: "Building Agents with Agents",
subtitle: [
"With LangChain, you build agents by hand — at 1× speed.",
"With PenguinHarness, agents build agents — at 100×.",
@@ -157,9 +168,7 @@ export const en: Strings = {
"Install with one command and let the Agent work from a desktop-grade interface — all data stays in your local ~/.penguin/data directory.",
step1: "Install",
step1Desc:
"Linux / macOS / Windows with a bundled Node runtime — unpack and run; upgrades never touch your data.",
installLabelPosix: "Linux / macOS",
installLabelWindows: "Windows (PowerShell)",
"Pick your OS and method — the online one-liner, or an offline package; the bundle carries its own Node runtime, and upgrades never touch your data.",
tabWeb: "Web UI",
tabCli: "CLI",
webStep2: "Open the web interface",
+28 -17
View File
@@ -13,9 +13,7 @@ export const zh = {
label: "公告",
prev: "上一条公告",
next: "下一条公告",
freeModels: "Ling 3.0 Flash 与 Free Models Router 等免费模型现已在 PenguinHarness 可用",
gemini: "Gemini 3.6 Flash 现已在 PenguinHarness 可用",
models: "Kimi K3 与 Qwen 3.8 Max 模型现已在 PenguinHarness 可用",
k3AndFree: "Kimi K3 与 Ling 3.0 Flash 等免费模型现已在 PenguinHarness 可用",
fireworks: "携手 AMD 开发者计划:$50 Fireworks API 额度免费领取中",
},
@@ -49,23 +47,20 @@ export const zh = {
},
hero: {
badge: "让 Agent 为你构建 Agent",
/**
* Headline: {titlePrefix}<nowrap>{titleNoWrap}{word}{titleSuffix}</nowrap>, the word
* rotating through titleWords (桌面/服务器 — where the builder lives; the README
* states Desktop only). The nowrap span pins the wrap point in front of it, so a
* break never splits "Agent Builder" / "Lives on Your Desktop" mid-phrase.
* rotating through titleWords (桌面/服务器 — where the builder runs; the README
* writes the pair as "桌面 / 服务器"). The nowrap span pins the wrap point in front
* of it, so a break never splits "Agent Builder" / "Right on Your Desktop" mid-phrase.
*/
titlePrefix: "全自动 Agent 构建器,",
titleNoWrap: "住在你的",
titlePrefix: "全自动 Agent 构建平台,",
titleNoWrap: "运行在你的",
titleWords: ["桌面", "服务器"],
titleSuffix: "上",
keywords: ["轻量", "高效", "开源"],
subtitle: "一键创建自进化 Agent",
ctaPrimary: "快速开始",
ctaGithub: "GitHub",
installHint: "一行命令安装(Linux / macOS / Windows,内嵌 Node 运行时,解压即用)",
installLabelPosix: "Linux / macOS",
installLabelWindows: "Windows",
installHint: "一行命令安装(内嵌 Node 运行时,解压即用)",
stats: [
{ value: "1000+", label: "支持模型数量" },
{ value: "1×CPU", label: "最低运行配置" },
@@ -74,6 +69,23 @@ export const zh = {
],
},
/** Install-method switcher (hero + quick start): OS tabs, online/offline methods. */
install: {
linux: "Linux",
macos: "macOS",
windows: "Windows",
online: "在线安装",
offline: "离线安装包",
offlineNote:
"每个 GitHub Release 附带五个自包含离线安装包(Linux / macOS 各 x64 与 arm64,Windows 为 x64),内含程序包、SHA256 校验文件与安装器:在有网机器下载,拷贝到目标机器安装,全程无需联网。",
offlineHints: {
linux: "arm64 机器换用 penguin-linux-arm64-offline.tar.gz。",
macos: "Apple 芯片用 arm64 包,Intel 芯片换用 penguin-darwin-x64-offline.tar.gz。",
windows: "解压后也可直接双击 install.cmd 完成安装。",
},
offlineRelease: "前往 GitHub Releases 下载离线安装包",
},
copy: {
copy: "复制",
copied: "已复制",
@@ -108,7 +120,7 @@ export const zh = {
compare: {
eyebrow: "对比 LangChain",
title: "1× 与 100× 的差距",
title: "用 Agent 构建 Agent",
subtitle: [
"使用 LangChain,以 1 倍速度人工构建 Agent;",
"使用 PenguinHarness,以 100 倍速度用 Agent 构建 Agent。",
@@ -158,9 +170,8 @@ export const zh = {
subtitle:
"一行命令安装,打开桌面级界面即可让 Agent 开始工作;数据全部保存在本地 ~/.penguin/data 目录。",
step1: "安装",
step1Desc: "Linux / macOS / Windows,产物内嵌 Node 运行时,解压即用;升级与重装不触碰数据。",
installLabelPosix: "Linux / macOS",
installLabelWindows: "Windows(PowerShell)",
step1Desc:
"选择你的系统与安装方式:在线一行命令,或离线安装包;产物内嵌 Node 运行时,解压即用,升级与重装不触碰数据。",
tabWeb: "Web 界面",
tabCli: "命令行",
webStep2: "启动 Web 界面",
+59 -50
View File
@@ -1,10 +1,10 @@
/**
* Hero: enlarged logo + product name, the slogan inside the pill badge, then the
* one-line headline whose rotating word crossfades through a gaussian blur
* (Desktop <-> Server, localized per dictionary), three keywords, the install one-liner
* and stats. The rotating word is a stacked inline-grid so line width never jumps.
* Hero: enlarged logo + product name, the one-line headline whose rotating word
* crossfades through a gaussian blur (Desktop <-> Server, localized per dictionary),
* the one-sentence subtitle, the install one-liner behind an OS switcher, and stats.
* The rotating word is a stacked inline-grid so line width never jumps.
*/
import { Fragment, useEffect, useState } from "react";
import { useEffect, useState } from "react";
import { Link } from "react-router";
import { S } from "../lib/strings";
import { INSTALL_CMD, INSTALL_CMD_WINDOWS, REPO_URL } from "../lib/links";
@@ -13,6 +13,54 @@ import { ArrowRightIcon, GitHubIcon } from "../components/icons";
const ROTATE_MS = 2600;
type InstallOs = "linux" | "macos" | "windows";
/**
* Install one-liner behind an OS tab row: one command visible at a time (Linux and
* macOS share the POSIX one-liner; the tabs still name them apart so nobody has to
* know that). Prompt char follows the shell: $ for POSIX, > for PowerShell.
*/
function InstallBox() {
const [os, setOs] = useState<InstallOs>("linux");
const cmd = os === "windows" ? INSTALL_CMD_WINDOWS : INSTALL_CMD;
const osBtn = (active: boolean) =>
`rounded-md px-2.5 py-1 text-xs font-medium transition-colors ${
active
? "bg-white text-gray-900 shadow-sm dark:bg-gray-700 dark:text-gray-100"
: "text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200"
}`;
return (
<div className="overflow-hidden rounded-xl border border-gray-200 bg-gray-50 text-left dark:border-gray-800 dark:bg-gray-900">
<div
className="flex items-center gap-1 border-b border-gray-200 bg-gray-100/70 px-2 py-1.5 dark:border-gray-800 dark:bg-gray-800/40"
role="tablist"
>
{(["linux", "macos", "windows"] as const).map((key) => (
<button
key={key}
type="button"
role="tab"
aria-selected={os === key}
className={osBtn(os === key)}
onClick={() => setOs(key)}
>
{S.install[key]}
</button>
))}
</div>
<div className="flex items-center gap-3 py-2.5 pr-2.5 pl-4">
<code className="min-w-0 flex-1 overflow-x-auto font-mono text-[13px] whitespace-nowrap text-gray-800 dark:text-gray-200">
<span className="mr-2 text-gray-400 select-none dark:text-gray-500">
{os === "windows" ? ">" : "$"}
</span>
{cmd}
</code>
<CopyButton text={cmd} className="shrink-0" />
</div>
</div>
);
}
function RotatingWord({ words }: { words: string[] }) {
const [active, setActive] = useState(0);
useEffect(() => {
@@ -50,16 +98,9 @@ export function Hero() {
/>
<span className="text-3xl font-semibold tracking-tight sm:text-4xl">{S.siteName}</span>
</div>
<p
className="anim-rise mx-auto mt-6 inline-flex items-center gap-2 rounded-full border border-gray-200 bg-white px-3.5 py-1 text-[13px] text-gray-600 dark:border-gray-800 dark:bg-gray-900 dark:text-gray-400"
style={{ animationDelay: "40ms" }}
>
<span className="h-1.5 w-1.5 rounded-full bg-brand-500" aria-hidden="true" />
{S.hero.badge}
</p>
{/* No text-balance: balance may break inside the breakable prefix ("…Agent /
Builder…"); greedy wrapping + the nowrap span pins the desktop break to
"Your Automated Agent Builder / Lives on Your Desktop". */}
"Your Automated Agent Builder, / Right on Your Desktop". */}
<h1
className="anim-rise mx-auto mt-6 max-w-full text-3xl font-semibold tracking-tight sm:text-5xl"
style={{ animationDelay: "80ms" }}
@@ -72,22 +113,12 @@ export function Hero() {
</span>
</h1>
<div
className="anim-rise mt-6 flex items-center justify-center gap-4 text-base font-medium text-gray-600 sm:text-lg dark:text-gray-300"
<p
className="anim-rise mt-6 text-base font-medium text-gray-600 sm:text-lg dark:text-gray-300"
style={{ animationDelay: "140ms" }}
>
{S.hero.keywords.map((keyword, i) => (
<Fragment key={keyword}>
{i > 0 && (
<span
className="h-1 w-1 rounded-full bg-gray-300 dark:bg-gray-600"
aria-hidden="true"
/>
)}
<span>{keyword}</span>
</Fragment>
))}
</div>
{S.hero.subtitle}
</p>
<div
className="anim-rise mt-8 flex flex-wrap items-center justify-center gap-3"
@@ -115,29 +146,7 @@ export function Hero() {
className="anim-rise mx-auto mt-10 w-fit max-w-full"
style={{ animationDelay: "240ms" }}
>
{/* One install row per OS (labels are language-neutral; prompt chars $ vs > match the shells). */}
<div className="overflow-hidden rounded-xl border border-gray-200 bg-gray-50 text-left dark:border-gray-800 dark:bg-gray-900">
<div className="flex items-center gap-3 py-2.5 pr-2.5 pl-4">
<span className="w-24 shrink-0 text-[11px] font-medium text-gray-400 dark:text-gray-500">
{S.hero.installLabelPosix}
</span>
<code className="min-w-0 flex-1 overflow-x-auto font-mono text-[13px] whitespace-nowrap text-gray-800 dark:text-gray-200">
<span className="mr-2 text-gray-400 select-none dark:text-gray-500">$</span>
{INSTALL_CMD}
</code>
<CopyButton text={INSTALL_CMD} className="shrink-0" />
</div>
<div className="flex items-center gap-3 border-t border-gray-200 py-2.5 pr-2.5 pl-4 dark:border-gray-800">
<span className="w-24 shrink-0 text-[11px] font-medium text-gray-400 dark:text-gray-500">
{S.hero.installLabelWindows}
</span>
<code className="min-w-0 flex-1 overflow-x-auto font-mono text-[13px] whitespace-nowrap text-gray-800 dark:text-gray-200">
<span className="mr-2 text-gray-400 select-none dark:text-gray-500">&gt;</span>
{INSTALL_CMD_WINDOWS}
</code>
<CopyButton text={INSTALL_CMD_WINDOWS} className="shrink-0" />
</div>
</div>
<InstallBox />
<p className="mt-2 text-xs text-gray-500 dark:text-gray-400">{S.hero.installHint}</p>
</div>
+69 -6
View File
@@ -11,7 +11,9 @@ import {
DEEPSEEK_KEYS_URL,
INSTALL_CMD,
INSTALL_CMD_WINDOWS,
OFFLINE_INSTALL_CMDS,
OPENROUTER_KEYS_URL,
RELEASES_URL,
} from "../lib/links";
import { Section } from "../components/section";
import { CodeCard } from "../components/code-card";
@@ -75,6 +77,8 @@ function KeyLinks() {
export function Quickstart() {
const [mode, setMode] = useState<"web" | "cli">("web");
const [provider, setProvider] = useState<"deepseek" | "openrouter">("deepseek");
const [os, setOs] = useState<"linux" | "macos" | "windows">("linux");
const [method, setMethod] = useState<"online" | "offline">("online");
const modeBtn = (active: boolean) =>
`inline-flex items-center gap-1.5 rounded-md px-3.5 py-1.5 text-sm font-medium transition-colors ${
@@ -130,12 +134,71 @@ export function Quickstart() {
title={S.quickstart.step1}
desc={S.quickstart.step1Desc}
>
<CodeCard code={INSTALL_CMD} label={S.quickstart.installLabelPosix} />
<CodeCard
code={INSTALL_CMD_WINDOWS}
label={S.quickstart.installLabelWindows}
className="mt-2"
/>
{/* Two switch rows — OS, then online/offline — so exactly one method shows at a time. */}
<div className="mb-2 flex flex-wrap gap-2">
<div
className="inline-flex gap-1 rounded-lg border border-gray-200 bg-white p-1 dark:border-gray-800 dark:bg-gray-900"
role="tablist"
>
{(["linux", "macos", "windows"] as const).map((key) => (
<button
key={key}
type="button"
role="tab"
aria-selected={os === key}
className={providerBtn(os === key)}
onClick={() => setOs(key)}
>
{S.install[key]}
</button>
))}
</div>
<div
className="inline-flex gap-1 rounded-lg border border-gray-200 bg-white p-1 dark:border-gray-800 dark:bg-gray-900"
role="tablist"
>
{(["online", "offline"] as const).map((key) => (
<button
key={key}
type="button"
role="tab"
aria-selected={method === key}
className={providerBtn(method === key)}
onClick={() => setMethod(key)}
>
{S.install[key]}
</button>
))}
</div>
</div>
{method === "online" ? (
<CodeCard
code={os === "windows" ? INSTALL_CMD_WINDOWS : INSTALL_CMD}
label={os === "windows" ? "PowerShell" : "curl | sh"}
/>
) : (
<>
<p className="mb-2 text-xs leading-5 text-gray-500 dark:text-gray-400">
{S.install.offlineNote}
</p>
<CodeCard
code={OFFLINE_INSTALL_CMDS[os]}
label={os === "windows" ? "PowerShell" : "shell"}
/>
<p className="mt-2 text-xs leading-5 text-gray-500 dark:text-gray-400">
{S.install.offlineHints[os]}{" "}
<a
href={RELEASES_URL}
target="_blank"
rel="noreferrer"
className="inline-flex items-center gap-1 text-brand-700 underline decoration-brand-300 underline-offset-2 transition-colors hover:text-brand-600 dark:text-brand-300 dark:decoration-brand-700"
>
{S.install.offlineRelease}
<ExternalLinkIcon className="h-3 w-3" />
</a>
</p>
</>
)}
</Step>
{mode === "web" ? (
+2 -1
View File
@@ -96,7 +96,7 @@ describe("frontmatter mapping (author / pinned / category)", () => {
it("reads the pinned flag and sorts the pinned post first", () => {
for (const locale of ["en", "zh"] as const) {
const posts = postsFor(locale);
expect(posts.length).toBe(12);
expect(posts.length).toBe(13);
// The launch post stays the single pinned post; newer posts sort under it by date.
expect(posts.filter((p) => p.pinned).map((p) => p.slug)).toEqual([
"introducing-penguinharness",
@@ -120,6 +120,7 @@ describe("frontmatter mapping (author / pinned / category)", () => {
it("filters by the news category, newest first", () => {
expect(postsFor("en", "news").map((p) => p.slug)).toEqual([
"introducing-penguinharness",
"penguinharness-0-1-5",
"penguinharness-0-1-4",
"free-models-in-penguin-harness",
"gemini-3-6-in-penguinharness",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@prismshadow/penguin-server",
"version": "0.1.4",
"version": "0.1.5",
"type": "module",
"description": "PenguinHarness web server: multi-user authentication and authorization, Session execution with SSE streaming, and usage accounting, built on @prismshadow/penguin-core.",
"license": "Apache-2.0",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@prismshadow/penguin-skills",
"version": "0.1.4",
"version": "0.1.5",
"type": "module",
"description": "PenguinHarness skill library: built-in SKILL.md documents and skill groups, decoupled from core.",
"license": "Apache-2.0",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@prismshadow/penguin-web",
"version": "0.1.4",
"version": "0.1.5",
"private": true,
"type": "module",
"description": "PenguinHarness web frontend SPA: React + Vite + Tailwind CSS, rendering conversations streamed over the OmniMessage protocol, plus Agent configuration, model configuration, usage stats, and Traces.",
@@ -30,7 +30,7 @@ const WIDTH_STORAGE_KEY = "penguin.panelWidth";
*
* REMOVAL: delete this constant and storedWidth()'s legacy branch in the release AFTER the one
* shipping this change, as part of preparing that release. Nothing else in the repo reads these
* keys, so it is a pure deletion. See changelog/unreleased/2026-07-30-backward-compatibility.md.
* keys, so it is a pure deletion. See changelog/0.1.5/2026-07-30-backward-compatibility.md.
*/
const LEGACY_WIDTH_KEYS = ["penguin.filesPanelWidth", "penguin.subagentsPanelWidth"] as const;