Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
9db1dc7
library: AI shelf and Magic default to Claude Opus 5.5 (Wolf, 2026-09…
manager Sep 22, 2026
cd740e3
widget: Copilot defaults to Claude Sonnet 5
manager Sep 22, 2026
d6c6b74
widget: Copilot's Claude calls go through the subscription relay
manager Sep 22, 2026
bac80e9
widget: drop the paid OpenAI fallback from Copilot
manager Sep 22, 2026
c1aaa5f
fix(library): cover hotspots find their library by owner id
manager Sep 23, 2026
fe006e0
feat(ai-atlas): Terminal pushes the Atlas guide, the page follows wit…
manager Sep 24, 2026
0efbc60
feat(ai-atlas): Terminal pushes the Atlas guide, the page follows wit…
manager Sep 24, 2026
bd98496
fix(ai-atlas): address review on the guide push route
manager Sep 24, 2026
7138b15
fix(ai-atlas): address review on the guide push route
manager Sep 24, 2026
a480025
Release: Atlas guide pushed by the Terminal, no rebuild needed (#242)
manager Sep 24, 2026
4732e4f
ai-atlas: Grok joins the Engine and Models cards, Delivered work name…
manager Sep 24, 2026
3e789e5
ai-atlas: Wolf's card texts can arrive with the pushed guide
manager Sep 24, 2026
0bd4d30
ai-atlas: Grok joins the Engine and Models cards, Delivered work name…
manager Sep 24, 2026
1a7ef29
ai-atlas: Wolf's card texts can arrive with the pushed guide
manager Sep 24, 2026
7450f41
ai-atlas: the Terminal's push sets the map and every page word
manager Sep 24, 2026
17e9dd2
ai-atlas: the Terminal's push sets the map and every page word
manager Sep 24, 2026
2199b5c
ai-atlas: pushed map labels stay short, empty text is refused
manager Sep 24, 2026
2efc36c
ai-atlas: pushed map labels stay short, empty text is refused
manager Sep 24, 2026
cf4cb94
ai-atlas: the brand title is a short label too
manager Sep 24, 2026
ca5066d
ai-atlas: the brand title is a short label too
manager Sep 24, 2026
3b9e5b4
Release: the Terminal push sets the Atlas map, cards and page words (…
manager Sep 24, 2026
c83b366
feat(library): more than 15 books opens the AI without the flag
manager Sep 25, 2026
b8ff463
Merge remote-tracking branch 'origin/main' into feat/uxcore-cybersecu…
manager Sep 25, 2026
c279756
Merge feat/library-ai-by-books: more than 15 books opens the Library AI
manager Sep 25, 2026
20428ab
fix(ai-atlas): a pushed guide with a field of the wrong shape is refused
manager Sep 25, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 16 additions & 8 deletions LIBRARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,17 +57,25 @@ owner's address. Phones and tablets stay read-only, so a signed-in owner
without a library reads "Use a desktop to create your library" there. A
visitor at an address no library answers to reads "No such library".

The AI shelf and the magic books are behind the `library-ai` account flag,
read from `GET /api/users/me` as `featureNames` (LIBRARY_AI_FLAG). The page
draws neither surface without it, and `/api/library/ai-shelf` and
`/api/library/magic-book` answer 403 without it through `ownerOfLibrary`, so
the hidden shelf is not the gate. Both surfaces are the owner's alone on
every account: a visitor never sees them. An operator hands the flag out in
The AI shelf and the magic books open to an owner who holds the `library-ai`
account flag, read from `GET /api/users/me` as `featureNames`
(LIBRARY_AI_FLAG), or whose library holds more than 15 books
(LIBRARY_AI_BOOKS_OVER, Wolf, 2026-09-25). Only objects of type book count,
on every shelf, private ones included; the count is read from the library on
every check, so the AI arrives with the sixteenth book and leaves if the
library drops back to fifteen, while the flag holds regardless. One check,
`opensLibraryAi` in `src/lib/library/flags.ts`, decides for the page and the
routes: the page draws neither surface without it, and
`/api/library/ai-shelf` and `/api/library/magic-book` answer 403 without it
through `ownerOfLibrary`, so the hidden shelf is not the gate. The AI shelf
itself stays locked until 30 books (AI_SHELF_MIN_BOOKS). Both surfaces are
the owner's alone on every account: a visitor never sees them. An operator hands the flag out in
the CMS admin panel (the user's Feature Flags relation) or ahead of signup
through the Mail Permission List; it takes effect on the account's next page
load, no new sign-in. Wolf names the accounts, one at a time, to the agent;
on 2026-09-12 they are Alina, Mary, Lemongrass and Wolf. Cover, video and
audio autofill stay open to everyone: they cost no model call.
on 2026-09-12 they are Alina, Mary, Lemongrass and Wolf. On 2026-09-25
Lilith and Maksim got it for holding more than 15 books, before the rule
above shipped. Cover, video and audio autofill stay open to everyone: they cost no model call.

A library carries `hidden`, set only in the CMS admin panel (the content API
refuses it on update). Hidden, it leaves the home list and
Expand Down
45 changes: 40 additions & 5 deletions scripts/release/library-batch-check.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ const path = require('node:path');
const vm = require('node:vm');
const assert = require('node:assert/strict');
const ts = require('typescript');

process.chdir(path.resolve(__dirname, '../..'));
function load(file, mocks = {}) {
const exports = {};
Expand Down Expand Up @@ -279,27 +280,61 @@ function check() {
// LIBRARY ACCESS. Creation needs no flag; the AI does, and the routes
// behind it check the same flag the page draws by, so a direct call is
// stopped where the shelf is not drawn.
const { holdsFlag } = load('src/lib/library/flags.ts');
const flags = load('src/lib/library/flags.ts', {
'@constants/library/common': {
LIBRARY_AI_FLAG: 'library-ai',
LIBRARY_AI_BOOKS_OVER: 15,
},
});
const { holdsFlag, countBooks, opensLibraryAi } = flags;
assert(holdsFlag({ featureNames: ['library-ai'] }, 'library-ai'));
assert(!holdsFlag({ featureNames: ['can-create-library'] }, 'library-ai'));
assert(!holdsFlag({ featureNames: 'library-ai' }, 'library-ai'));
assert(!holdsFlag({}, 'library-ai'));
assert(!holdsFlag(null, 'library-ai'));
// More than 15 books opens the AI without the flag (Wolf, 2026-09-25);
// only books count, on every shelf.
const shelf = (...types) => ({
attributes: {
objects: { data: types.map(type => ({ attributes: { type } })) },
},
});
const libraryOf = (...shelves) => ({
attributes: { singleShelves: { data: shelves } },
});
const fifteen = libraryOf(
shelf(...Array(10).fill('book'), 'audio', 'video'),
shelf(...Array(5).fill('book'), 'audio'),
);
const sixteen = libraryOf(
shelf(...Array(10).fill('book')),
shelf(...Array(6).fill('book')),
);
assert.equal(countBooks(fifteen), 15);
assert.equal(countBooks(sixteen), 16);
assert.equal(countBooks(null), 0);
assert(!opensLibraryAi({}, fifteen));
assert(opensLibraryAi({}, sixteen));
assert(opensLibraryAi({ featureNames: ['library-ai'] }, fifteen));
assert(!opensLibraryAi(null, null));
// The constants file carries icon components for its sample cards; the
// icons are not what is checked here.
const common = load('src/constants/library/common.ts', {
'@icons/library/svg': new Proxy({}, { get: () => () => null }),
});
assert.equal(common.LIBRARY_AI_FLAG, 'library-ai');
assert.equal(common.LIBRARY_AI_BOOKS_OVER, 15);
assert.equal(common.MAX_OBJECTS_PER_LIBRARY, 300);
for (const route of [
'src/pages/api/library/ai-shelf.ts',
'src/pages/api/library/magic-book.ts',
])
assert(
fs.readFileSync(route, 'utf8').includes('flag: LIBRARY_AI_FLAG'),
route,
);
assert(fs.readFileSync(route, 'utf8').includes('libraryAi: true'), route);
assert(
fs
.readFileSync('src/layouts/library/Library/Library.tsx', 'utf8')
.includes('opensLibraryAi(accountData, library)'),
);
for (const file of [
'src/layouts/library/Library/Library.tsx',
'src/layouts/library/Home/Home.tsx',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,11 +67,7 @@ function Hotspot({

// 768–1920px shows the wide artwork; 1920px+ swaps to the panorama, which
// frames the buildings differently and so carries its own geometry.
const label =
library?.libraryName ??
(hotspot.username
? `${hotspot.username}'s library`
: 'Nothing but ghosts...');
const label = library?.libraryName ?? 'Nothing but ghosts...';
const { hit, highlight, card } = isUltraWide
? hotspot.ultraWide
: hotspot.wide;
Expand Down Expand Up @@ -259,11 +255,13 @@ export function InteractiveCover({
<Hotspot
key={hotspot.id}
hotspot={hotspot}
library={libraries.find(
library =>
library.username?.toLowerCase() ===
hotspot.username?.toLowerCase(),
)}
library={
hotspot.ownerId === undefined
? undefined
: libraries.find(
library => library.userId === hotspot.ownerId,
)
}
mode={mode}
activeId={activeId}
setActiveId={setActiveId}
Expand Down
21 changes: 15 additions & 6 deletions src/components/library/organisms/InteractiveCover/coverHotspots.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,13 @@ export interface CoverHotspot {
* Derived from `wide` (see `toUltraWide`), with optional per-hotspot tweaks.
*/
ultraWide: HotspotGeometry;
username?: string;
/**
* Account id of the library's owner. Bound by id, not username: owners rename
* themselves (Mary13 became Mary, alinamarg became Alina) and a username
* binding then silently drops the library's data from the card. Staging's
* database is a copy of production's, so the ids hold on both.
*/
ownerId?: number;
}

// At the 1920px breakpoint the full-bleed cover frame is 1920px wide and the
Expand Down Expand Up @@ -109,13 +115,13 @@ const applyOverride = (
const makeHotspot = (
id: string,
wide: HotspotGeometry,
username?: string,
ownerId?: number,
ultraWideOverride?: GeometryOverride,
): CoverHotspot => ({
id,
wide,
ultraWide: applyOverride(toUltraWide(wide), ultraWideOverride),
username,
ownerId,
});

// Hit boxes are sized to the glow silhouette each hotspot lights up, so the
Expand All @@ -139,7 +145,8 @@ export const coverHotspots: CoverHotspot[] = [
},
card: { left: 53.0, top: 19.01 },
},
'Wolf',
// Wolf
7,
),
makeHotspot(
'house-1',
Expand Down Expand Up @@ -193,7 +200,8 @@ export const coverHotspots: CoverHotspot[] = [
},
card: { left: 30.62, top: 26.4 },
},
'Mary13',
// Mary
10,
{
hit: { top: 60.83 },
highlight: { left: 35.552, top: 28 },
Expand All @@ -215,6 +223,7 @@ export const coverHotspots: CoverHotspot[] = [
},
// Wolf, 2026-09-11: this library stands on the lantern now, not on the
// house above the water.
'alinamarg',
// Alina
538,
),
];
4 changes: 4 additions & 0 deletions src/constants/library/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,10 @@ export const LIBRARY_OBJECTS_FULL_MESSAGE =
// library needs no flag since 2026-09-12.
export const LIBRARY_AI_FLAG = 'library-ai';

// A library holding more books than this opens the AI to its owner without the
// flag (Wolf, 2026-09-25). Only objects of type book count.
export const LIBRARY_AI_BOOKS_OVER = 15;

export const SHELF_FULL_MESSAGE = 'This shelf is full.';

// The library-level twin, worded the same way so the two limits read as one
Expand Down
12 changes: 6 additions & 6 deletions src/layouts/library/Library/Library.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@ import React, {
} from 'react';

import {
LIBRARY_AI_FLAG,
LIBRARY_FULL_MESSAGE,
LIBRARY_SHELVES_REFETCH_EVENT,
MAX_OBJECTS_PER_LIBRARY,
Expand Down Expand Up @@ -61,7 +60,7 @@ import {
keepFavoriteFields,
sortFavorites,
} from '@lib/library/favorites';
import { holdsFlag } from '@lib/library/flags';
import { opensLibraryAi } from '@lib/library/flags';
import { libraryPath } from '@lib/library/libraryPath';
import { objectIdFromSlug } from '@lib/library/objectSlug';
import {
Expand Down Expand Up @@ -268,12 +267,13 @@ export function LibraryTemplate({
// and on first paint, so the markup hydrates identically everywhere.
const canEditHere = viewAsOwner && supportsEditing;

// The AI shelf and the magic books are behind the `library-ai` account
// flag from GET /api/users/me; the routes behind them check the same flag,
// so this decides what is drawn, not what is allowed. Creating a library
// The AI shelf and the magic books open with the `library-ai` account flag
// or with more than LIBRARY_AI_BOOKS_OVER books in this library; the routes
// behind them run the same check, so this decides what is drawn, not what
// is allowed. Creating a library
// needs no flag: any signed-in owner of this address bootstraps one from
// their first shelf.
const hasLibraryAi = holdsFlag(accountData, LIBRARY_AI_FLAG);
const hasLibraryAi = opensLibraryAi(accountData, library);

// The magic books are read once the owner is known to be editing here:
// desktop, own library, not previewing as a guest, and flagged. The
Expand Down
Loading
Loading