Save links from the web. Retrieve them and track what you’ve read through ChatGPT.
Checking your session…
diff --git a/docs/development/testing.md b/docs/development/testing.md
index a004343..e791a98 100644
--- a/docs/development/testing.md
+++ b/docs/development/testing.md
@@ -8,7 +8,7 @@ MCP route tests verify protocol initialization, the three-tool catalog, retrieva
Source extraction tests cover SSRF rejection, bounded response streams, supported content types, and extraction failure.
-Run `pnpm cf:build` and then `pnpm test:e2e` to check the built inbox and landing at phone, tablet, and desktop widths. Synthetic API responses cover URL capture, search, read/unread transitions, and the guest sign-in boundary. Screenshots stay in the ignored `.fleet-local/screenshots/` directory.
+Run `pnpm cf:build` and then `pnpm test:e2e` to check the built inbox and landing at phone, tablet, and desktop widths. Committed null and synthetic auth fixtures cover the stable guest entry, pending-to-guest and pending-to-inbox transitions, public extension installation guidance, protected key management, and retained sign-in callbacks. Synthetic API responses also cover URL capture, search, and read/unread transitions. Browser tests block external requests and WebSockets and disable service workers; they never contact auth providers. Screenshots stay in the ignored `.fleet-local/screenshots/` and `test-results/` directories.
The extension has a separate test runner. API tests verify its retained key slot, authenticated URL/title capture, and rejection when no connection exists.
diff --git a/landing-astro/src/content/reader.json b/landing-astro/src/content/reader.json
index 53362ec..ff1d0eb 100644
--- a/landing-astro/src/content/reader.json
+++ b/landing-astro/src/content/reader.json
@@ -56,8 +56,8 @@
"href": "/login"
},
"primary": {
- "label": "Open your inbox",
- "href": "/library",
+ "label": "Sign in to your inbox",
+ "href": "/login",
"icon": "ArrowUpRight"
}
},
@@ -66,8 +66,8 @@
"title": "Save a link. *Pick it up in ChatGPT.*",
"lede": "Keep the pages you want to come back to in one place. Add a URL, or save the current tab with the Chrome extension. ChatGPT can find your links, pull their content, and mark them read when you ask.",
"primary": {
- "label": "Open your inbox",
- "href": "/library",
+ "label": "Sign in to your inbox",
+ "href": "/login",
"icon": "ArrowUpRight"
},
"secondary": {
@@ -202,8 +202,8 @@
"title": "Bring one link you mean *to come back to.*",
"lede": "Sign in with Google, paste the address, and it is waiting in your inbox.",
"primary": {
- "label": "Open your inbox",
- "href": "/library",
+ "label": "Sign in to your inbox",
+ "href": "/login",
"icon": "ArrowUpRight"
},
"secondary": {
@@ -224,8 +224,8 @@
"title": "Product",
"links": [
{
- "label": "Open your inbox",
- "href": "/library"
+ "label": "Sign in to your inbox",
+ "href": "/login"
},
{
"label": "Connections",
diff --git a/playwright.config.ts b/playwright.config.ts
index ef248ad..2f91b3b 100644
--- a/playwright.config.ts
+++ b/playwright.config.ts
@@ -5,7 +5,7 @@ export default defineConfig({
forbidOnly: Boolean(process.env.CI),
retries: process.env.CI ? 2 : 0,
reporter: process.env.CI ? 'github' : 'list',
- use: { baseURL: 'http://127.0.0.1:47192', trace: 'on-first-retry' },
+ use: { baseURL: 'http://127.0.0.1:47192', trace: 'on-first-retry', serviceWorkers: 'block' },
projects: [
{ name: 'desktop', use: { ...devices['Desktop Chrome'] } },
{
diff --git a/src/components/ExtensionConnectClient.tsx b/src/components/ExtensionConnectClient.tsx
index 7019d27..7897c05 100644
--- a/src/components/ExtensionConnectClient.tsx
+++ b/src/components/ExtensionConnectClient.tsx
@@ -165,6 +165,36 @@ export function ExtensionConnectClient() {
+ {!user && (
+
+
+ In Chrome, open chrome://extensions, enable Developer mode, and choose Load
+ unpacked. Select the unzipped extension folder containing manifest.json.
+
+
+ Sign in to Reader, create an extension key here, then paste it into the extension
+ popup and click Connect. Use Save link to save the current page’s URL and title.
+
+
+
+ This is a local unpacked install. Extension keys connect to your Reader account;
+ ChatGPT uses a separate OAuth connection.
+
+
+ )}
+
{!user ? (
Sign in first
diff --git a/src/components/HomeClient.tsx b/src/components/HomeClient.tsx
index f06d994..18fe135 100644
--- a/src/components/HomeClient.tsx
+++ b/src/components/HomeClient.tsx
@@ -1,12 +1,12 @@
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { ArrowUpRight, Check, Loader2, Plus, Search, Undo2 } from 'lucide-react';
import { useState } from 'react';
-import { Navigate } from 'react-router-dom';
import type { SavedLink } from '@/lib/links-db';
import { trackActivatedOnce, trackCoreAction } from '@/lib/analytics';
import { useAuth } from './AuthProvider';
import { Navbar } from './Navbar';
+import { ReaderEntry } from './ReaderEntry';
import { Button } from './ui/button';
import { Input } from './ui/input';
@@ -73,13 +73,7 @@ export default function HomeClient() {
},
});
- if (loading)
- return (
-
-
Loading Reader…
-
- );
- if (!user) return ;
+ if (loading || !user) return ;
return (
diff --git a/src/components/ReaderEntry.tsx b/src/components/ReaderEntry.tsx
new file mode 100644
index 0000000..88b672f
--- /dev/null
+++ b/src/components/ReaderEntry.tsx
@@ -0,0 +1,39 @@
+import { Link } from 'react-router-dom';
+
+/** Public entry while auth resolves; pending never grants access or starts sign-in. */
+export function ReaderEntry({ pending = false }: { pending?: boolean }) {
+ return (
+
+
+
+ Saved-link inbox
+
+
Reader
+
+ Save links from the web. Retrieve them and track what you’ve read through ChatGPT.
+
+ {pending ? (
+
+ Checking your session…
+
+ ) : (
+
+ Sign in to your inbox
+
+ )}
+
+ Install and connect the Chrome extension
+
+
+
+ );
+}
diff --git a/src/router.tsx b/src/router.tsx
index cf42bae..1b39950 100644
--- a/src/router.tsx
+++ b/src/router.tsx
@@ -2,29 +2,17 @@ import type { ComponentType } from 'react';
import { createBrowserRouter, Navigate } from 'react-router-dom';
import RootLayout from './RootLayout';
+import { ReaderEntry } from './components/ReaderEntry';
function lazyPage(module: () => Promise<{ default: ComponentType }>) {
return module().then((m) => ({ Component: m.default }));
}
-function RouteLoading() {
- return (
-
-