Fix inaccuracies in CLAUDE.md and add desktop/mobile implementation plans - #5
Fix inaccuracies in CLAUDE.md and add desktop/mobile implementation plans#5radroid wants to merge 222 commits into
Conversation
…lans CLAUDE.md: correct boot description, BootSequence→BootScreen component name, and .woff2→.woff font path. Add pointer to DESKTOP-PLAN.md. DESKTOP-PLAN.md: fix container-type (inline-size→size for cqh support), correct Phase 4 branching (apps branch from mac-os-1984-desktop, not pre-app-foundation), fix branch tree diagram, drop redundant isSelected from WindowState, collapse isMaximized two-step into direct context usage. MOBILE-PLAN.md: initial planning document for iPhone OS 1 mobile experience. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…mobile gate - Window positions stored as container percentages (0–1) instead of pixels so they scale automatically on maximize toggle - isMaximized state lifted from WindowManager context to page.tsx, passed as props to IMacG3Frame and Desktop - Maximize trigger simplified to chin button only (removed menu bar option) - Clarified mobile never advances past welcome phase - Drag listeners attached to document to prevent fast-drag cursor loss Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
POSTHOG-RESEARCH.md: architectural reference notes on PostHog.com's desktop-OS implementation (state model, window chrome, desktop shell, styling primitives). Compiled via parallel subagent exploration of the posthog.com repo. DESKTOP-PLAN.md updates inspired by the research: - Derived focused window (no stored activeWindowId) — top of z-stack IS the focused window via useMemo - Contiguous zIndex reshuffle on focusApp (no counter drift) - 5px click-vs-drag threshold on draggable title bars - Lazy content mount after window entry animation - Zoom-from-origin open animation using clicked-icon Rect - Cascade-on-open position spec and close-normalizes-zIndex rule - Menu bar z-index sits above window stack - Reduced-motion covers maximize + open + close animations - Inspiration & Anti-patterns section (7 patterns adopted, 9 rejected, 4 future enhancements) - Readiness Checklist marking PR #1 ready and app content blockers CLAUDE.md: note that dev server is always running and should not be started by automation. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Introduces the `'desktop'` screen phase and maximize mode as the foundation for the interactive Mac OS 1984 desktop experience. Screen phases: - ScreenPhase gains `'desktop'` - Welcome screen renders without its menu bar (menu bar moves to the desktop phase in Phase 2+3) - Desktop viewports auto-advance from `'welcome'` → `'desktop'` after a brief hold; mobile stays on welcome permanently Maximize mode: - `isMaximized` state lifted to `page.tsx`, passed to IMacG3Frame and CRTScreen as props (per DESKTOP-PLAN architecture) - Chin button on the iMac frame toggles maximize; a floating Restore button appears when maximized so the control stays reachable after the chin hides - IMacG3Frame hides body/chin/stand when maximized; CRTScreen expands to fill the viewport - CRTScreen gains `container-type: size` to enable cqw/cqh-based window sizing in later phases - All transitions respect prefers-reduced-motion Containing-block fix: - The initial intro scale animation is released after 700ms (new `introDone` state), so a persistent `transform: scale(1)` no longer creates a containing block that traps the maximized IMacG3Frame's `position: fixed, inset: 0`. Assets (user-provided): - public/app-icons/: 1-bit SVGs for calculator, control-panel, finder, journal, music, notepad, trash, world-map (browser icon pending) - public/apple-icon.svg - public/cv/Raj_Dholakia_Resume_FullStack.pdf Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Introduces the full interactive desktop — draggable windows,
placeholder apps, icon grid, and a dynamic menu bar that adapts to
the focused app. This is the last piece of the foundation PR before
app PRs can branch off in parallel.
Window manager (app/components/desktop/window-manager.tsx)
- React Context holding a windows record, selectedIconId, and an
hasOpenedAnyApp flag
- Derived activeWindowId via useMemo — top of z-stack is the focused
window; no separate focus field to sync
- focusApp does a contiguous zIndex reshuffle (no counter drift)
- closeApp re-normalizes zIndex so gaps never appear
- Cascade-on-open: first window at {12%, 14%}, +3% per next, wraps
back to base past {60%, 55%}
- moveWindow clears fromOrigin so the zoom animation can't replay
App registry (app/components/desktop/app-registry.tsx)
- Typed AppDefinition/MenuConfig/MenuItem/Rect
- All 8 apps registered with the user-provided 1-bit SVG icons
(browser.svg now included)
- clamp() + cqw/cqh sizes per the DESKTOP-PLAN
- Shared ComingSoon placeholder component until Phase 4 replaces it
- FINDER_DEFAULT_MENUS exported for the fallback menu bar state
Window component (app/components/desktop/window.tsx)
- Title bar with striped-active pattern, close box with an inline-SVG
X, focus-on-click
- Drag uses direct DOM transform: translate3d() during the move,
rAF-throttled; only the final position is committed via moveWindow
on mouseup. Avoids re-rendering every context consumer per frame.
- 5px click-vs-drag threshold so taps don't jitter the window
- One-frame `transition: none` skip on drag commit so the window
snaps to the new position without a ghost animation
- Zoom-from-origin open animation: first render at the clicked icon's
CRT-relative rect, then flipped to the cascaded position on the
next frame so CSS interpolates the delta
- Lazy content mount — app component only renders after the entry
animation completes (skipped under prefers-reduced-motion)
- Drag is constrained to the CRT screen bounds via container ref
DesktopIcon (app/components/desktop/desktop-icon.tsx)
- Single click selects (invert colors), double click opens and
captures the icon's rect for the zoom-from-origin animation
- `large` prop scales the icon/label up when the desktop is maximized
Desktop shell (app/components/desktop/desktop.tsx)
- Wraps children in WindowManagerProvider
- 2-column icon grid in the top-right so all 8 apps fit in the small
CRT; icons scale up in maximize mode
- Decorative Trash bottom-right with pointer-events: none
- Windows layer renders every open window; click on bare desktop
deselects the active icon
MaximizeNudge (app/components/desktop/maximize-nudge.tsx)
- Small Mac-style tip dialog that appears once after the first app
opens, suggesting Full Screen. Auto-dismisses when user maximizes
or clicks Not Now. Persisted to sessionStorage.
Menu bar refactor (app/components/menu-bar.tsx)
- Now reads derived activeWindowId from WindowManagerContext
- Apple-glyph menu always leftmost (SVG apple)
- Falls back to Finder defaults when no window is focused
- Per-app menuItems (when provided) override the defaults
- z-index 9999 so dropdowns cover windows
- No longer mounted in welcome-screen.tsx
page.tsx
- Desktop phase now renders <Desktop /> (the real shell) instead of
the Phase 1 placeholder text
- Under-construction indicator removed from the welcome screen
Assets
- public/app-icons/browser.svg added to round out all 8 app icons
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Trash was previously a decorative element pinned bottom-right with pointer-events: none. It's now a real entry in APP_REGISTRY with a Coming Soon placeholder component and a Finder-style status bar. Desktop shell renders Trash as a normal DesktopIcon at bottom-right (filtered out of the top-right 2-column grid so layout is preserved), so single-click selects and double-click opens with the usual zoom-from-origin animation. DESKTOP-PLAN.md Decision #11 updated to match — Trash is clickable but not functional beyond opening. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Scaffolds the shadcn/ui context-menu component (app/components/ui/ context-menu.tsx) and wires it into desktop icons and the bare desktop surface. shadcn component - Installed via `shadcn add context-menu` which added @radix-ui/react-context-menu to package.json - File rewritten to drop the lucide-react dependency and the Tailwind v3 theme tokens (bg-popover, rounded-md, animate-in, …) that don't exist in our Tailwind v4 alpha setup - Same export surface as stock shadcn, restyled inline with Mac OS 1 aesthetics: Chicago font, white bg, 1px black border, 2px black drop shadow, text glyphs (▶, ✓, ●) in place of lucide icons - Hover/keyboard-focus highlighting driven by Radix's data-highlighted attribute via a small rule in global.css Right-click handlers - DesktopIcon: App name label → Open, Get Info, Duplicate, Move to Trash. Right-click also selects the icon so it shows the highlight state before the menu opens. Icon is split into DesktopIcon (handles context menu + data) and IconButton (forwardRef button used by ContextMenuTrigger asChild). - Desktop surface: Curly OS label → Go Full Screen / Restore Screen (live-toggles based on isMaximized), Clean Up Desktop, Change Wallpaper, About This Macintosh. Wrapped the desktop container in ContextMenu/ContextMenuTrigger so empty-area right-clicks work. Icon highlight fixes - whiteSpace: normal on the label so two-word names (Curly Browser, Control Panel, Note Pad, World Map) wrap at the space and the black selection background covers the whole label instead of overflowing the button width - outline: none on the icon button so the browser's default blue focus ring doesn't appear after a right-click - Removed the filter: invert(1) on icon images when selected — the label highlight alone is enough; the icon art stays untouched Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Boot icon - SIZING.macIconSize reduced ~30% (100 → 70 desktop, 60 → 42 mobile) so the Happy Mac on the boot screen sits more comfortably in the CRT frame. Chin toggle - The chin used to be stacked (CD slot on top, separate circular button below) and the button was only rendered on welcome/desktop phases. This caused a visible chin-height shift when the screen transitioned out of boot. Now there is no separate button — the CD slot itself IS the maximize toggle. Always rendered (disabled during boot) so the chin height is constant across phases. - Hover brightens the slot with a soft white glow to hint at interactivity without breaking the iMac hardware aesthetic. - aria-label flips between "Maximize screen" and "Restore screen". Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Foundation: Mac OS 1984 desktop — maximize, windows, shell, menus, context menus
Lock in Browser (bookmarks launcher, no iframes), Finder (public/ mirror with Mac-styled folders), World Map (react-simple-maps + 24 visited countries), and Music (Spotify live now-playing with vinyl presentation). Update readiness checklist — all 8 apps unblocked; Music gated only on Spotify OAuth setup. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Pre-installed on base so all app branches inherit the deps without each needing its own install commit. react-simple-maps wraps d3-geo and handles TopoJSON rendering; world-atlas provides the country TopoJSON data. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace the original parallel-worktree-per-app strategy with a single branch (feat/desktop-apps → feat/mac-os-1984-desktop) containing one commit per app. The previous plan assumed subagents could run git/tsc/ gh inside isolated worktrees; in practice the session sandbox denies Bash for subagents, so the main agent drives every git operation while subagents write component files only. Also document the Spotify integration architecture in App 8: refresh- token grant for runtime (no per-URL redirect registration), one-time OAuth helper on 127.0.0.1:8888/callback for dev-time capture, and a per-environment env-var table (.env.local / Cloudflare Preview / Production) so the same route.ts works everywhere. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Node 18+ ESM helper. Reads SPOTIFY_CLIENT_ID/SECRET from .env.local, starts a loopback server on 127.0.0.1:8888, opens the browser to Spotify's authorize endpoint, and on /callback exchanges the auth code for a refresh token. Appends SPOTIFY_REFRESH_TOKEN to .env.local if not already present. The callback URL is used only for this one-time ritual — at runtime the Music app uses the refresh token server-side via the refresh_token grant, so no redirect URI registration is needed for Cloudflare preview or production deploys. Run: node scripts/spotify-oauth.mjs Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Four-column grid with C / ÷ × − + operators, = spanning two rows on the right, left-to-right chain evaluation matching the original Mac calculator (2 + 3 * 4 = 20), division-by-zero → "Error", keyboard shortcuts, and authentic 1-bit black/white button invert on :active. No chrome, lives inside the generic Window. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Inline SVG zigzag for the torn top edge (stretches across any width via viewBox + preserveAspectRatio), repeating-linear-gradient for the lined paper background, and an auto-saving textarea persisted to localStorage key `curly-os-notepad`. Pre-populates with a welcome note on first load; empty string is respected once the user clears. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Stacked panel sections: user card (avatar, name, role, location, contact links), live clock updating every second, system diagnostics (browser, OS, screen resolution, window size, language, connection type, timezone), Battery Manager readings with charging/levelchange listeners, and derived location from Intl.DateTimeFormat timezone. All runtime data comes from browser APIs — no network calls. Battery section degrades gracefully on Firefox/Safari where the API is absent. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Classic icon grid with folder navigation (path stack + Back button), Mac-styled labels (Applications ← app-icons, Documents ← cv, Fonts, Developer ← tech-icons), and an internal status bar that stays in sync with the current folder. Double-click opens a file preview overlay inside the Finder window: images/SVGs via <img>, audio via <audio>, PDFs via <iframe> (browser native viewer), fonts show a sample, unknowns fall back to "not available". Right-click provides Open, Download, Get Info (disabled), Rename (disabled) via the existing Shadcn ContextMenu. Also hoists FINDER_DEFAULT_MENUS above APP_REGISTRY so the finder entry can reference it without a temporal-dead-zone error, and drops the unused `statusBar` fallback since the component renders its own. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
User feedback on the first live screenshot was that the content was
hard to read at the default sizing. Bumps all shell and app font
sizes up one Chicago-friendly step (~10%):
shell
- desktop-icon: label 10→11 / 12→13, icon image 30→33 / 44→48,
tile width 82→90 / 96→106, gaps 2→3 / 4→5
- window title bar: 11→12, status bar: 10→11
- menu bar default fontSize 12→13, height 22→24
apps
- calculator: button 13→14, display 20→22
- note-pad: textarea 13→14, line-height 20→22, lined-paper
gradient bumped to match
- control-panel: section-header / label / value 11→12,
user name 13→14, link buttons 10→11
- finder: folder/file labels 12→13, secondary 11→12, heading
18→20, status bar 10→11, breadcrumb 13→14
Stays inside the 1-bit Chicago aesthetic — no hierarchy changes,
just a single step up on every hard-coded px size.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Classic Mac Scrapbook-style paged viewer. Extracted the 3 blog posts from CONTENT-ARCHIVE.md as inline data (no markdown renderer added — body is rendered via <pre style="white-space:pre-wrap"> for a period- accurate look). Nav footer with ◀ / ▶ arrows disabled at the ends, a "1 / N" page counter, and Left/Right keyboard navigation. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replaces the inline-drawn folder SVG with the user-provided 1-bit folder asset. Keeps the Mac 1 aesthetic consistent with the rest of the app icons and fixes the earlier too-cartoony rendering. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The active title bar's horizontal-stripes pattern used to go edge-to-edge and touched the close box, making focused windows look crowded and unlike the classic Mac OS 1 reference. Restructure the title bar into an outer white wrapper with a 2px inset and an inner row that carries the stripes; the close box and title text sit in their own white-padded spans so the stripes never crowd them, and the right side mirrors the close-box cluster width so the title stays visually centered. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The three blog posts were originally mis-dated on the previous website — they should all be 2026. Fixes the inlined Scrapbook data and the source CONTENT-ARCHIVE.md so anything pulling from it in the future picks up the right year. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Control Panel avatar 56→84px so the user card reads at a glance - Title bar stripes now run edge-to-edge horizontally (removed the outer left/right padding) so they meet the window border like the Mac OS 1 reference. Close box has 4px of white against the window edge and 6px against the stripes; title text has 10px cushion each side. Right side mirrors the close-box footprint to keep the title visually centered. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Marks commits 1-6 (plan, Spotify helper, Calculator, Note Pad, Control Panel, Finder) as done, logs the extra commits surfaced from reviewing the live preview (font/icon polish, folder icon swap, title-bar stripe fix, blog year correction, avatar bump), and leaves Browser / World Map / Music as the remaining work. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Previous fix still had a white cushion span wrapping the close box, which created a solid white block from the window edge inward before the stripes began. In the Mac OS 1 reference, the stripes fill the title bar edge-to-edge horizontally and the close box is an opaque white element sitting ON the stripes, with only a tiny padding inset (4px) from the window border. This rewire puts the padding on the stripe container itself — since background-image extends through padding, stripes fill the 4px inset too, and the close box + its transparent right-side mirror sit as opaque / transparent elements on that background. The title text still cuts a white hole via its own background. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
No iframes. Only view is a bookmarks home with 11 tiles: 8 projects (Penguin Mail, ARK Experience, Bridger, Stella 56 Diamonds, Playground, Couples Budget, 75 Creates, KayVee Gems) + 3 tools (Google, Claude, ChatGPT). Click opens in a new real-browser tab via window.open; right-click exposes Open in New Tab + Copy Link via the existing Shadcn ContextMenu. Disabled Back/Forward buttons and a decorative read-only address bar match the Mac 1 chrome. Tan info banner below the toolbar explains the "iframes get blocked" quirk up front. Favicons pulled from Google's S2 service and pixelated. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Uses react-simple-maps + world-atlas (TopoJSON via jsdelivr CDN) to render the Natural Earth country borders. Visited countries match against geo.properties.name (with both common name variants for the USA and Czechia to cover either world-atlas naming convention). Visited fill is a 4×4 checkerboard SVG <pattern> (two 1×1 black rects) — the canonical Mac OS 1 50% stipple, matching the worldmap reference aesthetic. A hidden 0×0 <svg><defs> hosts a second pattern id so the bottom-right legend swatch (which lives in a <div> outside the map's SVG) can resolve its url(#id) fill. Hover tooltip follows the cursor via position: fixed (survives the window's transform container in maximized mode). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Hovering a visited country now shows an inverted tooltip — black background, white text, with a ✓ prefix — so the "you've been there" status is instantly visible without having to match the dither fill against the legend. Unvisited countries keep the plain white tooltip. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Drops the bottom-right legend (and the hidden legend-pattern SVG that supported it) to reclaim that space. Switches from geoMercator to geoEqualEarth which has a more compact aspect ratio — no wasted polar stretch — and bumps the scale to 175 plus a small center shift ([10, 15]) so the populated latitudes fill the window. Sets width/height to 800×400 so the viewBox matches the compact projection shape instead of the default 800×600 which left ~200px of empty vertical space. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Trivial one-liners (handleOpen, openInNewTab, copyLink) inlined. The inline 'opacity: 0.4, cursor: default' on disabled menu items was inconsistent with every other disabled ContextMenuItem in the app (finder/desktop/desktop-icon all use bare 'disabled'); global CSS already handles the look via [data-disabled] + pointer-events: none.
…ech-icons textareaRef was assigned but never read; the onFocus handler duplicated outline: none already in the textarea's style. public/tech-icons/ was empty; removed from filesystem and from the finder folder-rename list in DESKTOP-PLAN (never implemented).
makePlaceholder was used once (for Trash); inlined as component: () => <ComingSoon name="Trash" />. The bearer-auth + no-store options pattern was duplicated 3 times in the spotify route; extracted as bearerOpts(token) so multi-line fetch calls collapse to one line.
Phase 5 polish items were all marked done per commit 3939e93. The file was never referenced from code or other docs and duplicated commit history.
Both the <pattern> tag's 4 attrs (world-map) and the outer flex <div> (not-found) fit comfortably on one line.
Sweep across 10 files — chicago, nav/chrome/tile base styles, content/item styles in the context-menu wrapper, calculator btnBase, etc. All were 4–6 lines with no CSSProperties value that needed per-line separation.
Implementation step-by-step tables (Phases 1–5) and Subagent Contracts template were execution artifacts for work now complete. Design Decisions, Architecture, Applications specs, and Inspiration/Anti- patterns retained. Updated ToC to match.
The PR #3 workflow steps and "Why This Strategy" rationale were execution notes for completed work. Branch tree retained as the canonical merge path.
Spotify route returns genres already sorted descending by count; the client's defensive re-sort added no behavior. Pass data.genres directly.
…ne line Across curly-browser/music/scrapbook/crt-screen/maximize-nudge/imac-frame/layout. The verbose 'destructure + type' block form gained no clarity vs the one-line signature.
…ach" This reverts commit 73e891d.
…licate import" This reverts commit ab6b538.
…res to one line" This reverts commit 6ff5cf9.
…nuDropdown props" This reverts commit 65417f8.
…line" This reverts commit 5d8d2c4.
…s to one line" This reverts commit a1bc0ae.
This reverts commit 9257aa5.
…iners" This reverts commit fb56a3e.
Visitors opening Finder now see the Resume PDF as the first item (top-left of the icon grid) alongside a single Assets folder containing the existing Applications and Fonts sub-folders plus the loose root-level files. Generalizes the Finder's navigation to walk pathStack arbitrarily deep so Assets can hold nested folders. https://claude.ai/code/session_01M9ddciS17Eq3bLLyAsv7sB
Deploying with
|
| Status | Name | Latest Commit | Preview URL | Updated (UTC) |
|---|---|---|---|---|
| ✅ Deployment successful! View logs |
raj-portfolio | f0e07f5 | Commit Preview URL Branch Preview URL |
Apr 29 2026, 04:20 PM |
📝 WalkthroughSummary by CodeRabbit
WalkthroughThe pull request introduces a comprehensive macOS-styled desktop environment with window management, a suite of productivity and entertainment applications, Spotify integration, and supporting documentation. New APIs, components, planning documents, and configuration files establish the foundation for a desktop interface with draggable windows, app registry, and multiple built-in apps. Changes
Sequence Diagram(s)sequenceDiagram
participant User
participant Desktop
participant WindowManager as WindowManager<br/>(Context)
participant APP_MAP as APP_MAP<br/>(Registry)
participant Window as Window<br/>(Component)
participant App as App<br/>(Component)
User->>Desktop: Click app icon
Desktop->>WindowManager: openApp(appId)
WindowManager->>APP_MAP: Look up app definition
APP_MAP-->>WindowManager: Return AppDefinition
WindowManager->>WindowManager: Calculate cascade position<br/>Allocate z-index<br/>Create window record
WindowManager-->>Desktop: windows[id] updated
Desktop->>Window: Render with fromOrigin
Window->>Window: Trigger zoom-from-origin<br/>animation (2 RAF)
Window->>Window: Lazy mount content<br/>(after animation)
Window->>App: Render app component
App-->>Window: Content rendered
Desktop->>User: Window visible & animated
User->>Window: Click window title bar
Window->>WindowManager: focusApp(appId)
WindowManager->>WindowManager: focusReshuffle()<br/>Move to max z-index<br/>Decrement others
WindowManager-->>Desktop: windows z-indices updated
Desktop->>User: Window brought to front
sequenceDiagram
participant MusicApp as Music App<br/>(Client)
participant Cache as Module Cache
participant APIRoute as /api/spotify/<br/>now-playing
participant Spotify as Spotify API
participant MusicApp2 as Music App<br/>(Poll)
MusicApp->>Cache: prefetchSpotify()
Cache->>APIRoute: fetch() GET
APIRoute->>APIRoute: Read SPOTIFY_CLIENT_ID<br/>SPOTIFY_CLIENT_SECRET<br/>SPOTIFY_REFRESH_TOKEN
APIRoute->>Spotify: POST /token<br/>(refresh_token grant)
Spotify-->>APIRoute: access_token
APIRoute->>Spotify: GET /me/player/<br/>currently-playing
APIRoute->>Spotify: GET /me/top/artists<br/>GET /me/top/genres
Spotify-->>APIRoute: Track + Top data
APIRoute->>APIRoute: Compute top 5 genres<br/>top 10 artists<br/>Normalize response
APIRoute-->>Cache: JSON response<br/>(no-cache headers)
Cache-->>MusicApp: Cached data
MusicApp->>MusicApp: Render with vinyl,<br/>genres, artists
MusicApp2->>MusicApp2: Poll every 10s<br/>Update display
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Review rate limit: 2/3 reviews remaining, refill in 20 minutes. Comment |
There was a problem hiding this comment.
Actionable comments posted: 12
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@app/api/spotify/now-playing/route.ts`:
- Around line 129-133: Start the fetchTopData work before awaiting the Spotify
currently-playing fetch to run them in parallel: call fetchTopData(access_token)
and store the promise in topPromise prior to awaiting the currently-playing
request (nowRes) so both network calls overlap, then await/consume topPromise as
needed (e.g., await topPromise or use Promise.all with the currently-playing
response) when you need the results; locate the code around the nowRes variable
and the fetchTopData call to make this change.
- Around line 106-164: The GET handler can throw if any Spotify fetch (token
exchange, currently-playing, recently-played or fetchTopData) rejects, producing
a 500 and breaking the expected JSON shape; wrap the entire function body in a
try/catch and normalize all error paths to return the same JSON contract (error,
isPlaying:false, track:null or lastTrack, plus top-data defaults). Add
per-request timeouts (use AbortController or a timeout wrapper) for the token
fetch, currently-playing fetch, recently-played fetch and the parallel
fetchTopData call (reference bearerOpts, fetchTopData,
tokenRes/nowRes/recentRes, makeTrack) and ensure you catch/recover from
fetchTopData rejection (e.g., await topPromise inside try with a safe fallback)
so transient network/Spotify errors return the stable offline payload instead of
throwing.
In `@app/components/apps/calculator.tsx`:
- Around line 104-116: The global keyboard handler in useEffect (onKeyDown) must
ignore keystrokes when the calculator is not active; modify onKeyDown to
early-return unless the event target is inside the calculator or the calculator
has focus by using a container ref (e.g., add a React ref like containerRef and
attach it to the root element) and checking
containerRef.current?.contains(e.target as Node) (or use a prop/state like
isActive if present), then only call
handleDigit/handleDecimal/handleOperator/handleEquals/handleClear when that
check passes; keep the existing addEventListener/removeEventListener but update
the dependency list to include the new ref or isActive if needed.
In `@app/components/apps/control-panel.tsx`:
- Around line 56-60: The UA detection branch orders cause Opera (OPR/...) to
match the Chrome (/Chrome\/(\d+)/) case first; move the /OPR\/(\d+)/ check
before the /Chrome\/(\d+)/ check (or alter the Chrome pattern to explicitly
exclude OPR/Chromium) so Opera returns `Opera ${...}`; update the corresponding
match extraction calls (the existing ua.match usages) accordingly to use the
correct branch for `OPR\/(\d+)` and keep the other checks for /Chromium\/(\d+)/
and /Safari\/(\d+)/ intact.
- Around line 94-106: The effect must avoid updating state or attaching
listeners after unmount: introduce a cancellation flag (e.g., let cancelled =
false) before calling nav.getBattery(), then in the promise handlers check the
flag and return early if cancelled; only call update(), setUnavailable(), or add
event listeners (battery.addEventListener('levelchange'/'chargingchange',
update)) when cancelled is false; in the cleanup set cancelled = true and still
remove listeners from battery if it was set (and only if the listeners were
previously attached) to prevent leaks — reference nav.getBattery(), the battery
variable, update(), and setUnavailable() when applying these guards.
In `@app/components/apps/music.tsx`:
- Around line 257-301: The issue is duplicate in-flight fetches causing race
updates; fix by reusing the shared in-flight promise (currently
_prefetchPromise) or add a new module-level _inflightPromise and make fetch
calls single-flight: update fetchSpotify/prefetchSpotify or have load() check
for and await the existing _inflightPromise instead of starting a new fetch,
ensure any caller (prefetchSpotify and load in MusicApp) sets and clears that
promise around the fetch so only one network request runs at a time, and replace
the setInterval polling with completion-driven polling (schedule next fetch with
setTimeout after the current one finishes) so polls never start while a previous
request is inflight; update references to _prefetchPromise, fetchSpotify, load,
_cachedData, and _cacheTime accordingly.
In `@app/components/apps/note-pad.tsx`:
- Around line 128-132: Remove the side-effectful state updater: the
setPages((prev) => { ... }) wrapper should not call setCurrentPage; instead call
e.preventDefault() and then invoke setCurrentPage directly (using the functional
updater form to compute the new page with Math.min(currentPagesLength - 1, p +
1)), and leave setPages unchanged since it isn't modifying pages. Ensure you
reference setPages and setCurrentPage by name so the change moves the navigation
logic outside the setPages updater.
In `@app/components/apps/scrapbook.tsx`:
- Around line 141-152: The global keydown handler is active even when the
scrapbook isn't focused; create a ref (e.g., scrapbookRef) on the component root
and update the useEffect/onKeyDown to only handle ArrowLeft/ArrowRight when
viewMode === 'article' AND the scrapbook has focus (check document.hasFocus()
and scrapbookRef.current.contains(document.activeElement) or similar); keep
setPage/total logic but early-return otherwise, and keep the existing
addEventListener/removeEventListener cleanup.
In `@app/components/apps/world-map.tsx`:
- Line 8: The component hardcodes GEO_URL to a CDN which can fail; instead
vendor the topology locally by adding a build step that copies
node_modules/world-atlas/countries-110m.json into public/data/world-atlas/
(create that directory), then update the GEO_URL constant in
app/components/apps/world-map.tsx to point to the local path
(/data/world-atlas/countries-110m.json); alternatively implement an API route
that reads the file from node_modules and serves it, and change GEO_URL to that
route—refer to the GEO_URL constant in world-map.tsx and the build/copy script
or API route when making the change.
In `@DESKTOP-PLAN.md`:
- Around line 55-83: The markdown has lint violations from missing blank lines
around headings and fenced code blocks and from unnamed fenced blocks; fix by
adding a blank line before and after each heading and each fenced code block and
by adding language tags to all fenced blocks (e.g., change ``` to ```typescript
for the Window Manager (React Context) block that contains types like Rect,
WindowState, and WindowManagerContextType), and apply the same fixes to the
other referenced sections (lines ~147-165, ~197-219, ~325-388) so all fenced
examples and headings comply with MD022/MD031/MD040.
- Around line 239-319: The documented "Size" entries in DESKTOP-PLAN.md are out
of sync with the actual defaults used by the running app registry (e.g., the
Music app's documented clamp sizes don't match the values defined for the
"Music" entry in app-registry.tsx); update DESKTOP-PLAN.md so each app's Size
lines exactly reflect the corresponding defaults in
app/components/desktop/app-registry.tsx (verify the "Music" app and any others
that differ), keeping the app names (e.g., "Music — live Spotify now-playing",
"Finder — 'Documents'", "Curly Browser", etc.) as anchors so future edits match
the canonical values in app-registry.tsx.
In `@scripts/spotify-oauth.mjs`:
- Around line 136-140: When the callback handler sees no code (the !code
branch), the response is ended but the HTTP callback server is left running;
update that branch in scripts/spotify-oauth.mjs to close the server after
sending the 400 response by invoking the server's close() method (e.g.,
server.close() or the exact server variable used where http.createServer(...) is
assigned), then return; locate the server created earlier (the variable assigned
from createServer/createListener) and call its close() immediately after res.end
to ensure the process can exit cleanly.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: 389a7c3e-27eb-4344-88af-5e8e157c1767
⛔ Files ignored due to path filters (17)
bun.lockis excluded by!**/*.lockpublic/app-icons/browser.svgis excluded by!**/*.svgpublic/app-icons/calculator.svgis excluded by!**/*.svgpublic/app-icons/control-panel.svgis excluded by!**/*.svgpublic/app-icons/finder.svgis excluded by!**/*.svgpublic/app-icons/folder.svgis excluded by!**/*.svgpublic/app-icons/journal.svgis excluded by!**/*.svgpublic/app-icons/music.svgis excluded by!**/*.svgpublic/app-icons/notepad.svgis excluded by!**/*.svgpublic/app-icons/trash.svgis excluded by!**/*.svgpublic/app-icons/world-map.svgis excluded by!**/*.svgpublic/apple-icon.svgis excluded by!**/*.svgpublic/browser-icons/back.svgis excluded by!**/*.svgpublic/browser-icons/forward.svgis excluded by!**/*.svgpublic/browser-icons/home.svgis excluded by!**/*.svgpublic/cv/Raj_Dholakia_Resume_FullStack.pdfis excluded by!**/*.pdfpublic/worldmap.pngis excluded by!**/*.png
📒 Files selected for processing (44)
.gitignoreCLAUDE.mdCONTENT-ARCHIVE.mdDESKTOP-PLAN.mdMOBILE-PLAN.mdREADME.mdapp/api/spotify/now-playing/route.tsapp/components/apps/calculator.tsxapp/components/apps/control-panel.tsxapp/components/apps/curly-browser.tsxapp/components/apps/finder.tsxapp/components/apps/music.tsxapp/components/apps/note-pad.tsxapp/components/apps/scrapbook.tsxapp/components/apps/world-map.tsxapp/components/boot-screen.tsxapp/components/crt-screen.tsxapp/components/desktop/app-registry.tsxapp/components/desktop/desktop-icon.tsxapp/components/desktop/desktop.tsxapp/components/desktop/maximize-nudge.tsxapp/components/desktop/window-manager.tsxapp/components/desktop/window.tsxapp/components/imac-frame.tsxapp/components/menu-bar.tsxapp/components/types.tsapp/components/ui/context-menu.tsxapp/components/welcome-screen.tsxapp/global.cssapp/layout.tsxapp/lib/use-reduced-motion.tsapp/lib/utils.tsapp/not-found.tsxapp/page.tsxapp/robots.tsapp/sitemap.tscomponents.jsonlighthouse-report-23-12.jsonnext.config.tspackage.jsonplanning-details.mdscripts/spotify-oauth.mjstypes/mdx-raw.d.tswrangler.toml
💤 Files with no reviewable changes (5)
- types/mdx-raw.d.ts
- app/lib/use-reduced-motion.ts
- app/lib/utils.ts
- wrangler.toml
- planning-details.md
| export async function GET() { | ||
| const clientId = process.env.SPOTIFY_CLIENT_ID | ||
| const clientSecret = process.env.SPOTIFY_CLIENT_SECRET | ||
| const refreshToken = process.env.SPOTIFY_REFRESH_TOKEN | ||
|
|
||
| if (!clientId || !clientSecret || !refreshToken) { | ||
| return json({ error: 'Spotify env vars not configured', isPlaying: false, track: null }) | ||
| } | ||
|
|
||
| // 1. Exchange refresh token for access token | ||
| const tokenRes = await fetch('https://accounts.spotify.com/api/token', { | ||
| method: 'POST', | ||
| headers: { | ||
| 'Content-Type': 'application/x-www-form-urlencoded', | ||
| 'Authorization': 'Basic ' + btoa(`${clientId}:${clientSecret}`), | ||
| }, | ||
| body: new URLSearchParams({ grant_type: 'refresh_token', refresh_token: refreshToken }), | ||
| }) | ||
|
|
||
| if (!tokenRes.ok) return json({ error: 'Spotify token refresh failed', isPlaying: false, track: null }) | ||
|
|
||
| const { access_token } = await tokenRes.json() as { access_token: string } | ||
|
|
||
| // 2. Fetch currently playing | ||
| const nowRes = await fetch('https://api.spotify.com/v1/me/player/currently-playing', bearerOpts(access_token)) | ||
|
|
||
| // Fetch top genres in parallel with the currently-playing check | ||
| const topPromise = fetchTopData(access_token) | ||
|
|
||
| // 204 = nothing playing, 202 = processing | ||
| if (nowRes.status === 204 || nowRes.status === 202) { | ||
| const recentRes = await fetch('https://api.spotify.com/v1/me/player/recently-played?limit=1', bearerOpts(access_token)) | ||
| let lastTrack: any = null | ||
| if (recentRes.ok) { | ||
| const data = await recentRes.json() as any | ||
| const first = (data.items ?? [])[0] | ||
| if (first?.track) lastTrack = makeTrack(first.track) | ||
| } | ||
| return json({ isPlaying: false, track: lastTrack, ...await topPromise }) | ||
| } | ||
|
|
||
| if (!nowRes.ok) { | ||
| const top = await topPromise | ||
| return json({ error: 'Spotify currently-playing failed', isPlaying: false, track: null, ...top }) | ||
| } | ||
|
|
||
| const nowData = await nowRes.json() as any | ||
| const item = nowData?.item | ||
| if (!item) { | ||
| const top2 = await topPromise | ||
| return json({ isPlaying: false, track: null, ...top2 }) | ||
| } | ||
|
|
||
| return json({ | ||
| isPlaying: nowData.is_playing === true, | ||
| track: makeTrack(item, { progressMs: nowData.progress_ms ?? 0 }), | ||
| ...await topPromise, | ||
| }) | ||
| } |
There was a problem hiding this comment.
Normalize thrown Spotify failures to the same JSON contract.
Right now only non-OK responses are handled. If the token refresh, currently-playing, recently-played, or either top-data fetch rejects, the exception skips json() entirely and the client gets a generic 500 instead of the stable offline payload it expects. Adding a top-level try/catch and per-request timeouts keeps transient Spotify/network failures from turning into hung or shape-breaking responses.
🛡️ Suggested hardening
-function json(body: unknown) {
- return Response.json(body, { headers: NO_CACHE_HEADERS })
+function json(body: unknown, init?: ResponseInit) {
+ return Response.json(body, {
+ ...init,
+ headers: { ...NO_CACHE_HEADERS, ...(init?.headers ?? {}) },
+ })
}
@@
export async function GET() {
- const clientId = process.env.SPOTIFY_CLIENT_ID
- const clientSecret = process.env.SPOTIFY_CLIENT_SECRET
- const refreshToken = process.env.SPOTIFY_REFRESH_TOKEN
+ try {
+ const clientId = process.env.SPOTIFY_CLIENT_ID
+ const clientSecret = process.env.SPOTIFY_CLIENT_SECRET
+ const refreshToken = process.env.SPOTIFY_REFRESH_TOKEN
- if (!clientId || !clientSecret || !refreshToken) {
- return json({ error: 'Spotify env vars not configured', isPlaying: false, track: null })
- }
+ if (!clientId || !clientSecret || !refreshToken) {
+ return json({ error: 'Spotify env vars not configured', isPlaying: false, track: null, genres: [], artists: [] }, { status: 500 })
+ }
- const tokenRes = await fetch('https://accounts.spotify.com/api/token', {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/x-www-form-urlencoded',
- 'Authorization': 'Basic ' + btoa(`${clientId}:${clientSecret}`),
- },
- body: new URLSearchParams({ grant_type: 'refresh_token', refresh_token: refreshToken }),
- })
+ const tokenRes = await fetch('https://accounts.spotify.com/api/token', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/x-www-form-urlencoded',
+ 'Authorization': 'Basic ' + btoa(`${clientId}:${clientSecret}`),
+ },
+ body: new URLSearchParams({ grant_type: 'refresh_token', refresh_token: refreshToken }),
+ signal: AbortSignal.timeout(5000),
+ })
@@
- return json({
- isPlaying: nowData.is_playing === true,
- track: makeTrack(item, { progressMs: nowData.progress_ms ?? 0 }),
- ...await topPromise,
- })
+ return json({
+ isPlaying: nowData.is_playing === true,
+ track: makeTrack(item, { progressMs: nowData.progress_ms ?? 0 }),
+ ...await topPromise,
+ })
+ } catch {
+ return json(
+ { error: 'Spotify request failed', isPlaying: false, track: null, genres: [], artists: [] },
+ { status: 502 },
+ )
+ }
}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@app/api/spotify/now-playing/route.ts` around lines 106 - 164, The GET handler
can throw if any Spotify fetch (token exchange, currently-playing,
recently-played or fetchTopData) rejects, producing a 500 and breaking the
expected JSON shape; wrap the entire function body in a try/catch and normalize
all error paths to return the same JSON contract (error, isPlaying:false,
track:null or lastTrack, plus top-data defaults). Add per-request timeouts (use
AbortController or a timeout wrapper) for the token fetch, currently-playing
fetch, recently-played fetch and the parallel fetchTopData call (reference
bearerOpts, fetchTopData, tokenRes/nowRes/recentRes, makeTrack) and ensure you
catch/recover from fetchTopData rejection (e.g., await topPromise inside try
with a safe fallback) so transient network/Spotify errors return the stable
offline payload instead of throwing.
| // 2. Fetch currently playing | ||
| const nowRes = await fetch('https://api.spotify.com/v1/me/player/currently-playing', bearerOpts(access_token)) | ||
|
|
||
| // Fetch top genres in parallel with the currently-playing check | ||
| const topPromise = fetchTopData(access_token) |
There was a problem hiding this comment.
Start fetchTopData() before awaiting the currently-playing request.
The comment says these calls run in parallel, but Line 130 fully awaits currently-playing before Line 133 even starts the top-data fetches. Since this route is polled every 10 seconds, that extra serial RTT is paid on every refresh.
♻️ Minimal fix
- const nowRes = await fetch('https://api.spotify.com/v1/me/player/currently-playing', bearerOpts(access_token))
-
- // Fetch top genres in parallel with the currently-playing check
- const topPromise = fetchTopData(access_token)
+ const nowPromise = fetch('https://api.spotify.com/v1/me/player/currently-playing', bearerOpts(access_token))
+ const topPromise = fetchTopData(access_token)
+ const nowRes = await nowPromise📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| // 2. Fetch currently playing | |
| const nowRes = await fetch('https://api.spotify.com/v1/me/player/currently-playing', bearerOpts(access_token)) | |
| // Fetch top genres in parallel with the currently-playing check | |
| const topPromise = fetchTopData(access_token) | |
| // 2. Fetch currently playing | |
| const nowPromise = fetch('https://api.spotify.com/v1/me/player/currently-playing', bearerOpts(access_token)) | |
| const topPromise = fetchTopData(access_token) | |
| const nowRes = await nowPromise |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@app/api/spotify/now-playing/route.ts` around lines 129 - 133, Start the
fetchTopData work before awaiting the Spotify currently-playing fetch to run
them in parallel: call fetchTopData(access_token) and store the promise in
topPromise prior to awaiting the currently-playing request (nowRes) so both
network calls overlap, then await/consume topPromise as needed (e.g., await
topPromise or use Promise.all with the currently-playing response) when you need
the results; locate the code around the nowRes variable and the fetchTopData
call to make this change.
| useEffect(() => { | ||
| const onKeyDown = (e: KeyboardEvent) => { | ||
| const k = e.key | ||
| if (k >= '0' && k <= '9') handleDigit(k) | ||
| else if (k === '.') handleDecimal() | ||
| else if (k === '+' || k === '-' || k === '*') handleOperator(k) | ||
| else if (k === '/') { e.preventDefault(); handleOperator('/') } | ||
| else if (k === '=' || k === 'Enter') handleEquals() | ||
| else if (k === 'Escape' || k === 'c' || k === 'C') handleClear() | ||
| } | ||
| window.addEventListener('keydown', onKeyDown) | ||
| return () => window.removeEventListener('keydown', onKeyDown) | ||
| }, [handleDigit, handleDecimal, handleOperator, handleEquals, handleClear]) |
There was a problem hiding this comment.
Scope keyboard handling to the active calculator window.
Line 114 attaches a global window listener with no focus/active-window guard, so keystrokes can mutate calculator state even when another app/window is being used.
Suggested fix
-import { useState, useEffect, useCallback } from 'react'
+import { useState, useEffect, useCallback, useRef } from 'react'
@@
export function CalculatorApp() {
const [state, setState] = useState<CalcState>(initialState)
+ const rootRef = useRef<HTMLDivElement>(null)
@@
useEffect(() => {
const onKeyDown = (e: KeyboardEvent) => {
+ if (!rootRef.current?.contains(document.activeElement)) return
+ if (e.metaKey || e.ctrlKey || e.altKey) return
const k = e.key
@@
- return (
- <div style={{ flex: 1, display: 'flex', flexDirection: 'column', background: '#fff', overflow: 'hidden' }}>
+ return (
+ <div
+ ref={rootRef}
+ tabIndex={0}
+ onMouseDown={() => rootRef.current?.focus()}
+ style={{ flex: 1, display: 'flex', flexDirection: 'column', background: '#fff', overflow: 'hidden' }}
+ >🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@app/components/apps/calculator.tsx` around lines 104 - 116, The global
keyboard handler in useEffect (onKeyDown) must ignore keystrokes when the
calculator is not active; modify onKeyDown to early-return unless the event
target is inside the calculator or the calculator has focus by using a container
ref (e.g., add a React ref like containerRef and attach it to the root element)
and checking containerRef.current?.contains(e.target as Node) (or use a
prop/state like isActive if present), then only call
handleDigit/handleDecimal/handleOperator/handleEquals/handleClear when that
check passes; keep the existing addEventListener/removeEventListener but update
the dependency list to include the new ref or isActive if needed.
| if (/Chrome\/(\d+)/.test(ua) && !/Chromium/.test(ua)) return `Chrome ${ua.match(/Chrome\/(\d+)/)?.[1] ?? ''}` | ||
| if (/Firefox\/(\d+)/.test(ua)) return `Firefox ${ua.match(/Firefox\/(\d+)/)?.[1] ?? ''}` | ||
| if (/Safari\/(\d+)/.test(ua) && /Version\/(\d+)/.test(ua)) return `Safari ${ua.match(/Version\/(\d+)/)?.[1] ?? ''}` | ||
| if (/OPR\/(\d+)/.test(ua)) return `Opera ${ua.match(/OPR\/(\d+)/)?.[1] ?? ''}` | ||
| if (/Chromium\/(\d+)/.test(ua)) return `Chromium ${ua.match(/Chromium\/(\d+)/)?.[1] ?? ''}` |
There was a problem hiding this comment.
Opera user agents are detected as Chrome due branch order.
Line 56 checks Chrome/... before Line 59 checks OPR/..., so Opera is reported as Chrome.
Suggested fix
function detectBrowser(ua: string): string {
if (/Edg\/(\d+)/.test(ua)) return `Edge ${ua.match(/Edg\/(\d+)/)?.[1] ?? ''}`
+ if (/OPR\/(\d+)/.test(ua)) return `Opera ${ua.match(/OPR\/(\d+)/)?.[1] ?? ''}`
if (/Chrome\/(\d+)/.test(ua) && !/Chromium/.test(ua)) return `Chrome ${ua.match(/Chrome\/(\d+)/)?.[1] ?? ''}`
if (/Firefox\/(\d+)/.test(ua)) return `Firefox ${ua.match(/Firefox\/(\d+)/)?.[1] ?? ''}`
if (/Safari\/(\d+)/.test(ua) && /Version\/(\d+)/.test(ua)) return `Safari ${ua.match(/Version\/(\d+)/)?.[1] ?? ''}`
- if (/OPR\/(\d+)/.test(ua)) return `Opera ${ua.match(/OPR\/(\d+)/)?.[1] ?? ''}`
if (/Chromium\/(\d+)/.test(ua)) return `Chromium ${ua.match(/Chromium\/(\d+)/)?.[1] ?? ''}`
return 'Unknown'
}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@app/components/apps/control-panel.tsx` around lines 56 - 60, The UA detection
branch orders cause Opera (OPR/...) to match the Chrome (/Chrome\/(\d+)/) case
first; move the /OPR\/(\d+)/ check before the /Chrome\/(\d+)/ check (or alter
the Chrome pattern to explicitly exclude OPR/Chromium) so Opera returns `Opera
${...}`; update the corresponding match extraction calls (the existing ua.match
usages) accordingly to use the correct branch for `OPR\/(\d+)` and keep the
other checks for /Chromium\/(\d+)/ and /Safari\/(\d+)/ intact.
| nav.getBattery().then((b) => { | ||
| battery = b | ||
| update() | ||
| b.addEventListener('levelchange', update) | ||
| b.addEventListener('chargingchange', update) | ||
| }).catch(() => setUnavailable(true)) | ||
|
|
||
| return () => { | ||
| if (!battery) return | ||
| battery.removeEventListener('levelchange', update) | ||
| battery.removeEventListener('chargingchange', update) | ||
| } | ||
| }, []) |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Verify async cleanup pattern around navigator.getBattery in ControlPanelApp
rg -n -C4 "getBattery\\(\\)\\.then|let battery|return \\(\\) =>" app/components/apps/control-panel.tsxRepository: radroid/curly-cloud
Length of output: 1009
Guard getBattery() resolution after unmount to prevent state updates and listener leaks.
If unmount happens before navigator.getBattery() resolves, the promise's .then() callback will still execute, attaching listeners and calling setBatteryState after unmount. Similarly, the .catch() handler may call setUnavailable post-unmount. Add a cancellation flag to prevent all these operations if the effect cleans up first.
Suggested fix
useEffect(() => {
const nav = navigator as NavWithBattery
if (!nav.getBattery) { setUnavailable(true); return }
let battery: BatteryManager | null = null
+ let cancelled = false
const update = () => {
- if (battery) setBatteryState({ level: battery.level, charging: battery.charging })
+ if (!cancelled && battery) setBatteryState({ level: battery.level, charging: battery.charging })
}
nav.getBattery().then((b) => {
+ if (cancelled) return
battery = b
update()
b.addEventListener('levelchange', update)
b.addEventListener('chargingchange', update)
- }).catch(() => setUnavailable(true))
+ }).catch(() => {
+ if (!cancelled) setUnavailable(true)
+ })
return () => {
+ cancelled = true
if (!battery) return
battery.removeEventListener('levelchange', update)
battery.removeEventListener('chargingchange', update)
}
}, [])🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@app/components/apps/control-panel.tsx` around lines 94 - 106, The effect must
avoid updating state or attaching listeners after unmount: introduce a
cancellation flag (e.g., let cancelled = false) before calling nav.getBattery(),
then in the promise handlers check the flag and return early if cancelled; only
call update(), setUnavailable(), or add event listeners
(battery.addEventListener('levelchange'/'chargingchange', update)) when
cancelled is false; in the cleanup set cancelled = true and still remove
listeners from battery if it was set (and only if the listeners were previously
attached) to prevent leaks — reference nav.getBattery(), the battery variable,
update(), and setUnavailable() when applying these guards.
| useEffect(() => { | ||
| const onKeyDown = (e: KeyboardEvent) => { | ||
| if (viewMode !== 'article') return | ||
| if (e.key === 'ArrowLeft') { | ||
| setPage((p) => Math.max(0, p - 1)) | ||
| } else if (e.key === 'ArrowRight') { | ||
| setPage((p) => Math.min(total - 1, p + 1)) | ||
| } | ||
| } | ||
| window.addEventListener('keydown', onKeyDown) | ||
| return () => window.removeEventListener('keydown', onKeyDown) | ||
| }, [total, viewMode]) |
There was a problem hiding this comment.
Bind article hotkeys to focused scrapbook context only.
Line 150 registers a global listener; when this app is open in article mode, ArrowLeft/ArrowRight can change pages even if another window is active.
Suggested fix
-import { useState, useEffect } from 'react'
+import { useState, useEffect, useRef } from 'react'
@@
export function ScrapbookApp() {
+ const rootRef = useRef<HTMLDivElement>(null)
const [page, setPage] = useState(0)
@@
useEffect(() => {
+ if (viewMode !== 'article') return
const onKeyDown = (e: KeyboardEvent) => {
- if (viewMode !== 'article') return
+ if (!rootRef.current?.contains(document.activeElement)) return
if (e.key === 'ArrowLeft') {
setPage((p) => Math.max(0, p - 1))
} else if (e.key === 'ArrowRight') {
setPage((p) => Math.min(total - 1, p + 1))
}
@@
- return (
- <div
+ return (
+ <div
+ ref={rootRef}
+ tabIndex={0}
+ onMouseDown={() => rootRef.current?.focus()}
style={{🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@app/components/apps/scrapbook.tsx` around lines 141 - 152, The global keydown
handler is active even when the scrapbook isn't focused; create a ref (e.g.,
scrapbookRef) on the component root and update the useEffect/onKeyDown to only
handle ArrowLeft/ArrowRight when viewMode === 'article' AND the scrapbook has
focus (check document.hasFocus() and
scrapbookRef.current.contains(document.activeElement) or similar); keep
setPage/total logic but early-return otherwise, and keep the existing
addEventListener/removeEventListener cleanup.
|
|
||
| // ── Data ────────────────────────────────────────────────────────────────────── | ||
|
|
||
| const GEO_URL = 'https://cdn.jsdelivr.net/npm/world-atlas@2/countries-110m.json' |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Verify current geography source and check for local vendored topology asset
rg -n "GEO_URL|Geographies geography" app/components/apps/world-map.tsx
fd -i "countries-110m.json"Repository: radroid/curly-cloud
Length of output: 191
🏁 Script executed:
cat -n app/components/apps/world-map.tsxRepository: radroid/curly-cloud
Length of output: 5959
🏁 Script executed:
# Search for error handling, try-catch, or fallback mechanisms in the file
rg -i "error|catch|fallback|optional" app/components/apps/world-map.tsx -A 2Repository: radroid/curly-cloud
Length of output: 45
🏁 Script executed:
# Check if there are any other geography/topology data sources or build-time data
fd -i "atlas|geography|topology|geo" --type fRepository: radroid/curly-cloud
Length of output: 45
🏁 Script executed:
# Search for Error Boundary or error handling patterns in parent components
fd -e tsx -e ts app/components/ | head -20 | xargs rg -l "Error|Suspense|ErrorBoundary" 2>/dev/null | head -10Repository: radroid/curly-cloud
Length of output: 580
🏁 Script executed:
# Check public directory for any vendored geography data
find public -type f -name "*.json" 2>/dev/null | head -20Repository: radroid/curly-cloud
Length of output: 45
🏁 Script executed:
# Search for any build scripts or deployment configs that might handle this
fd -e json -e js -e mjs package.json tsconfig.json next.config.js next.config.mjs 2>/dev/null | xargs cat 2>/dev/null | grep -i "public\|public\|static\|build" | head -15Repository: radroid/curly-cloud
Length of output: 45
🏁 Script executed:
# Search across the codebase for any other references to world-atlas or GEO_URL
rg "world-atlas|GEO_URL" --type ts --type tsxRepository: radroid/curly-cloud
Length of output: 91
🏁 Script executed:
# Search for GEO_URL and world-atlas references with correct syntax
rg "world-atlas|GEO_URL" -iRepository: radroid/curly-cloud
Length of output: 658
🏁 Script executed:
# Check if public directory has any assets
ls -la public/ 2>/dev/null || echo "No public directory"Repository: radroid/curly-cloud
Length of output: 889
🏁 Script executed:
# Find where WorldMapApp is imported/used
rg "WorldMapApp" -iRepository: radroid/curly-cloud
Length of output: 285
🏁 Script executed:
# Check for Error Boundary components in the codebase
rg "ErrorBoundary|error.*boundary" -i --max-count=10Repository: radroid/curly-cloud
Length of output: 45
🏁 Script executed:
# Check if world-atlas has the file accessible in node_modules
find node_modules/world-atlas -name "countries-110m.json" 2>/dev/null || echo "Not found in node_modules"Repository: radroid/curly-cloud
Length of output: 89
🏁 Script executed:
# Check Next.js or build configuration
cat next.config.mjs next.config.js 2>/dev/null || echo "No Next.js config found"Repository: radroid/curly-cloud
Length of output: 87
🏁 Script executed:
# Check if there's a build or copy step in package.json
cat package.json | grep -A 5 '"scripts"'Repository: radroid/curly-cloud
Length of output: 334
🏁 Script executed:
# Search for any import or require of world-atlas in the codebase
rg "import.*world-atlas|require.*world-atlas"Repository: radroid/curly-cloud
Length of output: 45
Vendor map topology locally to prevent CDN outages.
Line 8 hardcodes a third-party CDN URL with no local fallback; if unavailable, the map renders nothing. The world-atlas package is already a dependency but unused. Either vendor the file in public/data/ with a build step to copy from node_modules, or load it via an API route. The suggested path below requires creating the directory and implementing the copy mechanism during build.
Suggested approach
- Add a build step to copy
node_modules/world-atlas/countries-110m.jsontopublic/data/world-atlas/ - Update the URL reference:
-const GEO_URL = 'https://cdn.jsdelivr.net/npm/world-atlas@2/countries-110m.json'
+const GEO_URL = '/data/world-atlas/countries-110m.json'Alternatively, import the file from node_modules at runtime via an API route instead of using a CDN.
Also applies to: 86-86
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@app/components/apps/world-map.tsx` at line 8, The component hardcodes GEO_URL
to a CDN which can fail; instead vendor the topology locally by adding a build
step that copies node_modules/world-atlas/countries-110m.json into
public/data/world-atlas/ (create that directory), then update the GEO_URL
constant in app/components/apps/world-map.tsx to point to the local path
(/data/world-atlas/countries-110m.json); alternatively implement an API route
that reads the file from node_modules and serves it, and change GEO_URL to that
route—refer to the GEO_URL constant in world-map.tsx and the build/copy script
or API route when making the change.
| ### Desktop Shell | ||
| - Replaces `WelcomeScreen` on desktop viewports after boot | ||
| - Crosshatch/dithered gray background (Mac OS 1984 pattern, already in `global.css`) | ||
| - Grid of app icons (classic Finder-style) | ||
| - Decorative Trash icon bottom-right | ||
| - Menu bar at top — dynamic per focused app | ||
|
|
||
| ### Window Manager (React Context) | ||
| ```typescript | ||
| type Rect = { x: number; y: number; width: number; height: number } // px relative to CRT screen (the drag container) | ||
|
|
||
| type WindowState = { | ||
| appId: string | ||
| position: { x: number; y: number } // percentage of container (0–1), e.g. { x: 0.15, y: 0.20 } | ||
| zIndex: number // contiguous 0..n across open windows (not a counter) | ||
| isOpen: boolean | ||
| fromOrigin?: Rect // clicked-icon rect (CRT-relative px) for zoom-from-origin animation; cleared after open animation completes | ||
| } | ||
|
|
||
| type WindowManagerContextType = { | ||
| windows: Record<string, WindowState> | ||
| selectedIconId: string | null // currently highlighted desktop icon (single-click selection) | ||
| openApp: (appId: string, fromOrigin?: Rect) => void // cascaded position; fromOrigin enables zoom-in animation | ||
| closeApp: (appId: string) => void // unmounts the app; re-normalizes zIndex to keep remaining windows contiguous | ||
| focusApp: (appId: string) => void // reshuffles zIndex so target becomes top of stack (see below) | ||
| selectIcon: (appId: string | null) => void // single-click highlight | ||
| moveWindow: (appId: string, pos: { x: number; y: number }) => void // pos in percentage (0–1) | ||
| } | ||
| ``` |
There was a problem hiding this comment.
Fix the markdownlint violations before this becomes the canonical plan.
This file still has repeated MD022/MD031/MD040 warnings: headings and fenced blocks are missing surrounding blank lines, and several fenced blocks have no language tag. On a long-lived reference doc, leaving lint noisy either breaks docs checks or trains future edits to ignore real warnings.
Also applies to: 147-165, 197-219, 325-388
🧰 Tools
🪛 LanguageTool
[uncategorized] ~57-~57: The operating system from Apple is written “macOS”
Context: ... - Crosshatch/dithered gray background (Mac OS 1984 pattern, already in global.css) ...
(MAC_OS)
🪛 markdownlint-cli2 (0.22.1)
[warning] 55-55: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below
(MD022, blanks-around-headings)
[warning] 62-62: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below
(MD022, blanks-around-headings)
[warning] 63-63: Fenced code blocks should be surrounded by blank lines
(MD031, blanks-around-fences)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@DESKTOP-PLAN.md` around lines 55 - 83, The markdown has lint violations from
missing blank lines around headings and fenced code blocks and from unnamed
fenced blocks; fix by adding a blank line before and after each heading and each
fenced code block and by adding language tags to all fenced blocks (e.g., change
``` to ```typescript for the Window Manager (React Context) block that contains
types like Rect, WindowState, and WindowManagerContextType), and apply the same
fixes to the other referenced sections (lines ~147-165, ~197-219, ~325-388) so
all fenced examples and headings comply with MD022/MD031/MD040.
| ### App 1: Web Browser — "Curly Browser" | ||
| - **Purpose**: Bookmarks launcher disguised as a Mac OS 1 browser | ||
| - **Design**: Mac OS 1 window chrome with **disabled** back/forward buttons, decorative read-only address bar, info banner, and a bookmarks "home page" as the only view. No internal navigation. | ||
| - **Key behavior**: Clicking any bookmark calls `window.open(url, '_blank', 'noopener,noreferrer')` — opens in the user's real browser, new tab. Right-click uses the existing Shadcn ContextMenu for "Open in New Tab" and "Copy Link" (actually copies via `navigator.clipboard`). | ||
| - **Info banner** (styled tan/yellow, top of content area): `"Curly Browser uses iframes, which most sites block for security. Links open in your real browser instead."` — this explains the quirk instead of hiding it. | ||
| - **Bookmarks** (11 total, grouped visually into "Projects" and "Tools"): | ||
| - **Projects (8)**: Penguin Mail (penguinmail.app), ARK Experience (funwithark.ca), Bridger (bridger.atawalk.ca), Stella 56 Diamonds (stella56diamonds.com), Playground (playground.createplus.club), Couples Budget (couplesbudget.ca), 75 Creates (75.createplus.club), KayVee Gems (kayveegems.com) | ||
| - **Tools (3)**: Google, Claude (claude.ai), ChatGPT (chatgpt.com) | ||
| - **Favicons**: fetched via `https://www.google.com/s2/favicons?domain=<host>&sz=128`, rendered with `imageRendering: pixelated` for lo-fi vibe. | ||
| - **Size**: `clamp(300px, 78cqw, 600px)` × `clamp(250px, 75cqh, 450px)` (large) | ||
| - **Does not iframe anything.** Don't try. The whole point is that it doesn't. | ||
|
|
||
| ### App 2: Note Pad | ||
| - **Purpose**: Fun interactive element | ||
| - **Design**: Mac OS 1 Note Pad (simple lined text area, torn-paper top edge) | ||
| - **Features**: Single pad, localStorage save, pre-populated welcome message | ||
| - **Menu**: File > Clear Note | ||
| - **Size**: `clamp(180px, 40cqw, 300px)` × `clamp(220px, 60cqh, 380px)` (medium) | ||
|
|
||
| ### App 3: System — "Control Panel" | ||
| - **Purpose**: Visitor diagnostics + Raj's info | ||
| - **Design**: Control Panel style (dark panels, icons) from reference image 2 | ||
| - **Features**: Browser, OS, screen res, timezone, connection, language; Raj's name, role, links | ||
| - **Size**: `clamp(250px, 55cqw, 420px)` × `clamp(200px, 55cqh, 350px)` (medium) | ||
|
|
||
| ### App 4: Calculator | ||
| - **Purpose**: Functional easter egg | ||
| - **Design**: Exact Mac OS 1 calculator (reference image 2) | ||
| - **Features**: C, E, =, *, 0-9, +, -, /, . — basic arithmetic | ||
| - **Size**: `clamp(120px, 25cqw, 200px)` × `clamp(180px, 45cqh, 280px)` (small) | ||
|
|
||
| ### App 5: Finder — "Documents" | ||
| - **Purpose**: File explorer that mirrors the real `public/` folder with Mac-styled folder names for flavor | ||
| - **Design**: Classic Finder icon grid with breadcrumb path + Back button, scrollable content area, status bar footer | ||
| - **Folder rename map** (display label ← actual path): | ||
| - "Macintosh HD" — root | ||
| - "Applications" ← `public/app-icons/` | ||
| - "Documents" ← `public/cv/` (contains Resume.pdf) | ||
| - "Fonts" ← `public/fonts/` | ||
| - Loose files at root (Apple Logo.svg ← apple-icon.svg, Startup Sound.wav ← StartupMacI.wav, Avatar.webp ← raj-avatar.webp, etc.) | ||
| - **Interactions**: | ||
| - Single-click: selects the item (text-inverted label highlight, matching desktop icons) | ||
| - Double-click folder: navigate into it (path stack) | ||
| - Double-click file: preview in an **absolute-positioned overlay** within the Finder window (not a new top-level window — overlay has its own close button, ESC dismisses) | ||
| - Right-click file (Shadcn ContextMenu): Open, Download, Get Info (disabled), Rename (disabled) | ||
| - Back button: pop path stack | ||
| - **Preview overlay by file type**: | ||
| - `.svg` / `.png` / `.webp` / `.jpg` → `<img>` with object-fit contain | ||
| - `.wav` / `.mp3` → `<audio controls>` | ||
| - `.pdf` → `<iframe src={path}>` (browser native PDF viewer) | ||
| - `.woff` / `.woff2` / `.ttf` → message + sample text rendered in the font | ||
| - anything else → "Preview not available" + Download hint | ||
| - **Download**: right-click → Download triggers `<a href={path} download>.click()` | ||
| - **No cover letter** — ship with just the resume for now. Nothing to gate on. | ||
| - **Status bar**: "N items, 72K in disk, 400K available" (fake-realistic) | ||
| - **Size**: `clamp(220px, 48cqw, 380px)` × `clamp(180px, 50cqh, 320px)` (medium) | ||
| - **Menu**: uses the existing `FINDER_DEFAULT_MENUS` export from `app-registry.tsx` | ||
|
|
||
| ### App 6: Scrapbook — "Journal" | ||
| - **Purpose**: Blog posts | ||
| - **Design**: Mac OS Scrapbook — page-by-page with arrows | ||
| - **Content**: 3 blog posts from CONTENT-ARCHIVE.md | ||
| - **Size**: `clamp(250px, 55cqw, 420px)` × `clamp(220px, 60cqh, 380px)` (medium) | ||
|
|
||
| ### App 7: World Map | ||
| - **Purpose**: Travel showcase — 24 visited countries highlighted | ||
| - **Design**: Public-domain world map, dithered SVG pattern fill on visited countries, plain white on unvisited, black stroke throughout. Aesthetic reference: `public/worldmap.png` (a Risk-style Mac OS screenshot — style only; **no** game chrome, Player1, Done/Fortify/Cards). | ||
| - **Visited countries (24, ISO alpha-3)**: | ||
| `CAN, USA, GBR, FRA, DEU, AUT, ITA, CHE, HUN, CZE, NLD, BEL, LUX, EGY, SAU, ARE, IND, JPN, THA, MYS, SGP, LKA, MUS, NZL` | ||
| (Canada, United States, United Kingdom, France, Germany, Austria, Italy, Switzerland, Hungary, Czechia, Netherlands, Belgium, Luxembourg, Egypt, Saudi Arabia, UAE, India, Japan, Thailand, Malaysia, Singapore, Sri Lanka, Mauritius, New Zealand) | ||
| - **Features**: Hover tooltip with country name; small legend at a corner (patterned box + plain box with labels); viewBox scales map to window; visited count shown subtly ("24 / ~195 countries"). | ||
| - **Map data**: `react-simple-maps` + `world-atlas` (TopoJSON, ~100KB) — installed as deps on `feat/mac-os-1984-desktop` before the app PR begins. | ||
| - **Size**: `clamp(280px, 70cqw, 540px)` × `clamp(220px, 60cqh, 380px)` (large) | ||
|
|
||
| ### App 8: Music — live Spotify now-playing (vinyl parody of Apple Music) | ||
| - **Purpose**: Show what Raj is currently listening to on Spotify, as a playful Mac-era vinyl record | ||
| - **Design**: Spinning vinyl record (CSS rotate animation) with the current album art as the record label (circular mask), playlist list beside it showing recent tracks. Chicago font captions. | ||
| - **Live data**: Spotify Web API `me/player/currently-playing` endpoint, polled every ~10s. Uses refresh-token OAuth flow so no user login is needed by visitors — it's always Raj's account. | ||
| - **Fallback**: If nothing playing, show a featured/last-played track (or a static "Offline" vinyl). | ||
| - **Reduced-motion**: freeze the vinyl rotation when `prefers-reduced-motion: reduce`. | ||
| - **Size**: `clamp(180px, 40cqw, 300px)` × `clamp(220px, 60cqh, 380px)` (medium) |
There was a problem hiding this comment.
Keep the documented app sizes in sync with app/components/desktop/app-registry.tsx.
Several Size entries here no longer match the defaults the desktop actually uses. For example, Line 319 documents Music as clamp(180px, 40cqw, 300px) × clamp(220px, 60cqh, 380px), but app/components/desktop/app-registry.tsx Lines 148-149 now use clamp(440px, 78cqw, 640px) × clamp(300px, 72cqh, 440px). Since this plan is meant to drive follow-up work, stale numbers will send later changes in the wrong direction.
Based on learnings: Reference DESKTOP-PLAN.md at the project root when creating implementation plans, working on features, or briefing subagents.
🧰 Tools
🪛 LanguageTool
[uncategorized] ~299-~299: The operating system from Apple is written “macOS”
Context: ...- Purpose: Blog posts - Design: Mac OS Scrapbook — page-by-page with arrows - ...
(MAC_OS)
[uncategorized] ~305-~305: The operating system from Apple is written “macOS”
Context: ...ce: public/worldmap.png (a Risk-style Mac OS screenshot — style only; no game ch...
(MAC_OS)
🪛 markdownlint-cli2 (0.22.1)
[warning] 239-239: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below
(MD022, blanks-around-headings)
[warning] 251-251: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below
(MD022, blanks-around-headings)
[warning] 258-258: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below
(MD022, blanks-around-headings)
[warning] 264-264: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below
(MD022, blanks-around-headings)
[warning] 270-270: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below
(MD022, blanks-around-headings)
[warning] 297-297: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below
(MD022, blanks-around-headings)
[warning] 303-303: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below
(MD022, blanks-around-headings)
[warning] 313-313: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below
(MD022, blanks-around-headings)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@DESKTOP-PLAN.md` around lines 239 - 319, The documented "Size" entries in
DESKTOP-PLAN.md are out of sync with the actual defaults used by the running app
registry (e.g., the Music app's documented clamp sizes don't match the values
defined for the "Music" entry in app-registry.tsx); update DESKTOP-PLAN.md so
each app's Size lines exactly reflect the corresponding defaults in
app/components/desktop/app-registry.tsx (verify the "Music" app and any others
that differ), keeping the app names (e.g., "Music — live Spotify now-playing",
"Finder — 'Documents'", "Curly Browser", etc.) as anchors so future edits match
the canonical values in app-registry.tsx.
| if (!code) { | ||
| res.writeHead(400, { 'Content-Type': 'text/plain' }) | ||
| res.end('Missing code in callback') | ||
| return | ||
| } |
There was a problem hiding this comment.
Close the callback server when code is missing.
This branch returns a 400 but leaves the server running, so the script can hang instead of failing fast.
Proposed fix
if (!code) {
res.writeHead(400, { 'Content-Type': 'text/plain' })
res.end('Missing code in callback')
+ console.error('❌ Missing code in callback')
+ server.close()
+ process.exit(1)
return
}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@scripts/spotify-oauth.mjs` around lines 136 - 140, When the callback handler
sees no code (the !code branch), the response is ended but the HTTP callback
server is left running; update that branch in scripts/spotify-oauth.mjs to close
the server after sending the 400 response by invoking the server's close()
method (e.g., server.close() or the exact server variable used where
http.createServer(...) is assigned), then return; locate the server created
earlier (the variable assigned from createServer/createListener) and call its
close() immediately after res.end to ensure the process can exit cleanly.
CLAUDE.md: correct boot description, BootSequence→BootScreen component name,
and .woff2→.woff font path. Add pointer to DESKTOP-PLAN.md.
DESKTOP-PLAN.md: fix container-type (inline-size→size for cqh support),
correct Phase 4 branching (apps branch from mac-os-1984-desktop, not
pre-app-foundation), fix branch tree diagram, drop redundant isSelected
from WindowState, collapse isMaximized two-step into direct context usage.
MOBILE-PLAN.md: initial planning document for iPhone OS 1 mobile experience.
Co-Authored-By: Claude Opus 4.6 (1M context) noreply@anthropic.com