mirror of
https://github.com/nexu-io/open-design.git
synced 2026-07-07 23:09:34 +08:00
Picking a Home example-prompt card (or "Replicate this content" in the plugin detail modal) seeds the composer with the plugin manifest's `description`. Many descriptions were authored with cosmetic hard wraps (a single newline mid-paragraph, ~75 cols, as if formatted for a terminal/markdown source). The composer renders the seed with `white-space: pre-wrap`, so those newlines survived as short ragged lines that never filled the editor width — leaving a large blank gutter on the right of the input box (e.g. the Trading Analysis Dashboard template: 4 hard breaks, ~200px / 29% of the editor blank). Reflow the offending `description` / `description_i18n` strings in the 56 affected bundled manifests into flowing text (wrapped lines joined, real blank-line paragraph breaks preserved; CJK joins carry no space). Surgical string edits only — manifest formatting/key order untouched (146 lines changed across 56 files). The full build spec still reaches the agent as plugin context (SKILL.md + example.html); only the human-facing seed copy changed. Add an e2e guard (plugin-description-hardwrap.test.ts) that fails if any bundled manifest description carries an intra-paragraph hard wrap, so the cleanup can't silently regress when new examples are added.
85 lines
3.3 KiB
TypeScript
85 lines
3.3 KiB
TypeScript
// Bundled plugin manifest descriptions are dropped verbatim into the Home
|
|
// composer when a user picks an example-prompt card (or "Replicate this
|
|
// content" in the plugin detail modal). The composer renders that seed with
|
|
// `white-space: pre-wrap`, so a cosmetic hard wrap (a single newline mid
|
|
// paragraph, as if the text were formatted for an ~80-col terminal) survives
|
|
// as a short ragged line that never fills the editor width — leaving a large
|
|
// blank gutter on the right of the input box.
|
|
//
|
|
// This guard keeps the manifest copy free of those cosmetic wraps. Real
|
|
// paragraph breaks (a blank line, i.e. `\n\n`) are allowed; a lone `\n` is
|
|
// not. If this fails, reflow the offending `description` / `description_i18n`
|
|
// value into flowing text (join wrapped lines, keeping blank-line paragraph
|
|
// breaks) rather than re-introducing a runtime reflow.
|
|
|
|
import { readdir, readFile } from 'node:fs/promises';
|
|
import path from 'node:path';
|
|
import { fileURLToPath } from 'node:url';
|
|
|
|
import { describe, expect, it } from 'vitest';
|
|
|
|
const repoRoot = fileURLToPath(new URL('../../', import.meta.url));
|
|
|
|
// A lone newline not adjacent to another newline = a cosmetic intra-paragraph
|
|
// hard wrap. `\n\n` (paragraph break) is intentional and allowed.
|
|
const INTRA_PARAGRAPH_WRAP = /(?<!\n)\n(?!\n)/;
|
|
|
|
async function findManifests(dir: string): Promise<string[]> {
|
|
const out: string[] = [];
|
|
let entries;
|
|
try {
|
|
entries = await readdir(dir, { withFileTypes: true });
|
|
} catch {
|
|
return out;
|
|
}
|
|
for (const entry of entries) {
|
|
const full = path.join(dir, entry.name);
|
|
if (entry.isDirectory()) {
|
|
out.push(...(await findManifests(full)));
|
|
} else if (entry.name === 'open-design.json') {
|
|
out.push(full);
|
|
}
|
|
}
|
|
return out;
|
|
}
|
|
|
|
function collectDescriptionStrings(manifest: Record<string, unknown>): Array<{ field: string; value: string }> {
|
|
const fields: Array<{ field: string; value: string }> = [];
|
|
if (typeof manifest.description === 'string') {
|
|
fields.push({ field: 'description', value: manifest.description });
|
|
}
|
|
const i18n = manifest.description_i18n;
|
|
if (i18n && typeof i18n === 'object') {
|
|
for (const [locale, value] of Object.entries(i18n as Record<string, unknown>)) {
|
|
if (typeof value === 'string') {
|
|
fields.push({ field: `description_i18n.${locale}`, value });
|
|
}
|
|
}
|
|
}
|
|
return fields;
|
|
}
|
|
|
|
describe('bundled plugin manifest descriptions', () => {
|
|
it('[P2] contain no cosmetic intra-paragraph hard wraps (would blank the composer right gutter)', async () => {
|
|
const manifests = await findManifests(path.join(repoRoot, 'plugins', '_official'));
|
|
expect(manifests.length).toBeGreaterThan(0);
|
|
|
|
const violations: string[] = [];
|
|
for (const file of manifests) {
|
|
let manifest: Record<string, unknown>;
|
|
try {
|
|
manifest = JSON.parse(await readFile(file, 'utf8')) as Record<string, unknown>;
|
|
} catch (error) {
|
|
throw new Error(`Failed to parse plugin manifest: ${file}`, { cause: error });
|
|
}
|
|
for (const { field, value } of collectDescriptionStrings(manifest)) {
|
|
if (INTRA_PARAGRAPH_WRAP.test(value)) {
|
|
violations.push(`${path.relative(repoRoot, file)} → ${field}`);
|
|
}
|
|
}
|
|
}
|
|
|
|
expect(violations, `Manifest descriptions with cosmetic hard wraps:\n${violations.join('\n')}`).toEqual([]);
|
|
});
|
|
});
|