Anchor

A focus companion that notices when you drift — and gently pulls you back.

Inspiration

Every focus tool we'd used was a warden. Site blockers, timers, "you've been on YouTube for 12 minutes" nags. They all share the same assumption: you are the problem, and the tool's job is to police you.

That's not how drifting actually feels. Nobody decides to lose forty minutes to a rabbit hole. You open one tab to check something, and the next thing you know you're three videos deep with no memory of leaving. What you needed wasn't a wall — it was a friend sitting next to you who'd glance over and say, "hey, weren't you working on that essay?"

So we built a cat.

Anchor is a Chrome extension with a small floating pet that lives on whatever page you're reading. It watches for the shape of drifting — not "which site are you on," but what's actually on the page and how you're moving through it — and when it's confident enough, it asks. One question, three buttons, no lecture. It also helps with the other hard part nobody talks about: starting. Tell it what you're working on and it hands you one physical first action small enough that refusing feels silly.

What it does

  • Starter coach. You type a task; it gives you one concrete thing your hands can do in the next ten seconds — and it looks at the page you already have open to do it, so the answer can be "press play on the lecture you have open" instead of "search for a tutorial." If your task is too vague to ever judge pages against, it asks one targeted follow-up. Never more.
  • Judges the page, not the domain. Every page is classified by an LLM against your task: a YouTube tutorial is work; a YouTube cat video isn't; same domain, different verdict. A site blocker can only say "youtube.com: blocked."
  • Reads the conversation, not just the tab. On AI-chat sites the tab title stops changing after the first message. Anchor reads your latest message and re-judges that — so a chat that drifts from "neural networks" to "what's for dinner" gets noticed even though the tab never changed.
  • Four signals, two channels. Context relevance, anchor detachment, interaction texture, and jump pattern feed two independent detectors: DRIFT (you left the task and stayed gone) and STUCK (you're on the right page but nothing has moved in fifteen minutes). Neither fires on a single blip.
  • Three moods, no fourth. The cat is calm while you work, glows while evidence is accumulating, and only then asks. You can see it thinking before it speaks.
  • Check-ins that respect you. Still on track / This counts as work / Drifted — pull me back. The last one actually switches you back to the tab you left. This counts as work whitelists the page for the session — scoped to the exact page on YouTube, Reddit or an AI chat, so correcting one video doesn't silently clear the whole site.
  • Rest mode. Say you're taking a break and it goes quiet — indefinitely, until you say you're back. A soft check-in after fifteen minutes, with a 5 more minutes option.
  • A session summary that reads like clocking out, not a report card.
  • Zero servers. Everything lives in chrome.storage.local. The only thing that ever leaves your machine is a page title, its URL, your task sentence — and, on AI-chat sites only, your most recent message.

How we built it

Two of us, split down a contract. One owns perception (Chrome APIs, signal collection, the classifier); the other owns decision (the detection engine, dialogue, the pet). The seam between them is a single typed object — FeatureFrame — and a contract document both halves are held to. Five red lines we agreed on day one: the LLM never blocks the engine; every LLM path has a local fallback; the demo has a backup video; the seam is synced before either side merges; and time scaling lives in exactly one function.

The engine is pure TypeScript with 316 unit tests and no Chrome dependencies, so we could replay recorded signal streams against it. Detection is built on evidence sustainers: a channel only fires when its evidence has held continuously for a window $W$:

$$\text{fire} \iff t_{\text{now}} - t_{\text{since}} \geq W, \quad W = 30\text{s}$$

Any frame where the condition fails resets $t_{\text{since}}$. After a check-in there's a cooldown — shorter after a confirmed drift — and evidence gathered before it is discarded, so ignoring the bubble can't get you asked the same thing twice. Profiles are data, not code: CREATOR, READER and VIEWER are rows in one policy table that decides which signals are allowed to count as evidence, and the detector never branches on them. For demos, every time constant runs through one scaling function:

$$t' = t \cdot \tfrac{1}{30}$$

so a five-minute drift plays out in ten seconds on stage.

Before the LLM is ever called, a page runs through a stack of fast paths — demo preset cache, session whitelist, short-video-feed shape, a built-in entertainment blacklist — each able to return a verdict without a network call. Only then does gpt-oss-20b (via Groq, temperature: 0) get asked, and the verdict is cached per page-and-title-and-message. The starter coach uses gpt-oss-120b. Check-in wording is local templates, not an LLM call — it has to work offline and sound the same every time. The floating pet is a Lottie animation (Kitty Cat Error 404 by Sepehr Radfar) rendered into a Shadow DOM on the host page, draggable, with position persistence and click-through everywhere the cat isn't.

Challenges we ran into

The prompt that taught the model to lie. Our first real starter-coach prompt told the model "if you cannot name the thing, you are being too vague." We got "Open the network textbook, flip to chapter 4" — for a task that mentioned no textbook and no chapter. We'd literally instructed it to fabricate. The prompt went through six numbered revisions in three days, and every version fixed the previous failure while introducing a new one: v2 stopped inventing chapters but assumed you owned notes; v3 stopped assuming but produced "open a doc and type the course name" — compliant and useless; v4 fixed that and the model collapsed to "open a new tab and search" for 10 of 12 tasks. Four stacked constraints had squeezed the solution space down to one move. The fix wasn't a fifth rule. It was giving the model the one fact it couldn't guess: the page you currently have open. That created v5's failure (eagerly using an unrelated Gmail tab) and v5.1's (sending you to search for docs already on screen) — each a smaller, more honest mistake than the one before.

The title that outvoted the message. Reading the latest chat message was supposed to catch conversations drifting. It didn't, at first: the classifier was still asked "is this page relevant?", and a strongly on-task title ("Basic English grammar") kept averaging an obviously off-task message ("plan me a tour of Crete") up to RELEVANT. The fix was in the question, not the data: when a message exists, ask about the message, and tell the model the title may be stale and doesn't get a vote.

Infinity doesn't survive chrome.storage. "Resting until you say otherwise" was restUntil = Infinity. Chrome's storage is JSON; Infinity comes back as null, and the moment the service worker was recycled, every rest silently ended. It's now a JSON-safe sentinel, with a regression test that round-trips state through storage.

A cat on a stranger's webpage. The host page's CSS set our container to display: none; page-level key handlers stole our keystrokes; a transparent card blocked clicks on the content underneath; hover states broke across pointer-event gaps. We debugged an invisible cat for three rounds against stale code because we'd skipped a page refresh — and then added a build stamp to the console so it could never happen again.

Accomplishments that we're proud of

It doesn't feel like a warden. This was the whole bet, and it held. Every decision that would have made Anchor "smarter" at the cost of trust, we said no to: no double-checking when you say a page counts as work, no interrogating you before you can start, no auto-ending your break because a timer ran out. The cat asks once and believes you.

The engine is real, and it's tested. 316 unit tests, zero Chrome dependencies, replayable against recorded signal streams. Real-device failures — a rest that ended itself, a playing video mistaken for "stuck," one corrected video whitelisting all of YouTube — each earned a permanent test before the fix. The suite is a record of real failures, not a checklist written in advance.

We measured our prompts instead of eyeballing them. After four rewrites that each fixed one failure and introduced another, we built an eval harness: 17 fixed tasks, 13 mechanical checks, run against the production prompt, with a distribution of action shapes alongside the pass rate. The first run scored 100% on a prompt whose every answer was "open a new tab and search" — and that number is what told us the problem was missing information, not missing rules. The next version dropped the search monoculture from 83% to 50%, and we could prove it.

"Press play on the video you already have open." The first time the coach said this — because we'd finally given it the page in front of you — was the first time it said something a real friend would say.

Two people, one contract, no merge disasters. 130 commits, three weeks, perception and decision developed in parallel on separate branches, and the integration merges were boring. Being boring was the point.

What we learned

  • When constraints stop helping, add information. Prohibitions produce safe nonsense. The model needed a fact, not a sixth rule.
  • Ask the question you actually mean. A classifier told "is this page relevant" will weigh everything on the page — including the stale parts. Naming the one thing to judge fixed more than any amount of extra context.
  • "Code exists, tests are green" is not "feature works." Four times, a feature was hollow in the real host because the pipeline feeding it was never connected. Every feature now gets a real-device check before it's marked done.
  • Design for the friend, not the supervisor. We deliberately don't double-check when you say a page counts as work, don't interrogate you before you start, and don't auto-end your break. Every one of those would have been easy to add and would have turned the cat into a warden.

What's next for Anchor

Beyond the browser. Drifting doesn't only happen in Chrome — it happens when you alt-tab to Discord, when you pick up your phone. We built the detection engine as pure TypeScript with zero Chrome or DOM dependencies for exactly this reason: src/engine/ is 316 tests of logic that has never heard of a tab. The next two hosts are:

  • A desktop app (Electron or Tauri) where the cat lives on your actual desktop and the perception layer reads the active window instead of the active tab. Same FeatureFrame, same two channels, same wording — only the signal collector changes.
  • A mobile companion that can't see your screen but can be the thing you check in with: declare a task on your phone, get your first step, and let the cat on your laptop know you've started. Later, on-device usage signals where the platform allows it.

The seam that let two people build this in parallel — one contract object between perception and decision — is the same seam that makes new hosts possible without touching the parts we've already debugged.

Within the extension, what we already know we want:

  • Real archetypes. READER and VIEWER exist as policy rows today, with their own thresholds and muting; everyone still runs on CREATOR. Next is inferring the profile from the task, and letting a researcher reading papers and a developer debugging get different first steps.
  • Cross-session learning — your best hours, your personal drift traps — once there's more than one session of data to learn from.
  • Verify the chat-message extractor on the other AI-chat sites (today only claude.ai is confirmed), and model a two-monitor setup as one task with a set of anchors instead of a single one.
  • Make the observing glow a genuine early signal rather than a thirty-second warning, and let you edit the coach's first step before committing to it — a step you wrote yourself is more likely to get done than one you were handed.

Built With

Share this project:

Updates

Submission history