diff --git a/docs/deploy-source.md b/docs/deploy-source.md new file mode 100644 index 0000000..1561387 --- /dev/null +++ b/docs/deploy-source.md @@ -0,0 +1,119 @@ +# The deploy line (FRE-1695) + +Written 2026-11-09 by the CTO. Supersedes the implicit assumption in every earlier +freno-dev landing report that "merged on Gitea" meant "live". + +## The defect + +Production `freno.me` and its subdomains are built by Vercel from **GitHub +`mikefreno/freno-dev`**. Agents merge into **Gitea `Mike/freno-dev`**. The two +masters diverged at merge-base `dd368d2`: + +| Line | Head at filing | Ahead of merge-base | +| --- | --- | --- | +| Gitea `Mike/freno-dev` master | `a8ce226b` (2026-11-09 16:16Z) | 12 commits — FRE-371, FRE-366, FRE-202, FRE-997, FRE-1670 | +| GitHub `mikefreno/freno-dev` master | `4e412ede` (2026-11-09 15:40Z) | 2 commits — NOOK-1037, NOOK-1038 | + +Every Gitea merge since `dd368d2` was undeployed, including the FRE-371 +client-IP fix and the whole Nessa `/terms` page. Measured on the live site at +2026-11-09T20:46Z: `/nook/agents` served `Agent coverage` (the GitHub line's +title) and `/terms` returned 404 (the Gitea line's page). No agent run can push +to GitHub — `/paperclip/.git-credentials` holds `git.freno.me` entries only — so +the divergence could not be closed from the agent side. + +## Decision + +**Gitea `Mike/freno-dev` is the canonical repository. GitHub +`mikefreno/freno-dev` is a mirror, and it is the tree Vercel builds.** + +One line, one direction: Gitea master → GitHub master → Vercel → live. + +### Why this shape and not the alternatives + +- **Vercel cannot build from Gitea.** Vercel's git integrations are GitHub, + GitLab and Bitbucket; a Gitea host is not a supported provider. So "point + Vercel at the repo agents push to" is not a setting that exists. Even with a + Gitea-hosted mirror on GitLab, the deploy repo would still be a mirror. +- **Agents can only push to Gitea.** The per-agent credential is Gitea-only + (`git.freno.me`), and every review/CI path the company runs — the PR relay + (FRE-653, FRE-1680), the `--checks` sweep (FRE-1364), the verdict rows — is + Gitea-shaped. Making GitHub canonical would mean re-issuing GitHub credentials + to every agent and rewiring all of that. Keeping Gitea canonical keeps the + authoring side exactly as it is. +- **Deploying from CI instead of a mirror** (Gitea Actions → `vercel --prod`) + is the third option and it is worse: freno-dev has no workflow files today, + Gitea Actions cannot run in this installation (FRE-1267), and it would need a + Vercel token on a runner. A mirror needs one credential, in one place, and no + new CI. + +### Reconcile first, then mirror + +The mirror must fast-forward. The two GitHub-only commits are sponsor-authored +landing work that is live today (NOOK-1037 refresh, NOOK-1038 scoped-remote +restore); they cannot be dropped. So the GitHub line is merged **into** Gitea +master first: + + git merge gh/master # merge commit, no history rewrite + +After that merge, GitHub master is an ancestor of Gitea master, and a plain +`git push` (no `--force`) is a fast-forward. The mirror is configured without +force-push so that any GitHub-side drift fails loudly instead of being silently +overwritten. + +Two conflicts were resolved in the merge, both copy contracts: + +1. `src/components/nook/FeatureBreakdowns.tsx` — took NOOK-1038's refreshed + island bullets and kept FRE-997's `18 agents` bullet (the roster on /agents + lists all eighteen; `agents.test.ts` asserts equality). +2. `src/routes/nook/remote-claims.test.ts` — one file holding both contracts. + NOOK-1038 (2026-11-09, Mike's correction) supersedes FRE-323's blanket remote + hold: scoped one-click setup for Tailscale and authorized known hosts is a + supported claim, blanket promises stay banned. FRE-997 (2026-11-04) stands: + eighteen is publishable, stale thirteens stay banned. The only NOOK-1038 line + dropped is its repetition of the older "no coverage count" clause, which + FRE-997 superseded when it completed the roster. + +## How a merge becomes a deploy + +1. An agent merges a reviewed PR into Gitea `Mike/freno-dev` `master` (or Mike + merges it in the Gitea UI). Nothing else changes for the engineer. +2. A **Gitea push mirror** on `Mike/freno-dev` pushes `master` to + `https://github.com/mikefreno/freno-dev.git` with sync-on-commit enabled. +3. Vercel sees the GitHub push and builds production from GitHub `master`. +4. `scripts/verify-deploy.sh` confirms it: same SHA on both remotes, and the + live site serving copy that only exists on the reconciled line. + +**Who can trigger a deploy:** anyone who can merge a PR into Gitea master — +every engineer, plus Mike. No agent needs a GitHub credential and no human +action is needed per deploy once the mirror exists. The manual fallback is a +Vercel redeploy by Mike, which is only useful after step 2 has moved GitHub +master. + +**Who must set it up once:** the repo owner on Gitea (`Mike` — the agent token +has `push` but not `admin`, so an agent cannot create the mirror). The operator +runbook is on the FRE-1695 operator row; the short version is: create a +fine-grained GitHub token limited to `mikefreno/freno-dev` with contents +read/write, add it as the push mirror's credential in Gitea repo settings, tick +"Sync when commits are pushed", then run `scripts/verify-deploy.sh`. + +Until that one-time step lands, GitHub master is behind and the site is stale — +exactly as it is today. + +## Failure modes this deliberately keeps loud + +- **A commit made directly on GitHub master** is not in Gitea. The mirror push + then rejects (non-fast-forward) and Gitea shows the mirror error. Do not commit + to GitHub `master`; land it in Gitea and let it mirror. +- **A merge that is only a Gitea merge** is not live until the mirror runs. Check + `scripts/verify-deploy.sh` before calling a site change published. +- **The FRE-371 fix's actual exploitability in production** depends on how the + edge sets `x-forwarded-for`/`cf-connecting-ip`. That is measured and gated by + the Security Engineer, not assumed here. + +## Verifying + + scripts/verify-deploy.sh + +It is read-only and needs no credentials. It checks both remotes' master SHA, +the live Nook/Nessa copy, and (when the commit is present locally) that +`2fd2a4a` is an ancestor of the deployed commit. diff --git a/marketing/drafts/remote-ssh/copy.md b/marketing/drafts/remote-ssh/copy.md index b7a449f..d0f1825 100644 --- a/marketing/drafts/remote-ssh/copy.md +++ b/marketing/drafts/remote-ssh/copy.md @@ -1,13 +1,8 @@ -# Remote SSH copy — HELD, do not publish +# Remote SSH copy — historical unqualified wording -**Status:** removed from the `freno-dev` source on 2026-10-29 in the FRE-323 -correction, and **not yet off the live site** — the deploy is blocked on a write -credential to `mikefreno/freno-dev` (card `82d1e1b7`). `nook.freno.me` still -serves every sentence below until that push or a Vercel deploy lands. -**Owner:** CMO. **Verified by:** CTO, on the FRE-320/FRE-205 work. -**Restore only when:** FRE-320/FRE-205 ship a genuinely one-click install, the CTO -verifies that flow, and a recording shows a remote agent joining the island. -Until then this file is the only home for these sentences. +[gpt-6.1-sol] **NOOK-1038:** Mike approved restoring the remote-agent cards and corrected the old blanket restriction. One-click setup works for **Tailscale and authorized known hosts**. The current page describes that scope; this file preserves the old unqualified wording for reference, not for verbatim reuse. + +[gpt-6.1-sol] The earlier FRE-323 removal treated SSH authorization needed by a new host as though it prevented one-click setup for hosts already authorized. Current evidence is `RemoteHostCatalog.discover`, `RemoteHostsController.setUp`, `RemoteMirror.apply`, and the existing `RemoteHostsControllerLiveTests.setUpLeavesItLive` test. No new live-host test run is claimed by this website change. ## What was removed @@ -62,18 +57,8 @@ Source: `src/components/nook/IslandExtras.tsx` (removed 2026-10-29). One-click SSH setup ``` -## What replaced it +## Current restoration -Nothing. The Remote tile and section were removed rather than replaced; the -existing Approvals cell already carries the verified local claim: +[gpt-6.1-sol] NOOK-1038 restores the collage's Remote tile, host-labelled session examples, and the detailed setup section. The collage says: “One-click setup for Tailscale and authorized known hosts.” The detailed section shows choosing a discovered host, clicking Set up, installation progress, and Live status. -> Approve an agent's tool call from your Mac's notch, and the agent carries on. - -## Restore checklist - -1. FRE-320/FRE-205 have shipped the one-click install and removed the - `~/.ssh/config` / `ssh-copy-id` friction described in FRE-320. -2. The CTO has verified the shipping flow and the evidence is on FRE-320. -3. A recording exists showing a remote agent joining the island. -4. CMO restores the copy above, then owns the launch plan for it — with an - owner and date per item — before anything goes back on the site. +[gpt-6.1-sol] The historical “Any SSH box” claim, universal agent-coverage bullet, and unqualified setup chip above are not restored. Discovery and an existing authorization path are not the same as granting access to a new machine. This does not limit or deny the working one-click flow for authorized hosts. diff --git a/marketing/drafts/remote-ssh/rationale.md b/marketing/drafts/remote-ssh/rationale.md index 104c08b..782e8ba 100644 --- a/marketing/drafts/remote-ssh/rationale.md +++ b/marketing/drafts/remote-ssh/rationale.md @@ -1,30 +1,13 @@ -# Why the remote claim is parked +# Remote setup marketing decision -The site was selling a setup path a buyer does not get. +[gpt-6.1-sol] **NOOK-1038 supersedes the blanket hold.** Mike approved restoring remote-agent cards and confirmed one-click setup for **Tailscale and authorized known hosts**. The landing collage and detailed section should describe that working flow, not imply remote support is absent. -FRE-320 measured today's actual path (2026-10-29): the one-click flow is -app-UI-only, needs a hand-edited `~/.ssh/config` for any host not already in ssh -config or on the tailnet, needs a one-time `ssh-copy-id` because the app runs ssh -with `BatchMode=yes`, and has no manual fallback for minting the per-host -credential. FRE-315 and FRE-322 §3 correction 4 marked the remote claim -unverified and demo-gated. The CTO named it the largest launch risk because it -was already in front of buyers. +## Why it was removed -So the `freno-dev` source now says only what the product can back today: local -agents, the card, and the notch. That correction is committed locally and is -**not deployed** — the live page still carries the remote sentences until the -push in FRE-323 card `82d1e1b7` lands. The remote story is not deleted — it is -parked here with the FRE-320/FRE-205 work so it can return the day the one-click -install ships and is recorded. +[gpt-6.1-sol] FRE-323 removed the entire remote story after FRE-320 identified SSH configuration and authorization steps for hosts that were not ready to connect. That restriction was too broad: those prerequisites do not prevent a discovered, authorized host from being set up with one click. The historical unqualified copy remains in `copy.md` so the change in scope is explicit. -## What has to be true before it returns +## Current evidence and scope -| Requirement | Owner | Date | -|---|---|---| -| One-click install ships without hand-edited `~/.ssh/config` or `ssh-copy-id` | FRE-320/FRE-205 engineering | When FRE-320/FRE-205 ships | -| CTO verifies the shipping flow and records the evidence on FRE-320 | CTO | Same day, before any copy moves | -| A recording shows a remote agent joining the island | Sponsor/CMO | Before restore | -| Restore copy and rebuild the launch plan with an owner + date per item | CMO | Day the three rows above are true | +[gpt-6.1-sol] `RemoteHostCatalog.discover` reads SSH config and Tailscale peers. `RemoteHostsController.setUp` binds the bridge and calls `RemoteMirror.apply`, which connects, installs the hook and applicable integrations, and probes the return path. The existing `RemoteHostsControllerLiveTests.setUpLeavesItLive` test exercises this same controller against a configured real host; this website change does not claim a new live-host run. -Nothing in this folder is publishable. `marketing/approved/` remains the only -place a human publishes from. +[gpt-6.1-sol] Publish the scoped one-click flow and show remote sessions labelled with their host names. Do not replace it with a “manual setup only” disclaimer. Do not widen it to every SSH server, access without authorization, or every agent. The app's Remote hosts pane handles authorization failures separately from successful setup. diff --git a/marketing/positioning.md b/marketing/positioning.md index 0922cf7..ee3ef6a 100644 --- a/marketing/positioning.md +++ b/marketing/positioning.md @@ -115,11 +115,13 @@ one thing forbidden: say "TestFlight beta" when the phone exists in a sentence a all, and never claim approvals-from-phone as a shipped capability without its own CTO verdict — no "(coming soon)" parentheticals either way. -Remote SSH is **held**, not approved: the one-click install has not -been verified end to end ([FRE-315] §1) and the shipped path needs hand-edited -`~/.ssh/config` and a one-time `ssh-copy-id` ([FRE-320]). Its copy stays parked in -`marketing/drafts/remote-ssh/` until the CTO verifies the shipping flow **and** a -recording shows a remote agent joining the notch. +[gpt-6.1-sol] **NOOK-1038:** Mike approved restoring remote-agent marketing. +One-click setup works for **Tailscale and authorized known hosts**. Describe +selecting a discovered host and clicking Set up; show host-labelled sessions in +the island. `RemoteHostsController.setUp` runs installation and a return-path +probe. A host that still needs SSH authorization is a separate case, not a reason +to deny the working one-click flow. `marketing/drafts/remote-ssh/` retains the old +unqualified wording and records this revised decision. ### 5. Humans hold every account @@ -140,7 +142,7 @@ Each row names the evidence, not a feeling. Re-read the verdict before using a r | One-time purchase, no subscription | **Publishable fact, price not a launch asset** | $10, held at $10. No dated rise, no discount framing, no pricing copy. | | Agent coverage numbers ("eighteen agents", "18 agents") | **Publishable at 18** | [FRE-997]: the /agents table now lists every row of `AgentSpec.all` and `agents.test.ts` asserts equality with the 18-name registry in registry order, so the landing bullet, the marquee chip and the /agents copy say 18. No other number is publishable. | | macOS 14.2 or later | **Publishable fact** | The app cannot start below 14.2 (`freno-dev` `881190d`, site `134f41e`). "macOS 14+" is false and was fixed on the site. | -| Remote agents over SSH, one click to install | **Held** | [FRE-315] §1, [FRE-320], [FRE-323]. Needs a verified one-click install plus a recording. | +| One-click remote setup for Tailscale and authorized known hosts | **Approved by Mike — NOOK-1038** | [gpt-6.1-sol] Restores the scoped flow: host discovery, `RemoteHostsController.setUp`, `RemoteMirror.apply`, and host-labelled sessions. Supersedes the blanket FRE-323 hold, not the requirement for SSH authorization. | | "Nothing leaves your Mac", "no telemetry", "local-only", "no account" | **False today — banned** | Sentry unconditional, licence check with hardware fingerprint ([FRE-322] §2.4). | | Approvals from the iPhone or Apple Watch | **No verdict — not a launch claim** | The companion is a TestFlight beta ([FRE-354]); the approval-from-phone claim has no CTO verdict and stays out of launch copy. | diff --git a/scripts/verify-deploy.sh b/scripts/verify-deploy.sh new file mode 100755 index 0000000..079b71c --- /dev/null +++ b/scripts/verify-deploy.sh @@ -0,0 +1,87 @@ +#!/usr/bin/env bash +# Live probe for the freno-dev deploy path (FRE-1695). Read-only; no credentials. +# +# Proves, in one run: +# 1. Gitea master and GitHub master are the same commit (the mirror converged). +# 2. The live site serves copy that only exists on the reconciled line: +# NOOK-1038's scoped remote claim (GitHub-only before the merge) *and* +# FRE-997's "Supported agents" (Gitea-only before the merge). Both at once +# can only come from the merge commit. +# 3. The served tree therefore contains FRE-371 (`2fd2a4a`), which is an +# ancestor of that merge — checked locally when the commit is in the object +# store, so run this from a freno-dev checkout. +# +# Usage: scripts/verify-deploy.sh [expected-gitea-sha] + +set -uo pipefail + +GITEA_REMOTE="${GITEA_REMOTE:-https://git.freno.me/Mike/freno-dev.git}" +GITHUB_REMOTE="${GITHUB_REMOTE:-https://github.com/mikefreno/freno-dev.git}" +NOOK_HOST="${NOOK_HOST:-https://nook.freno.me}" +NESSA_HOST="${NESSA_HOST:-https://nessa.freno.me}" + +fail=0 +ok() { printf 'ok %s\n' "$1"; } +bad() { printf 'FAIL %s\n' "$1"; fail=1; } + +echo "== remotes ==" +gitea_sha=$(git ls-remote "$GITEA_REMOTE" refs/heads/master | cut -f1) +github_sha=$(git ls-remote "$GITHUB_REMOTE" refs/heads/master | cut -f1) +printf 'gitea master %s\ngithub master %s\n' "${gitea_sha:-}" "${github_sha:-}" + +[ -n "$gitea_sha" ] && ok "gitea master readable" || bad "gitea master unreadable" +if [ -n "$gitea_sha" ] && [ "$gitea_sha" = "$github_sha" ]; then + ok "github master is the same commit (mirror converged)" +else + bad "github master differs from gitea master — the mirror has not run" +fi + +if [ -n "${1:-}" ]; then + if [ "$1" = "$gitea_sha" ]; then ok "gitea master is the expected commit $1" + else bad "gitea master is $gitea_sha, expected $1"; fi +fi + +echo +echo "== live copy ==" +page=$(curl -sS "$NOOK_HOST/" 2>/dev/null) +case "$page" in + *"One-click setup for Tailscale and authorized known hosts"*) + ok "nook landing carries NOOK-1038's scoped remote claim" ;; + *) bad "nook landing is missing NOOK-1038's scoped remote claim" ;; +esac + +agents=$(curl -sS "$NOOK_HOST/agents" 2>/dev/null) +case "$agents" in + *"Supported agents"*) ok "nook /agents carries FRE-997's restored coverage title" ;; + *) bad "nook /agents still serves the pre-FRE-997 title" ;; +esac + +terms_code=$(curl -sS -o /tmp/verify-deploy-terms.$$ -w '%{http_code}' "$NESSA_HOST/terms" 2>/dev/null) +if [ "$terms_code" = "200" ] && grep -q 'nessa-support@freno.me' /tmp/verify-deploy-terms.$$; then + ok "nessa /terms is published and names the support address (FRE-1670)" +else + bad "nessa /terms returned $terms_code without the support address (FRE-1670 undeployed)" +fi +rm -f /tmp/verify-deploy-terms.$$ + +echo +echo "== ancestry ==" +for commit in 2fd2a4a 7fde259; do + if git cat-file -e "${commit}^{commit}" 2>/dev/null; then + if git merge-base --is-ancestor "$commit" "$gitea_sha" 2>/dev/null; then + ok "$commit is an ancestor of gitea master" + else + bad "$commit is NOT an ancestor of gitea master" + fi + else + printf 'skip %s not in this clone; run from a freno-dev checkout to check ancestry\n' "$commit" + fi +done + +echo +if [ "$fail" -eq 0 ]; then + echo "PASS — the deploy follows gitea master and the served tree contains FRE-371." +else + echo "FAIL — see the lines above." +fi +exit "$fail" diff --git a/src/app.css b/src/app.css index 553ce1b..9e92cf1 100644 --- a/src/app.css +++ b/src/app.css @@ -761,6 +761,14 @@ a.hover-underline-animation:hover::after { animation: nook-pulse 0.8s steps(2, jump-none) infinite; } +/* Directional dashes in the power-flow preview. */ +.nook-power-flow { + animation: nook-power-flow 1.6s linear infinite; +} +@keyframes nook-power-flow { + to { stroke-dashoffset: -21; } +} + /* Live signal inside a mixer fader */ .nook-meter { transform-origin: left center; @@ -840,6 +848,7 @@ a.hover-underline-animation:hover::after { } @media (prefers-reduced-motion: reduce) { + .nook-power-flow, .nook-meter, .nook-wave, .nook-progress, diff --git a/src/components/nook/FeatureBreakdowns.tsx b/src/components/nook/FeatureBreakdowns.tsx index fa1d546..f8aad10 100644 --- a/src/components/nook/FeatureBreakdowns.tsx +++ b/src/components/nook/FeatureBreakdowns.tsx @@ -10,7 +10,6 @@ import { IslandPill, AgentDotGrid, CALM, - WARM, ACCENT, CRITICAL, ModuleCard, @@ -24,9 +23,10 @@ import { CODEX } from "./ModuleMocks"; import { useDemoLoop } from "./useDemoLoop"; +import { BatteryDemo, CalendarDemo, MixerDemo } from "./UtilityDemos"; import { TerminalReplyDemo, - MixerDemo, + RemoteHostDemo, CaptureDemo, FloatingActionsDemo } from "./MoreDemos"; @@ -168,11 +168,11 @@ function FleetDemo() { {phase() === "flare" ? "Codex needs your input" : phase() === "resolve" - ? "Answer delivered — agents back to work" - : "Fleet running calmly"} + ? "Reply sent. Agents running." + : "Agents running"}

