mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-20 17:19:33 +02:00
feat: announce each release once with a toast generated from the changelog
This commit is contained in:
1 parent
a8a8bb71a3
commit
d18deed51d
8 files changed
+181
-1
No files matched your search
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
'aicodeman': minor
|
||||||
|
---
|
||||||
|
|
||||||
|
Each release now announces itself. `npm run version-packages` writes `src/web/public/whats-new.json` from the new CHANGELOG section (the bold headline of every change), and the first time a browser loads a newer version it shows one toast naming the headlines, with a link to the full release notes. A browser that has never opened Codeman before records the current version and stays quiet.
|
||||||
+1
-1
@@ -39,7 +39,7 @@
|
|||||||
"check:public-assets": "node scripts/check-public-assets.mjs",
|
"check:public-assets": "node scripts/check-public-assets.mjs",
|
||||||
"capture:subagents": "node scripts/capture-subagent-screenshots.mjs",
|
"capture:subagents": "node scripts/capture-subagent-screenshots.mjs",
|
||||||
"changeset": "changeset",
|
"changeset": "changeset",
|
||||||
"version-packages": "changeset version && node scripts/sync-plugin.mjs && npm install --package-lock-only && node scripts/check-lockfile-sync.mjs",
|
"version-packages": "changeset version && node scripts/generate-whats-new.mjs && prettier --write src/web/public/whats-new.json && node scripts/sync-plugin.mjs && npm install --package-lock-only && node scripts/check-lockfile-sync.mjs",
|
||||||
"check:lockfile": "node scripts/check-lockfile-sync.mjs",
|
"check:lockfile": "node scripts/check-lockfile-sync.mjs",
|
||||||
"check:plugin": "node scripts/sync-plugin.mjs --check && claude plugin validate --strict plugins/codeman && claude plugin validate --strict .claude-plugin/marketplace.json",
|
"check:plugin": "node scripts/sync-plugin.mjs --check && claude plugin validate --strict plugins/codeman && claude plugin validate --strict .claude-plugin/marketplace.json",
|
||||||
"knip": "npx --yes knip@latest --config config/knip.json",
|
"knip": "npx --yes knip@latest --config config/knip.json",
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
// Writes src/web/public/whats-new.json for the version in package.json from CHANGELOG.md.
|
||||||
|
// Runs as part of `npm run version-packages`, so every release carries its own notes and the
|
||||||
|
// UI can announce them once per version without anyone remembering to.
|
||||||
|
import { readFileSync, writeFileSync } from 'node:fs';
|
||||||
|
import { dirname, resolve } from 'node:path';
|
||||||
|
import { fileURLToPath } from 'node:url';
|
||||||
|
import { buildWhatsNew } from './lib/whats-new.mjs';
|
||||||
|
|
||||||
|
const root = resolve(dirname(fileURLToPath(import.meta.url)), '..');
|
||||||
|
const { version } = JSON.parse(readFileSync(resolve(root, 'package.json'), 'utf8'));
|
||||||
|
const changelog = readFileSync(resolve(root, 'CHANGELOG.md'), 'utf8');
|
||||||
|
const notes = buildWhatsNew(changelog, version);
|
||||||
|
if (notes.highlights.length === 0) {
|
||||||
|
console.warn(`whats-new: no bold highlights found for ${version}; the in-app announcement will be skipped`);
|
||||||
|
}
|
||||||
|
writeFileSync(resolve(root, 'src/web/public/whats-new.json'), JSON.stringify(notes, null, 2) + '\n');
|
||||||
|
console.log(`whats-new: ${version} -> ${notes.highlights.length} highlight(s)`);
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
/**
|
||||||
|
* Pulls the headline of each change out of one version's section of CHANGELOG.md.
|
||||||
|
*
|
||||||
|
* Release notes here lead each change with a bold phrase ("**Tab layouts (#538, optional).** ..."),
|
||||||
|
* so a highlight is that phrase. The Thanks block is credit, not a feature.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const MAX_HIGHLIGHTS = 8;
|
||||||
|
|
||||||
|
/** The text between `## <version>` and the next `## ` heading, or '' when absent. */
|
||||||
|
export function versionSection(changelog, version) {
|
||||||
|
const lines = changelog.split(/\r?\n/);
|
||||||
|
const start = lines.findIndex((l) => l.trim() === `## ${version}`);
|
||||||
|
if (start === -1) return '';
|
||||||
|
let end = lines.findIndex((l, i) => i > start && /^## /.test(l));
|
||||||
|
if (end === -1) end = lines.length;
|
||||||
|
return lines.slice(start + 1, end).join('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Bold lead-ins at the start of a paragraph or bullet, cleaned for display. */
|
||||||
|
export function extractHighlights(section) {
|
||||||
|
const out = [];
|
||||||
|
for (const line of section.split('\n')) {
|
||||||
|
const m = /^\s*(?:[-*]\s+)?\*\*([^*]{3,120})\*\*/.exec(line);
|
||||||
|
if (!m) continue;
|
||||||
|
const text = m[1].replace(/[\s.:]+$/, '').trim();
|
||||||
|
if (!text || /^thanks\b/i.test(text)) continue;
|
||||||
|
if (!out.includes(text)) out.push(text);
|
||||||
|
}
|
||||||
|
return out.slice(0, MAX_HIGHLIGHTS);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildWhatsNew(changelog, version) {
|
||||||
|
return { version, highlights: extractHighlights(versionSection(changelog, version)) };
|
||||||
|
}
|
||||||
@@ -4843,6 +4843,8 @@ class CodemanApp {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (data.version) this.checkWhatsNew?.(data.version);
|
||||||
|
|
||||||
// Stop any active voice recording on reconnect
|
// Stop any active voice recording on reconnect
|
||||||
VoiceInput.cleanup();
|
VoiceInput.cleanup();
|
||||||
|
|
||||||
|
|||||||
@@ -369,6 +369,39 @@ Object.assign(CodemanApp.prototype, {
|
|||||||
return select?.value.trim() || '';
|
return select?.value.trim() || '';
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Announce a new release once per browser: one toast naming its headline changes.
|
||||||
|
* The notes come from /whats-new.json, which `npm run version-packages` regenerates from
|
||||||
|
* CHANGELOG.md for each release. A browser that has never seen any version just records the
|
||||||
|
* current one, so a fresh install is not greeted with "what's new".
|
||||||
|
*/
|
||||||
|
async checkWhatsNew(runningVersion) {
|
||||||
|
if (this._whatsNewChecked) return;
|
||||||
|
this._whatsNewChecked = true;
|
||||||
|
const KEY = 'codeman-whats-new-seen';
|
||||||
|
let seen = null;
|
||||||
|
try { seen = localStorage.getItem(KEY); } catch { /* storage blocked: skip the announcement */ return; }
|
||||||
|
if (seen === runningVersion) return;
|
||||||
|
const remember = () => { try { localStorage.setItem(KEY, runningVersion); } catch { /* ignore */ } };
|
||||||
|
if (!seen) { remember(); return; }
|
||||||
|
try {
|
||||||
|
const res = await fetch('/whats-new.json', { cache: 'no-cache' });
|
||||||
|
if (!res.ok) return;
|
||||||
|
const notes = await res.json();
|
||||||
|
if (notes?.version !== runningVersion || !Array.isArray(notes.highlights) || !notes.highlights.length) return;
|
||||||
|
const shown = notes.highlights.slice(0, 3).join(' · ');
|
||||||
|
const more = notes.highlights.length > 3 ? ` (+${notes.highlights.length - 3} more)` : '';
|
||||||
|
remember();
|
||||||
|
this.showToast(`Codeman updated to v${runningVersion}: ${shown}${more}`, 'info', {
|
||||||
|
duration: 20000,
|
||||||
|
action: {
|
||||||
|
label: "See what's new",
|
||||||
|
onClick: () => window.open(`https://github.com/Ark0N/Codeman/releases/tag/codeman%40${runningVersion}`, '_blank', 'noopener'),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch { /* offline or malformed: try again next load */ }
|
||||||
|
},
|
||||||
|
|
||||||
openAppSettings() {
|
openAppSettings() {
|
||||||
// Load current settings
|
// Load current settings
|
||||||
const settings = this.loadAppSettingsFromStorage();
|
const settings = this.loadAppSettingsFromStorage();
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"version": "1.41.1",
|
||||||
|
"highlights": [
|
||||||
|
"HTML files open as pages, and markdown opens rendered from the terminal (#547)",
|
||||||
|
"`codeman mcp`: drive Codeman from any MCP client (#615)",
|
||||||
|
"The codeman skill for every agent CLI (#613)",
|
||||||
|
"Tiles: move them by hand, or tear one off into its own window (#584)",
|
||||||
|
"Focus section on the vertical rail (#582)",
|
||||||
|
"Tabs land in groups (#528)",
|
||||||
|
"Folder grid and more clickable paths (#604)",
|
||||||
|
"Phone Attach button and videos (#606)"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
import { describe, it, expect } from 'vitest';
|
||||||
|
import { readFileSync } from 'node:fs';
|
||||||
|
import { resolve } from 'node:path';
|
||||||
|
// @ts-expect-error plain .mjs helper shared with scripts/generate-whats-new.mjs
|
||||||
|
import { extractHighlights, versionSection, buildWhatsNew } from '../scripts/lib/whats-new.mjs';
|
||||||
|
|
||||||
|
const root = resolve(import.meta.dirname, '..');
|
||||||
|
|
||||||
|
const CHANGELOG = `# aicodeman
|
||||||
|
|
||||||
|
## 1.2.0
|
||||||
|
|
||||||
|
### Minor Changes
|
||||||
|
|
||||||
|
- ### Thanks
|
||||||
|
- @someone for a fix
|
||||||
|
|
||||||
|
**The tile grid: six sessions side by side.** Body text with **inline bold** later.
|
||||||
|
|
||||||
|
- **Split view (#560).** More text.
|
||||||
|
- A bullet with no bold lead.
|
||||||
|
|
||||||
|
**Behaviour change: \`Ctrl+W\` no longer closes a session.** Details.
|
||||||
|
|
||||||
|
## 1.1.0
|
||||||
|
|
||||||
|
- **Old feature.** Not for 1.2.0.
|
||||||
|
`;
|
||||||
|
|
||||||
|
describe('whats-new extraction', () => {
|
||||||
|
it('limits itself to the requested version section', () => {
|
||||||
|
const section = versionSection(CHANGELOG, '1.2.0');
|
||||||
|
expect(section).toContain('tile grid');
|
||||||
|
expect(section).not.toContain('Old feature');
|
||||||
|
expect(versionSection(CHANGELOG, '9.9.9')).toBe('');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('takes bold lead-ins only, cleaned, and skips the Thanks block', () => {
|
||||||
|
expect(extractHighlights(versionSection(CHANGELOG, '1.2.0'))).toEqual([
|
||||||
|
'The tile grid: six sessions side by side',
|
||||||
|
'Split view (#560)',
|
||||||
|
'Behaviour change: `Ctrl+W` no longer closes a session',
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('caps the list', () => {
|
||||||
|
const many = Array.from({ length: 20 }, (_, i) => `**Feature number ${i}.** text`).join('\n\n');
|
||||||
|
expect(extractHighlights(many)).toHaveLength(8);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('builds the payload the client reads', () => {
|
||||||
|
expect(buildWhatsNew(CHANGELOG, '1.2.0').version).toBe('1.2.0');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('shipped whats-new.json', () => {
|
||||||
|
it('matches the package version and has highlights, so the release announces itself', () => {
|
||||||
|
const pkg = JSON.parse(readFileSync(resolve(root, 'package.json'), 'utf8'));
|
||||||
|
const notes = JSON.parse(readFileSync(resolve(root, 'src/web/public/whats-new.json'), 'utf8'));
|
||||||
|
expect(notes.version).toBe(pkg.version);
|
||||||
|
expect(notes.highlights.length).toBeGreaterThan(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('is regenerated by the release script and consumed once per version by the client', () => {
|
||||||
|
const pkg = readFileSync(resolve(root, 'package.json'), 'utf8');
|
||||||
|
expect(pkg).toMatch(/"version-packages": "changeset version && node scripts\/generate-whats-new\.mjs/);
|
||||||
|
const ui = readFileSync(resolve(root, 'src/web/public/settings-ui.js'), 'utf8');
|
||||||
|
expect(ui).toContain("'codeman-whats-new-seen'");
|
||||||
|
expect(ui).toContain("fetch('/whats-new.json'");
|
||||||
|
expect(readFileSync(resolve(root, 'src/web/public/app.js'), 'utf8')).toContain(
|
||||||
|
'this.checkWhatsNew?.(data.version)'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in new issue
Block a user