Skip to content

Commit 600fd42

Browse files
committed
v0.7.3: WarBit mood set + wired across error pages, empty states, welcome surfaces
WarBit goes from single mascot to ten-mood curated set. Mascot wired into seven UI surfaces: three error pages plus four warmer states (cold-clone welcome, demo banner, placeholder channels, channel chat empty state). ## v0.7.3 Added: - Curated mood set at public/war-bit/ (default, confused, calm, focused, alert, friendly, happy, angry, sleepy, done). Mood map documented in public/war-bit/README.md. - WelcomeBanner: happy.png 48px replaces the Sparkles glyph - DemoBanner: happy.png 20px left of the demo notice - PlaceholderChannel (system/approvals + system/sessions): sleepy.png 112px above the 'nothing pending' copy - Channel chat Welcome: friendly.png 80px next to the channel intro Changed: - not-found.tsx (404) uses confused.png - error.tsx (runtime error) uses focused.png - global-error.tsx (layout crash) uses angry.png ## v0.7.2 Added: - WarBit on the three error surfaces (404, runtime error, layout crash). Asset bundled at public/war-bit.png (1920x1920 RGBA, served as-is, rendered with image-rendering: pixelated for crisp upscaling). - Copy follows OpenWar voice: 'Hit a wall,' not 'Oh no!' ## Release-session cleanup - update-frameworks.mjs sanity patterns kept generic (operator-specific patterns belong in a fork-local override, not in the committed file) - Source-material WarBit variants (War_Bit (1-28).png, War_Bit_Story (1-6).png) gitignored. Curated public sets are in public/war-bit/ and branding/warbit-story/. - public/war-bit/README.md 'Source' link points to the OpenWar repo instead of a local Windows path. - sync.sh patched to auto-strip source variants and auto-restore curated public-only branding paths on every sync. Sanity regex clean. Em-dash sweep clean across all public-facing markdown. Local build verified.
1 parent e6580d1 commit 600fd42

24 files changed

Lines changed: 334 additions & 29 deletions

.gitignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,3 +58,8 @@ notes.txt
5858
/blob-report/
5959
/playwright/.cache/
6060
DEFERRED.md
61+
62+
# Source-material WarBit variants stay local; curated set lives in
63+
# public/war-bit/ (mood map) and branding/warbit-story/ (storyboards).
64+
branding/War_Bit (*.png
65+
branding/War_Bit_Story (*.png

CHANGELOG.md

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,37 @@ For full details on any release, see the corresponding entry on [GitHub Releases
88

99
---
1010

11+
## [0.7.3] - 2026-05-16
12+
13+
WarBit goes from single mascot to ten-mood set. Mascot wired into error pages, empty-state placeholders, and the cold-clone welcome banner.
14+
15+
### Added
16+
- **Curated WarBit mood set** at `public/war-bit/` (default, confused, calm, focused, alert, friendly, happy, angry, sleepy, done). Each variant maps to a specific UI mood per `public/war-bit/README.md`. Drop new variants in with a semantic filename to extend.
17+
- Mascot wired into warmer surfaces:
18+
- **Cold-clone WelcomeBanner**: `happy.png` 48px avatar replaces the Sparkles glyph
19+
- **DemoBanner**: tiny `happy.png` 20px left of the demo notice text
20+
- **PlaceholderChannel** (system/approvals + system/sessions empty states): `sleepy.png` 112px above the "nothing pending" copy
21+
- **Channel chat Welcome** (empty thread state): `friendly.png` 80px next to the channel intro
22+
23+
### Changed
24+
- Error surfaces use mood-specific variants:
25+
- `app/not-found.tsx` (404) uses `confused.png`
26+
- `app/error.tsx` (runtime error) uses `focused.png`
27+
- `app/global-error.tsx` (layout crash) uses `angry.png`
28+
- `public/war-bit.png` (the single original) stays available for any surface that wants the generic mascot without picking a mood.
29+
30+
## [0.7.2] - 2026-05-16
31+
32+
WarBit's first wiring into the app surface. The mascot now shows up when things go wrong.
33+
34+
### Added
35+
- **WarBit lands on the error surfaces.** The pixel-art knight sits at the top of three new pages:
36+
- `app/not-found.tsx` (404)
37+
- `app/error.tsx` (runtime error boundary)
38+
- `app/global-error.tsx` (top-level layout-failure fallback)
39+
Copy follows OpenWar voice: "Hit a wall," not "Oh no!"
40+
- Asset bundled at `public/war-bit.png` (1920x1920 RGBA, served as-is, rendered with `image-rendering: pixelated` for crisp upscaling).
41+
1142
## [0.7.1] - 2026-05-16
1243

1344
Patch on v0.7.0. Bundled OpenWar bumped to upstream v0.3.0, mid-conversation framework switch confirmed via modal, graceful degrade when a framework file is missing.
@@ -191,6 +222,8 @@ First public release.
191222
- Optional Electron desktop wrapper.
192223
- Optional self-hosted LiveKit boardroom voice channel.
193224

225+
[0.7.3]: https://github.com/pythonluvr/war-room/releases/tag/v0.7.3
226+
[0.7.2]: https://github.com/pythonluvr/war-room/releases/tag/v0.7.2
194227
[0.7.1]: https://github.com/pythonluvr/war-room/releases/tag/v0.7.1
195228
[0.7.0]: https://github.com/pythonluvr/war-room/releases/tag/v0.7.0
196229
[0.6.0]: https://github.com/pythonluvr/war-room/releases/tag/v0.6.0

app/error.tsx

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
"use client";
2+
3+
// Runtime error boundary for the app shell. Catches React render errors,
4+
// server-action errors, and unhandled promise rejections that bubble to
5+
// the root segment.
6+
//
7+
// Per OpenWar voice rules: report what broke + what's possible next,
8+
// no "oops!" filler. Errors are explanations, not apologies.
9+
10+
import { useEffect } from "react";
11+
import Link from "next/link";
12+
13+
export default function Error({
14+
error,
15+
reset,
16+
}: {
17+
error: Error & { digest?: string };
18+
reset: () => void;
19+
}) {
20+
useEffect(() => {
21+
// Log to console for forkers running dev tools open. Production
22+
// builds also surface this to electron-log via the IPC bridge.
23+
console.error("[war-room] error boundary tripped:", error);
24+
}, [error]);
25+
26+
return (
27+
<div className="min-h-dvh bg-neutral-950 text-neutral-200 flex items-center justify-center px-6 py-12">
28+
<div className="max-w-lg w-full text-center">
29+
{/* eslint-disable-next-line @next/next/no-img-element */}
30+
<img
31+
src="/war-bit/focused.png"
32+
alt="WarBit, focused on the problem"
33+
width={256}
34+
height={256}
35+
className="w-40 h-40 mx-auto mb-6 [image-rendering:pixelated] opacity-90"
36+
/>
37+
<h1 className="text-3xl font-semibold mb-2">Something broke.</h1>
38+
<p className="text-sm text-neutral-400 leading-relaxed mb-4">
39+
An unhandled error reached the render boundary. Hit retry to
40+
re-render this surface; if it keeps happening, the error
41+
message below is the place to start.
42+
</p>
43+
{error.message && (
44+
<pre className="text-left text-[11px] font-mono bg-neutral-900 border border-neutral-800 rounded-md p-3 overflow-x-auto mb-2 text-red-300/90">
45+
{error.message}
46+
</pre>
47+
)}
48+
{error.digest && (
49+
<p className="text-[10px] text-neutral-600 mb-6">
50+
digest: <code className="text-neutral-500">{error.digest}</code>
51+
</p>
52+
)}
53+
<div className="flex items-center justify-center gap-2">
54+
<button
55+
onClick={reset}
56+
className="inline-flex items-center gap-2 px-4 py-2 text-sm rounded-md bg-amber-500/20 border border-amber-500/40 text-amber-200 hover:bg-amber-500/30"
57+
>
58+
Retry
59+
</button>
60+
<Link
61+
href="/c/home"
62+
className="inline-flex items-center gap-2 px-4 py-2 text-sm rounded-md border border-neutral-800 hover:bg-neutral-900 text-neutral-300"
63+
>
64+
Back to the dashboard
65+
</Link>
66+
</div>
67+
</div>
68+
</div>
69+
);
70+
}

app/global-error.tsx

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
"use client";
2+
3+
// Top-level error boundary. Catches errors thrown by the root layout
4+
// itself, which app/error.tsx can't reach because that one renders
5+
// INSIDE the layout. Must include its own <html> + <body> since the
6+
// layout failed.
7+
8+
import { useEffect } from "react";
9+
10+
export default function GlobalError({
11+
error,
12+
reset,
13+
}: {
14+
error: Error & { digest?: string };
15+
reset: () => void;
16+
}) {
17+
useEffect(() => {
18+
console.error("[war-room] global error boundary tripped:", error);
19+
}, [error]);
20+
21+
return (
22+
<html lang="en">
23+
<body
24+
style={{
25+
margin: 0,
26+
minHeight: "100dvh",
27+
background: "#0a0a0a",
28+
color: "#e5e5e5",
29+
fontFamily:
30+
"ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, sans-serif",
31+
display: "flex",
32+
alignItems: "center",
33+
justifyContent: "center",
34+
padding: "3rem 1.5rem",
35+
}}
36+
>
37+
<div style={{ maxWidth: "32rem", width: "100%", textAlign: "center" }}>
38+
{/* eslint-disable-next-line @next/next/no-img-element */}
39+
<img
40+
src="/war-bit/angry.png"
41+
alt="WarBit, angry the layout crashed"
42+
width={160}
43+
height={160}
44+
style={{
45+
width: 160,
46+
height: 160,
47+
margin: "0 auto 1.5rem",
48+
imageRendering: "pixelated",
49+
opacity: 0.9,
50+
}}
51+
/>
52+
<h1
53+
style={{
54+
fontSize: "1.875rem",
55+
fontWeight: 600,
56+
margin: "0 0 0.5rem",
57+
}}
58+
>
59+
App layout crashed.
60+
</h1>
61+
<p
62+
style={{
63+
fontSize: "0.875rem",
64+
color: "#a3a3a3",
65+
lineHeight: 1.6,
66+
margin: "0 0 1rem",
67+
}}
68+
>
69+
The shell itself threw an error before the dashboard could
70+
render. Retry to reload the layout; if it keeps crashing,
71+
the error message below is the place to start.
72+
</p>
73+
{error.message && (
74+
<pre
75+
style={{
76+
textAlign: "left",
77+
fontSize: "0.6875rem",
78+
fontFamily:
79+
"ui-monospace, SFMono-Regular, Menlo, monospace",
80+
background: "#171717",
81+
border: "1px solid #262626",
82+
borderRadius: "0.375rem",
83+
padding: "0.75rem",
84+
overflowX: "auto",
85+
margin: "0 0 0.5rem",
86+
color: "rgba(252, 165, 165, 0.9)",
87+
}}
88+
>
89+
{error.message}
90+
</pre>
91+
)}
92+
<button
93+
onClick={reset}
94+
style={{
95+
padding: "0.5rem 1rem",
96+
fontSize: "0.875rem",
97+
borderRadius: "0.375rem",
98+
background: "rgba(245, 158, 11, 0.2)",
99+
border: "1px solid rgba(245, 158, 11, 0.4)",
100+
color: "rgb(254, 215, 170)",
101+
cursor: "pointer",
102+
}}
103+
>
104+
Retry
105+
</button>
106+
</div>
107+
</body>
108+
</html>
109+
);
110+
}

app/not-found.tsx

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
// 404 surface. Fires for any unmatched route. Voice matches OpenWar:
2+
// peer-level, no filler, no "Oh no!" performative copy.
3+
4+
import Link from "next/link";
5+
6+
export const dynamic = "force-static";
7+
8+
export default function NotFound() {
9+
return (
10+
<div className="min-h-dvh bg-neutral-950 text-neutral-200 flex items-center justify-center px-6 py-12">
11+
<div className="max-w-md w-full text-center">
12+
{/* eslint-disable-next-line @next/next/no-img-element */}
13+
<img
14+
src="/war-bit/confused.png"
15+
alt="WarBit, looking confused"
16+
width={256}
17+
height={256}
18+
className="w-40 h-40 mx-auto mb-6 [image-rendering:pixelated] opacity-90"
19+
/>
20+
<h1 className="text-3xl font-semibold mb-2">404</h1>
21+
<p className="text-sm text-neutral-400 leading-relaxed mb-6">
22+
Hit a wall. This page doesn&apos;t exist or got moved.
23+
</p>
24+
<Link
25+
href="/c/home"
26+
className="inline-flex items-center gap-2 px-4 py-2 text-sm rounded-md bg-amber-500/20 border border-amber-500/40 text-amber-200 hover:bg-amber-500/30"
27+
>
28+
Back to the dashboard
29+
</Link>
30+
</div>
31+
</div>
32+
);
33+
}

branding/openwar-logo.png

12.8 KB
Loading

components/channel-chat.tsx

Lines changed: 21 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -356,18 +356,28 @@ function Welcome({
356356
description?: string;
357357
}) {
358358
return (
359-
<div className="py-6 mb-2">
360-
<div className="text-2xl font-semibold mb-1">Welcome to #{channelName}!</div>
361-
{description ? (
362-
<div className="text-sm text-neutral-300 mb-3 max-w-2xl">{description}</div>
363-
) : (
364-
<div className="text-sm text-neutral-500 mb-3">
365-
This is the start of the{" "}
366-
<span className="text-neutral-300 font-medium">#{channelName}</span> channel.
359+
<div className="py-6 mb-2 flex items-start gap-4">
360+
{/* eslint-disable-next-line @next/next/no-img-element */}
361+
<img
362+
src="/war-bit/friendly.png"
363+
alt=""
364+
width={96}
365+
height={96}
366+
className="w-20 h-20 [image-rendering:pixelated] shrink-0"
367+
/>
368+
<div className="flex-1 min-w-0">
369+
<div className="text-2xl font-semibold mb-1">Welcome to #{channelName}.</div>
370+
{description ? (
371+
<div className="text-sm text-neutral-300 mb-3 max-w-2xl">{description}</div>
372+
) : (
373+
<div className="text-sm text-neutral-500 mb-3">
374+
This is the start of the{" "}
375+
<span className="text-neutral-300 font-medium">#{channelName}</span> channel.
376+
</div>
377+
)}
378+
<div className="text-xs text-neutral-500 flex items-center gap-2">
379+
<code className="text-neutral-400">📍 {projectPath}</code>
367380
</div>
368-
)}
369-
<div className="text-xs text-neutral-500 flex items-center gap-2">
370-
<code className="text-neutral-400">📍 {projectPath}</code>
371381
</div>
372382
</div>
373383
);
Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,7 @@
1-
import { CheckSquare, MessageSquare } from "lucide-react";
2-
3-
const ICONS = {
4-
approvals: CheckSquare,
5-
sessions: MessageSquare,
6-
} as const;
1+
// Placeholder surface for System channels that don't have real content
2+
// yet (approvals queue when nothing is pending, sessions list before
3+
// any chat has run). WarBit's "sleepy" variant communicates "feature
4+
// exists, nothing to show right now" instead of broken-empty.
75

86
export function PlaceholderChannel({
97
kind,
@@ -14,14 +12,21 @@ export function PlaceholderChannel({
1412
title: string;
1513
hint: string;
1614
}) {
17-
const Icon = ICONS[kind];
1815
return (
1916
<div className="flex-1 flex flex-col items-center justify-center text-center px-6">
20-
<div className="w-14 h-14 rounded-2xl bg-neutral-900 border border-neutral-800 flex items-center justify-center mb-4">
21-
<Icon className="w-6 h-6 text-neutral-500" />
22-
</div>
17+
{/* eslint-disable-next-line @next/next/no-img-element */}
18+
<img
19+
src="/war-bit/sleepy.png"
20+
alt=""
21+
width={128}
22+
height={128}
23+
className="w-28 h-28 mb-4 [image-rendering:pixelated] opacity-80"
24+
/>
2325
<h2 className="text-xl font-semibold mb-1">{title}</h2>
2426
<p className="text-sm text-neutral-500 max-w-md">{hint}</p>
27+
<p className="text-[10px] text-neutral-600 mt-3 uppercase tracking-wider">
28+
{kind}
29+
</p>
2530
</div>
2631
);
2732
}

components/demo-banner.tsx

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77
// set.
88

99
import { useEffect, useState } from "react";
10-
import { Sparkles } from "lucide-react";
1110

1211
export function DemoBanner() {
1312
const [demo, setDemo] = useState(false);
@@ -20,7 +19,14 @@ export function DemoBanner() {
2019
if (!demo) return null;
2120
return (
2221
<div className="bg-amber-500/15 border-b border-amber-500/30 text-amber-200 text-[11px] leading-relaxed px-4 py-1.5 flex items-center gap-2 justify-center">
23-
<Sparkles className="w-3.5 h-3.5 shrink-0" />
22+
{/* eslint-disable-next-line @next/next/no-img-element */}
23+
<img
24+
src="/war-bit/happy.png"
25+
alt=""
26+
width={20}
27+
height={20}
28+
className="w-5 h-5 [image-rendering:pixelated] shrink-0"
29+
/>
2430
<span>
2531
<strong className="text-amber-100">Demo data.</strong> Servers, channels, jobs, and chat
2632
history below are synthetic so you can see the app populated. Run{" "}

components/welcome-banner.tsx

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -70,9 +70,14 @@ export function WelcomeBanner() {
7070
return (
7171
<div className="mb-6 rounded-2xl border border-amber-500/30 bg-gradient-to-br from-amber-500/[0.08] to-neutral-950 p-5">
7272
<div className="flex items-start gap-3">
73-
<div className="w-10 h-10 rounded-full bg-amber-500/15 border border-amber-500/40 flex items-center justify-center shrink-0">
74-
<Sparkles className="w-5 h-5 text-amber-300" />
75-
</div>
73+
{/* eslint-disable-next-line @next/next/no-img-element */}
74+
<img
75+
src="/war-bit/happy.png"
76+
alt="WarBit waving"
77+
width={64}
78+
height={64}
79+
className="w-12 h-12 [image-rendering:pixelated] shrink-0"
80+
/>
7681
<div className="flex-1 min-w-0">
7782
<h3 className="text-lg font-semibold text-neutral-50 mb-1">Welcome to War Room.</h3>
7883
<p className="text-sm text-neutral-300 leading-relaxed mb-3">

0 commit comments

Comments
 (0)