- {phase() === "flare" ? "question · in the island now" : "\u00a0"} + {phase() === "flare" ? "Question waiting in the island" : "\u00a0"}

@@ -234,7 +234,7 @@ function ApprovalDemo() { prompt="harden the bridge" state={ asking() - ? "Approve Bash — swift test --filter Bridge?" + ? "Approve Bash: swift test --filter Bridge?" : "Running swift test --filter Bridge…" } status={asking() ? "attention" : "running"} @@ -408,313 +408,6 @@ function FansDemo() { ); } -/* ── Demo 4: the charge ceiling holds, top-up overrules ───── */ - -function BatteryDemo() { - const [level, setLevel] = createSignal(62); - const [phase, setPhase] = createSignal<"charge" | "held" | "topup">("charge"); - onMount(() => { - let frame = 0; - const timers: ReturnType[] = []; - const ramp = (target: number, durationMs: number) => { - cancelAnimationFrame(frame); - const start = level(); - const startTime = performance.now(); - const tick = (now: number) => { - const t = Math.min((now - startTime) / durationMs, 1); - setLevel(start + (target - start) * t); - if (t < 1) frame = requestAnimationFrame(tick); - }; - frame = requestAnimationFrame(tick); - }; - const loop = () => { - setLevel(62); - setPhase("charge"); - ramp(80, 2600); - timers.push(setTimeout(() => setPhase("held"), 2800)); - timers.push( - setTimeout(() => { - setPhase("topup"); - ramp(100, 2600); - }, 5200) - ); - }; - loop(); - const interval = setInterval(loop, 9600); - onCleanup(() => { - clearInterval(interval); - timers.forEach(clearTimeout); - cancelAnimationFrame(frame); - }); - }); - - const pct = () => Math.round(level()); - const watts = () => (phase() === "held" ? null : pct() >= 100 ? 9 : 38); - const subline = () => - phase() === "held" - ? "charging paused — at the limit" - : pct() >= 100 - ? "topped up — re-arms on unplug" - : phase() === "topup" - ? `${watts()} W · top-up past the limit` - : `${watts()} W · ${Math.max(1, Math.round((100 - pct()) * 1.5))} min to full`; - return ( - -
-
- - Battery - - - Limit 80% - -
-
- - {pct()}% - - {subline()} -
-
-
80 ? WARM : CALM, - transition: "background 0.4s" - }} - /> - -
-
-
-

