From 9e8787150e25f32f70f47a93e8149b93615a6a26 Mon Sep 17 00:00:00 2001 From: Michael Baas Date: Fri, 1 May 2026 11:53:13 +0000 Subject: [PATCH 1/6] Ignore Firefox build output and signed .xpi packages The Firefox-targeted build (npm run build:firefox) emits to dist-firefox/ and the packaging step produces a top-level .xpi. Both are derived artifacts and shouldn't live in version control. --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index 8cdaa07..1783e17 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,8 @@ node_modules dist +dist-firefox .vite .DS_Store *.log send-to-thebrain-*.zip +*.xpi From 02a62c46ea34126acf2cc4581dcde11619d5278c Mon Sep 17 00:00:00 2001 From: Michael Baas Date: Fri, 1 May 2026 11:54:26 +0000 Subject: [PATCH 2/6] Time-bound HTTP requests in TheBrainLocalClient MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit TheBrain's desktop app picks a fresh listening port every session, so the saved endpoint frequently points at a port that's still in TIME_WAIT or has been claimed by an unrelated process. Without an explicit timeout, fetch() sits on a half-open socket for tens of seconds and the popup appears to hang on load. Add an opt-in timeoutMs option (default 4s) backed by AbortSignal.timeout and rethrow as NotRunningError, matching the existing connection-refused path. Also expose getBaseUrl() so callers can record which origin actually worked — needed by the upcoming port-discovery probe. --- src/api/TheBrainLocalClient.ts | 26 +++++++++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/src/api/TheBrainLocalClient.ts b/src/api/TheBrainLocalClient.ts index 182d111..c9de3d4 100644 --- a/src/api/TheBrainLocalClient.ts +++ b/src/api/TheBrainLocalClient.ts @@ -20,15 +20,30 @@ export interface TheBrainLocalClientOptions { /** Either the bare server origin (`http://localhost:52341`) or the URL shown * in the desktop app's Local API widget (`http://localhost:52341/api/`). */ endpoint: string; + /** Per-request timeout in milliseconds. A short bound is essential because + * TheBrain's desktop app picks a fresh listening port every session, so the + * saved endpoint frequently points at a stale port. Without a timeout, the + * popup can sit on a TCP RST/half-open socket for tens of seconds. */ + timeoutMs?: number; } +export const DEFAULT_REQUEST_TIMEOUT_MS = 4000; + export class TheBrainLocalClient { private readonly apiKey: string; private readonly baseUrl: string; + private readonly timeoutMs: number; - constructor({ apiKey, endpoint }: TheBrainLocalClientOptions) { + constructor({ apiKey, endpoint, timeoutMs }: TheBrainLocalClientOptions) { this.apiKey = apiKey; this.baseUrl = normalizeEndpoint(endpoint); + this.timeoutMs = timeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS; + } + + /** The normalized origin currently in use (e.g. `http://localhost:52341`). + * Exposed so callers can record the working port after a successful call. */ + getBaseUrl(): string { + return this.baseUrl; } private async request( @@ -40,7 +55,11 @@ export class TheBrainLocalClient { const headers: Record = { Authorization: `Bearer ${this.apiKey}`, }; - const init: RequestInit = { method, headers }; + const init: RequestInit = { + method, + headers, + signal: AbortSignal.timeout(this.timeoutMs), + }; if(body !== undefined) { headers["Content-Type"] = "application/json"; init.body = JSON.stringify(body); @@ -50,7 +69,8 @@ export class TheBrainLocalClient { try { response = await fetch(url, init); } catch { - // TypeError from fetch when the local server isn't reachable. + // TypeError from fetch when the local server isn't reachable, or + // AbortError when our timeout fires (e.g. stale port). throw new NotRunningError(); } From 74acb8e135632a018413eba9b633b4a4d1a3b32c Mon Sep 17 00:00:00 2001 From: Michael Baas Date: Fri, 1 May 2026 11:54:46 +0000 Subject: [PATCH 3/6] Add port-discovery helper: probe localhost ports in parallel Browser extensions can't enumerate listening sockets the way a PowerShell script can, so when the saved endpoint goes silent (typical after TheBrain's desktop app restarts) the only signal available is 'does an HTTP request to localhost:/api/app/state succeed'. discoverEndpoint() fans out to every candidate in parallel with a 700ms per-probe timeout, capped by AbortSignal.timeout, and returns the first ok winner along with all settled results so callers can distinguish 'nothing answered' from 'port answered but key was rejected'. buildCandidates() composes the candidate list deterministically: the current endpoint first, then MRU history, then a small set of fallback ports (8001, 52341, 8081). Order matters because we want a tie to go to the user's last-known-good port, not whichever race we won by 2ms. Tests cover candidate construction, the auth-vs-no-answer split, and the empty-input edge case. fetch is injected for testability so we don't need a real network. --- src/lib/portDiscovery.test.ts | 123 ++++++++++++++++++++++++++++++++ src/lib/portDiscovery.ts | 130 ++++++++++++++++++++++++++++++++++ 2 files changed, 253 insertions(+) create mode 100644 src/lib/portDiscovery.test.ts create mode 100644 src/lib/portDiscovery.ts diff --git a/src/lib/portDiscovery.test.ts b/src/lib/portDiscovery.test.ts new file mode 100644 index 0000000..2f068e8 --- /dev/null +++ b/src/lib/portDiscovery.test.ts @@ -0,0 +1,123 @@ +import { describe, it, expect, vi } from "vitest"; +import { + buildCandidates, + discoverEndpoint, + portFromEndpoint, + DEFAULT_FALLBACK_PORTS, +} from "./portDiscovery"; + +describe("portFromEndpoint", () => { + it("extracts the port from a normalized endpoint", () => { + expect(portFromEndpoint("http://localhost:52341")).toBe(52341); + }); + it("works with the trailing /api form", () => { + expect(portFromEndpoint("http://localhost:8001/api/")).toBe(8001); + }); + it("returns null when no explicit port is present", () => { + expect(portFromEndpoint("http://localhost/api")).toBeNull(); + }); + it("returns null for unparseable input", () => { + expect(portFromEndpoint("not a url")).toBeNull(); + }); +}); + +describe("buildCandidates", () => { + it("starts with the current endpoint, then MRU, then fallbacks", () => { + const out = buildCandidates("http://localhost:52341/api/", [8001, 9000]); + const ports = out.map((c) => c.port); + expect(ports[0]).toBe(52341); + expect(ports.slice(1, 3)).toEqual([8001, 9000]); + // Defaults follow, with 8001 already deduped. + expect(ports).toContain(DEFAULT_FALLBACK_PORTS[1]); + }); + it("dedupes when the current port is also in the MRU", () => { + const out = buildCandidates("http://localhost:52341", [52341, 8001]); + const ports = out.map((c) => c.port); + expect(ports.filter((p) => p === 52341)).toHaveLength(1); + expect(ports[0]).toBe(52341); + expect(ports[1]).toBe(8001); + }); + it("falls back to defaults when nothing else is known", () => { + const out = buildCandidates("", []); + expect(out.map((c) => c.port)).toEqual([...DEFAULT_FALLBACK_PORTS]); + }); + it("ignores invalid ports", () => { + const out = buildCandidates("http://localhost:99999", [-1, 0, 70000]); + expect(out.every((c) => c.port > 0 && c.port < 65536)).toBe(true); + }); +}); + +describe("discoverEndpoint", () => { + it("returns the first ok candidate in candidate order", async () => { + const fetchImpl = vi.fn(async (input: RequestInfo | URL) => { + const url = String(input); + if(url.startsWith("http://localhost:8001/")) { + return new Response("{}", { status: 200, headers: { "content-type": "application/json" } }); + } + if(url.startsWith("http://localhost:9000/")) { + return new Response("{}", { status: 200, headers: { "content-type": "application/json" } }); + } + throw new TypeError("connection refused"); + }); + const result = await discoverEndpoint({ + apiKey: "k", + candidates: [ + { origin: "http://localhost:52341", port: 52341 }, + { origin: "http://localhost:8001", port: 8001 }, + { origin: "http://localhost:9000", port: 9000 }, + ], + timeoutMs: 100, + fetchImpl: fetchImpl as unknown as typeof fetch, + }); + expect(result.winner?.port).toBe(8001); + expect(result.all).toHaveLength(3); + }); + + it("reports auth when the port responds with 401 but no port is OK", async () => { + const fetchImpl = vi.fn(async (input: RequestInfo | URL) => { + const url = String(input); + if(url.startsWith("http://localhost:52341/")) { + return new Response("nope", { status: 401 }); + } + throw new TypeError("connection refused"); + }); + const result = await discoverEndpoint({ + apiKey: "k", + candidates: [ + { origin: "http://localhost:8001", port: 8001 }, + { origin: "http://localhost:52341", port: 52341 }, + ], + timeoutMs: 100, + fetchImpl: fetchImpl as unknown as typeof fetch, + }); + expect(result.winner).toBeNull(); + expect(result.all.some((r) => r.kind === "auth" && r.port === 52341)).toBe(true); + }); + + it("returns null winner when every probe fails", async () => { + const fetchImpl = vi.fn(async () => { + throw new TypeError("connection refused"); + }); + const result = await discoverEndpoint({ + apiKey: "k", + candidates: [ + { origin: "http://localhost:8001", port: 8001 }, + { origin: "http://localhost:9000", port: 9000 }, + ], + timeoutMs: 100, + fetchImpl: fetchImpl as unknown as typeof fetch, + }); + expect(result.winner).toBeNull(); + expect(result.all.every((r) => r.kind === "no")).toBe(true); + }); + + it("handles an empty candidate list", async () => { + const result = await discoverEndpoint({ + apiKey: "k", + candidates: [], + fetchImpl: vi.fn() as unknown as typeof fetch, + }); + expect(result.winner).toBeNull(); + expect(result.all).toEqual([]); + }); +}); diff --git a/src/lib/portDiscovery.ts b/src/lib/portDiscovery.ts new file mode 100644 index 0000000..a95cc3b --- /dev/null +++ b/src/lib/portDiscovery.ts @@ -0,0 +1,130 @@ +// Probe localhost ports in parallel to find the one TheBrain's desktop app +// is currently listening on. Browser extensions can't enumerate processes +// (no equivalent of `Get-NetTCPConnection`), so the only signal available +// is "does an HTTP request to localhost:/api/app/state succeed". + +import { normalizeEndpoint } from "./endpoint"; + +/** Origin to probe and the port we extracted from it (handy for callers + * that want to record a successful port in the MRU list). */ +export interface PortCandidate { + origin: string; + port: number; +} + +/** Outcome for a single probe. `ok` means "TheBrain answered"; `auth` means + * the port has a TheBrain instance but the saved API key was rejected + * (probably stale). `no` means nothing usable on that port. */ +export type ProbeResult = + | { kind: "ok"; origin: string; port: number } + | { kind: "auth"; origin: string; port: number } + | { kind: "no"; origin: string; port: number }; + +export interface DiscoveryOptions { + apiKey: string; + candidates: PortCandidate[]; + /** Per-probe timeout in ms. Total wall time is bounded by this because + * all probes run in parallel. Default 700ms keeps the popup snappy. */ + timeoutMs?: number; + /** Injected for tests. Falls back to global fetch. */ + fetchImpl?: typeof fetch; +} + +export interface DiscoveryResult { + /** First "ok" probe — the endpoint to use. Null if none answered. */ + winner: ProbeResult | null; + /** All results, in the order they settled. Useful for diagnostics + * (e.g. to surface "key was rejected on port X" if no ok). */ + all: ProbeResult[]; +} + +const DEFAULT_PROBE_TIMEOUT_MS = 700; +// Default ports to try when the user has no history yet. 8001 is the +// observed default for self-hosted local APIs; 52341 is what TheBrain's +// desktop widget often shows; 8081 is a frequent collision-avoidance pick. +export const DEFAULT_FALLBACK_PORTS: readonly number[] = [8001, 52341, 8081]; + +/** Build the candidate list from settings + defaults, deduplicating while + * preserving order: current endpoint first, then MRU history, then + * hard-coded fallbacks. */ +export function buildCandidates( + currentEndpoint: string, + recentPorts: readonly number[], + fallbacks: readonly number[] = DEFAULT_FALLBACK_PORTS, +): PortCandidate[] { + const seen = new Set(); + const out: PortCandidate[] = []; + const add = (port: number) => { + if(!Number.isInteger(port) || port <= 0 || port >= 65536) return; + if(seen.has(port)) return; + seen.add(port); + out.push({ origin: `http://localhost:${port}`, port }); + }; + const currentPort = portFromEndpoint(currentEndpoint); + if(currentPort !== null) add(currentPort); + for(const p of recentPorts) add(p); + for(const p of fallbacks) add(p); + return out; +} + +/** Pull the port out of an endpoint string. Returns null if the string + * isn't a parseable URL or has no explicit port. */ +export function portFromEndpoint(endpoint: string): number | null { + const normalized = normalizeEndpoint(endpoint); + if(!normalized) return null; + try { + const url = new URL(normalized); + if(!url.port) return null; + const n = Number(url.port); + return Number.isInteger(n) && n > 0 && n < 65536 ? n : null; + } catch { + return null; + } +} + +/** Probe every candidate in parallel. Resolves once all probes have + * settled (success, failure, or timeout). The caller gets the first + * "ok" result if any; otherwise can inspect `all` for "auth" hits. */ +export async function discoverEndpoint(opts: DiscoveryOptions): Promise { + const { apiKey, candidates } = opts; + const timeoutMs = opts.timeoutMs ?? DEFAULT_PROBE_TIMEOUT_MS; + const fetchImpl = opts.fetchImpl ?? fetch; + + if(candidates.length === 0) { + return { winner: null, all: [] }; + } + + const probes = candidates.map((c) => probeOne(c, apiKey, timeoutMs, fetchImpl)); + const all = await Promise.all(probes); + // Pick winner in candidate order so "current endpoint" or top of MRU + // wins ties — this is the user's expected behavior, not "whichever + // race we won by 2ms". + const winner = all.find((r) => r.kind === "ok") ?? null; + return { winner, all }; +} + +async function probeOne( + candidate: PortCandidate, + apiKey: string, + timeoutMs: number, + fetchImpl: typeof fetch, +): Promise { + const url = `${candidate.origin}/api/app/state`; + try { + const response = await fetchImpl(url, { + method: "GET", + headers: { Authorization: `Bearer ${apiKey}` }, + signal: AbortSignal.timeout(timeoutMs), + }); + if(response.ok) { + return { kind: "ok", origin: candidate.origin, port: candidate.port }; + } + if(response.status === 401 || response.status === 403) { + return { kind: "auth", origin: candidate.origin, port: candidate.port }; + } + return { kind: "no", origin: candidate.origin, port: candidate.port }; + } catch { + // Connection refused / RST / our timeout fired. + return { kind: "no", origin: candidate.origin, port: candidate.port }; + } +} From 40803d475d9f3d0777fa02f1918019fff7e1ee1d Mon Sep 17 00:00:00 2001 From: Michael Baas Date: Fri, 1 May 2026 11:55:32 +0000 Subject: [PATCH 4/6] Persist recently-used local API ports in settings The port-discovery probe needs a list of likely candidate ports to try. Hard-coding them is brittle: TheBrain's desktop app appears to pick ports from a small but session-dependent pool, and after a few launches the user's distribution becomes specific to their machine. Track an MRU list of ports that have actually answered (cap 8). The popup will push to it on every successful connect and seed candidate generation from it. Stored in chrome.storage.local alongside the rest of the settings; existing users default to []. --- src/lib/settings.ts | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/src/lib/settings.ts b/src/lib/settings.ts index 91e52f6..55bae90 100644 --- a/src/lib/settings.ts +++ b/src/lib/settings.ts @@ -17,10 +17,16 @@ export interface Settings { // YouTube's ?v=VIDEO_ID). Entries are bare hostnames; subdomains match. trimQueryParamsExceptions: string[]; autoProceed: boolean; + // Most-recently-successful local API ports, MRU-ordered. Used by the + // port-discovery probe to find TheBrain after the desktop app rotates + // to a new port across sessions. + recentPorts: number[]; } export const AUTO_PROCEED_MS = 3000; +export const RECENT_PORTS_LIMIT = 8; + export const DEFAULT_TRIM_EXCEPTIONS: readonly string[] = [ "youtube.com", "youtu.be", @@ -34,6 +40,7 @@ const DEFAULTS: Settings = { trimQueryParams: false, trimQueryParamsExceptions: [...DEFAULT_TRIM_EXCEPTIONS], autoProceed: false, + recentPorts: [], }; const KEYS: (keyof Settings)[] = [ @@ -44,6 +51,7 @@ const KEYS: (keyof Settings)[] = [ "trimQueryParams", "trimQueryParamsExceptions", "autoProceed", + "recentPorts", ]; export async function getSettings(): Promise { @@ -72,9 +80,28 @@ export async function getSettings(): Promise { typeof stored.autoProceed === "boolean" ? stored.autoProceed : DEFAULTS.autoProceed, + recentPorts: Array.isArray(stored.recentPorts) + ? (stored.recentPorts as unknown[]) + .filter( + (v): v is number => + typeof v === "number" && + Number.isInteger(v) && + v > 0 && + v < 65536, + ) + .slice(0, RECENT_PORTS_LIMIT) + : [...DEFAULTS.recentPorts], }; } +// Push a port to the front of the recent-ports MRU list, deduping and +// capping the length. Returns the new list (not persisted — the caller +// passes it to updateSettings). +export function pushRecentPort(existing: number[], port: number): number[] { + const filtered = existing.filter((p) => p !== port); + return [port, ...filtered].slice(0, RECENT_PORTS_LIMIT); +} + // Parse a free-form list (newlines or commas) into normalized hostnames. // Strips scheme/path/whitespace and lowercases. Used by the options UI. export function parseExceptionList(input: string): string[] { From 646c6e72d51298ae3e24c240d87db3316902f7b2 Mon Sep 17 00:00:00 2001 From: Michael Baas Date: Fri, 1 May 2026 11:59:34 +0000 Subject: [PATCH 5/6] Auto-rotate to discovered port on popup load MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When the popup opens and the saved endpoint doesn't answer, fan out a parallel probe to the candidate set (last-used + recentPorts MRU + a few defaults) and rotate onto the first port that actually responds. The new origin is persisted to settings, and its port is pushed to the recent-ports MRU so the next launch starts on a known-good candidate. Two specific cases the failure path now distinguishes: - Nothing answers → 'TheBrain isn't reachable on any known port' with a hint to copy the Local API URL from the desktop app's widget. - A port answers but rejects the API key → InvalidKeyError, so the user knows it's a credential problem, not a port problem (otherwise we'd silently ignore the 401 and report 'not running'). After every successful probe (whether on the saved port or a rotated one) we also push the active port to the MRU, so the history grows even for users whose port rarely changes. --- src/popup/PopupApp.tsx | 102 ++++++++++++++++++++++++++++++++++++++--- 1 file changed, 95 insertions(+), 7 deletions(-) diff --git a/src/popup/PopupApp.tsx b/src/popup/PopupApp.tsx index e1a5c4d..2011303 100644 --- a/src/popup/PopupApp.tsx +++ b/src/popup/PopupApp.tsx @@ -1,6 +1,11 @@ import { useCallback, useEffect, useState } from "react"; import { TheBrainLocalClient } from "../api/TheBrainLocalClient"; -import { TheBrainError, NoBrainOpenError } from "../api/errors"; +import { + TheBrainError, + NoBrainOpenError, + NotRunningError, + InvalidKeyError, +} from "../api/errors"; import { Alert } from "../components/Alert"; import { Button } from "../components/Button"; import { Card, CardContent, CardFooter, CardHeader, CardTitle } from "../components/Card"; @@ -9,11 +14,17 @@ import { Logo } from "../components/Logo"; import { Spinner } from "../components/Spinner"; import { tabs, runtime, type ActiveTab } from "../lib/browser"; import { DEFAULT_ENDPOINT, isValidEndpoint } from "../lib/endpoint"; +import { + buildCandidates, + discoverEndpoint, + portFromEndpoint, +} from "../lib/portDiscovery"; import { sendToBrain, type SendOutcome } from "../lib/sendToBrain"; import { stripUnreadCountPrefix } from "../lib/titleSplit"; import { AUTO_PROCEED_MS, getSettings, + pushRecentPort, updateSettings, type SendMode, type Settings, @@ -48,14 +59,38 @@ export function PopupApp() { // Verify that the desktop app is reachable, the API key works, and a brain // is open — so the user sees a problem immediately, not only after clicking // Send. Called on popup open and again when the user hits Try again. + // + // If the saved endpoint is unreachable (typical: TheBrain restarted and now + // listens on a different port), we transparently probe a small set of + // candidate ports (last-used + MRU history + a few defaults) and rotate + // onto the first one that answers. The new endpoint is persisted so the + // next launch starts on the right port. const probeConnection = useCallback(async (s: Settings) => { setView({ kind: "probing" }); + let effective = s; + let client = new TheBrainLocalClient({ + apiKey: s.apiKey, + endpoint: s.endpoint, + }); try { - const client = new TheBrainLocalClient({ apiKey: s.apiKey, endpoint: s.endpoint }); - const state = await client.getAppState(); + let state; + try { + state = await client.getAppState(); + } catch(error) { + if(!(error instanceof NotRunningError)) throw error; + const rotated = await tryDiscoverNewPort(s); + if(!rotated) throw error; + effective = rotated.settings; + client = rotated.client; + setSettings(rotated.settings); + state = await client.getAppState(); + } if(!state.currentBrainId || !state.activeThoughtId) { throw new NoBrainOpenError(); } + // Record the working port so future launches start on a known-good + // candidate even before the saved endpoint gets refreshed elsewhere. + await rememberWorkingPort(effective, client.getBaseUrl()); setActiveThought({ id: state.activeThoughtId, name: state.activeThoughtName ?? "active thought", @@ -65,11 +100,13 @@ export function PopupApp() { } catch(error) { setActiveThought(null); const message = - error instanceof TheBrainError - ? error.message - : error instanceof Error + error instanceof NotRunningError + ? "TheBrain isn't reachable on any known port. Open the desktop app, copy the Local API URL into Settings, then try again." + : error instanceof TheBrainError ? error.message - : "Could not reach TheBrain."; + : error instanceof Error + ? error.message + : "Could not reach TheBrain."; setView({ kind: "error", message, recoverable: true }); } }, []); @@ -270,6 +307,57 @@ export function PopupApp() { ); } +// When the saved endpoint goes silent (typical: TheBrain restarted with a +// new ephemeral port), fan out to a handful of likely ports in parallel +// and return a refreshed client + settings if any of them answers. Browser +// extensions can't enumerate listening sockets the way a PowerShell script +// can, so this best-effort probe is the closest analogue. +async function tryDiscoverNewPort(s: Settings): Promise<{ + client: TheBrainLocalClient; + settings: Settings; +} | null> { + const candidates = buildCandidates(s.endpoint, s.recentPorts); + if(candidates.length === 0) return null; + const result = await discoverEndpoint({ + apiKey: s.apiKey, + candidates, + }); + if(result.winner === null) { + // If every candidate that answered did so with 401, the port is + // fine but the saved key is stale — surface that explicitly so the + // user knows to refresh it instead of chasing port numbers. + const auth = result.all.find((r) => r.kind === "auth"); + if(auth) { + throw new InvalidKeyError(); + } + return null; + } + const newEndpoint = result.winner.origin; + const newRecent = pushRecentPort(s.recentPorts, result.winner.port); + await updateSettings({ endpoint: newEndpoint, recentPorts: newRecent }); + const next: Settings = { ...s, endpoint: newEndpoint, recentPorts: newRecent }; + const client = new TheBrainLocalClient({ + apiKey: next.apiKey, + endpoint: next.endpoint, + }); + return { client, settings: next }; +} + +// Push the active port to the front of the MRU list whenever a probe +// succeeds. We swallow storage errors silently — failing to update the +// history must never break a working send. +async function rememberWorkingPort(s: Settings, baseUrl: string): Promise { + const port = portFromEndpoint(baseUrl); + if(port === null) return; + if(s.recentPorts[0] === port) return; + const next = pushRecentPort(s.recentPorts, port); + try { + await updateSettings({ recentPorts: next }); + } catch(error) { + console.warn("[Send to TheBrain] failed to record recent port:", error); + } +} + function Header() { return (
From f54d4597493ecd72a72c92e9aac7c361962ef7ff Mon Sep 17 00:00:00 2001 From: Michael Baas Date: Fri, 1 May 2026 11:59:49 +0000 Subject: [PATCH 6/6] Bump to 0.1.2 Port-discovery probe + per-request timeout. See preceding commits. --- manifest.json | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/manifest.json b/manifest.json index 628da53..130a77b 100644 --- a/manifest.json +++ b/manifest.json @@ -1,7 +1,7 @@ { "manifest_version": 3, "name": "Send to TheBrain", - "version": "0.1.1", + "version": "0.1.2", "description": "Save the current page as a thought in your Brain.", "action": { "default_popup": "src/popup/index.html", diff --git a/package.json b/package.json index 7ba90b3..085b010 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "send-to-thebrain", - "version": "0.1.1", + "version": "0.1.2", "private": true, "type": "module", "description": "Chrome browser extension that saves the active page into TheBrain via the desktop app's local HTTP API.",