Compare commits
8 Commits
4d6550f0d1
...
freno-dev/
| Author | SHA1 | Date | |
|---|---|---|---|
| 6ff31346ff | |||
| 25ec84ceb4 | |||
| dd368d21e7 | |||
|
|
d44133fcb3 | ||
|
|
578ee58f9c | ||
|
|
d2c2e6a4ee | ||
|
|
c718370674 | ||
|
|
210a7e6e75 |
@@ -1,76 +0,0 @@
|
|||||||
# Remote SSH copy — HELD, do not publish
|
|
||||||
|
|
||||||
**Status:** removed from the live site on 2026-10-29 in the FRE-323 correction.
|
|
||||||
**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.
|
|
||||||
|
|
||||||
## What was removed
|
|
||||||
|
|
||||||
### 1. Landing collage tile
|
|
||||||
|
|
||||||
Source: `src/components/nook/FeatureCollage.tsx` (removed 2026-10-29).
|
|
||||||
|
|
||||||
```
|
|
||||||
kicker: Remote
|
|
||||||
title: Agents on other machines
|
|
||||||
body: Run agents on Linux boxes and SSH servers — one click installs the
|
|
||||||
nook-hook, and their sessions join the same island.
|
|
||||||
```
|
|
||||||
|
|
||||||
The mock attached to it showed two session rows whose agent chips named
|
|
||||||
`build-box` and `gpu-rig`.
|
|
||||||
|
|
||||||
### 2. Landing feature breakdown section
|
|
||||||
|
|
||||||
Source: `src/components/nook/FeatureBreakdowns.tsx` (removed 2026-10-29).
|
|
||||||
|
|
||||||
```
|
|
||||||
kicker: Remote
|
|
||||||
title: Any SSH box, one click away
|
|
||||||
body: Pick a host from your ssh config or your tailnet and click once — the
|
|
||||||
nook-hook installs itself and its agents appear beside your local ones,
|
|
||||||
indistinguishable.
|
|
||||||
|
|
||||||
bullets:
|
|
||||||
- One click: connect, push the hook, configure agents, check it can reach this Mac
|
|
||||||
- Hosts mirror this Mac's installed integrations automatically
|
|
||||||
- Remote sessions are ordinary rows; the agent chip names the machine
|
|
||||||
```
|
|
||||||
|
|
||||||
The section's demo animated a `gpu-rig` host moving from "Not set up" through
|
|
||||||
`connecting` → `pushing the hook binary` → `configuring agents` → `checking the
|
|
||||||
host can reach this Mac` → `Live`, then showed a remote session row.
|
|
||||||
|
|
||||||
### 3. Approvals bullet
|
|
||||||
|
|
||||||
Source: `src/components/nook/FeatureBreakdowns.tsx` (removed 2026-10-29).
|
|
||||||
|
|
||||||
```
|
|
||||||
- Works across every hooked agent, local or remote
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4. Feature chip
|
|
||||||
|
|
||||||
Source: `src/components/nook/IslandExtras.tsx` (removed 2026-10-29).
|
|
||||||
|
|
||||||
```
|
|
||||||
One-click SSH setup
|
|
||||||
```
|
|
||||||
|
|
||||||
## What replaced it
|
|
||||||
|
|
||||||
Nothing. The Remote tile and section were removed rather than replaced; the
|
|
||||||
existing Approvals cell already carries the verified local claim:
|
|
||||||
|
|
||||||
> 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.
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
# Why the remote claim is parked
|
|
||||||
|
|
||||||
The site was selling a setup path a buyer does not get.
|
|
||||||
|
|
||||||
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.
|
|
||||||
|
|
||||||
So the landing page now says only what the product can back today: local agents,
|
|
||||||
the card, and the notch. 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.
|
|
||||||
|
|
||||||
## What has to be true before it returns
|
|
||||||
|
|
||||||
| 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 |
|
|
||||||
|
|
||||||
Nothing in this folder is publishable. `marketing/approved/` remains the only
|
|
||||||
place a human publishes from.
|
|
||||||
@@ -4,13 +4,39 @@ Every drafted asset draws its voice, claims, and constraints from this file. If
|
|||||||
artifact invents its own framing, fix the artifact or fix this file — never both
|
artifact invents its own framing, fix the artifact or fix this file — never both
|
||||||
silently diverging.
|
silently diverging.
|
||||||
|
|
||||||
Primary source: `~/Code/the-nook/docs/marketing-strategy.md`.
|
**Reconciled 2026-10-29**, and again **2026-11-03** for the FRE-997 correction,
|
||||||
|
against the CTO's verdicts ([FRE-315], [FRE-322], [FRE-354], [FRE-997]), the Chief
|
||||||
|
of Staff's price decision (2026-10-29 04:01), the Plan B launch route ([FRE-346] →
|
||||||
|
[FRE-203] `plan-b`), and `The-Nook` master (app floor is **14.2**, not 14.0 —
|
||||||
|
`freno-dev` `881190d`, site merge `134f41e`). The previous version of this file
|
||||||
|
named an upstream `the-nook/docs/marketing-strategy.md` that no longer exists; the
|
||||||
|
verdicts below are the source of truth it was pointing at.
|
||||||
|
|
||||||
|
[FRE-315]: https://paperclip.freno.me/FRE/issues/FRE-315
|
||||||
|
[FRE-322]: https://paperclip.freno.me/FRE/issues/FRE-322
|
||||||
|
[FRE-354]: https://paperclip.freno.me/FRE/issues/FRE-354
|
||||||
|
[FRE-320]: https://paperclip.freno.me/FRE/issues/FRE-320
|
||||||
|
[FRE-346]: https://paperclip.freno.me/FRE/issues/FRE-346
|
||||||
|
[FRE-203]: https://paperclip.freno.me/FRE/issues/FRE-203
|
||||||
|
[FRE-997]: https://paperclip.freno.me/FRE/issues/FRE-997
|
||||||
|
|
||||||
## The sentence
|
## The sentence
|
||||||
|
|
||||||
> **Your Mac and your agents, under control.** The Nook puts your whole coding-agent
|
There is no single approved public sentence yet, and saying otherwise is the one
|
||||||
> fleet in the notch — thirteen agents — so you see what is running,
|
mistake this file exists to prevent. What exists is a **launch claim set** — two
|
||||||
> unblock it in one tap, and keep the machine from overheating while it works.
|
sentences, both from one 15-second take, recorded as the chosen route by the Chief
|
||||||
|
of Staff on 2026-10-29 ([FRE-346] `decision` §2–§4):
|
||||||
|
|
||||||
|
| Slot | Exact sentence | Status today |
|
||||||
|
|---|---|---|
|
||||||
|
| Launch line | **"Every agent you're running reports into one notch, and the one waiting on you pins above the rest."** | Verified by code ([FRE-322] §3). Needs the one-Mac take before it is publishable. |
|
||||||
|
| Second line, same take | **"Approve an agent's tool call from your Mac's notch, and the agent carries on."** | Needs the demo ([FRE-322] §1). The same take settles it. |
|
||||||
|
|
||||||
|
Working line for internal writing, not for publication:
|
||||||
|
|
||||||
|
> **Your Mac and your agents, under control.** One notch for the whole fleet: you
|
||||||
|
> see what is running, you unblock what is waiting, and the machine stays cool
|
||||||
|
> while it works.
|
||||||
|
|
||||||
## The wedge
|
## The wedge
|
||||||
|
|
||||||
@@ -22,15 +48,18 @@ The thing to say over and over, in every channel, in slightly different words:
|
|||||||
It is acute, quantifiable, and true — and it is the demo: a card appears, you tap
|
It is acute, quantifiable, and true — and it is the demo: a card appears, you tap
|
||||||
Allow, work resumes. Ten seconds, no explanation needed.
|
Allow, work resumes. Ten seconds, no explanation needed.
|
||||||
|
|
||||||
## The bundling reframe
|
## The consolidation reframe
|
||||||
|
|
||||||
Three audiences (agent herders, hardware tinkerers, glanceable-widget users) in one
|
Three audiences (agent herders, hardware tinkerers, glanceable-widget users) in one
|
||||||
$10 app reads like a grab bag. Reframe as consolidation, which is the honest story:
|
app reads like a grab bag. Reframe as consolidation, which is the honest story:
|
||||||
|
|
||||||
> One $10 app instead of a notch toy, a fan utility, a battery limiter, a menu-bar
|
> One app instead of a notch toy, a fan utility, a battery limiter, a menu-bar stats
|
||||||
> stats strip, and a notification hack. Same one-time purchase, no subscription.
|
> strip, and a notification hack. One purchase, no subscription.
|
||||||
|
|
||||||
Price rises to $15 at 1.0 — dated and honest, not a permanent beta.
|
**No price, no discount and no future price in copy.** The price is $10, it stays
|
||||||
|
$10, and the dated rise to $15 is withdrawn (Chief of Staff, 2026-10-29 04:01). A
|
||||||
|
price change is a Chief of Staff + sponsor call; it is never a marketing decision
|
||||||
|
and never a launch asset.
|
||||||
|
|
||||||
## Hard rules
|
## Hard rules
|
||||||
|
|
||||||
@@ -48,30 +77,54 @@ pages, marketing copy, comparison tables, meta descriptions, emails, FAQs, all o
|
|||||||
community replies written by a human, where quoting what a user said about another
|
community replies written by a human, where quoting what a user said about another
|
||||||
tool is normal conversation.
|
tool is normal conversation.
|
||||||
|
|
||||||
### 2. A claim requires a test to exist
|
### 2. A claim needs a verdict, not just a test
|
||||||
|
|
||||||
Copy that says "13 agents" or "no data leaves the device" must have a test backing it.
|
Copy that says "every agent", "13 agents", "no data leaves the device" must point at
|
||||||
If a claim cannot be pointed at code or a test, it does not go in copy.
|
both a test **and** a CTO verdict naming the exact sentence. A passing test is not a
|
||||||
|
verdict; a verdict is not a recording. If a sentence cannot be traced to a row in the
|
||||||
|
tables below, it does not go in copy.
|
||||||
|
|
||||||
### 3. Honest privacy scoping
|
### 3. Honest privacy scoping
|
||||||
|
|
||||||
Telemetry and the trial email **do** leave the device. The defensible claim is:
|
Telemetry and the licence check **do** leave the device: Sentry runs on **every
|
||||||
|
launch, unconditionally** (with the Mac's IP address via `sendDefaultPii`), and the
|
||||||
|
trial check sends `IOPlatformUUID` and the device name to `freno.me` ([FRE-322]
|
||||||
|
§2.4, corrected by [FRE-354] `(b)`). Never publish, in any wording:
|
||||||
|
|
||||||
> **No prompt or code content leaves the device.**
|
- "no data leaves the device" / "local-only" / "no telemetry" / "no account"
|
||||||
|
- "nothing leaves your Mac"
|
||||||
|
|
||||||
Never "no data leaves the device".
|
The one favourable engine sentence — about the **hook and bridge only**, never
|
||||||
|
"The Nook" — as corrected by [FRE-354] `(j)`: the hook and bridge **are not a cloud
|
||||||
|
service**. Do not write "talk to nothing but your Mac" (too absolute: the
|
||||||
|
`NOOK_PORTS` fan-out resolves `host.docker.internal` and the default gateway, and
|
||||||
|
`defaultGateway()` opens a no-packet UDP socket to `8.8.8.8:80`), and do not narrow
|
||||||
|
it to "over SSH" (a tailnet peer dials directly).
|
||||||
|
|
||||||
|
It is verified as a code fact ([FRE-322] §2, [FRE-354] `(j)`), and it is still
|
||||||
|
**held** until the disclosures land: Sentry gated behind a preference or removed,
|
||||||
|
and `/privacy` + `/faq` corrected against the egress inventory (patch on branch
|
||||||
|
`faq/from-objections-2026-10-29`, verified by [FRE-354]). A true sentence next to
|
||||||
|
false ones is worse than no sentence.
|
||||||
|
|
||||||
### 4. Sell what works today
|
### 4. Sell what works today
|
||||||
|
|
||||||
Phone approvals are deferred until a shipped iOS app exists — no "(coming soon)"
|
An iPhone + Apple Watch companion **is** in users' hands as a TestFlight beta
|
||||||
parentheticals. The launch story is the approval card: an agent blocked on a
|
(`ios-v0.3.2`, 2026-10-22; [FRE-354] open question). That makes one thing true and
|
||||||
permission prompt, unblocked from the Mac's notch, and work resumes.
|
one thing forbidden: say "TestFlight beta" when the phone exists in a sentence at
|
||||||
|
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 shipped and
|
Remote SSH is **publishable**, and the site sells it again ([FRE-997]). The
|
||||||
has not been recorded (FRE-320, FRE-205), and the current path needs hand-edited
|
copy that was pulled on 2026-10-29 had been checked against claims it never made:
|
||||||
`~/.ssh/config` and a one-time `ssh-copy-id` (FRE-320). Its copy is parked in
|
hosts come from `~/.ssh/config` and Tailscale (`RemoteHostDiscovery`), the
|
||||||
`marketing/drafts/remote-ssh/` until the CTO verifies the shipping flow and a
|
one-time `ssh-copy-id` is a prerequisite the row hands the user as a command
|
||||||
demo exists.
|
(`RemoteHostStatus.fixCommand`), the per-host credential is minted and copied
|
||||||
|
over (`RemoteMirror`), and a remote host mirrors this Mac's integrations
|
||||||
|
(`RemoteMirrorReconcile`). Two things in the old copy were wrong and were fixed
|
||||||
|
rather than restored: the "every hooked agent" overstatement, and a count that
|
||||||
|
said thirteen against a registry of eighteen. Muse, Cline and Amp are
|
||||||
|
`.unsupported` for remote in `AgentSpec.remote`, and the copy names them.
|
||||||
|
|
||||||
### 5. Humans hold every account
|
### 5. Humans hold every account
|
||||||
|
|
||||||
@@ -79,28 +132,30 @@ No agent authenticates, posts, upvotes, or DMs. No sock puppets, no self-mention
|
|||||||
without disclosure. `approved/` **is the only thing a human ever publishes from** —
|
without disclosure. `approved/` **is the only thing a human ever publishes from** —
|
||||||
nothing posts itself.
|
nothing posts itself.
|
||||||
|
|
||||||
## Approved claims (each backed by code)
|
## Claims: status at 2026-11-03
|
||||||
|
|
||||||
| Claim | Backing |
|
Each row names the evidence, not a feeling. Re-read the verdict before using a row.
|
||||||
|---|---|
|
|
||||||
| Thirteen hooked agents, one glance | `AgentSpec.all` — claude, codex, opencode, pi, omp, muse, hermes, openclaw, zai, gemini, cline, qwen, deepseek |
|
|
||||||
| Permission gate answered from a card | awaitPolicyTable + bridge directive path (`hook/normalize.go`, `serialize.go`) |
|
|
||||||
| Fan / thermal / charge control | `helper/` (privileged helper, SMC, no kext) |
|
|
||||||
| One-time purchase, no subscription | $10 → $15 at 1.0; Stripe checkout |
|
|
||||||
| macOS 14+ | 27 source files use the `@Observable` framework |
|
|
||||||
| No prompt or code content leaves the device | envelope carries session/event/terminal metadata only (see `docs/agent-interface.md`) |
|
|
||||||
|
|
||||||
## Held claims (not publishable)
|
| Claim | Status | Evidence / next step |
|
||||||
|
|
||||||
| Claim | Why it is held | What has to be true first |
|
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| Remote agents over SSH, one click to install | The one-click path is app-UI-only and needs hand-edited `~/.ssh/config` plus a one-time `ssh-copy-id`; no demo exists (FRE-320). FRE-315 and FRE-322 hold it unverified and demo-gated. | FRE-320/FRE-205 ship a genuinely one-click install, the CTO verifies it, and a recording shows a remote agent joining the island. Copy parked in `marketing/drafts/remote-ssh/`. |
|
| Approve an agent's tool call from the notch, and it carries on | **Needs demo** — not publishable | [FRE-322] §1. `AgentLiveTurnTests` has never run on a signed-in host; one observed local run settles it. Say "an agent", never "any agent", never "always". |
|
||||||
|
| Every agent you're running reports into one notch; the blocked one pins above the rest | **Verified by code, gated on the take** | [FRE-322] §3. Same one-Mac take as above. No count in the sentence. |
|
||||||
|
| The hook and bridge are not a cloud service | **Verified code fact, held on disclosure** | [FRE-322] §2, wording corrected by [FRE-354] `(j)`. Held until Sentry is gated/removed and `/privacy` + `/faq` land. Never widen to "The Nook", never "nothing but your Mac". |
|
||||||
|
| Fan, thermal and charge control | **No claim verdict yet — do not lead with it** | Backed by `helper/` (privileged helper, SMC, no kext), but no CTO sentence exists for it. Documentary, not launch. |
|
||||||
|
| 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, and `every` / `all` are still banned without naming the three local-only agents. |
|
||||||
|
| 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 | **Publishable, with the three local-only agents named** | [FRE-997]: `RemoteHostDiscovery` (ssh config + tailnet), `RemoteMirror` (hook push, credential, mirroring), `RemoteMirrorReconcile`, `RemoteHostStatus.fixCommand` (the one-time `ssh-copy-id` the row shows). `AgentSpec.remote` is `.unsupported` for Muse, Cline and Amp — the landing bullet and /agents paragraph name them; never say "every hooked agent". |
|
||||||
|
| "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. |
|
||||||
|
|
||||||
## Voice
|
## Voice
|
||||||
|
|
||||||
- Short, concrete, no hype adjectives. Numbers over adjectives ("thirteen agents",
|
- Short, concrete, no hype adjectives. Numbers over adjectives — the only count
|
||||||
"one tap", "$10 once").
|
that ships is eighteen, and it is the registry's own size (see the coverage row
|
||||||
- The notch/island is the product's home; agents are the product's point.
|
above).
|
||||||
|
- The notch is the product's home; agents are the product's point.
|
||||||
- Never apologize for being a utility. The utility is the point.
|
- Never apologize for being a utility. The utility is the point.
|
||||||
- Banned phrasings: competitor names (see rule 1), "AI-powered", "revolutionary",
|
- Banned phrasings: competitor names (see rule 1), "AI-powered", "revolutionary",
|
||||||
"game-changer", "seamlessly". Unverifiable superlatives generally.
|
"game-changer", "seamlessly", any unverifiable superlative — and the specific
|
||||||
|
banned sentences in rule 3 and the table above.
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import {
|
import {
|
||||||
For,
|
For,
|
||||||
|
Show,
|
||||||
createEffect,
|
createEffect,
|
||||||
createSignal,
|
createSignal,
|
||||||
onCleanup,
|
onCleanup,
|
||||||
@@ -20,6 +21,7 @@ import { Campfire, type CampfireState } from "./Campfire";
|
|||||||
import {
|
import {
|
||||||
ApprovalCardMock,
|
ApprovalCardMock,
|
||||||
SessionRowMock,
|
SessionRowMock,
|
||||||
|
Glyph,
|
||||||
CLAUDE,
|
CLAUDE,
|
||||||
CODEX
|
CODEX
|
||||||
} from "./ModuleMocks";
|
} from "./ModuleMocks";
|
||||||
@@ -35,7 +37,7 @@ import {
|
|||||||
* Detailed feature breakdowns: alternating copy + live-demo sections.
|
* Detailed feature breakdowns: alternating copy + live-demo sections.
|
||||||
* Each demo stages the real interaction loop: attention flares, approval
|
* Each demo stages the real interaction loop: attention flares, approval
|
||||||
* resolves, fans throttle, the charge ceiling holds, meetings surface,
|
* resolves, fans throttle, the charge ceiling holds, meetings surface,
|
||||||
* the camera wakes. Newer demos live in
|
* the camera wakes, a remote host goes live. Newer demos live in
|
||||||
* MoreDemos.tsx; app-anatomy pieces in ModuleMocks.tsx.
|
* MoreDemos.tsx; app-anatomy pieces in ModuleMocks.tsx.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@@ -805,6 +807,112 @@ function CameraDemo() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ── Demo 7: Settings ▸ Remote hosts — one click to Live ──────────── */
|
||||||
|
|
||||||
|
/** RemoteMirror's progress steps, in order. */
|
||||||
|
const SETUP_STEPS = [
|
||||||
|
"connecting",
|
||||||
|
"pushing the hook binary",
|
||||||
|
"configuring agents",
|
||||||
|
"checking the host can reach this Mac"
|
||||||
|
];
|
||||||
|
|
||||||
|
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 live = () => step() === 4;
|
||||||
|
const working = () => step() >= 0 && step() < 4;
|
||||||
|
const chip = () =>
|
||||||
|
live()
|
||||||
|
? { color: "rgb(48,209,88)", 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 (
|
||||||
|
<div class="relative mx-auto w-full max-w-[360px] space-y-3">
|
||||||
|
{/* the host's card in Settings ▸ Agents ▸ Remote hosts */}
|
||||||
|
<ModuleCard>
|
||||||
|
<div class="space-y-2 text-left">
|
||||||
|
<div class="flex items-center gap-2.5">
|
||||||
|
<div class="min-w-0 flex-1">
|
||||||
|
<p class="text-[13px] font-semibold text-white">gpu-rig</p>
|
||||||
|
<p class="truncate text-[11px] text-white/45">tailnet peer</p>
|
||||||
|
</div>
|
||||||
|
<Show when={working()}>
|
||||||
|
<span class="h-3 w-3 animate-spin rounded-full border-[1.5px] border-white/25 border-t-white/80" />
|
||||||
|
</Show>
|
||||||
|
<span class="flex items-center gap-1.5 text-[10.5px] font-medium text-white/85">
|
||||||
|
<span
|
||||||
|
class="h-[7px] w-[7px] rounded-full transition-colors duration-300"
|
||||||
|
style={{ background: chip().color }}
|
||||||
|
/>
|
||||||
|
{chip().label}
|
||||||
|
</span>
|
||||||
|
<Show
|
||||||
|
when={live()}
|
||||||
|
fallback={
|
||||||
|
<span
|
||||||
|
class="rounded-md px-2.5 py-1 text-[11px] font-semibold text-white transition-all duration-200"
|
||||||
|
style={{
|
||||||
|
background: ACCENT,
|
||||||
|
opacity: working() ? 0 : 1,
|
||||||
|
transform: pressed() ? "scale(0.92)" : "scale(1)"
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Set up
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Glyph name="refresh" size={11} class="text-white/50" />
|
||||||
|
<span class="rounded-md bg-white/10 px-2 py-1 text-[11px] font-semibold text-[#ff6961]">
|
||||||
|
Remove
|
||||||
|
</span>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
<p class="h-4 text-[11px] text-white/50">
|
||||||
|
{working() ? SETUP_STEPS[step()] : " "}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</ModuleCard>
|
||||||
|
|
||||||
|
{/* its agents arrive as ordinary rows, the machine named in the chip */}
|
||||||
|
<div
|
||||||
|
class="transition-all duration-500"
|
||||||
|
style={{
|
||||||
|
opacity: live() ? 1 : 0,
|
||||||
|
transform: live() ? "translateY(0)" : "translateY(6px)"
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<SessionRowMock
|
||||||
|
title="trainer · fp16"
|
||||||
|
prompt="benchmark fp16"
|
||||||
|
state="Running python bench.py…"
|
||||||
|
status="running"
|
||||||
|
agent={CODEX}
|
||||||
|
host="gpu-rig"
|
||||||
|
origin="iTerm2"
|
||||||
|
age="now"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<FakeCursor
|
||||||
|
phase={pressed() ? "press" : step() === -1 ? "idle" : "live"}
|
||||||
|
at={{ x: 88, y: 16 }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/* ── Demo 8: campfire states ───────────────────────────────────────── */
|
/* ── Demo 8: campfire states ───────────────────────────────────────── */
|
||||||
|
|
||||||
function CampfireDemo() {
|
function CampfireDemo() {
|
||||||
@@ -851,7 +959,7 @@ export default function FeatureBreakdowns() {
|
|||||||
title="Your whole fleet, in one pixel strip"
|
title="Your whole fleet, in one pixel strip"
|
||||||
body="Every session, every agent, one square each. The pill never wraps, never grows — an arriving question flashes the fire and swaps the slot, then hands the notch back."
|
body="Every session, every agent, one square each. The pill never wraps, never grows — an arriving question flashes the fire and swaps the slot, then hands the notch back."
|
||||||
bullets={[
|
bullets={[
|
||||||
"13 agents: Claude Code, Codex, Gemini CLI, OpenCode, Pi, Muse, and more",
|
"18 agents: Claude Code, Codex, Gemini CLI, OpenCode, Pi, Muse, and more",
|
||||||
"Custom agents: your own coding agent can wire itself in from the published contract",
|
"Custom agents: your own coding agent can wire itself in from the published contract",
|
||||||
"Slot rankings — you choose which data earns the notch",
|
"Slot rankings — you choose which data earns the notch",
|
||||||
"You don't need a notch — external displays get the same island, floating top-center"
|
"You don't need a notch — external displays get the same island, floating top-center"
|
||||||
@@ -868,6 +976,7 @@ export default function FeatureBreakdowns() {
|
|||||||
"Approval cards pin above the session list; questions answer inside their row",
|
"Approval cards pin above the session list; questions answer inside their row",
|
||||||
"Allow for session waves through the rest of that session's requests",
|
"Allow for session waves through the rest of that session's requests",
|
||||||
"Answer from iPhone or Apple Watch too, now in TestFlight beta",
|
"Answer from iPhone or Apple Watch too, now in TestFlight beta",
|
||||||
|
"Works across local and remote sessions; Muse, Cline and Amp are this Mac only"
|
||||||
]}
|
]}
|
||||||
reversed
|
reversed
|
||||||
>
|
>
|
||||||
@@ -887,6 +996,20 @@ export default function FeatureBreakdowns() {
|
|||||||
<TerminalReplyDemo />
|
<TerminalReplyDemo />
|
||||||
</BreakdownSection>
|
</BreakdownSection>
|
||||||
|
|
||||||
|
<BreakdownSection
|
||||||
|
kicker="Remote"
|
||||||
|
title="Any SSH box, one click away"
|
||||||
|
body="Pick a host from your ssh config or your tailnet and click once — the nook-hook installs itself and its agents appear beside your local ones, indistinguishable."
|
||||||
|
bullets={[
|
||||||
|
"One click: connect, push the hook, configure agents, check it can reach this Mac",
|
||||||
|
"Hosts mirror this Mac's installed integrations automatically",
|
||||||
|
"Remote sessions are ordinary rows; the agent chip names the machine"
|
||||||
|
]}
|
||||||
|
reversed
|
||||||
|
>
|
||||||
|
<RemoteHostDemo />
|
||||||
|
</BreakdownSection>
|
||||||
|
|
||||||
<BreakdownSection
|
<BreakdownSection
|
||||||
kicker="The campfire"
|
kicker="The campfire"
|
||||||
title="A hearth that tells the truth"
|
title="A hearth that tells the truth"
|
||||||
|
|||||||
@@ -1,6 +1,12 @@
|
|||||||
import { For, createSignal, onCleanup, onMount, type JSX } from "solid-js";
|
import { For, createSignal, onCleanup, onMount, type JSX } from "solid-js";
|
||||||
import { IslandPill, STATUS, CALM, CRITICAL, ModuleCard } from "./IslandMock";
|
import { IslandPill, STATUS, CALM, CRITICAL, ModuleCard } from "./IslandMock";
|
||||||
import { ApprovalCardMock, DieLine, CLAUDE } from "./ModuleMocks";
|
import {
|
||||||
|
ApprovalCardMock,
|
||||||
|
SessionRowMock,
|
||||||
|
DieLine,
|
||||||
|
CLAUDE,
|
||||||
|
CODEX
|
||||||
|
} from "./ModuleMocks";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Feature collage: five cells sold as the app's actual UI. The hero cell
|
* Feature collage: five cells sold as the app's actual UI. The hero cell
|
||||||
@@ -99,6 +105,35 @@ function FansPanelMock() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remote agents as the panel draws them: ordinary session rows whose agent
|
||||||
|
* chip names the machine ("Claude Code · build-box").
|
||||||
|
*/
|
||||||
|
function RemoteMock() {
|
||||||
|
return (
|
||||||
|
<div class="space-y-1.5">
|
||||||
|
<SessionRowMock
|
||||||
|
title="api · main"
|
||||||
|
prompt="fix the flaky test"
|
||||||
|
state="Running pytest -x…"
|
||||||
|
status="running"
|
||||||
|
agent={CLAUDE}
|
||||||
|
host="build-box"
|
||||||
|
age="4m"
|
||||||
|
/>
|
||||||
|
<SessionRowMock
|
||||||
|
title="trainer · fp16"
|
||||||
|
prompt="benchmark fp16"
|
||||||
|
state="Done · replied 1m ago"
|
||||||
|
status="ready"
|
||||||
|
agent={CODEX}
|
||||||
|
host="gpu-rig"
|
||||||
|
age="12m"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/* Deterministic usage wander for the traces. */
|
/* Deterministic usage wander for the traces. */
|
||||||
const wander = (n: number, k: number) =>
|
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);
|
0.42 + 0.22 * Math.sin(n * 0.45 + k * 1.7) + 0.12 * Math.sin(n * 1.3 + k);
|
||||||
@@ -221,6 +256,19 @@ export default function FeatureCollage() {
|
|||||||
</InkStage>
|
</InkStage>
|
||||||
</CollageCell>
|
</CollageCell>
|
||||||
|
|
||||||
|
{/* Remote agents */}
|
||||||
|
<CollageCell
|
||||||
|
kicker="Remote"
|
||||||
|
title="Agents on other machines"
|
||||||
|
body="Run agents on Linux boxes and SSH servers — one click installs the nook-hook, and their sessions join the same island."
|
||||||
|
>
|
||||||
|
<InkStage class="w-full">
|
||||||
|
<div class="w-full px-3 text-left">
|
||||||
|
<RemoteMock />
|
||||||
|
</div>
|
||||||
|
</InkStage>
|
||||||
|
</CollageCell>
|
||||||
|
|
||||||
{/* Gauges */}
|
{/* Gauges */}
|
||||||
<CollageCell
|
<CollageCell
|
||||||
kicker="Your Mac"
|
kicker="Your Mac"
|
||||||
|
|||||||
@@ -722,7 +722,7 @@ function LayoutMock() {
|
|||||||
/* ── Marquee ─────────────────────────────────────────────────────── */
|
/* ── Marquee ─────────────────────────────────────────────────────── */
|
||||||
|
|
||||||
const CHIPS_A = [
|
const CHIPS_A = [
|
||||||
"13 agents",
|
"18 agents",
|
||||||
"Custom agents",
|
"Custom agents",
|
||||||
"Six chiptunes",
|
"Six chiptunes",
|
||||||
"Focus-aware slots",
|
"Focus-aware slots",
|
||||||
@@ -740,6 +740,7 @@ const CHIPS_B = [
|
|||||||
"tmux · iTerm2 · kitty",
|
"tmux · iTerm2 · kitty",
|
||||||
"Markdown replies",
|
"Markdown replies",
|
||||||
"Tailscale hosts",
|
"Tailscale hosts",
|
||||||
|
"One-click SSH setup",
|
||||||
"iPhone widgets",
|
"iPhone widgets",
|
||||||
"Watch-face widgets",
|
"Watch-face widgets",
|
||||||
"Screenshots to clipboard",
|
"Screenshots to clipboard",
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* Regression test for the /agents roster (NOOK-336).
|
* Regression test for the /agents roster and its coverage count.
|
||||||
*
|
*
|
||||||
* The page mirrors the app's registry, `AgentSpec.all` in
|
* The page mirrors the app's registry, `AgentSpec.all` in
|
||||||
* the-nook/Sources/NookCore/Agents/AgentRegistry.swift. When Muse joined
|
* the-nook/Sources/NookCore/Agents/AgentRegistry.swift. When Muse joined
|
||||||
@@ -8,6 +8,12 @@
|
|||||||
* them. The registry lives in another repo, so the expected roster is
|
* them. The registry lives in another repo, so the expected roster is
|
||||||
* restated here; when an agent is added there, add it here and on the page.
|
* restated here; when an agent is added there, add it here and on the page.
|
||||||
*
|
*
|
||||||
|
* FRE-997 restored the count. The page said thirteen while the registry held
|
||||||
|
* eighteen, so the count was dropped rather than corrected (FRE-359 verdict B);
|
||||||
|
* now that all eighteen rows are on the page, the roster check is an equality
|
||||||
|
* check again and the page states eighteen. A subset check would let a shipped
|
||||||
|
* agent go missing behind a passing suite, which is the NOOK-336 failure.
|
||||||
|
*
|
||||||
* STATIC SOURCE AUDIT, the same pattern as the downloads regressions.
|
* STATIC SOURCE AUDIT, the same pattern as the downloads regressions.
|
||||||
*/
|
*/
|
||||||
import { describe, it, expect } from "bun:test";
|
import { describe, it, expect } from "bun:test";
|
||||||
@@ -16,7 +22,13 @@ import { join } from "node:path";
|
|||||||
|
|
||||||
const SOURCE = readFileSync(join(import.meta.dir, "agents.tsx"), "utf8");
|
const SOURCE = readFileSync(join(import.meta.dir, "agents.tsx"), "utf8");
|
||||||
|
|
||||||
/** AgentSpec.all display names, in registry order. */
|
/** User-facing copy only: comments are not copy and may name the old wording. */
|
||||||
|
const COPY = SOURCE.replace(/\/\*[\s\S]*?\*\//g, "").replace(/^\s*\/\/.*$/gm, "");
|
||||||
|
|
||||||
|
/** JSX wraps prose across lines; match a sentence, not its indentation. */
|
||||||
|
const PROSE = COPY.replace(/\s+/g, " ");
|
||||||
|
|
||||||
|
/** AgentSpec.all display names, in registry order (18 rows). @ The-Nook f4258034, 2026-10-29 */
|
||||||
const REGISTRY = [
|
const REGISTRY = [
|
||||||
"Claude Code",
|
"Claude Code",
|
||||||
"Codex",
|
"Codex",
|
||||||
@@ -30,25 +42,49 @@ const REGISTRY = [
|
|||||||
"Gemini CLI",
|
"Gemini CLI",
|
||||||
"Cline",
|
"Cline",
|
||||||
"Qwen Code",
|
"Qwen Code",
|
||||||
"DeepSeek"
|
"DeepSeek",
|
||||||
|
"Kimi",
|
||||||
|
"GitHub Copilot CLI",
|
||||||
|
"Crush",
|
||||||
|
"Cursor",
|
||||||
|
"Amp"
|
||||||
];
|
];
|
||||||
|
|
||||||
const NUMBER_WORDS = ["twelve", "thirteen", "fourteen", "fifteen"];
|
|
||||||
|
|
||||||
const entries = [...SOURCE.matchAll(/^\s{4}name: "([^"]+)",$/gm)].map((m) => m[1]);
|
const entries = [...SOURCE.matchAll(/^\s{4}name: "([^"]+)",$/gm)].map((m) => m[1]);
|
||||||
|
|
||||||
describe("Nook /agents roster (NOOK-336)", () => {
|
describe("Nook /agents roster", () => {
|
||||||
it("lists every registry agent, in registry order", () => {
|
it("lists every registry agent, once, in registry order", () => {
|
||||||
expect(entries).toEqual(REGISTRY);
|
expect(entries).toEqual(REGISTRY);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("states the same count everywhere it names one", () => {
|
it("states the registry's size", () => {
|
||||||
const word = NUMBER_WORDS[entries.length - 12];
|
expect(entries.length).toBe(18);
|
||||||
for (const other of NUMBER_WORDS.filter((w) => w !== word)) {
|
expect(COPY).toContain("all eighteen");
|
||||||
expect(SOURCE.toLowerCase()).not.toContain(other);
|
expect(COPY).toContain("The 18 agents");
|
||||||
}
|
expect(COPY).toContain("eighteen coding agents");
|
||||||
expect(SOURCE).toContain(`all ${word}`);
|
});
|
||||||
expect(SOURCE).toContain(`The ${entries.length} agents`);
|
|
||||||
expect(SOURCE).toContain(`watches ${word} coding agents`);
|
it("carries no stale count", () => {
|
||||||
|
expect(COPY.toLowerCase()).not.toContain("thirteen");
|
||||||
|
expect(COPY).not.toMatch(/\b13 agents\b/);
|
||||||
|
expect(COPY).not.toContain("all thirteen");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Nook /agents remote coverage", () => {
|
||||||
|
it("says agents can run on remote machines over SSH", () => {
|
||||||
|
expect(PROSE).toContain("remote machines");
|
||||||
|
expect(PROSE).toContain("remote sessions report into the same grid");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("names the three agents that stay on this Mac", () => {
|
||||||
|
expect(PROSE).toContain("Muse, Cline and Amp run on this Mac only");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("discloses the local-only rows where they are listed", () => {
|
||||||
|
const localOnly = [
|
||||||
|
...SOURCE.matchAll(/This Mac only — remote hosts skip it/g)
|
||||||
|
];
|
||||||
|
expect(localOnly.length).toBe(3);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
/**
|
/**
|
||||||
* `/agents` on the Nook subdomain — supported agents and their event coverage.
|
* `/agents` on the Nook subdomain — all eighteen agents in the registry and
|
||||||
|
* each row's event coverage.
|
||||||
*
|
*
|
||||||
* The registry below mirrors `AgentSpec.all` in the app
|
* The registry below mirrors `AgentSpec.all` in the app
|
||||||
* (~/Code/the-nook/Sources/NookCore/Agents/AgentSpec.swift) and each agent's
|
* (~/Code/the-nook/Sources/NookCore/Agents/AgentRegistry.swift) and each
|
||||||
* installed hook surface (hook/setup.go) or plugin event subscription
|
* agent's installed hook surface (hook/setup.go) or plugin event subscription
|
||||||
* (plugins/*), normalized onto the shared event vocabulary
|
* (plugins/*), normalized onto the shared event vocabulary
|
||||||
* (~/Code/the-nook/docs/agent-interface.md). Copy follows
|
* (~/Code/the-nook/docs/agent-interface.md). Copy follows
|
||||||
* marketing/positioning.md: category nouns, honest scoping, no unverifiable
|
* marketing/positioning.md: category nouns, honest scoping, no unverifiable
|
||||||
@@ -208,7 +209,8 @@ const AGENTS: AgentEntry[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Cline",
|
name: "Cline",
|
||||||
wiring: "One hook file per event in its hooks directory",
|
wiring:
|
||||||
|
"One hook file per event in its hooks directory. This Mac only — remote hosts skip it",
|
||||||
events: [
|
events: [
|
||||||
"SessionStart",
|
"SessionStart",
|
||||||
"SessionEnd",
|
"SessionEnd",
|
||||||
@@ -259,6 +261,93 @@ const AGENTS: AgentEntry[] = [
|
|||||||
],
|
],
|
||||||
gating:
|
gating:
|
||||||
"Tool calls can be allowed or denied from a card; permission prompts stay with the agent"
|
"Tool calls can be allowed or denied from a card; permission prompts stay with the agent"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Kimi",
|
||||||
|
wiring: "Hook rules in its TOML config, one per event",
|
||||||
|
events: [
|
||||||
|
"SessionStart",
|
||||||
|
"SessionEnd",
|
||||||
|
"UserPromptSubmit",
|
||||||
|
"AgentStart",
|
||||||
|
"PreToolUse",
|
||||||
|
"PermissionRequest",
|
||||||
|
"PermissionResolved",
|
||||||
|
"PostToolUse",
|
||||||
|
"PostToolUseFailure",
|
||||||
|
"Stop",
|
||||||
|
"SessionError",
|
||||||
|
"SubagentStart",
|
||||||
|
"SubagentStop",
|
||||||
|
"PreCompact",
|
||||||
|
"Notification"
|
||||||
|
],
|
||||||
|
gating:
|
||||||
|
"Observation — every event appears as a card; its PreToolUse fires before its own permission check, so the agent keeps its own prompting"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "GitHub Copilot CLI",
|
||||||
|
wiring: "One hook file of ours in its hooks directory",
|
||||||
|
events: [
|
||||||
|
"SessionStart",
|
||||||
|
"SessionEnd",
|
||||||
|
"UserPromptSubmit",
|
||||||
|
"PreToolUse",
|
||||||
|
"PermissionRequest",
|
||||||
|
"PostToolUse",
|
||||||
|
"PostToolUseFailure",
|
||||||
|
"Stop",
|
||||||
|
"SubagentStart",
|
||||||
|
"SubagentStop",
|
||||||
|
"ErrorOccurred",
|
||||||
|
"PreCompact",
|
||||||
|
"Notification"
|
||||||
|
],
|
||||||
|
gating:
|
||||||
|
"Observation — every event appears as a card; its permission request runs before its own policy check, so the agent keeps its own prompting"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Crush",
|
||||||
|
wiring:
|
||||||
|
"One entry in its JSONC config, the only event it documents",
|
||||||
|
events: ["PreToolUse"],
|
||||||
|
gating:
|
||||||
|
"Observation — tool calls appear as cards; its own permission prompts decide"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Cursor",
|
||||||
|
wiring:
|
||||||
|
"Hook entries in its hooks.json, merged with your own. No permission hook, on purpose",
|
||||||
|
events: [
|
||||||
|
"SessionStart",
|
||||||
|
"SessionEnd",
|
||||||
|
"UserPromptSubmit",
|
||||||
|
"PostToolUse",
|
||||||
|
"PostToolUseFailure",
|
||||||
|
"Stop",
|
||||||
|
"SessionError",
|
||||||
|
"AfterAgentResponse",
|
||||||
|
"SubagentStop",
|
||||||
|
"PreCompact"
|
||||||
|
],
|
||||||
|
gating:
|
||||||
|
"Observation — a permission hook that stays silent blocks the action here, so Cursor keeps its own prompting"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Amp",
|
||||||
|
wiring:
|
||||||
|
"In-process extension package. This Mac only — remote hosts skip it",
|
||||||
|
events: [
|
||||||
|
"SessionStart",
|
||||||
|
"SessionEnd",
|
||||||
|
"UserPromptSubmit",
|
||||||
|
"PreToolUse",
|
||||||
|
"PostToolUse",
|
||||||
|
"Stop",
|
||||||
|
"SessionError"
|
||||||
|
],
|
||||||
|
gating:
|
||||||
|
"Observation — its plugin API has no permission-wait event, so the agent keeps its own prompting"
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -283,8 +372,8 @@ export default function NookAgentsPage() {
|
|||||||
<EdgeCacheHeaders maxAge={300} />
|
<EdgeCacheHeaders maxAge={300} />
|
||||||
<PageHead
|
<PageHead
|
||||||
title="Supported agents"
|
title="Supported agents"
|
||||||
description="Every coding agent The Nook supports — all thirteen, with each hook's exact event coverage and what you can do from a card: watch sessions live, answer permission prompts, unblock work."
|
description="Every coding agent The Nook supports — all eighteen, with each hook's exact event coverage and what you can do from a card: watch sessions live, answer permission prompts, unblock work."
|
||||||
ogTitle="The 13 agents The Nook supports, and what each hook sees"
|
ogTitle="The 18 agents The Nook supports, and what each hook sees"
|
||||||
/>
|
/>
|
||||||
<SubdomainHeader />
|
<SubdomainHeader />
|
||||||
<main class="min-h-screen px-[8vw] py-[8vh]">
|
<main class="min-h-screen px-[8vw] py-[8vh]">
|
||||||
@@ -292,7 +381,7 @@ export default function NookAgentsPage() {
|
|||||||
Supported agents
|
Supported agents
|
||||||
</h1>
|
</h1>
|
||||||
<p class="text-subtext0 mb-2 max-w-2xl text-lg">
|
<p class="text-subtext0 mb-2 max-w-2xl text-lg">
|
||||||
The Nook watches thirteen coding agents in one place — your menu bar and
|
The Nook watches eighteen coding agents in one place — your menu bar and
|
||||||
notch show every session that is running, thinking, or blocked, and a
|
notch show every session that is running, thinking, or blocked, and a
|
||||||
card answers permission prompts without leaving your keyboard.
|
card answers permission prompts without leaving your keyboard.
|
||||||
</p>
|
</p>
|
||||||
@@ -301,7 +390,8 @@ export default function NookAgentsPage() {
|
|||||||
means the agent hands the decision to The Nook while it waits;
|
means the agent hands the decision to The Nook while it waits;
|
||||||
"Observe" means every event still appears live, but the agent keeps
|
"Observe" means every event still appears live, but the agent keeps
|
||||||
its own prompting. Agents can run on this Mac or on remote machines
|
its own prompting. Agents can run on this Mac or on remote machines
|
||||||
over SSH — remote sessions report into the same grid.
|
over SSH — remote sessions report into the same grid. Muse, Cline and
|
||||||
|
Amp run on this Mac only.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div class="grid gap-4 lg:grid-cols-2">
|
<div class="grid gap-4 lg:grid-cols-2">
|
||||||
|
|||||||
112
src/routes/nook/remote-claims.test.ts
Normal file
112
src/routes/nook/remote-claims.test.ts
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
/**
|
||||||
|
* Guard for the remote / SSH story on the Nook site, and for the two phrasings
|
||||||
|
* that must not come back with it (FRE-323 → FRE-997).
|
||||||
|
*
|
||||||
|
* FRE-323 removed the remote copy because it had been checked against claims
|
||||||
|
* the copy never made: the app does list hosts from `~/.ssh/config` and
|
||||||
|
* Tailscale, a one-time `ssh-copy-id` is a normal prerequisite the row explains,
|
||||||
|
* and the per-host credential is minted and copied automatically
|
||||||
|
* (`RemoteHostDiscovery`, `RemoteHostStatus.fixCommand`, `RemoteMirror`). Two
|
||||||
|
* things in the old copy were genuinely wrong, and this file guards those
|
||||||
|
* instead of the copy as a whole:
|
||||||
|
*
|
||||||
|
* - "Works across every hooked agent, local or remote" overstated it. Muse,
|
||||||
|
* Cline and Amp are `.unsupported` for remote in `AgentSpec.remote`, so the
|
||||||
|
* landing bullet and the /agents paragraph name them.
|
||||||
|
* - The count said thirteen while `AgentSpec.all` holds eighteen. The fix was
|
||||||
|
* to say eighteen, not to drop the count.
|
||||||
|
*
|
||||||
|
* So the removal guards from `d44133f` are gone: a removed sentence is exactly
|
||||||
|
* what this task restores. What stays is the pair of bans, because either one
|
||||||
|
* coming back would repeat the original defect.
|
||||||
|
*
|
||||||
|
* STATIC SOURCE AUDIT, the same pattern as `agents.test.ts` and
|
||||||
|
* `downloads.test.ts` (SolidJS does not render under `bun:test`).
|
||||||
|
*/
|
||||||
|
import { describe, it, expect } from "bun:test";
|
||||||
|
import { readFileSync } from "node:fs";
|
||||||
|
import { join } from "node:path";
|
||||||
|
|
||||||
|
const ROOT = join(import.meta.dir, "../../..");
|
||||||
|
|
||||||
|
/** Shipped page and component source. */
|
||||||
|
const AUDITED = [
|
||||||
|
"src/routes/nook/index.tsx",
|
||||||
|
"src/routes/nook/agents.tsx",
|
||||||
|
"src/routes/nook/faq.tsx",
|
||||||
|
"src/components/nook/FeatureCollage.tsx",
|
||||||
|
"src/components/nook/FeatureBreakdowns.tsx",
|
||||||
|
"src/components/nook/IslandExtras.tsx"
|
||||||
|
];
|
||||||
|
|
||||||
|
/** The overstatements the restored copy must never carry again. */
|
||||||
|
const BANNED_REMOTE_CLAIMS = [
|
||||||
|
// a universal claim: three registry agents cannot mirror over SSH
|
||||||
|
"every hooked agent, local or remote",
|
||||||
|
"Works across every hooked agent",
|
||||||
|
// the stale count this task replaced
|
||||||
|
"13 agents",
|
||||||
|
"all thirteen",
|
||||||
|
"thirteen coding agents"
|
||||||
|
];
|
||||||
|
|
||||||
|
/** User-facing copy only: comments are not copy and may name the old wording. */
|
||||||
|
function copyOf(file: string): string {
|
||||||
|
return readFileSync(join(ROOT, file), "utf8")
|
||||||
|
.replace(/\/\*[\s\S]*?\*\//g, "")
|
||||||
|
.replace(/^\s*\/\/.*$/gm, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("the restored remote copy is on the site (FRE-997)", () => {
|
||||||
|
it("the landing collage sells agents on other machines", () => {
|
||||||
|
const copy = copyOf("src/components/nook/FeatureCollage.tsx");
|
||||||
|
for (const phrase of [
|
||||||
|
"Agents on other machines",
|
||||||
|
"one click installs the nook-hook",
|
||||||
|
"Remote"
|
||||||
|
]) {
|
||||||
|
expect(copy).toContain(phrase);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("the landing breakdown keeps the SSH section and its demo", () => {
|
||||||
|
const copy = copyOf("src/components/nook/FeatureBreakdowns.tsx");
|
||||||
|
for (const phrase of [
|
||||||
|
"Any SSH box, one click away",
|
||||||
|
"Pick a host from your ssh config or your tailnet and click once",
|
||||||
|
"Remote sessions are ordinary rows",
|
||||||
|
"RemoteHostDemo"
|
||||||
|
]) {
|
||||||
|
expect(copy).toContain(phrase);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("the chip is back", () => {
|
||||||
|
expect(copyOf("src/components/nook/IslandExtras.tsx")).toContain(
|
||||||
|
"One-click SSH setup"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("/agents says remote sessions report into the same grid", () => {
|
||||||
|
const copy = copyOf("src/routes/nook/agents.tsx");
|
||||||
|
expect(copy).toContain("remote machines");
|
||||||
|
expect(copy).toContain("remote sessions report into the same grid");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("the two original overstatements stay off the site (FRE-323, FRE-359)", () => {
|
||||||
|
for (const file of AUDITED) {
|
||||||
|
it(`${file} claims no universal remote coverage and no stale count`, () => {
|
||||||
|
const copy = copyOf(file);
|
||||||
|
for (const phrase of BANNED_REMOTE_CLAIMS) {
|
||||||
|
expect(copy.toLowerCase()).not.toContain(phrase.toLowerCase());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
it("the landing names the agents that stay local", () => {
|
||||||
|
expect(copyOf("src/components/nook/FeatureBreakdowns.tsx")).toContain(
|
||||||
|
"Muse, Cline and Amp are this Mac only"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
139
src/routes/smarter-remote/privacy.tsx
Normal file
139
src/routes/smarter-remote/privacy.tsx
Normal file
@@ -0,0 +1,139 @@
|
|||||||
|
/**
|
||||||
|
* A Smarter Remote privacy policy — `freno.me/smarter-remote/privacy`.
|
||||||
|
*
|
||||||
|
* Linked from the iOS app's paywall (App Review 3.1.2). Reflects what the app
|
||||||
|
* actually does: TV control stays on the local network, pairing data stays on
|
||||||
|
* the phone, purchases go through Apple and RevenueCat, and sharing is a QR
|
||||||
|
* code passed between phones.
|
||||||
|
*/
|
||||||
|
import { A } from "@solidjs/router";
|
||||||
|
import { PageHead } from "~/components/PageHead";
|
||||||
|
import { EdgeCacheHeaders } from "~/components/EdgeCacheHeaders";
|
||||||
|
|
||||||
|
export default function SmarterRemotePrivacyPolicy() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<EdgeCacheHeaders maxAge={300} />
|
||||||
|
<PageHead
|
||||||
|
title="A Smarter Remote Privacy Policy"
|
||||||
|
description="Privacy policy for A Smarter Remote, a universal TV remote app that works over your local Wi-Fi network."
|
||||||
|
/>
|
||||||
|
<div class="bg-base">
|
||||||
|
<div class="min-h-screen px-[8vw] py-[8vh]">
|
||||||
|
<div class="py-4 text-xl">A Smarter Remote's Privacy Policy</div>
|
||||||
|
<div class="py-2">Last Updated: November 2, 2026</div>
|
||||||
|
<div class="py-2">
|
||||||
|
Welcome to A Smarter Remote ('We', 'Us',
|
||||||
|
'Our'). The app controls your TVs over your own Wi-Fi
|
||||||
|
network. It has no accounts, no ads, and no analytics. This policy
|
||||||
|
explains the little data it does handle.
|
||||||
|
</div>
|
||||||
|
<ol>
|
||||||
|
<div class="py-2">
|
||||||
|
<div class="pb-2 text-lg">
|
||||||
|
<span class="-ml-4 pr-2">1.</span> Data We Collect
|
||||||
|
</div>
|
||||||
|
<div class="pl-4">
|
||||||
|
<div class="pb-2">
|
||||||
|
<div class="-ml-6">(a) Your TVs:</div> The app finds TVs on
|
||||||
|
your local network and stores their names, network addresses,
|
||||||
|
and platform on your phone. Pairing codes and tokens are kept
|
||||||
|
in your phone's Keychain. This data is sent only to the
|
||||||
|
TV itself, over your local network. It never reaches us.
|
||||||
|
</div>
|
||||||
|
<div class="pb-2">
|
||||||
|
<div class="-ml-6">(b) Purchases:</div> Payments are handled
|
||||||
|
by Apple. We use RevenueCat to check whether you own A Smarter
|
||||||
|
Remote Pro. RevenueCat receives an anonymous app user ID, your
|
||||||
|
purchase and subscription history, and basic device and app
|
||||||
|
details such as OS version, app version, and region. It does
|
||||||
|
not receive your name, email, or payment details.
|
||||||
|
</div>
|
||||||
|
<div class="pb-2">
|
||||||
|
<div class="-ml-6">(c) Voice input:</div> If you use voice
|
||||||
|
search, your speech is converted to text by Apple's
|
||||||
|
speech recognition. Apple may process it on its servers. The
|
||||||
|
resulting text is sent only to your TV.
|
||||||
|
</div>
|
||||||
|
<div class="pb-2">
|
||||||
|
<div class="-ml-6">(d) Sharing:</div> If you share a TV with
|
||||||
|
someone, its name, address, and platform are put into a QR
|
||||||
|
code on your screen for them to scan. It goes from one phone
|
||||||
|
to the other and does not pass through our servers.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="py-2">
|
||||||
|
<div class="pb-2 text-lg">
|
||||||
|
<span class="-ml-4 pr-2">2.</span> How We Use It
|
||||||
|
</div>
|
||||||
|
<div class="pb-2 pl-4">
|
||||||
|
Data about your TVs is used only to control them. Purchase
|
||||||
|
records are used only to unlock Pro. We do not use any of it for
|
||||||
|
advertising, and we do not sell it.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="py-2">
|
||||||
|
<div class="pb-2 text-lg">
|
||||||
|
<span class="-ml-4 pr-2">3.</span> Local Network Access
|
||||||
|
</div>
|
||||||
|
<div class="pb-2 pl-4">
|
||||||
|
iOS asks for permission before the app can use your local
|
||||||
|
network. The app needs it to find and control your TVs. If you
|
||||||
|
decline, the app cannot reach your TVs. You can change this in
|
||||||
|
Settings > Privacy & Security > Local Network.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="py-2">
|
||||||
|
<div class="pb-2 text-lg">
|
||||||
|
<span class="-ml-4 pr-2">4.</span> Deleting Your Data
|
||||||
|
</div>
|
||||||
|
<div class="pb-2 pl-4">
|
||||||
|
Removing a TV in the app deletes its stored details and
|
||||||
|
credentials from your phone. Deleting the app removes all of
|
||||||
|
them. Subscriptions are managed and cancelled in your App Store
|
||||||
|
account settings.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="py-2">
|
||||||
|
<div class="pb-2 text-lg">
|
||||||
|
<span class="-ml-4 pr-2">5.</span> Children's Privacy
|
||||||
|
</div>
|
||||||
|
<div class="pb-2 pl-4">
|
||||||
|
The app does not knowingly collect personal information from
|
||||||
|
anyone, including children under 13.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="py-2">
|
||||||
|
<div class="pb-2 text-lg">
|
||||||
|
<span class="-ml-4 pr-2">6.</span> Changes to the Privacy Policy
|
||||||
|
</div>
|
||||||
|
<div class="pb-2 pl-4">
|
||||||
|
We may update this policy periodically. Any changes are posted on
|
||||||
|
this page, so please review it from time to time.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="py-2">
|
||||||
|
<div class="pb-2 text-lg">
|
||||||
|
<span class="-ml-4 pr-2">7.</span> Contact Us
|
||||||
|
</div>
|
||||||
|
<div class="pb-2 pl-4">
|
||||||
|
If you have any questions about this policy, you can contact us{" "}
|
||||||
|
<A href="/contact" class="text-blue hover-underline-animation">
|
||||||
|
here
|
||||||
|
</A>
|
||||||
|
.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ol>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
141
src/routes/smarter-remote/terms.tsx
Normal file
141
src/routes/smarter-remote/terms.tsx
Normal file
@@ -0,0 +1,141 @@
|
|||||||
|
/**
|
||||||
|
* A Smarter Remote terms of use — `freno.me/smarter-remote/terms`.
|
||||||
|
*
|
||||||
|
* Linked from the iOS app's paywall (App Review 3.1.2). Builds on Apple's
|
||||||
|
* standard EULA and adds the Pro subscription and lifetime purchase terms.
|
||||||
|
*/
|
||||||
|
import { A } from "@solidjs/router";
|
||||||
|
import { PageHead } from "~/components/PageHead";
|
||||||
|
import { EdgeCacheHeaders } from "~/components/EdgeCacheHeaders";
|
||||||
|
|
||||||
|
export default function SmarterRemoteTerms() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<EdgeCacheHeaders maxAge={300} />
|
||||||
|
<PageHead
|
||||||
|
title="A Smarter Remote Terms of Use"
|
||||||
|
description="Terms of use for A Smarter Remote and its Pro subscription."
|
||||||
|
/>
|
||||||
|
<div class="bg-base">
|
||||||
|
<div class="min-h-screen px-[8vw] py-[8vh]">
|
||||||
|
<div class="py-4 text-xl">A Smarter Remote's Terms of Use</div>
|
||||||
|
<div class="py-2">Last Updated: November 2, 2026</div>
|
||||||
|
<div class="py-2">
|
||||||
|
These terms cover your use of the A Smarter Remote app ('the
|
||||||
|
App'). By using the App you agree to them. The App is licensed
|
||||||
|
to you under Apple's{" "}
|
||||||
|
<a
|
||||||
|
href="https://www.apple.com/legal/internet-services/itunes/dev/stdeula/"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
class="text-blue hover-underline-animation"
|
||||||
|
>
|
||||||
|
Licensed Application End User License Agreement
|
||||||
|
</a>
|
||||||
|
, and the terms below add to it.
|
||||||
|
</div>
|
||||||
|
<ol>
|
||||||
|
<div class="py-2">
|
||||||
|
<div class="pb-2 text-lg">
|
||||||
|
<span class="-ml-4 pr-2">1.</span> A Smarter Remote Pro
|
||||||
|
</div>
|
||||||
|
<div class="pl-4">
|
||||||
|
<div class="pb-2">
|
||||||
|
<div class="-ml-6">(a) What it includes:</div> The free App
|
||||||
|
controls one TV. Pro removes that limit and unlocks the
|
||||||
|
features marked Pro in the App.
|
||||||
|
</div>
|
||||||
|
<div class="pb-2">
|
||||||
|
<div class="-ml-6">(b) Yearly subscription:</div> Payment is
|
||||||
|
charged to your Apple account when you confirm the purchase,
|
||||||
|
or when any free trial ends. The subscription renews
|
||||||
|
automatically at the same price unless you cancel at least 24
|
||||||
|
hours before the end of the current period. Your account is
|
||||||
|
charged for renewal within 24 hours before the period ends.
|
||||||
|
Any unused part of a free trial is forfeited when you buy a
|
||||||
|
subscription.
|
||||||
|
</div>
|
||||||
|
<div class="pb-2">
|
||||||
|
<div class="-ml-6">(c) Lifetime purchase:</div> A one-time
|
||||||
|
payment that unlocks Pro with no recurring charge.
|
||||||
|
</div>
|
||||||
|
<div class="pb-2">
|
||||||
|
<div class="-ml-6">(d) Managing and refunds:</div> You can
|
||||||
|
manage or cancel a subscription in your App Store account
|
||||||
|
settings. Refunds are handled by Apple under its policies.
|
||||||
|
Restore Purchases in the App re-unlocks Pro on another device
|
||||||
|
signed in to the same Apple account.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="py-2">
|
||||||
|
<div class="pb-2 text-lg">
|
||||||
|
<span class="-ml-4 pr-2">2.</span> Your TVs
|
||||||
|
</div>
|
||||||
|
<div class="pb-2 pl-4">
|
||||||
|
The App controls TVs through the network features their makers
|
||||||
|
provide. Which features work depends on your TV's model and
|
||||||
|
software, which can change without notice. We do not guarantee
|
||||||
|
that every feature works on every TV. Only control TVs you own
|
||||||
|
or have permission to use.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="py-2">
|
||||||
|
<div class="pb-2 text-lg">
|
||||||
|
<span class="-ml-4 pr-2">3.</span> No Warranty
|
||||||
|
</div>
|
||||||
|
<div class="pb-2 pl-4">
|
||||||
|
The App is provided 'as is', without warranties of any
|
||||||
|
kind, to the extent the law allows. To the extent the law allows,
|
||||||
|
we are not liable for indirect or consequential damages arising
|
||||||
|
from your use of the App.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="py-2">
|
||||||
|
<div class="pb-2 text-lg">
|
||||||
|
<span class="-ml-4 pr-2">4.</span> Privacy
|
||||||
|
</div>
|
||||||
|
<div class="pb-2 pl-4">
|
||||||
|
How the App handles data is described in the{" "}
|
||||||
|
<A
|
||||||
|
href="/smarter-remote/privacy"
|
||||||
|
class="text-blue hover-underline-animation"
|
||||||
|
>
|
||||||
|
Privacy Policy
|
||||||
|
</A>
|
||||||
|
.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="py-2">
|
||||||
|
<div class="pb-2 text-lg">
|
||||||
|
<span class="-ml-4 pr-2">5.</span> Changes to These Terms
|
||||||
|
</div>
|
||||||
|
<div class="pb-2 pl-4">
|
||||||
|
We may update these terms periodically. Any changes are posted on
|
||||||
|
this page. Continuing to use the App after a change means you
|
||||||
|
accept the new terms.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="py-2">
|
||||||
|
<div class="pb-2 text-lg">
|
||||||
|
<span class="-ml-4 pr-2">6.</span> Contact Us
|
||||||
|
</div>
|
||||||
|
<div class="pb-2 pl-4">
|
||||||
|
If you have any questions about these terms, you can contact us{" "}
|
||||||
|
<A href="/contact" class="text-blue hover-underline-animation">
|
||||||
|
here
|
||||||
|
</A>
|
||||||
|
.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ol>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user