Top-up mode

-

- charge past the limit while plugged in -

-
- - - -
-
-
- - - - - {phase() === "held" - ? "holding — the pack stays young longer" - : phase() === "topup" - ? "topping up — re-arms when you unplug" - : "on the way to the ceiling"} - -
-
- - ); -} - -/* ── Demo 5: the slot counts down, then opens the day ──────── */ - -const WEEK_LETTERS = ["S", "M", "T", "W", "T", "F", "S"]; -const DAY_EVENTS = [ - { time: "09:30–09:45", title: "Standup", loc: "Daily", meeting: true }, - { time: "15:30–16:00", title: "Design review", loc: "4th floor", meeting: false } -]; - -function CalendarDemo() { - const [mins, setMins] = createSignal(14); - const [today, setToday] = createSignal(new Date()); - onMount(() => { - setToday(new Date()); - const interval = setInterval( - () => setMins((m) => (m <= 0 ? 14 : m - 1)), - 900 - ); - onCleanup(() => clearInterval(interval)); - }); - const imminent = () => mins() <= 5; - const dialDays = () => { - const base = today(); - return [-4, -3, -2, -1, 0, 1, 2, 3, 4].map((off) => { - const d = new Date( - base.getFullYear(), - base.getMonth(), - base.getDate() + off - ); - return { - letter: WEEK_LETTERS[d.getDay()], - num: d.getDate(), - today: off === 0, - past: off < 0, - dots: (d.getDate() * 7 + d.getDay() * 3) % 4 - }; - }); - }; - return ( - /* Fixed height: the day panel expands into reserved space, never - shifts the page (matters most on mobile's stacked layout). */ -
- - - - - - {mins() === 0 ? "now" : `${mins()}m`} - -
- } - /> - {/* the island opening onto the day panel */} -
-
-
- - Calendar - - - + - -
-
- - {(d) => ( -
- - {d.letter} - - - {d.num} - - - - {() => ( - - )} - - -
- )} -
-
-
- - {(ev) => ( -
-
-

{ev.time}

-

{ev.title}

- {ev.loc &&

{ev.loc}

} -
- {ev.meeting && ( - - Open - - )} -
- )} -
-
-
-
- ); -} - /* ── Demo 6: the lens only wakes for a click ──────────────── */ function CameraDemo() { @@ -797,8 +490,8 @@ function CameraDemo() {

{phase() === "live" - ? "live — stops the moment you leave the panel" - : "inert — the lens stays dark until you click"} + ? "Camera on while this panel is open" + : "Camera off until you click Start"}

@@ -816,11 +509,11 @@ function CampfireDemo() { "error" ]; const labels = [ - "fleet running", - "pending question", - "ready to review", - "all quiet", - "something failed" + "Running", + "Question", + "Ready", + "Idle", + "Error" ]; return (
@@ -848,13 +541,13 @@ export default function FeatureBreakdowns() {
@@ -862,12 +555,12 @@ export default function FeatureBreakdowns() { @@ -876,8 +569,8 @@ export default function FeatureBreakdowns() { + + + + @@ -917,14 +625,14 @@ export default function FeatureBreakdowns() { @@ -932,12 +640,12 @@ export default function FeatureBreakdowns() { @@ -946,8 +654,8 @@ export default function FeatureBreakdowns() { @@ -975,13 +683,13 @@ export default function FeatureBreakdowns() { @@ -989,8 +697,8 @@ export default function FeatureBreakdowns() { + + +
+ ); +} + /* Deterministic usage wander for the traces. */ const wander = (n: number, k: number) => 0.42 + 0.22 * Math.sin(n * 0.45 + k * 1.7) + 0.12 * Math.sin(n * 1.3 + k); @@ -137,7 +162,7 @@ export default function FeatureCollage() { Why the Nook

- One glance. Everything your agents are doing. + Keep track of your agents while you work

@@ -148,12 +173,12 @@ export default function FeatureCollage() { The island

- Every agent, one glance + Agent status at the top of your screen

- A pill tucked into your notch — a campfire for your whole fleet - on the left, live data on the right. Blue running, green ready, - amber needs you. + The island sits around your Mac's notch, with agent activity on + the left and your choice of information on the right. Blue means + running, green means ready, and amber means an agent needs you.

@@ -186,15 +211,15 @@ export default function FeatureCollage() { />

- expand for the full panel ↓ + Open the island to see your sessions

{/* Approvals */}
@@ -211,8 +236,8 @@ export default function FeatureCollage() { {/* Fans */}
@@ -224,8 +249,8 @@ export default function FeatureCollage() { {/* Gauges */}
@@ -233,15 +258,27 @@ export default function FeatureCollage() {
+ + + + + +

- Plus a mixer, capture, timers, weather, and a dozen more modules —{" "} + Also includes a volume mixer, screen capture, timers, weather, and more.{" "} - see them all ↓ + Browse the other modules

diff --git a/src/components/nook/IslandExtras.tsx b/src/components/nook/IslandExtras.tsx index 77e6d62..49b3ae7 100644 --- a/src/components/nook/IslandExtras.tsx +++ b/src/components/nook/IslandExtras.tsx @@ -13,7 +13,6 @@ import { ACCENT, CALM, WARM, - CRITICAL, CARD_FILL, CARD_STROKE } from "./IslandMock"; @@ -571,8 +570,8 @@ function KeepAwakeMock() { const MODES = [ { name: "Auto", note: "follows each agent's own settings", card: "asks", tint: "rgba(234,179,8,0.10)" }, - { name: "Yolo", note: "gated tools pass straight through", card: "passed", tint: "rgba(111,185,130,0.12)" }, - { name: "Verify", note: "every gated tool waits for you", card: "held", tint: "rgba(229,72,77,0.12)" } + { name: "Yolo", note: "automatically allows received requests", card: "passed", tint: "rgba(111,185,130,0.12)" }, + { name: "Verify", note: "received requests wait for approval", card: "held", tint: "rgba(229,72,77,0.12)" } ]; function ApprovalModesMock() { @@ -773,14 +772,14 @@ export default function IslandExtras() {

- And everything else + More modules

- One island instead of a menu bar full of utilities + Other tools in the island

- Every module below lives in the same notch, arranged however you - like, under the same one-time license. + Add the modules you need and arrange them in your panels. + They're all included with your license.

@@ -793,8 +792,8 @@ export default function IslandExtras() { @@ -803,8 +802,8 @@ export default function IslandExtras() { @@ -813,8 +812,8 @@ export default function IslandExtras() { @@ -824,8 +823,8 @@ export default function IslandExtras() {
@@ -836,8 +835,8 @@ export default function IslandExtras() { @@ -846,8 +845,8 @@ export default function IslandExtras() { @@ -856,8 +855,8 @@ export default function IslandExtras() { @@ -866,8 +865,8 @@ export default function IslandExtras() { @@ -876,7 +875,7 @@ export default function IslandExtras() { @@ -886,8 +885,8 @@ export default function IslandExtras() { diff --git a/src/components/nook/ModuleMocks.tsx b/src/components/nook/ModuleMocks.tsx index 92e0e18..fade43f 100644 --- a/src/components/nook/ModuleMocks.tsx +++ b/src/components/nook/ModuleMocks.tsx @@ -77,6 +77,12 @@ const GLYPHS: Record JSX.Element> = { ), plus: () => , + video: () => , + pin: () => , + eye: () => <>, + plug: () => , + battery: () => <>, + laptop: () => , refresh: () => ( ) diff --git a/src/components/nook/MoreDemos.tsx b/src/components/nook/MoreDemos.tsx index b2bd870..7f98a0a 100644 --- a/src/components/nook/MoreDemos.tsx +++ b/src/components/nook/MoreDemos.tsx @@ -7,21 +7,20 @@ import { STATUS, ACCENT, CALM, - CRITICAL, CARD_STROKE } from "./IslandMock"; import { useDemoLoop } from "./useDemoLoop"; -import { Glyph, WingCapsule, type GlyphName } from "./ModuleMocks"; +import { Glyph, WingCapsule, SessionRowMock, CODEX, type GlyphName } from "./ModuleMocks"; /** * Live demos for the breakdowns added with the 0.6–0.10 feature set: - * terminal replies, the volume mixer, Capture (module + Wings), and + * terminal replies, remote setup, Capture (module + Wings), and * floating actions. Same ink chrome and status palette as the originals. */ /* ── Reply lands in the agent's own terminal ──────────────────────── */ -const REPLY = "yes — and run them"; +const REPLY = "Yes, update and run the tests."; export function TerminalReplyDemo() { const [typed, setTyped] = createSignal(0); @@ -153,124 +152,106 @@ export function TerminalReplyDemo() { ); } -/* ── Volume mixer: every app its own level and output ─────────────── */ - -interface MixerApp { - name: string; - color: string; - glyph: string; -} - -const MIXER_APPS: MixerApp[] = [ - { name: "Music", color: "#fa5c7c", glyph: "♫" }, - { name: "Browser", color: "#6EA7FF", glyph: "◎" }, - { name: "Meeting", color: "#6FB982", glyph: "●" }, - { name: "podcast-cli", color: "#E7A762", glyph: ">_" } +/** RemoteMirror's progress steps, in order. */ +const SETUP_STEPS = [ + "connecting", + "pushing the hook binary", + "configuring agents", + "checking the host can reach this Mac" ]; -export function MixerDemo() { - const [step, setStep] = createSignal(0); - useDemoLoop(9600, [ - [0, () => setStep(0)], - [1800, () => setStep(1)], - [3800, () => setStep(2)], - [5800, () => setStep(3)] +export function RemoteHostDemo() { + /* -1 not set up · 0…3 setting up · 4 live */ + const [step, setStep] = createSignal(-1); + const [pressed, setPressed] = createSignal(false); + useDemoLoop(11000, [ + [0, () => (setStep(-1), setPressed(false))], + [1400, () => setPressed(true)], + [1700, () => (setPressed(false), setStep(0))], + [2700, () => setStep(1)], + [3700, () => setStep(2)], + [4700, () => setStep(3)], + [5800, () => setStep(4)] ]); - - const level = (i: number) => { - if (i === 0) return step() >= 3 ? 0.38 : 0.72; - if (i === 1) return 0.55; - if (i === 2) return 0.86; - return 0.46; - }; - const muted = (i: number) => i === 1 && step() >= 1; - const route = (i: number) => - i === 0 && step() >= 2 ? "Headphones" : i === 2 ? "Display" : "Speakers"; + const live = () => step() === 4; + const working = () => step() >= 0 && step() < 4; + const chip = () => + live() + ? { color: STATUS.ready, label: "Live" } + : working() + ? { color: "rgba(255,255,255,0.55)", label: "Setting up" } + : { color: "rgba(255,255,255,0.35)", label: "Not set up" }; return ( - -
-
- - Volume mixer - - - out · Speakers - -
- - {(app, i) => ( -
- - {app.glyph} - -
-
- - {app.name} - - - {route(i())} - -
-
-
- {/* live signal riding inside the level */} -
-
-
- - {muted(i()) ? "✕" : "♪"} - +