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/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()) ? "✕" : "♪"} - +