diff --git a/.github/release-notes/v1.8.0.md b/.github/release-notes/v1.8.0.md new file mode 100644 index 00000000..7ceab862 --- /dev/null +++ b/.github/release-notes/v1.8.0.md @@ -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. + + + +--- diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a2996398..d0299ebb 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -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: @@ -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<> "$GITHUB_OUTPUT" - name: Create draft release id: create @@ -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 }} diff --git a/Cargo.lock b/Cargo.lock index 28451f18..28340c11 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1923,7 +1923,7 @@ checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" [[package]] name = "hk-cli" -version = "1.7.0" +version = "1.8.0" dependencies = [ "anyhow", "clap", @@ -1941,7 +1941,7 @@ dependencies = [ [[package]] name = "hk-core" -version = "1.7.0" +version = "1.8.0" dependencies = [ "anyhow", "chrono", @@ -1971,7 +1971,7 @@ dependencies = [ [[package]] name = "hk-desktop" -version = "1.7.0" +version = "1.8.0" dependencies = [ "anyhow", "chrono", @@ -1995,7 +1995,7 @@ dependencies = [ [[package]] name = "hk-web" -version = "1.7.0" +version = "1.8.0" dependencies = [ "anyhow", "axum", diff --git a/Cargo.toml b/Cargo.toml index 27b0d8fc..e0ac6351 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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" diff --git a/crates/hk-desktop/tauri.conf.json b/crates/hk-desktop/tauri.conf.json index 438c6292..1af2cb1e 100644 --- a/crates/hk-desktop/tauri.conf.json +++ b/crates/hk-desktop/tauri.conf.json @@ -1,6 +1,6 @@ { "productName": "HarnessKit", - "version": "1.7.0", + "version": "1.8.0", "identifier": "com.harnesskit.app", "build": { "frontendDist": "../../dist", diff --git a/package.json b/package.json index 01e70a4c..7116d3a3 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/scripts/notes-desktop-variant.mjs b/scripts/notes-desktop-variant.mjs new file mode 100644 index 00000000..7712f58e --- /dev/null +++ b/scripts/notes-desktop-variant.mjs @@ -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 +// (``) so the GitHub release page renders English only. +// Desktop clients ≤1.7.0 only understand the legacy fence format +// (`` / ``), so for latest.json this script +// converts comment blocks back into fences: +// +// +// +// +// +// +// 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 +// `` fence and concatenate both languages (the v1.7.0 bug). +// The updater dialog hides those sections anyway. +// +// Usage: node scripts/notes-desktop-variant.mjs +// 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 = //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 = [`\n${remainder}`]; +for (const { code, content } of translations) { + parts.push(`\n${content}`); +} +process.stdout.write(parts.join("\n\n") + "\n"); diff --git a/src/lib/i18n/__tests__/changelog.test.ts b/src/lib/i18n/__tests__/changelog.test.ts index 86d49acf..c9befcd8 100644 --- a/src/lib/i18n/__tests__/changelog.test.ts +++ b/src/lib/i18n/__tests__/changelog.test.ts @@ -32,3 +32,44 @@ describe("localizeChangelog", () => { ); }); }); + +const COMMENT_BLOCK = `## What's new +English line + + + +## 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("` / ``. const LANG_FENCE = //gi; +// Matches a language comment BLOCK: the whole translation lives inside one +// HTML comment, e.g. ``. 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 = //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 ... + * + * + * 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: * * * ## What's new ... @@ -14,24 +32,40 @@ const LANG_FENCE = //gi; * ## 更新内容 ... * * 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 = {}; - 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 (