Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand Down
1 change: 1 addition & 0 deletions components/channel-header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,7 @@ function IconBtn({
return (
<button
title={title}
aria-label={title}
onClick={onClick}
className="w-8 h-8 rounded-md hover:bg-neutral-900 hover:text-neutral-300 flex items-center justify-center"
>
Expand Down
9 changes: 8 additions & 1 deletion components/channel-list.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -318,6 +318,7 @@ export function ChannelList() {
</button>
<button
title={`Create channel in ${g.label}`}
aria-label={`Create channel in ${g.label}`}
onPointerDown={(e) => e.stopPropagation()}
onClick={() => {
setCollapsed({ ...collapsed, [g.label]: false });
Expand All @@ -330,6 +331,7 @@ export function ChannelList() {
{g.userCreated && (
<button
title={`Delete category "${g.label}"`}
aria-label={`Delete category "${g.label}"`}
onPointerDown={(e) => e.stopPropagation()}
onClick={() => deleteGroup(g.label)}
className="opacity-0 group-hover:opacity-100 text-neutral-500 hover:text-red-400 p-0.5"
Expand Down Expand Up @@ -459,7 +461,11 @@ function InlineCreate({
placeholder={placeholder}
className="flex-1 bg-neutral-900 border border-neutral-800 rounded text-xs px-2 py-1 focus:outline-none focus:border-neutral-700"
/>
<button onClick={onCancel} className="p-1 text-neutral-600 hover:text-neutral-300">
<button
onClick={onCancel}
aria-label="Cancel new category"
className="p-1 text-neutral-600 hover:text-neutral-300"
>
<X className="w-3 h-3" />
</button>
</div>
Expand Down Expand Up @@ -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"
>
<MoreVertical className="w-3.5 h-3.5" />
</button>
Expand Down
14 changes: 13 additions & 1 deletion components/rail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ export function Rail() {
<button
onClick={() => setCreating(true)}
title="Create server"
aria-label="Create server"
className="w-11 h-11 rounded-full bg-neutral-900 border border-neutral-800 hover:border-emerald-500/40 hover:bg-emerald-500/10 text-neutral-400 hover:text-emerald-300 flex items-center justify-center transition-all"
>
<Plus className="w-4 h-4" />
Expand All @@ -81,20 +82,23 @@ export function Rail() {

<button
title="Invite teammates"
aria-label="Invite teammates"
onClick={() => setInviting(true)}
className="w-11 h-11 rounded-full hover:bg-neutral-900 text-neutral-600 hover:text-amber-300 flex items-center justify-center"
>
<UserPlus className="w-4 h-4" />
</button>
<button
title="Sync status"
aria-label="Sync status"
onClick={() => setSettingsOpen("sync")}
className="w-11 h-11 rounded-full hover:bg-neutral-900 text-neutral-600 hover:text-neutral-300 flex items-center justify-center"
>
<Cloud className="w-4 h-4" />
</button>
<button
title="Settings"
aria-label="Settings"
onClick={() => setSettingsOpen("general")}
className="w-11 h-11 rounded-full hover:bg-neutral-900 text-neutral-500 hover:text-neutral-300 flex items-center justify-center"
>
Expand Down Expand Up @@ -170,6 +174,7 @@ function ServerIcon({
onEdit();
}}
title={`${server.name} · right-click to edit`}
aria-label={`Open ${server.name} server`}
className="relative group"
>
{active && (
Expand Down Expand Up @@ -239,6 +244,7 @@ function CreateServerModal({
</div>
<button
onClick={onClose}
aria-label="Close create server dialog"
className="text-neutral-500 hover:text-neutral-300 p-1"
>
<X className="w-4 h-4" />
Expand Down Expand Up @@ -288,6 +294,7 @@ function CreateServerModal({
<button
key={c}
onClick={() => setColor(c)}
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]" : ""
}`}
Expand Down Expand Up @@ -342,7 +349,11 @@ function EditServerModal({
Rename, change the icon, or recolor the wrapper.
</p>
</div>
<button onClick={onClose} className="text-neutral-500 hover:text-neutral-300 p-1">
<button
onClick={onClose}
aria-label="Close edit server dialog"
className="text-neutral-500 hover:text-neutral-300 p-1"
>
<X className="w-4 h-4" />
</button>
</div>
Expand Down Expand Up @@ -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`}
Expand Down
9 changes: 8 additions & 1 deletion components/settings-modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,11 @@ export function SettingsModal({
<SettingsIcon className="w-5 h-5 text-neutral-400" />
<h2 className="text-lg font-semibold">Settings</h2>
</div>
<button onClick={onClose} className="text-neutral-500 hover:text-neutral-200 p-1">
<button
onClick={onClose}
aria-label="Close settings"
className="text-neutral-500 hover:text-neutral-200 p-1"
>
<X className="w-4 h-4" />
</button>
</div>
Expand Down Expand Up @@ -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
Expand All @@ -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 */}
Expand Down Expand Up @@ -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"}`}
/>
))}
Expand Down
47 changes: 47 additions & 0 deletions tests/icon-button-labels.test.ts
Original file line number Diff line number Diff line change
@@ -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}`);
}
});
13 changes: 9 additions & 4 deletions tests/migration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
Expand Down Expand Up @@ -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();
}
Expand Down