Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions .github/release-notes/v1.8.0.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
## ✨ Highlights

- **New agent: Oh My Pi** — HarnessKit now manages [Oh My Pi](https://omp.sh) (`omp`) as its 11th agent: skills, MCP servers, rules, and slash commands at both user and project scope, plus its TypeScript extension modules as plugins.

<!-- lang:zh
## ✨ 更新亮点

- **新增 Agent:Oh My Pi** — HarnessKit 现已支持 [Oh My Pi](https://omp.sh)(`omp`),第 11 个受管 Agent:用户级与项目级的 Skills、MCP servers、Rules 和斜杠命令,以及其 TypeScript 扩展模块(作为 Plugins 管理)。
-->

---
19 changes: 17 additions & 2 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ jobs:
outputs:
release_id: ${{ steps.create.outputs.release_id }}
changelog: ${{ steps.changelog.outputs.body }}
# Desktop (latest.json) variant: translation comment blocks converted to
# legacy language fences so clients ≤1.7.0 still localize the updater
# dialog. Identical to `changelog` when the notes have no comment block.
changelog_desktop: ${{ steps.changelog.outputs.body_desktop }}
steps:
- uses: actions/checkout@v4
with:
Expand Down Expand Up @@ -53,6 +57,15 @@ jobs:
echo "$BODY"
echo "CHANGELOG_EOF"
} >> "$GITHUB_OUTPUT"
# Desktop variant for latest.json: convert translation comment
# blocks into legacy fences (see scripts/notes-desktop-variant.mjs).
echo "$BODY" > /tmp/release-body.md
BODY_DESKTOP=$(node scripts/notes-desktop-variant.mjs /tmp/release-body.md)
{
echo "body_desktop<<CHANGELOG_EOF"
echo "$BODY_DESKTOP"
echo "CHANGELOG_EOF"
} >> "$GITHUB_OUTPUT"

- name: Create draft release
id: create
Expand Down Expand Up @@ -143,8 +156,10 @@ jobs:
with:
releaseId: ${{ needs.create-release.outputs.release_id }}
# Required so tauri-action populates `latest.json`'s `notes` field;
# without it the in-app updater shows an empty changelog.
releaseBody: ${{ needs.create-release.outputs.changelog }}
# without it the in-app updater shows an empty changelog. Uses the
# fence-format desktop variant so ≤1.7.0 clients localize it too —
# the GitHub release body keeps the comment-block format instead.
releaseBody: ${{ needs.create-release.outputs.changelog_desktop }}
tauriScript: cargo tauri
args: --target ${{ matrix.target }}

Expand Down
8 changes: 4 additions & 4 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ members = ["crates/hk-core", "crates/hk-cli", "crates/hk-desktop", "crates/hk-we
resolver = "2"

[workspace.package]
version = "1.7.0"
version = "1.8.0"
edition = "2024"
license = "Apache-2.0"

Expand Down
2 changes: 1 addition & 1 deletion crates/hk-desktop/tauri.conf.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"productName": "HarnessKit",
"version": "1.7.0",
"version": "1.8.0",
"identifier": "com.harnesskit.app",
"build": {
"frontendDist": "../../dist",
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "harnesskit-frontend",
"private": true,
"version": "1.7.0",
"version": "1.8.0",
"description": "Cross-platform extension management for AI coding agents",
"license": "Apache-2.0",
"type": "module",
Expand Down
66 changes: 66 additions & 0 deletions scripts/notes-desktop-variant.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
#!/usr/bin/env node
// Build the desktop (latest.json) variant of the release notes.
//
// The canonical release body keeps translations inside HTML comment blocks
// (`<!-- lang:zh ... -->`) so the GitHub release page renders English only.
// Desktop clients ≤1.7.0 only understand the legacy fence format
// (`<!-- lang:en -->` / `<!-- lang:zh -->`), so for latest.json this script
// converts comment blocks back into fences:
//
// <!-- lang:en -->
// <plain-text notes + What's Changed>
// <!-- lang:zh -->
// <translation>
//
// The translation section goes LAST and the "## New Contributors" /
// "**Full Changelog**" tails are dropped: old cleanChangelog skips from those
// lines until the next `## ` heading, which would swallow a following
// `<!-- lang:xx -->` fence and concatenate both languages (the v1.7.0 bug).
// The updater dialog hides those sections anyway.
//
// Usage: node scripts/notes-desktop-variant.mjs <body-file>
// Reads the full release body, writes the desktop variant to stdout.
// Bodies without a comment block pass through unchanged.

import { readFileSync } from "node:fs";

const LANG_COMMENT_BLOCK = /<!--\s*lang:([a-z-]+)[ \t]*\n([\s\S]*?)-->/gi;

const body = readFileSync(process.argv[2], "utf8");

const translations = [];
let remainder = body
.replace(LANG_COMMENT_BLOCK, (_match, code, content) => {
translations.push({ code: code.toLowerCase(), content: content.trim() });
return "";
})
.trim();

if (translations.length === 0) {
process.stdout.write(body.trim() + "\n");
process.exit(0);
}

// Drop the tail sections that cleanChangelog would hide anyway — they must
// not precede a fence (see header comment).
const lines = [];
let skip = false;
for (const line of remainder.split("\n")) {
if (
line.startsWith("## New Contributors") ||
line.startsWith("**Full Changelog**")
) {
skip = true;
continue;
}
if (skip && line.startsWith("## ")) skip = false;
if (skip) continue;
lines.push(line);
}
remainder = lines.join("\n").trim();

const parts = [`<!-- lang:en -->\n${remainder}`];
for (const { code, content } of translations) {
parts.push(`<!-- lang:${code} -->\n${content}`);
}
process.stdout.write(parts.join("\n\n") + "\n");
41 changes: 41 additions & 0 deletions src/lib/i18n/__tests__/changelog.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,44 @@ describe("localizeChangelog", () => {
);
});
});

