diff --git a/CHANGELOG.md b/CHANGELOG.md index 55a2d429..df005d20 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,23 @@ # Changelog +## 0.1.26 - 2026-08-21 + +- Move Settings, the release-notes dialog, project home and the project + directory onto one visual contract: shared spacing rhythm, radius scale, type + scale and button hierarchy, replacing four competing row containers, two + untokenised nav rails and two loading treatments for the same card grid. +- Show the provider beside the model on every task row, and state which task an + unassigned route inherits from instead of a bare "Auto". +- Fix the project-directory header above ~1100px, where the description + rendered beside the title instead of below it, ran under the brandmark and + squeezed the primary action between them. +- Restore the model count beside each provider. All four translations bound the + placeholder as `{0}` while the source message names it `{catalogSize}`, so the + number rendered empty. A catalog parity test now fails on that class of + mismatch, which nothing in lint, typecheck or the test suites could see. +- Stop previewing non-raster attachments through an object URL, and remove a + duplicate nested "Updates" landmark introduced with the new section headers. + ## 0.1.25 - 2026-08-20 - Split provider configuration into three layers that cannot be collapsed: the diff --git a/README.md b/README.md index 0858bc4e..b6e578ee 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ They consume the same `.cutout` state. They do not control each other through GU ## Install the macOS app -Download the Apple Silicon DMG from [Cutout v0.1.25](https://github.com/Nebutra/cutout/releases/tag/v0.1.25), then drag Cutout into Applications. +Download the Apple Silicon DMG from [Cutout v0.1.26](https://github.com/Nebutra/cutout/releases/tag/v0.1.26), then drag Cutout into Applications. The public macOS build is Developer ID signed, Apple notarized, and stapled before publication. @@ -32,7 +32,7 @@ Windows, updater-signature, reviewer, and privacy release gates. Use Codex CLI `0.144.5` or a compatible plugin-enabled version. ```bash -codex plugin marketplace add Nebutra/cutout --ref v0.1.25 +codex plugin marketplace add Nebutra/cutout --ref v0.1.26 codex plugin add cutout@cutout-local codex plugin list ``` @@ -40,7 +40,7 @@ codex plugin list `codex plugin list` should show: ```text -cutout@cutout-local installed, enabled 0.1.25 +cutout@cutout-local installed, enabled 0.1.26 ``` Codex captures plugin Skills and MCP tools when a conversation starts. Open a new conversation after installing or updating the plugin. diff --git a/cutout.agent-capabilities.json b/cutout.agent-capabilities.json index 249dd39d..cfa70409 100644 --- a/cutout.agent-capabilities.json +++ b/cutout.agent-capabilities.json @@ -4,7 +4,7 @@ "product": { "name": "Cutout", "positioning": "Agent-native Design OS with a visual workbench and repo-native control plane", - "packageVersion": "0.1.25" + "packageVersion": "0.1.26" }, "protocol": { "control": "cutout.control.v1", @@ -120,7 +120,7 @@ "No cloud collaboration or remote project service.", "No arbitrary command execution, process spawning, absolute-path scans or arbitrary export destinations.", "No provider executor or bundled Coding workspace is available in the default CLI/MCP headless host; coding.* requires injected controlled adapters and tool.invoke returns capability-required.", - "The separate 0.1.25 Commerce production binary is restricted to evaluator-signed held-out packages, opaque private jobs and a fixed native command union. Its adjacent signed credential setup accepts only one bounded secret for the fixed Commerce Provider into an operator-only Keychain namespace; neither binary is exposed through cutout.control.v1, CLI or MCP or acts as a generic headless Provider executor.", + "The separate 0.1.26 Commerce production binary is restricted to evaluator-signed held-out packages, opaque private jobs and a fixed native command union. Its adjacent signed credential setup accepts only one bounded secret for the fixed Commerce Provider into an operator-only Keychain namespace; neither binary is exposed through cutout.control.v1, CLI or MCP or acts as a generic headless Provider executor.", "The desktop Commerce workbench has a separate Project mode for one bounded ordinary product record, offline catalogs and one to three local product images. It executes the fixed eleven-role Commerce DAG through an enabled keyed first-party DashScope Provider with native receipts, semantic QA, retained-byte review and browser downloads. Project receipts carry no evaluator commitment and do not produce benchmark or productionReady claims; this Provider workflow is not exposed through cutout.control.v1, CLI or MCP.", "The desktop codex-system adapter has an internal native-owned zero-tool turn path with isolated staged context, negotiated app-server schema checks and sanitized terminal evidence. Public conversation binding and turnExecution remain capability-required until a signed packaged turn succeeds against a healthy real upstream.", "The desktop app has an internal durable local Agent Host, a native-routed Provider executor, and a bounded static-site Coding backend under an opaque host-managed workspace. Those desktop lifecycle bindings are not exposed through cutout.control.v1, CLI, or MCP, and no arbitrary-shell executor is provided.", diff --git a/package.json b/package.json index a9cbe661..de9df6ef 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "cutout", "private": true, "license": "Apache-2.0", - "version": "0.1.25", + "version": "0.1.26", "type": "module", "scripts": { "dev": "vite", diff --git a/plugins/cutout/.codex-plugin/plugin.json b/plugins/cutout/.codex-plugin/plugin.json index 018a1e5f..69efacbb 100644 --- a/plugins/cutout/.codex-plugin/plugin.json +++ b/plugins/cutout/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "cutout", - "version": "0.1.25", + "version": "0.1.26", "description": "Repo-native Cutout design control for Codex", "author": { "name": "Cutout" diff --git a/plugins/cutout/runtime-data/cutout.agent-capabilities.json b/plugins/cutout/runtime-data/cutout.agent-capabilities.json index 249dd39d..cfa70409 100644 --- a/plugins/cutout/runtime-data/cutout.agent-capabilities.json +++ b/plugins/cutout/runtime-data/cutout.agent-capabilities.json @@ -4,7 +4,7 @@ "product": { "name": "Cutout", "positioning": "Agent-native Design OS with a visual workbench and repo-native control plane", - "packageVersion": "0.1.25" + "packageVersion": "0.1.26" }, "protocol": { "control": "cutout.control.v1", @@ -120,7 +120,7 @@ "No cloud collaboration or remote project service.", "No arbitrary command execution, process spawning, absolute-path scans or arbitrary export destinations.", "No provider executor or bundled Coding workspace is available in the default CLI/MCP headless host; coding.* requires injected controlled adapters and tool.invoke returns capability-required.", - "The separate 0.1.25 Commerce production binary is restricted to evaluator-signed held-out packages, opaque private jobs and a fixed native command union. Its adjacent signed credential setup accepts only one bounded secret for the fixed Commerce Provider into an operator-only Keychain namespace; neither binary is exposed through cutout.control.v1, CLI or MCP or acts as a generic headless Provider executor.", + "The separate 0.1.26 Commerce production binary is restricted to evaluator-signed held-out packages, opaque private jobs and a fixed native command union. Its adjacent signed credential setup accepts only one bounded secret for the fixed Commerce Provider into an operator-only Keychain namespace; neither binary is exposed through cutout.control.v1, CLI or MCP or acts as a generic headless Provider executor.", "The desktop Commerce workbench has a separate Project mode for one bounded ordinary product record, offline catalogs and one to three local product images. It executes the fixed eleven-role Commerce DAG through an enabled keyed first-party DashScope Provider with native receipts, semantic QA, retained-byte review and browser downloads. Project receipts carry no evaluator commitment and do not produce benchmark or productionReady claims; this Provider workflow is not exposed through cutout.control.v1, CLI or MCP.", "The desktop codex-system adapter has an internal native-owned zero-tool turn path with isolated staged context, negotiated app-server schema checks and sanitized terminal evidence. Public conversation binding and turnExecution remain capability-required until a signed packaged turn succeeds against a healthy real upstream.", "The desktop app has an internal durable local Agent Host, a native-routed Provider executor, and a bounded static-site Coding backend under an opaque host-managed workspace. Those desktop lifecycle bindings are not exposed through cutout.control.v1, CLI, or MCP, and no arbitrary-shell executor is provided.", diff --git a/plugins/cutout/runtime/runtime-build.json b/plugins/cutout/runtime/runtime-build.json index 5578d7af..4f8832e5 100644 --- a/plugins/cutout/runtime/runtime-build.json +++ b/plugins/cutout/runtime/runtime-build.json @@ -1,6 +1,6 @@ { "protocol": "cutout.codex-plugin-runtime.v1", - "packageVersion": "0.1.25", + "packageVersion": "0.1.26", "sourceHashes": { "node_modules/.pnpm/@tauri-apps+api@2.11.1/node_modules/@tauri-apps/api/core.js": "b2187a1c0c0a25806dc64f8823757ce553c09d3bbe3e409bd05cab32e245f1c9", "node_modules/.pnpm/@tauri-apps+api@2.11.1/node_modules/@tauri-apps/api/external/tslib/tslib.es6.js": "76dace0a1ae47c1d3e604591b8a15db2f534b776009b9e68e786dae7fe763f70", diff --git a/scripts/build-commerce-operator.mjs b/scripts/build-commerce-operator.mjs index bc7b720d..6ee0289f 100644 --- a/scripts/build-commerce-operator.mjs +++ b/scripts/build-commerce-operator.mjs @@ -33,7 +33,7 @@ const version = validateReleaseVersions({ bundledAgentCapabilities: bundledCapabilities.product?.packageVersion, bundledRuntime: runtimeBuild.packageVersion, }, - expected: '0.1.25', + expected: '0.1.26', }) if (JSON.stringify(capabilities) !== JSON.stringify(bundledCapabilities)) { throw new Error('Bundled Agent capability manifest drifted from the release source.') diff --git a/scripts/commerce-held-out-evaluator.test.ts b/scripts/commerce-held-out-evaluator.test.ts index fe266e6d..80e4e1cc 100644 --- a/scripts/commerce-held-out-evaluator.test.ts +++ b/scripts/commerce-held-out-evaluator.test.ts @@ -41,14 +41,14 @@ function pending() { })) return { commitment: { - challengeSelection: { payload: { expiresAt: 2_000, hostBuildVersion: '0.1.25' } }, + challengeSelection: { payload: { expiresAt: 2_000, hostBuildVersion: '0.1.26' } }, }, completionRequest: { protocol: 'cutout.commerce-held-out-evaluator-completion.v2', challengeId: 'challenge:test', challengeHash: HASH, evaluatorKeyId: 'evaluator:minisign:test', - hostBuildVersion: '0.1.25', + hostBuildVersion: '0.1.26', commitmentHash: HASH, inputManifestHash: HASH, runId: 'run:test', @@ -79,7 +79,7 @@ async function inspectablePending() { protocol: 'cutout.commerce-held-out-challenge-selection.v2', benchmark: { id: 'benchmark:commerce-profile:p1-p7', version: 2 }, profile: { id: 'profile:commerce-materials', version: '1.1.0' }, - hostBuildVersion: '0.1.25', + hostBuildVersion: '0.1.26', challengeId, challengeNonce: 'n'.repeat(32), inputManifestHash, @@ -92,7 +92,7 @@ async function inspectablePending() { }, challengeHash, evaluatorKeyId, - hostBuildVersion: '0.1.25', + hostBuildVersion: '0.1.26', inputManifest: { schema: 'commerce.held-out-input-manifest.v1', rehearsalIdentity: bundle.identity, @@ -118,7 +118,7 @@ async function inspectablePending() { challengeId, challengeHash, evaluatorKeyId, - hostBuildVersion: '0.1.25', + hostBuildVersion: '0.1.26', commitmentHash, inputManifestHash, runId: bundle.runId, @@ -333,7 +333,7 @@ describe('independent Commerce evaluator handoff (not benchmark evidence)', () = const payload = createChallengePayload({ inputManifestHash: HASH, evaluatorKeyId: 'evaluator:minisign:test', - hostBuildVersion: '0.1.25', + hostBuildVersion: '0.1.26', ttlMinutes: 30, now: 1_000, }) @@ -341,7 +341,7 @@ describe('independent Commerce evaluator handoff (not benchmark evidence)', () = protocol: 'cutout.commerce-held-out-challenge-selection.v2', inputManifestHash: HASH, evaluatorKeyId: 'evaluator:minisign:test', - hostBuildVersion: '0.1.25', + hostBuildVersion: '0.1.26', issuedAt: 1_000, expiresAt: 1_801_000, }) @@ -351,11 +351,11 @@ describe('independent Commerce evaluator handoff (not benchmark evidence)', () = it('derives one authoritative evaluator build version and rejects package/Cargo drift', () => { expect(resolveAuthoritativeHostBuildVersion({ - packageVersion: '0.1.25', - cargoVersion: '0.1.25', - })).toBe('0.1.25') + packageVersion: '0.1.26', + cargoVersion: '0.1.26', + })).toBe('0.1.26') expect(() => resolveAuthoritativeHostBuildVersion({ - packageVersion: '0.1.25', + packageVersion: '0.1.26', cargoVersion: '0.1.19', })).toThrow(/drifted/) }) @@ -373,7 +373,7 @@ describe('independent Commerce evaluator handoff (not benchmark evidence)', () = expect(validateAcceptedReview(accepted, value)).toEqual(accepted) expect(createCompletionPayload({ pending: value, review: accepted, completedAt: 1_600 })).toMatchObject({ challengeId: 'challenge:test', - hostBuildVersion: '0.1.25', + hostBuildVersion: '0.1.26', bundleHash: HASH, decision: 'accepted', deliverableCount: 11, diff --git a/scripts/commerce-operator.test.ts b/scripts/commerce-operator.test.ts index eca1de6c..f435768c 100644 --- a/scripts/commerce-operator.test.ts +++ b/scripts/commerce-operator.test.ts @@ -143,7 +143,7 @@ describe('Commerce operator release identity', () => { expect(runner).toContain('`-R=${NATIVE_HOST_MACOS_REQUIREMENT}`') }) - it('keeps product, Cargo, Tauri, capability, and plugin versions at 0.1.25', async () => { + it('keeps product, Cargo, Tauri, capability, and plugin versions at 0.1.26', async () => { const [pkg, tauri, capabilities, bundledCapabilities, plugin, runtimeBuild, cargo] = await Promise.all([ readFile('package.json', 'utf8').then(JSON.parse), readFile('src-tauri/tauri.conf.json', 'utf8').then(JSON.parse), @@ -153,7 +153,7 @@ describe('Commerce operator release identity', () => { readFile('plugins/cutout/runtime/runtime-build.json', 'utf8').then(JSON.parse), readFile('src-tauri/Cargo.toml', 'utf8'), ]) - expect(pkg.version).toBe('0.1.25') + expect(pkg.version).toBe('0.1.26') expect(tauri.version).toBe(pkg.version) expect(capabilities.product.packageVersion).toBe(pkg.version) expect(bundledCapabilities).toEqual(capabilities) diff --git a/scripts/release-notes.test.ts b/scripts/release-notes.test.ts index 16769454..fb1e232c 100644 --- a/scripts/release-notes.test.ts +++ b/scripts/release-notes.test.ts @@ -36,10 +36,16 @@ function catalog(entries: unknown[] = [entry()]) { } describe('release-note catalog', () => { - it('loads the reviewed v0.1.25 entry with all five shipped locales and no historical backfill', async () => { + it('loads the reviewed v0.1.26 entry with all five shipped locales and no historical backfill', async () => { const loaded = await loadReleaseNotesCatalog(undefined, { requireAllLocales: true }) - expect(loaded.entries.map((value) => value.version)).toEqual(['0.1.25', '0.1.24', '0.1.20', '0.1.19']) - expect(Object.keys(requireReleaseNotesEntry(loaded, '0.1.25').locales)).toEqual(RELEASE_NOTES_LOCALES) + // Derived, not pinned: the head is whatever package.json ships and the tail is + // the retained history. A literal list means every release edits this test. + const shipping = JSON.parse(await readFile('package.json', 'utf8')).version as string + expect(loaded.entries[0]?.version).toBe(shipping) + expect(loaded.entries.map((value) => value.version)).toEqual( + [...new Set([shipping, ...loaded.entries.map((value) => value.version)])], + ) + expect(Object.keys(requireReleaseNotesEntry(loaded, '0.1.26').locales)).toEqual(RELEASE_NOTES_LOCALES) expect(findReleaseNotesEntry(loaded, '0.1.16')).toBeUndefined() }) @@ -106,8 +112,10 @@ describe('release-note catalog', () => { }) it('renders all release inputs through the CLI from one exact entry', async () => { + const version = JSON.parse(await readFile('package.json', 'utf8')).version as string + const catalog = await loadReleaseNotesCatalog(undefined, { requireAllLocales: true }) const output = await mkdtemp(join(tmpdir(), 'cutout-release-notes-')) - const result = spawnSync(process.execPath, ['scripts/release-notes.mjs', 'render', '--version', '0.1.25', '--output', output, '--require-all-locales'], { cwd: process.cwd(), encoding: 'utf8' }) + const result = spawnSync(process.execPath, ['scripts/release-notes.mjs', 'render', '--version', version, '--output', output, '--require-all-locales'], { cwd: process.cwd(), encoding: 'utf8' }) expect(result.status, result.stderr).toBe(0) const [plainText, updater, bundled, github] = await Promise.all([ readFile(join(output, 'updater-notes.txt'), 'utf8'), @@ -116,9 +124,17 @@ describe('release-note catalog', () => { readFile(join(output, 'github-release.md'), 'utf8'), ]) expect(JSON.parse(updater)).toEqual(JSON.parse(bundled)) - expect(plainText).toContain('A provider no longer carries a model') - expect(plainText).toContain('Every task slot now takes effect') - expect(plainText).toContain('No call reaches a model you never configured') - expect(github).toContain('Cutout v0\\.1\\.25') + // Assert the render carries the catalog's own copy rather than a snapshot of + // it, so new release notes do not require editing this test. + const shipped = requireReleaseNotesEntry(catalog, version, { requireAllLocales: true }) + for (const highlight of shipped.locales.en.highlights) { + expect(plainText).toContain(highlight.title) + } + expect(plainText).toContain(shipped.locales.en.headline) + // The renderer markdown-escapes dots, so the heading reads `Cutout v0\.1\.26`. + // Built with split/join rather than a `.replace` regex: escaping only `.` and + // not `\` is incomplete sanitization, and this assertion never needed a + // sanitizer — it needs the literal shape the renderer emits. + expect(github).toContain(`Cutout v${version.split('.').join('\\.')}`) }) }) diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 718e66c7..875fa3ba 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -103,7 +103,7 @@ checksum = "2a4385e2e34eb35d6b3efe798b9eb88096925d87726c0798709bf56d9ed84af3" [[package]] name = "app" -version = "0.1.25" +version = "0.1.26" dependencies = [ "base64 0.22.1", "block2", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index c51a77fa..27a57ecd 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "app" -version = "0.1.25" +version = "0.1.26" default-run = "app" description = "Local-first AI design production workspace" authors = ["Nebutra Team"] diff --git a/src-tauri/src/commands/ai/commerce_held_out.rs b/src-tauri/src/commands/ai/commerce_held_out.rs index f2152913..70335ee3 100644 --- a/src-tauri/src/commands/ai/commerce_held_out.rs +++ b/src-tauri/src/commands/ai/commerce_held_out.rs @@ -1700,7 +1700,7 @@ mod tests { #[test] fn challenge_window_and_exact_identity_fail_closed() { let mut value = challenge(); - assert_eq!(HOST_BUILD_VERSION, "0.1.25"); + assert_eq!(HOST_BUILD_VERSION, "0.1.26"); assert!(validate_challenge_binding(&value, &"a".repeat(64), "evaluator:test", 20).is_ok()); assert!( validate_challenge_binding(&value, &"a".repeat(64), "evaluator:test", 101).is_err() @@ -1812,7 +1812,7 @@ mod tests { fn challenge_payload_uses_the_cross_runtime_canonical_json_contract() { let encoded = String::from_utf8(canonical_json_bytes(&challenge()).unwrap()).unwrap(); assert_eq!(encoded, format!( - "{{\"allowedRunId\":\"run:test\",\"benchmark\":{{\"id\":\"benchmark:commerce-profile:p1-p7\",\"version\":2}},\"challengeId\":\"challenge:test\",\"challengeNonce\":\"{}\",\"evaluatorKeyId\":\"evaluator:test\",\"expiresAt\":100,\"hostBuildVersion\":\"0.1.25\",\"inputManifestHash\":\"{}\",\"issuedAt\":10,\"profile\":{{\"id\":\"profile:commerce-materials\",\"version\":\"1.1.0\"}},\"protocol\":\"cutout.commerce-held-out-challenge-selection.v2\"}}", + "{{\"allowedRunId\":\"run:test\",\"benchmark\":{{\"id\":\"benchmark:commerce-profile:p1-p7\",\"version\":2}},\"challengeId\":\"challenge:test\",\"challengeNonce\":\"{}\",\"evaluatorKeyId\":\"evaluator:test\",\"expiresAt\":100,\"hostBuildVersion\":\"0.1.26\",\"inputManifestHash\":\"{}\",\"issuedAt\":10,\"profile\":{{\"id\":\"profile:commerce-materials\",\"version\":\"1.1.0\"}},\"protocol\":\"cutout.commerce-held-out-challenge-selection.v2\"}}", "n".repeat(32), "a".repeat(64) )); } diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index dc9e485c..434cf24d 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "../node_modules/@tauri-apps/cli/config.schema.json", "productName": "Cutout", - "version": "0.1.25", + "version": "0.1.26", "identifier": "com.nebutra.cutout", "build": { "frontendDist": "../dist", diff --git a/src/commerce-profile/held-out.test.ts b/src/commerce-profile/held-out.test.ts index 9dcc62b8..4c350836 100644 --- a/src/commerce-profile/held-out.test.ts +++ b/src/commerce-profile/held-out.test.ts @@ -81,7 +81,7 @@ describe('held-out Commerce protocol binding (not benchmark evidence)', () => { protocol: 'cutout.commerce-held-out-challenge-selection.v2', benchmark: { id: 'benchmark:commerce-profile:p1-p7', version: 2 }, profile: { id: 'profile:commerce-materials', version: '1.1.0' }, - hostBuildVersion: '0.1.25', + hostBuildVersion: '0.1.26', challengeId: 'challenge:test', challengeNonce: 'n'.repeat(32), inputManifestHash: HASH, @@ -99,7 +99,7 @@ describe('held-out Commerce protocol binding (not benchmark evidence)', () => { challengeHash: HASH, challengeId: 'challenge:test', evaluatorKeyId: 'evaluator:minisign:test', - hostBuildVersion: '0.1.25', + hostBuildVersion: '0.1.26', commitmentHash: HASH, inputManifestHash: HASH, runId: 'run:test', @@ -109,7 +109,7 @@ describe('held-out Commerce protocol binding (not benchmark evidence)', () => { completedAt: 42, }) expect(encodeCommerceHeldOutEvaluatorPayload(payload)).toBe( - '{"attestationId":"attestation:test","bundleHash":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","challengeHash":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","challengeId":"challenge:test","commitmentHash":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","completedAt":42,"decision":"accepted","deliverableCount":11,"evaluatorKeyId":"evaluator:minisign:test","hostBuildVersion":"0.1.25","inputManifestHash":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","protocol":"cutout.commerce-held-out-evaluator-completion.v2","runId":"run:test"}', + '{"attestationId":"attestation:test","bundleHash":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","challengeHash":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","challengeId":"challenge:test","commitmentHash":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","completedAt":42,"decision":"accepted","deliverableCount":11,"evaluatorKeyId":"evaluator:minisign:test","hostBuildVersion":"0.1.26","inputManifestHash":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","protocol":"cutout.commerce-held-out-evaluator-completion.v2","runId":"run:test"}', ) expect(() => commerceHeldOutEvaluatorAttestationPayloadSchema.parse({ ...payload, @@ -129,7 +129,7 @@ describe('held-out Commerce protocol binding (not benchmark evidence)', () => { challengeSelection: { payload: challenge, signature: 'x'.repeat(100) }, challengeHash: HASH, evaluatorKeyId: 'evaluator:test', - hostBuildVersion: '0.1.25', + hostBuildVersion: '0.1.26', inputManifest: {}, inputManifestHash: HASH, runId: 'run:test', @@ -162,7 +162,7 @@ describe('held-out Commerce protocol binding (not benchmark evidence)', () => { challengeId: 'challenge:test', challengeHash: HASH, evaluatorKeyId: 'evaluator:test', - hostBuildVersion: '0.1.25', + hostBuildVersion: '0.1.26', commitmentId: 'commitment:test', commitmentHash: HASH, attestationId: 'attestation:test', @@ -196,7 +196,7 @@ describe('held-out Commerce protocol binding (not benchmark evidence)', () => { protocol: 'cutout.commerce-held-out-challenge-selection.v2', benchmark: { id: 'benchmark:commerce-profile:p1-p7', version: 2 }, profile: { id: 'profile:commerce-materials', version: '1.1.0' }, - hostBuildVersion: '0.1.25', + hostBuildVersion: '0.1.26', challengeId: 'challenge:package', challengeNonce: 'p'.repeat(32), inputManifestHash: await fingerprint(inputManifest), @@ -233,7 +233,7 @@ describe('held-out Commerce protocol binding (not benchmark evidence)', () => { protocol: 'cutout.commerce-held-out-challenge-selection.v2', benchmark: { id: 'benchmark:commerce-profile:p1-p7', version: 2 }, profile: { id: 'profile:commerce-materials', version: '1.1.0' }, - hostBuildVersion: '0.1.25', + hostBuildVersion: '0.1.26', challengeId: 'challenge:pending', challengeNonce: 'q'.repeat(32), inputManifestHash, @@ -254,7 +254,7 @@ describe('held-out Commerce protocol binding (not benchmark evidence)', () => { challengeSelection: challenge, challengeHash: 'c'.repeat(64), evaluatorKeyId: challenge.payload.evaluatorKeyId, - hostBuildVersion: '0.1.25', + hostBuildVersion: '0.1.26', inputManifest, inputManifestHash, runId: fixture.bundle.runId, @@ -267,7 +267,7 @@ describe('held-out Commerce protocol binding (not benchmark evidence)', () => { challengeId: challenge.payload.challengeId, challengeHash: 'c'.repeat(64), evaluatorKeyId: challenge.payload.evaluatorKeyId, - hostBuildVersion: '0.1.25', + hostBuildVersion: '0.1.26', commitmentHash: 'b'.repeat(64), inputManifestHash, runId: fixture.bundle.runId, diff --git a/src/design-os-benchmark/contracts.test.ts b/src/design-os-benchmark/contracts.test.ts index c2c1cd32..096dd904 100644 --- a/src/design-os-benchmark/contracts.test.ts +++ b/src/design-os-benchmark/contracts.test.ts @@ -119,7 +119,7 @@ describe('Design OS evidence benchmark', () => { challengeId: 'challenge:caller', challengeHash: 'd'.repeat(64), evaluatorKeyId: 'evaluator:caller', - hostBuildVersion: '0.1.25', + hostBuildVersion: '0.1.26', commitmentId: 'commitment:caller', commitmentHash: 'a'.repeat(64), attestationId: 'attestation:caller', diff --git a/src/release-notes/catalog.json b/src/release-notes/catalog.json index 3a91a0b0..038086fe 100644 --- a/src/release-notes/catalog.json +++ b/src/release-notes/catalog.json @@ -1,6 +1,112 @@ { "protocol": "cutout.release-notes.catalog.v1", "entries": [ + { + "version": "0.1.26", + "releasedOn": "2026-08-21", + "locales": { + "en": { + "headline": "Settings, home and the project directory on one visual system", + "highlights": [ + { + "id": "one-visual-system", + "title": "Four surfaces, one set of rules", + "body": "Settings, the release-notes dialog, home and the project directory had each drifted into their own dialect — four competing row containers in Settings alone, two nav rails sharing no tokens, and two different loading treatments for the same card grid. They now share one spacing rhythm, radius scale, type scale and button hierarchy." + }, + { + "id": "task-rows-name-their-provider", + "title": "Every task row names its provider", + "body": "A task row now reads as provider and model rather than a bare model id, and a task you have not assigned says which task it inherits from instead of showing a bare Auto. Expanding it states the route a call would actually take." + }, + { + "id": "gallery-header-fix", + "title": "The project directory header no longer breaks on wide windows", + "body": "Above roughly 1100 pixels the description rendered beside the title instead of below it, running under the brandmark and squeezing the primary action between them. The model count beside each provider also renders again after a translation bound the wrong placeholder name." + } + ] + }, + "zh-CN": { + "headline": "设置、首页与项目目录统一到一套视觉系统", + "highlights": [ + { + "id": "one-visual-system", + "title": "四个界面,一套规则", + "body": "设置、发布说明弹窗、首页和项目目录各自长出了自己的方言 —— 仅设置里就有四种互相竞争的行容器,两条导航栏不共享任何样式变量,同一种卡片网格有两套加载态。现在它们共用一套间距节奏、圆角、字阶和按钮层级。" + }, + { + "id": "task-rows-name-their-provider", + "title": "每个任务行都写明提供商", + "body": "任务行现在显示「提供商 · 模型」,而不再是一个孤零零的模型 ID;未指派的任务会说明它继承自哪个任务,而不是只显示「自动」。展开后会写明调用实际会走哪条路由。" + }, + { + "id": "gallery-header-fix", + "title": "项目目录页头在宽窗口下不再错位", + "body": "约 1100 像素以上时,描述文字会排到标题右边而不是下方,压在品牌标记上并把主按钮夹在中间。此外,提供商旁边的模型数量也恢复显示 —— 此前一处翻译绑错了占位符名称。" + } + ] + }, + "ja": { + "headline": "設定・ホーム・プロジェクト一覧をひとつのビジュアルシステムに", + "highlights": [ + { + "id": "one-visual-system", + "title": "4 つの画面に 1 つのルール", + "body": "設定、リリースノート、ホーム、プロジェクト一覧はそれぞれ独自の方言に分かれていました。設定だけで 4 種類の行コンテナが競合し、2 つのナビゲーションはトークンを共有せず、同じカードグリッドに 2 通りのローディング表現がありました。今はひとつの余白リズム・角丸・文字スケール・ボタン階層を共有します。" + }, + { + "id": "task-rows-name-their-provider", + "title": "各タスク行がプロバイダー名を表示", + "body": "タスク行は「プロバイダー · モデル」と表示され、モデル ID だけが並ぶことはなくなりました。未割り当てのタスクは、単なる「自動」ではなく、どのタスクを継承するかを示します。展開すると実際に使われる経路がわかります。" + }, + { + "id": "gallery-header-fix", + "title": "広い画面でプロジェクト一覧の見出しが崩れません", + "body": "およそ 1100 ピクセルを超えると、説明文が見出しの下ではなく右側に回り込み、ブランドマークに重なって主要ボタンを挟み込んでいました。あわせて、プロバイダー横のモデル数も再び表示されます(翻訳が誤ったプレースホルダー名を参照していました)。" + } + ] + }, + "fr": { + "headline": "Réglages, accueil et répertoire de projets sur un seul système visuel", + "highlights": [ + { + "id": "one-visual-system", + "title": "Quatre surfaces, un seul jeu de règles", + "body": "Les réglages, la fenêtre de notes de version, l’accueil et le répertoire de projets avaient chacun dérivé vers leur propre dialecte : quatre conteneurs de ligne concurrents dans les seuls réglages, deux rails de navigation sans jetons communs, deux traitements de chargement pour la même grille. Ils partagent désormais un rythme d’espacement, une échelle de rayons, une échelle typographique et une hiérarchie de boutons." + }, + { + "id": "task-rows-name-their-provider", + "title": "Chaque tâche nomme son fournisseur", + "body": "Une ligne de tâche affiche le fournisseur et le modèle plutôt qu’un identifiant seul, et une tâche non affectée indique de quelle tâche elle hérite au lieu d’un simple « Auto ». Son détail précise la route qu’un appel emprunterait réellement." + }, + { + "id": "gallery-header-fix", + "title": "L’en-tête du répertoire ne casse plus sur les fenêtres larges", + "body": "Au-delà d’environ 1100 pixels, la description se plaçait à côté du titre au lieu d’en dessous, passant sous la marque et coinçant l’action principale. Le nombre de modèles affiché près de chaque fournisseur revient également : une traduction pointait vers un mauvais nom de variable." + } + ] + }, + "es": { + "headline": "Ajustes, inicio y directorio de proyectos en un solo sistema visual", + "highlights": [ + { + "id": "one-visual-system", + "title": "Cuatro superficies, un conjunto de reglas", + "body": "Los ajustes, el diálogo de notas de versión, el inicio y el directorio de proyectos habían derivado cada uno hacia su propio dialecto: cuatro contenedores de fila en competencia solo en ajustes, dos barras de navegación sin tokens comunes y dos tratamientos de carga para la misma cuadrícula. Ahora comparten un ritmo de espaciado, escala de radios, escala tipográfica y jerarquía de botones." + }, + { + "id": "task-rows-name-their-provider", + "title": "Cada fila de tarea nombra su proveedor", + "body": "Una fila de tarea muestra proveedor y modelo en lugar de un identificador suelto, y una tarea sin asignar indica de cuál hereda en vez de un simple «Auto». Al desplegarla se ve la ruta que tomaría realmente una llamada." + }, + { + "id": "gallery-header-fix", + "title": "La cabecera del directorio ya no se rompe en ventanas anchas", + "body": "Por encima de unos 1100 píxeles la descripción se colocaba junto al título en lugar de debajo, pasando bajo la marca y estrujando la acción principal. También vuelve a mostrarse el número de modelos junto a cada proveedor: una traducción apuntaba a un nombre de marcador incorrecto." + } + ] + } + } + }, { "version": "0.1.25", "releasedOn": "2026-08-20", diff --git a/src/updater/release-notes.test.ts b/src/updater/release-notes.test.ts index 25e9d7d4..e344b8ad 100644 --- a/src/updater/release-notes.test.ts +++ b/src/updater/release-notes.test.ts @@ -34,7 +34,7 @@ const migrationBundled: LocalizedReleaseNotes = { describe("release notes model", () => { it("bundles exact-version notes and applies whole-locale English fallback", () => { - expect(bundled.version).toBe("0.1.25"); + expect(bundled.version).toBe("0.1.26"); expect(selectLocalizedReleaseNotes(bundled, "zh-CN")?.headline).not.toBe( bundled.locales.en.headline, ); @@ -46,7 +46,7 @@ describe("release notes model", () => { it("prefers typed localized updater notes and safely falls back to plain text", () => { expect(resolveUpdateReleaseNotes({ - version: "0.1.25", + version: "0.1.26", localizedNotes: bundled, notes: "English fallback", }, "ja")?.headline).toBe(bundled.locales.ja.headline); diff --git a/tests/visual/update-settings.spec.ts b/tests/visual/update-settings.spec.ts index 07d2223b..19152f21 100644 --- a/tests/visual/update-settings.spec.ts +++ b/tests/visual/update-settings.spec.ts @@ -21,13 +21,13 @@ test("Updates stays truthful and configurable without a desktop runtime", async await expect(updates.getByRole("status")).toContainText("available only in the Cutout desktop app"); await expect(updates.getByRole("button", { name: "Check now" })).toBeDisabled(); await expect(updates.getByText("What's New", { exact: true })).toBeVisible(); - await expect(updates.getByText("Review the highlights for Cutout 0.1.25.")).toBeVisible(); + await expect(updates.getByText("Review the highlights for Cutout 0.1.26.")).toBeVisible(); const openReleaseNotes = updates.getByRole("button", { name: "Open" }); await expect(openReleaseNotes).toBeEnabled(); await openReleaseNotes.click(); - const releaseNotes = page.getByRole("dialog").filter({ hasText: "Cutout v0.1.25" }); - await expect(releaseNotes).toContainText("Providers are connections; models are assigned per task"); - await expect(releaseNotes).toContainText("A provider no longer carries a model"); + const releaseNotes = page.getByRole("dialog").filter({ hasText: "Cutout v0.1.26" }); + await expect(releaseNotes).toContainText("Settings, home and the project directory on one visual system"); + await expect(releaseNotes).toContainText("Four surfaces, one set of rules"); await page.keyboard.press("Escape"); // Browser builds have no compiled updater endpoints, so they must not offer