diff --git a/AGENTS.md b/AGENTS.md index 5d094bec20..676161344e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1894,6 +1894,7 @@ Sorting and the rest: - Decisions, with the count of questions that record a decision once sent. - Settings. - "A new version is ready, Restart" when health reports `update`. Restart is `POST /api/inbox/restart`: the server stops, the binary on disk starts detached, and the page reloads on the new `serverSession`. + - The frame is Workspaces' sidebar primitive, copied by the owner's word (2026-10-09) into `packages/inbox/shell/` (each file names its Workspaces source and commit): the toggle at the head of each page and ⌘B / Ctrl+B close and open it on Workspaces' one spring (`shell-motion.ts`, MotionValues from `motion`, reduced motion lands without travel); ⌘B does nothing in an input, a textarea or an editor; the state is the Inbox's own cookie `plannotator-inbox-sidebar-open` (open when absent). Closed, a 600 ms rest on the left edge peeks it (1024 px and wider). The edge collapses on a click and resizes on a drag (200 to 520 px, 232 by default, cookie `plannotator-inbox-sidebar-width`), closing past 100 px and reopening if the held pointer comes back. Below 768 px it is a sheet. A file beside a thread closes it on the same spring without changing the stored choice. - A wide row shows: - the project label - the badge ("Stopped", "Holds up N") @@ -2099,6 +2100,7 @@ Sorting and the rest: - `inbox-attachments.spec.ts` - `inbox-guides.spec.ts` - `inbox-order.spec.ts` (three agents' questions read newest first in Waiting on you; a reply moves its row to Sent) + - `inbox-sidebar.spec.ts` (the sidebar's close and open sampled every frame, ⌘B and Ctrl+B, ⌘B left to the reply box, the reload, the peek, the edge's click, drag, snap-close and keyboard, reduced motion, the phone sheet; its PNGs, video and curves in `.local/proof/sidebar/`) - `inbox-phones.spec.ts` (Pair a phone, the device list and Remove; Reach from this Wi-Fi with a pinned phone pairing over the LAN, the no-Bonjour note and `openssl` failing; with `PLANNOTATOR_E2E_TAILNET=1` on a Mac signed in to Tailscale, the door at the MagicDNS name on 8443) PNGs land in `.local/proof/`. diff --git a/bun.lock b/bun.lock index 3dc3ea59e8..17ca0a7b47 100644 --- a/bun.lock +++ b/bun.lock @@ -278,10 +278,12 @@ "name": "@plannotator/inbox", "version": "0.0.1", "dependencies": { + "@base-ui/react": "^1.7.0", "@plannotator/core": "workspace:*", "@plannotator/guide-viewer": "workspace:*", "@plannotator/review-editor": "workspace:*", "@plannotator/ui": "workspace:*", + "motion": "^12.38.0", "react": "^19.2.8", "react-dom": "^19.2.8", "uqr": "^0.1.3", diff --git a/packages/inbox/App.tsx b/packages/inbox/App.tsx index c2b655de6c..e825e55b6d 100644 --- a/packages/inbox/App.tsx +++ b/packages/inbox/App.tsx @@ -13,6 +13,7 @@ import { useCallback, useEffect, useMemo, useRef, useState, type ReactNode } from 'react'; import { ThemeProvider } from '@plannotator/ui/components/ThemeProvider'; import { configurePlannotatorUI } from '@plannotator/ui/configure'; +import { storage } from '@plannotator/ui/utils/storage'; import type { InboxDecision, InboxHealth, InboxListRow, InboxListSection, InboxQuestion, InboxThread } from '@plannotator/core/inbox-types'; import { InboxApiError, @@ -28,6 +29,7 @@ import { filterSections, heldNotice, refreshHeldRows, waitingCount } from './hel import { tildePath } from './format'; import type { ConnectContext } from './harnesses'; import { Sidebar } from './components/Sidebar'; +import { SidebarShell } from './shell/SidebarShell'; import { InboxList } from './components/InboxList'; import { ThreadPane } from './components/Thread'; import { EmptyState } from './components/EmptyState'; @@ -54,6 +56,8 @@ configurePlannotatorUI({ }, }); +const SIDEBAR_OPEN_COOKIE = 'plannotator-inbox-sidebar-open'; + interface Route { page: 'inbox' | 'settings' | 'decisions'; project: string | null; @@ -122,6 +126,12 @@ function hasRows(sections: readonly InboxListSection[] | null): boolean { function Inbox() { const [route, setRoute] = useState(readRoute); + // Open or closed as the person left it (⌘B or the toggle): a cookie of the Inbox's own, open when absent. + const [sidebarOpen, setSidebarOpen] = useState(() => storage.getItem(SIDEBAR_OPEN_COOKIE) !== 'false'); + const changeSidebar = useCallback((next: boolean) => { + setSidebarOpen(next); + storage.setItem(SIDEBAR_OPEN_COOKIE, String(next)); + }, []); const [latest, setLatest] = useState(null); /** The list on screen: held until the person acts (the "N new" notice). */ const [shown, setShown] = useState(null); @@ -477,24 +487,31 @@ function Inbox() { return (
-
- go({ page: 'inbox', project: null, thread: null })} - onProject={(projectId) => go({ page: 'inbox', project: projectId, thread: null })} - onSettings={() => go({ page: 'settings', project: null, thread: null })} - onDecisions={() => go({ page: 'decisions', project: null, thread: null })} - onRestart={() => void restart()} - /> + {/* A file beside the thread takes the sidebar's room (record 2.2): it closes on the same spring, and the person's choice stays as it was. */} + go({ page: 'inbox', project: null, thread: null })} + onProject={(projectId) => go({ page: 'inbox', project: projectId, thread: null })} + onSettings={() => go({ page: 'settings', project: null, thread: null })} + onDecisions={() => go({ page: 'decisions', project: null, thread: null })} + onRestart={() => void restart()} + /> + } + >
{main}
-
+ {guideMessage && thread && (
+

Decisions