// The macOS desktop scene: menu bar, dock, draggable app windows
// and their simulated contents (what Puck patrols).
import * as React from "react";
import type { EffectId } from "../engine";
import { type Pos, useDrag } from "../lib/useDrag";
export interface WinState {
claude: "running" | "done";
build: "running" | "done" | "fail";
mail: string[];
discordNoise: boolean;
discordMention: boolean;
calSoon: boolean;
}
export const initialWinState = (): WinState => ({
claude: "running",
build: "running",
mail: ["hf"],
discordNoise: true,
discordMention: false,
calSoon: false,
});
/** How an engine effect mutates the simulated windows. Pure; colocated with WinState. */
export function applyEffect(w: WinState, eff: EffectId): WinState {
switch (eff) {
case "claude_done":
return { ...w, claude: "done" };
case "build_done":
return { ...w, build: "done" };
case "build_fail":
return { ...w, build: "fail" };
case "mail_unread":
return { ...w, mail: Array.from(new Set([...w.mail, w.mail.includes("hf") ? "fin" : "hf"])) };
case "discord_mention":
return { ...w, discordMention: true };
case "discord_noise":
return { ...w, discordNoise: true };
case "calendar_soon":
return { ...w, calSoon: true };
case "none":
return w;
}
}
// ---- menu bar ---------------------------------------------------------------
export function MenuBar({
moodLabel,
puckActive,
badge,
onPuck,
clock,
}: {
moodLabel: string;
puckActive: boolean;
badge: number;
onPuck: () => void;
clock: string;
}) {
return (
Finder
File
Edit
View
Go
Window
{clock}
);
}
// ---- dock ---------------------------------------------------------------------
export interface DockApp {
id: string;
name: string;
glyph: string;
running?: boolean;
badge?: number;
tint?: string;
}
export function Dock({ apps, onOpen }: { apps: DockApp[]; onOpen: (id: string) => void }) {
return (
{apps.map((a) => (
onOpen(a.id)}
style={a.tint ? { color: a.tint } : undefined}
>
{a.glyph}
{(a.badge ?? 0) > 0 && {a.badge}}
))}
);
}
// ---- generic draggable window -------------------------------------------------
function AppWindow({
id,
pos,
onPos,
w,
h,
title,
icon,
sub,
attn,
dimmed,
onFocus,
z,
children,
}: {
id: string;
pos: Pos;
onPos: (id: string, p: Pos) => void;
w: number;
h: number;
title: string;
icon: string;
sub?: string;
attn: boolean;
dimmed: boolean;
onFocus: (id: string) => void;
z: number;
children: React.ReactNode;
}) {
const drag = useDrag(pos, (p) => onPos(id, p));
return (
onFocus(id)}
>
{
onFocus(id);
drag(e);
}}
>
{icon}
{title}
{sub &&
{sub}
}
{children}
);
}
// ---- window bodies --------------------------------------------------------
function ClaudeBody({ st }: { st: WinState }) {
const done = st.claude === "done";
return (
⏵ implement the memory garden panel
edit src/garden/Plot.tsx +48 −6
edit src/garden/seed.ts +12 −0
write src/garden/decay.ts new
{done ? ✓ : }
{done ? "Memory Garden — completed · 7 files" : "Memory Garden — running…"}
{done ? "done" : "····"}
);
}
function TermBody({ st }: { st: WinState }) {
const s = st.build;
return (
vu@workshop ~/puck $ npm run build
vite v5.3.1 building for production…
transforming modules ·····
{s === "running" && (
<>
bundling…
>
)}
{s === "done" && (
<>
✓ built in 2m 14s — 0 errors
>
)}
{s === "fail" && (
<>
✗ 3 tests failing — auth.test.ts
expected 200, received 401
>
)}
);
}
interface ListRow {
from: string;
sub: string;
time: string;
unread?: boolean;
mention?: boolean;
noise?: boolean;
muted?: boolean;
av: string;
}
function MsgList({ rows }: { rows: ListRow[] }) {
return (
{rows.map((r) => (
{r.av}
{r.from}
{r.time}
{r.sub}
))}
);
}
function MailBody({ st }: { st: WinState }) {
return (
);
}
function DiscordBody({ st }: { st: WinState }) {
return (
);
}
function CalBody({ st }: { st: WinState }) {
return (
Today · Thu Jun 5
Demo sync
{st.calSoon ? "in 10 min · 11:00" : "11:00 – 11:30"}
Deep work — Puck UI
14:00 – 16:00
);
}
// ---- the set of windows -----------------------------------------------------
interface WinDef {
id: string;
title: string;
icon: string;
sub: string;
w: number;
h: number;
Body: React.ComponentType<{ st: WinState }>;
}
export const WIN_DEFS: WinDef[] = [
{ id: "claude", title: "Claude Code", icon: "✳", sub: "Desktop 2", w: 432, h: 296, Body: ClaudeBody },
{ id: "terminal", title: "workshop — build", icon: "▦", sub: "zsh", w: 404, h: 224, Body: TermBody },
{ id: "mail", title: "Mail", icon: "✉", sub: "Inbox", w: 364, h: 318, Body: MailBody },
{ id: "discord", title: "Discord", icon: "◍", sub: "Social Bog", w: 332, h: 236, Body: DiscordBody },
{ id: "calendar", title: "Calendar", icon: "◷", sub: "", w: 252, h: 184, Body: CalBody },
];
// memo: SimApp re-renders every sprite flight; the window scene only depends on these props
export const DesktopWindows = React.memo(function DesktopWindows({
positions,
onPos,
onFocus,
focused,
attnId,
winState,
}: {
positions: Record;
onPos: (id: string, p: Pos) => void;
onFocus: (id: string) => void;
focused: string | null;
attnId: string | null;
winState: WinState;
}) {
return (
<>
{WIN_DEFS.map((d, i) => (
))}
>
);
});