const COMMENT_BLOCK = `## What's new
English line

<!-- lang:zh
## 更新内容
中文行
-->

## What's Changed
* some PR`;

describe("localizeChangelog comment blocks", () => {
it("extracts the block for zh and drops the plain text", () => {
const zh = localizeChangelog(COMMENT_BLOCK, "zh");
expect(zh).toContain("中文行");
expect(zh).not.toContain("English line");
});

it("treats the un-commented plain text as the English section", () => {
const en = localizeChangelog(COMMENT_BLOCK, "en");
expect(en).toContain("English line");
expect(en).toContain("What's Changed");
expect(en).not.toContain("中文行");
expect(en).not.toContain("<!--");
});

it("falls back to the plain text for unsupported languages", () => {
expect(localizeChangelog(COMMENT_BLOCK, "fr")).toContain("English line");
});

it("normalizes regional codes for blocks", () => {
expect(localizeChangelog(COMMENT_BLOCK, "zh-CN")).toContain("中文行");
});

it("does not mistake legacy fences for comment blocks", () => {
const zh = localizeChangelog(BILINGUAL, "zh");
expect(zh).toContain("中文行");
expect(zh).not.toContain("English line");
});
});
66 changes: 50 additions & 16 deletions src/lib/i18n/changelog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,35 +3,69 @@ import { mapLocaleToSupportedLanguage } from "./index";
// Matches a language fence like `<!-- lang:en -->` / `<!-- lang:zh -->`.
const LANG_FENCE = /<!--\s*lang:([a-z-]+)\s*-->/gi;

// Matches a language comment BLOCK: the whole translation lives inside one
// HTML comment, e.g. `<!-- lang:zh\n## 更新内容 ...\n-->`. The newline after
// the language code is what distinguishes a block from a legacy fence (whose
// `-->` closes on the same line), so the two formats can't shadow each other.
const LANG_COMMENT_BLOCK = /<!--\s*lang:([a-z-]+)[ \t]*\n([\s\S]*?)-->/gi;

/**
* Pick the section of a changelog body matching `language`.
*
* Release notes can be authored bilingually by fencing each language:
* Preferred authoring format — English as plain text, translations inside
* comment blocks, so GitHub's release page (which doesn't render comments)
* shows only English while clients localize from the same body:
*
* ## What's new ...
* <!-- lang:zh
* ## 更新内容 ...
* -->
*
* Clients without comment-block support (≤1.7.0) don't display comments in
* rendered markdown, so old versions degrade gracefully to English.
*
* The legacy fence format is still supported:
*
* <!-- lang:en -->
* ## What's new ...
* <!-- lang:zh -->
* ## 更新内容 ...
*
* Returns the section for the active language, falling back to English, then to
* the first section present. Notes without any fence are returned unchanged, so
* single-language releases keep working.
* the first section present. Notes without any fence or block are returned
* unchanged, so single-language releases keep working.
*/
export function localizeChangelog(body: string, language: string): string {
const fences = [...body.matchAll(LANG_FENCE)];
if (fences.length === 0) return body.trim();

const sections: Record<string, string> = {};
fences.forEach((fence, i) => {
const start = (fence.index ?? 0) + fence[0].length;
const end =
i + 1 < fences.length
? (fences[i + 1].index ?? body.length)
: body.length;
const key =
mapLocaleToSupportedLanguage(fence[1]) ?? fence[1].toLowerCase();
sections[key] = body.slice(start, end).trim();
});

// Pass 1: pull out comment-block translations; what remains is plain text.
const remainder = body
.replace(LANG_COMMENT_BLOCK, (_match, code: string, content: string) => {
const key = mapLocaleToSupportedLanguage(code) ?? code.toLowerCase();
sections[key] = content.trim();
return "";
})
.trim();

// Pass 2: legacy fences split the remaining text into per-language sections.
const fences = [...remainder.matchAll(LANG_FENCE)];
if (fences.length > 0) {
fences.forEach((fence, i) => {
const start = (fence.index ?? 0) + fence[0].length;
const end =
i + 1 < fences.length
? (fences[i + 1].index ?? remainder.length)
: remainder.length;
const key =
mapLocaleToSupportedLanguage(fence[1]) ?? fence[1].toLowerCase();
sections[key] = remainder.slice(start, end).trim();
});
} else if (remainder && !sections.en) {
// Comment-block format: the un-commented plain text IS the English section.
sections.en = remainder;
}

if (Object.keys(sections).length === 0) return body.trim();

const lang = mapLocaleToSupportedLanguage(language) ?? "en";
return (
Expand Down
Loading