From 45ae0646cc510c5007bf7a6f3157ba52e0386515 Mon Sep 17 00:00:00 2001 From: Codex Date: Mon, 1 Jun 2026 12:01:52 +0200 Subject: [PATCH 1/2] fix: label icon-only controls --- components/channel-header.tsx | 1 + components/channel-list.tsx | 9 +++++- components/rail.tsx | 14 +++++++++- components/settings-modal.tsx | 9 +++++- tests/icon-button-labels.test.ts | 47 ++++++++++++++++++++++++++++++++ 5 files changed, 77 insertions(+), 3 deletions(-) create mode 100644 tests/icon-button-labels.test.ts diff --git a/components/channel-header.tsx b/components/channel-header.tsx index 0d3cbe7..367b830 100644 --- a/components/channel-header.tsx +++ b/components/channel-header.tsx @@ -215,6 +215,7 @@ function IconBtn({ return ( @@ -565,6 +571,7 @@ function ChannelRow({ onClick={onOpenMenu} className="opacity-0 group-hover:opacity-100 text-neutral-500 hover:text-neutral-200 hover:bg-neutral-800 rounded p-0.5" title="Channel options (or right-click)" + aria-label="Channel options" > diff --git a/components/rail.tsx b/components/rail.tsx index 10f0d1f..fd7e211 100644 --- a/components/rail.tsx +++ b/components/rail.tsx @@ -72,6 +72,7 @@ export function Rail() { @@ -445,6 +456,7 @@ function EditServerModal({ key={c} onClick={() => setColor(c)} disabled={isWarRoom} + aria-label={`Set server color to ${c}`} className={`w-8 h-8 rounded-lg bg-gradient-to-br border ${COLOR_MAP[c]} ${ color === c ? "ring-2 ring-white/40 ring-offset-2 ring-offset-[#0d0d0f]" : "" } disabled:opacity-40 disabled:cursor-not-allowed`} diff --git a/components/settings-modal.tsx b/components/settings-modal.tsx index 87776d0..200f331 100644 --- a/components/settings-modal.tsx +++ b/components/settings-modal.tsx @@ -59,7 +59,11 @@ export function SettingsModal({

Settings

- @@ -656,6 +660,7 @@ function ProfileEditor({ onClick={() => setIconUrl("")} className={`relative w-11 h-11 rounded-full overflow-hidden border-2 bg-neutral-950 ${iconUrl === "" ? "border-amber-400" : "border-neutral-800 hover:border-neutral-700"}`} title="Built-in" + aria-label="Use built-in logo" > {adapter.defaultIconUrl ? ( // eslint-disable-next-line @next/next/no-img-element @@ -670,6 +675,7 @@ function ProfileEditor({ key={p.url} onClick={() => setIconUrl(p.url)} title={p.label} + aria-label={`Use ${p.label} logo`} className={`w-11 h-11 rounded-full overflow-hidden border-2 bg-neutral-950 ${iconUrl === p.url ? "border-amber-400" : "border-neutral-800 hover:border-neutral-700"}`} > {/* eslint-disable-next-line @next/next/no-img-element */} @@ -702,6 +708,7 @@ function ProfileEditor({ key={opt.value} onClick={() => setAccent(opt.value)} title={opt.label} + aria-label={`Use ${opt.label} accent color`} className={`w-7 h-7 rounded-full border-2 ${opt.swatch} ${accent === opt.value ? "border-white" : "border-transparent hover:border-neutral-700"}`} /> ))} diff --git a/tests/icon-button-labels.test.ts b/tests/icon-button-labels.test.ts new file mode 100644 index 0000000..cc0554a --- /dev/null +++ b/tests/icon-button-labels.test.ts @@ -0,0 +1,47 @@ +import { test } from "node:test"; +import assert from "node:assert/strict"; +import fs from "node:fs"; +import path from "node:path"; + +function read(relPath: string): string { + return fs.readFileSync(path.join(process.cwd(), relPath), "utf8"); +} + +test("issue #8 icon-only buttons have explicit aria-labels", () => { + const channelHeader = read("components/channel-header.tsx"); + assert.match(channelHeader, /title=\{title\}\s+aria-label=\{title\}/, "channel header icon buttons should reuse title as aria-label"); + + const rail = read("components/rail.tsx"); + for (const label of [ + 'aria-label="Create server"', + 'aria-label="Invite teammates"', + 'aria-label="Sync status"', + 'aria-label="Settings"', + 'aria-label={`Open ${server.name} server`}', + 'aria-label="Close create server dialog"', + 'aria-label="Close edit server dialog"', + 'aria-label={`Set server color to ${c}`}', + ]) { + assert.ok(rail.includes(label), `rail is missing ${label}`); + } + + const channelList = read("components/channel-list.tsx"); + for (const label of [ + 'aria-label={`Create channel in ${g.label}`}', + 'aria-label={`Delete category "${g.label}"`}', + 'aria-label="Cancel new category"', + 'aria-label="Channel options"', + ]) { + assert.ok(channelList.includes(label), `channel list is missing ${label}`); + } + + const settingsModal = read("components/settings-modal.tsx"); + for (const label of [ + 'aria-label="Close settings"', + 'aria-label="Use built-in logo"', + 'aria-label={`Use ${p.label} logo`}', + 'aria-label={`Use ${opt.label} accent color`}', + ]) { + assert.ok(settingsModal.includes(label), `settings modal is missing ${label}`); + } +}); From 324a9a297f9f092395f437febd30d7f48bf8b41b Mon Sep 17 00:00:00 2001 From: Codex Date: Mon, 1 Jun 2026 19:52:09 +0200 Subject: [PATCH 2/2] test: align OpenWar opt-in migration expectation --- README.md | 2 +- tests/migration.test.ts | 13 +++++++++---- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index ec79721..7525056 100644 --- a/README.md +++ b/README.md @@ -62,7 +62,7 @@ Full walkthrough per mode: [`docs/sync-hosting.md`](./docs/sync-hosting.md). Syn War Room ships with [**OpenWar**](https://github.com/pythonluvr/openwar) as the bundled default agent framework. OpenWar is a system prompt that makes any agent (Claude, GPT, Gemini, custom CLI) behave like a senior peer: confirms briefs before acting, breaks work into phases, asks before destructive actions, refuses to invent next steps not grounded in the brief. -The framework is opt-in per channel and globally. The default selection lives in `system_settings.default_framework`; new installs are seeded to `openwar`. Frameworks are plain markdown files at `presets/frameworks/*.md`; drop a new one in and it shows up automatically in the wizard picker and channel header chip. No registration code, no manifest. +The framework is opt-in per channel and globally. The default selection lives in `system_settings.default_framework`; new installs leave it unset until you opt in. Frameworks are plain markdown files at `presets/frameworks/*.md`; drop a new one in and it shows up automatically in the wizard picker and channel header chip. No registration code, no manifest. Update bundled frameworks from upstream: diff --git a/tests/migration.test.ts b/tests/migration.test.ts index fc6606a..aa35d6e 100644 --- a/tests/migration.test.ts +++ b/tests/migration.test.ts @@ -172,13 +172,13 @@ test("migrate adds adapter_id + agent_id + new seeds without losing legacy rows" ); } - // Cold-clone seeds default.framework=openwar. Legacy installs that - // hadn't seen this seed before get it added now (no existing setting - // row → seedDefaultFramework inserts). + // OpenWar is opt-in/default-off on cold-clone installs now. Legacy + // installs that never set a framework should stay unset instead of + // getting the old default-on seed reintroduced by migration. const fw = db.prepare(`SELECT value FROM settings WHERE key = ?`).get("default.framework") as | { value: string } | undefined; - assert.equal(fw?.value, "openwar", "default.framework setting should be seeded to openwar"); + assert.equal(fw, undefined, "default.framework should stay unset until the user opts in"); } finally { cleanup(); } @@ -237,6 +237,11 @@ test("fresh DB (no legacy tables) gets full schema from cold", () => { assert.ok(warRoom, "cold-clone DB should have War Room seeded"); const personal = db.prepare(`SELECT * FROM user_servers WHERE is_personal = 1`).get(); assert.ok(personal, "cold-clone DB should have a personal workspace seeded"); + + // Behavioral overlays are explicit opt-ins. A new database should not + // silently enable OpenWar for users who already bring their own prompt. + const fw = db.prepare(`SELECT value FROM settings WHERE key = ?`).get("default.framework"); + assert.equal(fw, undefined, "cold-clone DB should not auto-enable OpenWar"); } finally { cleanup(); }