Language-aware house style and a resizable chat split
House style now follows the app language: Chinese UI forces the new bundled humanizer-zh skill into every run, English forces humanizer - one variant per run, picked by preference with cross-language fallback so an older catalogue never silently loses house style. Both variants are protected from grant/revoke, list_skills labels each with its language, the bundle guard requires both, and startup sync imports the new skill into existing orgs on next launch. Covered by a unit test that flips app_state.language and asserts which humanizer compiles. The chat split is now adjustable: drag the conversation list's right edge (200-480px, persisted in localStorage) and the composer textarea grew a native vertical resize grip capped at 40vh.
This commit is contained in:
1 parent
c027508b97
commit
eaaf36f859
16 files changed
+931
-45
No files matched your search
@@ -14,6 +14,12 @@ export function seedBuiltinSkillsFixture(root: string): string {
|
||||
"---\nname: humanizer\nversion: 2.1.1\ndescription: |\n Remove signs of AI-generated writing from text.\n---\n\nWrite like a person.\n",
|
||||
"utf8",
|
||||
);
|
||||
mkdirSync(join(root, "Humanizer-zh"), { recursive: true });
|
||||
writeFileSync(
|
||||
join(root, "Humanizer-zh", "SKILL.md"),
|
||||
"---\nname: humanizer-zh\nversion: 1.0.0\ndescription: |\n 去除文本中的 AI 生成痕迹。\n---\n\n像真人一样写作。\n",
|
||||
"utf8",
|
||||
);
|
||||
mkdirSync(join(root, "engineering"), { recursive: true });
|
||||
writeFileSync(
|
||||
join(root, "engineering", "engineering-backend-architect.md"),
|
||||
|
||||
@@ -182,7 +182,7 @@ test("onboards, hires, and creates work in the Electron shell", async () => {
|
||||
const result = await api.loadSnapshot() as { ok: true; data: { skillDefinitions: Array<{ slug: string }> } };
|
||||
return result.data.skillDefinitions.map((skill) => skill.slug).sort();
|
||||
});
|
||||
expect(skillSlugs).toEqual(["engineering-backend-architect", "humanizer"]);
|
||||
expect(skillSlugs).toEqual(["engineering-backend-architect", "humanizer", "humanizer-zh"]);
|
||||
|
||||
// The global pause toggle lives in the topbar on every page.
|
||||
const pauseToggle = page.getByRole("button", { name: "Pause agents" });
|
||||
|
||||
@@ -17,6 +17,7 @@ describe("bundled skill catalogue", () => {
|
||||
const staged = stageBuiltinSkillsLibrary(resolve(process.cwd(), "built-in skills"), libraryRoot);
|
||||
expect(staged.length).toBeGreaterThan(200);
|
||||
expect(staged.some((unit) => unit.slug === "humanizer")).toBe(true);
|
||||
expect(staged.some((unit) => unit.slug === "humanizer-zh")).toBe(true);
|
||||
const withDescription = staged.filter((unit) => unit.manifest.description).length;
|
||||
expect(withDescription / staged.length).toBeGreaterThan(0.9);
|
||||
} finally {
|
||||
|
||||
@@ -55,6 +55,12 @@ function harness(): Harness {
|
||||
mkdirSync(join(sourceRoot, "humanizer"), { recursive: true });
|
||||
writeFileSync(join(sourceRoot, "humanizer", "SKILL.md"), HUMANIZER_MD, "utf8");
|
||||
writeFileSync(join(sourceRoot, "humanizer", "README.md"), "# About\n", "utf8");
|
||||
mkdirSync(join(sourceRoot, "Humanizer-zh"), { recursive: true });
|
||||
writeFileSync(
|
||||
join(sourceRoot, "Humanizer-zh", "SKILL.md"),
|
||||
"---\nname: humanizer-zh\ndescription: 去除文本中的 AI 生成痕迹。\n---\n\n像真人一样写作。\n",
|
||||
"utf8",
|
||||
);
|
||||
mkdirSync(join(sourceRoot, "engineering"), { recursive: true });
|
||||
writeFileSync(join(sourceRoot, "engineering", "engineering-backend-architect.md"), ARCHITECT_MD, "utf8");
|
||||
writeFileSync(join(sourceRoot, "engineering", "README.md"), "docs, not a skill\n", "utf8");
|
||||
@@ -75,8 +81,9 @@ describe("built-in skills", () => {
|
||||
test("stages both bundle shapes into the library and imports them once per org", () => {
|
||||
const h = harness();
|
||||
const staged = stageBuiltinSkillsLibrary(h.sourceRoot, h.libraryRoot);
|
||||
expect(staged.map((unit) => unit.slug).sort()).toEqual(["engineering-backend-architect", "humanizer"]);
|
||||
expect(staged.map((unit) => unit.slug).sort()).toEqual(["engineering-backend-architect", "humanizer", "humanizer-zh"]);
|
||||
expect(existsSync(join(h.libraryRoot, "humanizer", "SKILL.md"))).toBe(true);
|
||||
expect(existsSync(join(h.libraryRoot, "humanizer-zh", "SKILL.md"))).toBe(true);
|
||||
expect(existsSync(join(h.libraryRoot, "engineering-backend-architect", "SKILL.md"))).toBe(true);
|
||||
const architect = staged.find((unit) => unit.slug === "engineering-backend-architect");
|
||||
expect(architect?.manifest.name).toBe("Backend Architect");
|
||||
@@ -86,12 +93,12 @@ describe("built-in skills", () => {
|
||||
expect(humanizer?.manifest.description).toContain("AI-generated writing");
|
||||
|
||||
const first = h.connection.transaction(() => importStagedSkills(h.skills, h.orgId, staged));
|
||||
expect(first).toMatchObject({ imported: 2, skipped: 0, retired: 0, failures: [] });
|
||||
expect(first).toMatchObject({ imported: 3, skipped: 0, retired: 0, failures: [] });
|
||||
const second = h.connection.transaction(() => importStagedSkills(h.skills, h.orgId, staged));
|
||||
expect(second).toMatchObject({ imported: 0, skipped: 2, retired: 0 });
|
||||
expect(second).toMatchObject({ imported: 0, skipped: 3, retired: 0 });
|
||||
const slugs = (h.connection.db.prepare("select slug from skill_definitions where org_id = ? order by slug").all(h.orgId) as Array<{ slug: string }>)
|
||||
.map((row) => row.slug);
|
||||
expect(slugs).toEqual(["engineering-backend-architect", "humanizer"]);
|
||||
expect(slugs).toEqual(["engineering-backend-architect", "humanizer", "humanizer-zh"]);
|
||||
h.connection.close();
|
||||
});
|
||||
|
||||
@@ -191,6 +198,43 @@ describe("built-in skills", () => {
|
||||
h.connection.close();
|
||||
});
|
||||
|
||||
test("the forced humanizer follows the app language, with cross-language fallback", () => {
|
||||
const h = harness();
|
||||
const staged = stageBuiltinSkillsLibrary(h.sourceRoot, h.libraryRoot);
|
||||
h.connection.transaction(() => importStagedSkills(h.skills, h.orgId, staged));
|
||||
let agentId = "";
|
||||
h.connection.transaction(() => {
|
||||
agentId = h.organization.createAgent({
|
||||
orgId: h.orgId, name: "Ada", role: "Engineer", department: "Engineering",
|
||||
modelProviderId: null, modelId: null, budgetCents: 1000, managerAgentId: null,
|
||||
});
|
||||
});
|
||||
const promptFor = (runId: string) => {
|
||||
const snapshotId = h.connection.transaction(() => h.skills.resolveRuntimeSnapshot({ orgId: h.orgId, agentId, runId }));
|
||||
return (h.connection.db.prepare("select compiled_prompt from skill_runtime_snapshots where id = ?").get(snapshotId) as { compiled_prompt: string }).compiled_prompt;
|
||||
};
|
||||
|
||||
// English UI (default): the English humanizer, and only that one.
|
||||
const englishPrompt = promptFor("run_lang_en");
|
||||
expect(englishPrompt).toContain("# Skill: humanizer");
|
||||
expect(englishPrompt).not.toContain("humanizer-zh");
|
||||
|
||||
// Chinese UI: the Chinese variant replaces it.
|
||||
h.connection.transaction(() => h.organization.updateUiPreferences({ language: "zh" }));
|
||||
const chinesePrompt = promptFor("run_lang_zh");
|
||||
expect(chinesePrompt).toContain("# Skill: humanizer-zh");
|
||||
expect(chinesePrompt).not.toContain("# Skill: humanizer\n");
|
||||
|
||||
// Chinese UI against a catalogue without the zh variant falls back to
|
||||
// English house style instead of silently dropping it.
|
||||
h.connection.transaction(() => {
|
||||
h.connection.db.prepare("update skill_definitions set status = 'retired' where org_id = ? and slug = 'humanizer-zh'").run(h.orgId);
|
||||
});
|
||||
const fallbackPrompt = promptFor("run_lang_fallback");
|
||||
expect(fallbackPrompt).toContain("# Skill: humanizer");
|
||||
h.connection.close();
|
||||
});
|
||||
|
||||
test("frontmatter parsing reads inline values and block scalars", () => {
|
||||
expect(parseFrontmatter(ARCHITECT_MD)).toMatchObject({
|
||||
name: "Backend Architect",
|
||||
|
||||
@@ -68,6 +68,10 @@ function importSkill(h: Harness, name: string, slug: string): void {
|
||||
insert into skill_revisions(id, skill_id, version_semver, manifest_json, source_type, source_ref, published_at, immutable_checksum)
|
||||
values (?, ?, '1.0.0', '{}', 'local_directory', '/tmp', ?, 'checksum')
|
||||
`).run(`skr_${slug}`, `skl_${slug}`, now);
|
||||
h.connection.db.prepare(`
|
||||
insert into skill_artifacts(id, revision_id, artifact_type, object_key, sha256, size_bytes)
|
||||
values (?, ?, 'entrypoint', ?, 'sha', 1)
|
||||
`).run(`ska_${slug}`, `skr_${slug}`, `/tmp/${slug}/SKILL.md`);
|
||||
}
|
||||
|
||||
describe("skill grants", () => {
|
||||
|
||||
Reference in new issue
Block a user