diff --git a/apps/website/qa/agents-spec-contract.mjs b/apps/website/qa/agents-spec-contract.mjs index d9eef813e5..2d7aea60d7 100644 --- a/apps/website/qa/agents-spec-contract.mjs +++ b/apps/website/qa/agents-spec-contract.mjs @@ -360,7 +360,12 @@ assert.ok(triModule && HUD_VIEWS.includes('tri'), 'TRI (the app inside the game) assert.equal(triModule.key, 'r', 'TRI opens on r: digits spent, t is TOOLS, p is PROJECT') assert.ok(triModule.en.hint.includes('(key r)') && triModule.ru.hint.includes('(клавиша r)'), 'TRI names its letter key in both hints') for (const lang of ['en', 'ru']) assert.ok(triModule[lang].name && triModule[lang].body.length > 40, `tri: ${lang} copy missing`) -assert.equal(HUD_KEYS.slice(0, HUD_VIEWS.length).join(''), '1234567890tpr', 'the rail keys are 1-9, 0, t, p, r in that order') +const lanesModule = MODULES.find((m) => m.tab === 'lanes') +assert.ok(lanesModule && HUD_VIEWS.includes('lanes'), 'LANES (the swarm\'s width) is a module and a view') +assert.equal(lanesModule.key, 'l', 'LANES opens on l: digits spent, t is TOOLS, p is PROJECT, r is TRI') +assert.ok(lanesModule.en.hint.includes('(key l)') && lanesModule.ru.hint.includes('(клавиша l)'), 'LANES names its letter key in both hints') +for (const lang of ['en', 'ru']) assert.ok(lanesModule[lang].name && lanesModule[lang].body.length > 40, `lanes: ${lang} copy missing`) +assert.equal(HUD_KEYS.slice(0, HUD_VIEWS.length).join(''), '1234567890tprl', 'the rail keys are 1-9, 0, t, p, r, l in that order') // 6. Translations are connected through .t27 contract specs, never hardcoded. // Every specs/i18n/*.t27 the corpus carries is in both catalogs' i18n lists diff --git a/apps/website/scripts/agents-from-specs.mjs b/apps/website/scripts/agents-from-specs.mjs index ef956c8589..f4dd9d4bb8 100644 --- a/apps/website/scripts/agents-from-specs.mjs +++ b/apps/website/scripts/agents-from-specs.mjs @@ -870,7 +870,12 @@ export function buildSpecCatalogs({ skillSpecs, cronSpecs, agentSpecs = [], func collisions, // Grouping the navigator shows: tri commands by owning letter (unbound under '-'), MCP servers by repo. groups: { - triByAgent: Object.fromEntries([...new Set(triTools.flatMap((x) => (x.agents.length ? x.agents.map((a) => a.letter) : ['-'])))].sort().map((l) => [l, triTools.filter((x) => (l === '-' ? x.agents.length === 0 : x.agents.some((a) => a.letter === l))).map((x) => x.id)])), + // '-' is always present, empty when nothing is unbound. It used to appear + // only while some command had no agent, so the day every command gained + // one the bucket vanished and the contract read undefined where it had + // asked for a list. "Nothing is unbound" is a fact worth stating, not an + // absence to infer. + triByAgent: Object.fromEntries([...new Set(['-', ...triTools.flatMap((x) => x.agents.map((a) => a.letter))])].sort().map((l) => [l, triTools.filter((x) => (l === '-' ? x.agents.length === 0 : x.agents.some((a) => a.letter === l))).map((x) => x.id)])), mcpByRepo: Object.fromEntries(['gHashTag/t27', 'gHashTag/trinity'].map((r) => [r, mcpTools.filter((x) => x.repo === r).map((x) => x.id)])), triByRepo: Object.fromEntries(TOOL_REPOS.map((r) => [r, triTools.filter((x) => x.repo === r).map((x) => x.id)])), }, diff --git a/apps/website/src/components/QueenLanes.css b/apps/website/src/components/QueenLanes.css new file mode 100644 index 0000000000..a406688d61 --- /dev/null +++ b/apps/website/src/components/QueenLanes.css @@ -0,0 +1,155 @@ +/* QueenLanes.css - LANES, the view that says how many bees can work at once and + refuses to let utilisation stand in for work. Same panel skin as QueenIntel: + colours come from the --hud-* variables Queen.css defines, with the live + palette as fallback, and the body is the only scroll owner. */ + +.queen27-lanes { + display: grid; + grid-template-rows: auto minmax(0, 1fr); + min-height: 0; + height: 100%; + border: 1px solid var(--hud-line, rgba(0, 255, 136, 0.22)); + background: var(--hud-panel, rgba(2, 8, 6, 0.86)); + clip-path: polygon(10px 0, 100% 0, 100% calc(100% - 10px), calc(100% - 10px) 100%, 0 100%, 0 10px); +} + +.queen27-lanes-head { + padding: 12px 16px; + border-bottom: 1px solid var(--hud-line, rgba(0, 255, 136, 0.22)); +} + +.queen27-lanes-head h2 { + margin: 0; + font-size: 0.82rem; + letter-spacing: 0.18em; + color: var(--hud-accent, #00ff88); +} + +.queen27-lanes-head p { + margin: 6px 0 0; + font-size: 0.78rem; + line-height: 1.5; + color: var(--hud-dim, rgba(214, 255, 235, 0.68)); +} + +.queen27-lanes-body { + min-height: 0; + overflow-y: auto; + padding: 14px 16px 20px; + display: grid; + gap: 12px; + align-content: start; +} + +/* ---- the four figures ------------------------------------------------- */ + +.queen27-lanes-figures { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(112px, 1fr)); + gap: 10px; +} + +.queen27-lanes-figure { + margin: 0; + padding: 10px 12px; + border: 1px solid var(--hud-line, rgba(0, 255, 136, 0.22)); + background: rgba(0, 255, 136, 0.04); + display: grid; + gap: 2px; +} + +.queen27-lanes-figure figcaption { + font-size: 0.64rem; + letter-spacing: 0.16em; + color: var(--hud-dim, rgba(214, 255, 235, 0.68)); +} + +.queen27-lanes-figure b { + font-size: 1.6rem; + font-weight: 600; + line-height: 1.1; + color: var(--hud-accent, #00ff88); + font-variant-numeric: tabular-nums; +} + +.queen27-lanes-figure small { + font-size: 0.66rem; + color: var(--hud-dim, rgba(214, 255, 235, 0.68)); +} + +.queen27-lanes-figure-wide { + grid-column: span 2; +} + +/* ---- cards ------------------------------------------------------------ */ + +.queen27-lanes-card { + padding: 12px 14px; + border: 1px solid var(--hud-line, rgba(0, 255, 136, 0.22)); + background: rgba(2, 8, 6, 0.5); +} + +.queen27-lanes-card h3 { + margin: 0 0 8px; + font-size: 0.72rem; + letter-spacing: 0.16em; + color: var(--hud-accent, #00ff88); +} + +.queen27-lanes-card h4 { + margin: 12px 0 4px; + font-size: 0.68rem; + letter-spacing: 0.14em; + color: var(--hud-accent, #00ff88); +} + +.queen27-lanes-card p { + margin: 0 0 6px; + font-size: 0.78rem; + line-height: 1.55; + color: var(--hud-text, rgba(233, 255, 244, 0.9)); +} + +/* The alarm skin is earned, not decorative: it appears only when the last + dispatch was refused in under a second, which is a spent quota. */ +.queen27-lanes-card-alarm { + border-color: rgba(255, 77, 94, 0.55); + background: rgba(255, 77, 94, 0.07); +} + +.queen27-lanes-alarm { + color: #ff4d5e; + font-weight: 600; +} + +.queen27-lanes-warn { + color: #ffc24d; +} + +.queen27-lanes-note, +.queen27-lanes-meta { + font-size: 0.72rem; + color: var(--hud-dim, rgba(214, 255, 235, 0.68)); +} + +.queen27-lanes-meta { + font-variant-numeric: tabular-nums; +} + +.queen27-lanes-formula { + display: block; + margin: 6px 0; + padding: 8px 10px; + font-size: 0.72rem; + line-height: 1.5; + overflow-x: auto; + border: 1px dashed var(--hud-line, rgba(0, 255, 136, 0.22)); + color: var(--hud-accent, #00ff88); + background: rgba(0, 0, 0, 0.35); +} + +.queen27-lanes-error { + margin: 0; + font-size: 0.74rem; + color: #ff4d5e; +} diff --git a/apps/website/src/components/QueenLanes.tsx b/apps/website/src/components/QueenLanes.tsx new file mode 100644 index 0000000000..f357f71cb9 --- /dev/null +++ b/apps/website/src/components/QueenLanes.tsx @@ -0,0 +1,243 @@ +// The Queen's LANES view: how many bees can work at once, why that number is +// what it is, and how a player raises it. +// +// The view exists because on 2026-09-17 the swarm reached 100% utilisation for +// the first time and produced nothing at all. Ten lanes were busy; every turn +// ended `refused` in zero seconds, because all five provider keys answered +// HTTP 429 (z.ai code 1302, "Rate limit reached"). A lane occupied by a refusal +// looks exactly like a lane doing work, so the headline number lied. +// +// Hence the shape of this panel: utilisation is shown, and immediately beside it +// the evidence for whether it is *work*. The two are never printed as one. +// +// Following the contract in queenHud.ts: every number here is a field the public +// /queen/status endpoint already sends. The ones the swarm knows but does not +// publish — how many keys are live, who contributed them, what each has spent — +// are named as missing, with the endpoint that would carry them. A panel that +// invents them would be the same lie in a different font. + +import { useI18n } from '../i18n/context' +import './QueenLanes.css' + +export interface LanesWorkers { + capacity: number + active: number + idle: number + utilization: number +} + +export interface LanesDispatch { + issue: number + dispatchedAt: string + finishedAt: string | null + outcome: string | null +} + +export interface LanesStatus { + workers?: LanesWorkers | null + dispatches?: { + total: number + finished: number + running: number + unreviewed?: number + latest?: LanesDispatch | null + } | null +} + +export interface LanesCopy { + directive: string + directiveBody: string +} + +const COPY = { + en: { + capacity: 'LANES', + capacityHint: 'bees that can work at once', + active: 'BUSY', + idle: 'FREE', + util: 'UTILISATION', + lawTitle: 'THE LANE LAW', + law: 'A lane is not a setting. Capacity is the number of provider keys that answer, times the lanes each key is allowed to open. The worker ceiling is only a ceiling: it cannot conjure a lane a key does not pay for.', + lawFormula: 'capacity = live keys × lanes per key', + lawUnknown: 'How many keys are live is not on the public endpoint. This panel will not guess it.', + workTitle: 'IS IT WORK?', + workLead: 'Utilisation counts occupied lanes. A refused turn occupies one exactly like a working turn, so the two must be read together, never as one number.', + lastDispatch: 'last dispatch', + instantRefusal: 'Zero-second refusal — the lane was occupied and nothing was attempted. This is what a spent quota looks like from outside.', + refusalHint: 'Refusals are cheap and fast, so a starved swarm reports its best utilisation ever.', + healthy: 'The last dispatch ran for a measurable time. That is necessary for real work, and not sufficient: only a branch proves it.', + pending: 'The last dispatch is still running.', + noDispatch: 'No dispatch recorded yet.', + unreviewed: 'finished, awaiting review', + donateTitle: 'DONATE A LANE', + donateLead: 'The currency is quota, not lanes. A key that answers adds both; a lane without quota multiplies zero. This is the one contribution that raises the ceiling for everybody at once.', + xpTitle: 'HOW XP IS EARNED', + xpRule: 'XP accrues to the owner of the key a turn ran on — but only for a turn that reached a branch. Never per dispatch: a refused turn occupies a lane exactly like a working one, and paying for it would pay best when the swarm is most broken.', + neverTitle: 'THIS PAGE NEVER TAKES A KEY', + never: 'No field here accepts a provider key, and none ever will. This is a public static page; a secret typed into it is a secret published. Keys are handed over out of band and held by the server alone.', + missingTitle: 'NOT WIRED YET', + missing: 'The contributor board, the per-key health and the XP ledger need an endpoint that does not exist yet:', + missingEndpoint: 'GET /queen/public-lanes → { keys: [{ owner, state: "live" | "cooldown" | "spent", lanes, turnsToBranch }] }', + missingWhy: 'Until it answers, this view shows the swarm-wide numbers above and says nothing about who paid for them.', + branchTitle: 'WHERE THE WORK GOES', + branchLead: 'A finished turn writes a branch inside the container, which deliberately holds no push credential — so nothing reaches GitHub until something outside pushes it. That publishing step is where a day of bee work can sit unseen.', + branchGit: 'GitButler is the intended safe path for it: virtual branches let the queue be published and reviewed in slices instead of as one irreversible flood.', + }, + ru: { + capacity: 'ПОЛОСЫ', + capacityHint: 'пчёл могут работать одновременно', + active: 'ЗАНЯТО', + idle: 'СВОБОДНО', + util: 'ЗАГРУЗКА', + lawTitle: 'ЗАКОН ПОЛОСЫ', + law: 'Полоса — не настройка. Ёмкость это число отвечающих ключей провайдера, умноженное на число полос, разрешённых одному ключу. Потолок воркеров — только потолок: он не создаст полосу, за которую не платит ключ.', + lawFormula: 'ёмкость = живые ключи × полос на ключ', + lawUnknown: 'Сколько ключей живо — публичный эндпоинт не сообщает. Эта панель не станет угадывать.', + workTitle: 'А ЭТО РАБОТА?', + workLead: 'Загрузка считает занятые полосы. Отказной ход занимает полосу ровно так же, как рабочий, поэтому два числа читаются вместе и никогда не сливаются в одно.', + lastDispatch: 'последняя выдача', + instantRefusal: 'Отказ за ноль секунд — полоса была занята, а попытки не было. Так снаружи выглядит исчерпанная квота.', + refusalHint: 'Отказы дёшевы и мгновенны, поэтому голодающий рой показывает лучшую загрузку в своей истории.', + healthy: 'Последняя выдача длилась измеримое время. Это необходимо для настоящей работы и недостаточно: доказывает её только ветка.', + pending: 'Последняя выдача ещё выполняется.', + noDispatch: 'Выдач пока не записано.', + unreviewed: 'завершено, ждёт ревью', + donateTitle: 'ОТДАТЬ ПОЛОСУ', + donateLead: 'Валюта — квота, а не полосы. Отвечающий ключ добавляет и то и другое; полоса без квоты умножает ноль. Это единственный вклад, который поднимает потолок сразу для всех.', + xpTitle: 'КАК НАЧИСЛЯЕТСЯ XP', + xpRule: 'XP идёт владельцу ключа, на котором прошёл ход, — но только за ход, дошедший до ветки. Никогда за диспатч: отказной ход занимает полосу так же, как рабочий, и плата за него платила бы лучше всего тогда, когда рой сломан сильнее всего.', + neverTitle: 'ЭТА СТРАНИЦА НИКОГДА НЕ ПРИНИМАЕТ КЛЮЧ', + never: 'Здесь нет поля для ключа провайдера и не будет. Это публичная статическая страница; секрет, введённый в неё, — секрет опубликованный. Ключи передаются вне игры и хранятся только на сервере.', + missingTitle: 'ЕЩЁ НЕ ПОДКЛЮЧЕНО', + missing: 'Доска вкладчиков, здоровье по каждому ключу и журнал XP требуют эндпоинта, которого пока нет:', + missingEndpoint: 'GET /queen/public-lanes → { keys: [{ owner, state: "live" | "cooldown" | "spent", lanes, turnsToBranch }] }', + missingWhy: 'Пока он не отвечает, вкладка показывает общие числа роя выше и молчит о том, кто за них заплатил.', + branchTitle: 'КУДА УХОДИТ РАБОТА', + branchLead: 'Завершённый ход пишет ветку внутри контейнера, который намеренно не держит push-креденшел, — поэтому до GitHub ничего не доходит, пока её не выложит что-то снаружи. Именно на этом шаге день работы пчёл может простоять невидимым.', + branchGit: 'GitButler — предполагаемый безопасный путь для этого: виртуальные ветки позволяют публиковать и ревьюить очередь порциями, а не одним необратимым потоком.', + }, +} as const + +/** Milliseconds a dispatch lasted, or null when it has not finished. */ +const durationMs = (d: LanesDispatch): number | null => { + if (!d.finishedAt) return null + const ms = Date.parse(d.finishedAt) - Date.parse(d.dispatchedAt) + return Number.isFinite(ms) ? ms : null +} + +export function QueenLanes({ + status, + error, + c, +}: { + status: LanesStatus | null + error: string | null + c: LanesCopy +}) { + const { lang } = useI18n() + const t = lang === 'ru' ? COPY.ru : COPY.en + + const workers = status?.workers ?? null + const dispatches = status?.dispatches ?? null + const latest = dispatches?.latest ?? null + const ran = latest ? durationMs(latest) : null + // A refusal that took no measurable time never reached the provider. That is + // the signature of a spent quota, and the only way to tell it apart from a + // busy swarm without reading the container's own logs. + const instant = latest?.outcome === 'refused' && ran !== null && ran < 1000 + + return ( +
+
+

{c.directive}

+

{c.directiveBody}

+
+ +
+
+
+
{t.capacity}
+ {workers ? workers.capacity : '—'} + {t.capacityHint} +
+
+
{t.active}
+ {workers ? workers.active : '—'} +
+
+
{t.idle}
+ {workers ? workers.idle : '—'} +
+
+
{t.util}
+ {workers ? `${workers.utilization}%` : '—'} +
+
+ + {error ?

{error}

: null} + +
+

{t.workTitle}

+

{t.workLead}

+ {latest ? ( + <> +

+ {t.lastDispatch}: #{latest.issue} + {latest.outcome ? ` · ${latest.outcome}` : ''} + {ran !== null ? ` · ${(ran / 1000).toFixed(1)}s` : ''} +

+ {instant ? ( + <> +

{t.instantRefusal}

+

{t.refusalHint}

+ + ) : ran === null ? ( +

{t.pending}

+ ) : ( +

{t.healthy}

+ )} + + ) : ( +

{t.noDispatch}

+ )} + {dispatches?.unreviewed ? ( +

+ {dispatches.unreviewed} {t.unreviewed} +

+ ) : null} +
+ +
+

{t.lawTitle}

+

{t.law}

+ {t.lawFormula} +

{t.lawUnknown}

+
+ +
+

{t.donateTitle}

+

{t.donateLead}

+

{t.xpTitle}

+

{t.xpRule}

+

{t.neverTitle}

+

{t.never}

+
+ +
+

{t.missingTitle}

+

{t.missing}

+ {t.missingEndpoint} +

{t.missingWhy}

+
+ +
+

{t.branchTitle}

+

{t.branchLead}

+

{t.branchGit}

+
+
+
+ ) +} + +export default QueenLanes diff --git a/apps/website/src/components/queenHud.ts b/apps/website/src/components/queenHud.ts index f9a549abd3..12b3218231 100644 --- a/apps/website/src/components/queenHud.ts +++ b/apps/website/src/components/queenHud.ts @@ -7,7 +7,7 @@ // derivable from these types, the number does not exist yet and the panel // must say so rather than invent it. -export type HudView = "comb" | "specs" | "kanban" | "map" | "factory" | "research" | "skills" | "crons" | "agents" | "functions" | "tools" | "project" | "tri"; +export type HudView = "comb" | "specs" | "kanban" | "map" | "factory" | "research" | "skills" | "crons" | "agents" | "functions" | "tools" | "project" | "tri" | "lanes"; // In command-panel order: the key that opens a view is HUD_KEYS at the same // position, and `?tab=` accepts exactly these names. Kept identical to // lib/queenModules (qa/agents-spec-contract.mjs checks the two lists agree), so @@ -30,12 +30,15 @@ export const HUD_VIEWS: readonly HudView[] = [ // Thirteenth, on the letter r: TRI, the app at app.t27.ai inside the game (the // digits are spent, t is TOOLS, p is PROJECT). "tri", + // Fourteenth, on the letter l: LANES -- how many bees can work at once, and + // why utilisation is not the same question as whether they are working. + "lanes", ] as const; // The keyboard shortcut per view, by position: the digits 1-9, then 0, then // letters once the digits are spent. The rail prints HUD_KEYS[i] on button i and // the shell binds exactly these keys; a tenth or eleventh view takes the next // entry here and nothing else changes. -export const HUD_KEYS: readonly string[] = ["1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "t", "p", "r"] as const; +export const HUD_KEYS: readonly string[] = ["1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "t", "p", "r", "l"] as const; export const hudKeyOf = (view: HudView): string => HUD_KEYS[HUD_VIEWS.indexOf(view)] ?? ""; export type Territory = "held" | "neutral" | "fog"; diff --git a/apps/website/src/lib/queenModules.ts b/apps/website/src/lib/queenModules.ts index 991da22f83..a321d7cc8a 100644 --- a/apps/website/src/lib/queenModules.ts +++ b/apps/website/src/lib/queenModules.ts @@ -210,6 +210,21 @@ export const MODULES = [ body: 'Приложение app.t27.ai внутри игры. Каждый экран — настоящая страница приложения во фрейме: лента, агент, ИИ-конвейер от сценария через голос, фото, липсинк и видео к редактору, профиль и CRM владельца. Экран записан в адресе (?tab=tri&screen=chat, а для одного профиля ещё path=), поэтому ссылка его открывает, а перезагрузка сохраняет. На t27.ai Telegram не даёт своему входу загрузиться внутри чужого сайта, поэтому экраны, которым нужен человек, говорят об этом и ведут в само приложение; лента работает для всех. Пока приложение не разрешит t27.ai показывать себя во фрейме, экран не отвечает и говорит об этом. Открывается буквой r.', }, }, + { + tab: 'lanes', + key: 'l', + glyph: '≡', + en: { + name: 'LANES', + hint: 'How many bees can work at once, and whether they are working (key l)', + body: 'The swarm\'s width, and the one question the width cannot answer. Capacity is the number of provider keys that answer times the lanes each key may open — a worker ceiling cannot conjure a lane a key does not pay for. Beside the utilisation figure the view prints the evidence for whether it is work at all: a turn refused in under a second never reached the provider, and a starved swarm reports its best utilisation ever, because a refusal occupies a lane exactly like a real turn. It also states how a player widens the swarm — a donated key adds quota, which is the actual currency — and how XP is earned for it: per turn that reached a branch, never per dispatch. No field on this page accepts a provider key and none ever will; it is a public static page, and a secret typed into it is a secret published. The digits are spent, so this view opens on the letter l.', + }, + ru: { + name: 'ПОЛОСЫ', + hint: 'Сколько пчёл работают одновременно и работают ли вообще (клавиша l)', + body: 'Ширина роя и единственный вопрос, на который ширина не отвечает. Ёмкость — это число отвечающих ключей провайдера, умноженное на число полос, разрешённых одному ключу: потолок воркеров не создаст полосу, за которую не платит ключ. Рядом с цифрой загрузки вид печатает свидетельство того, работа ли это вообще: ход, отказанный быстрее секунды, до провайдера не дошёл, а голодающий рой показывает лучшую загрузку в своей истории, потому что отказ занимает полосу так же, как настоящий ход. Здесь же сказано, чем игрок расширяет рой — отданный ключ добавляет квоту, а квота и есть настоящая валюта — и как за это начисляется XP: за ход, дошедший до ветки, и никогда за диспатч. Ни одно поле этой страницы не принимает ключ провайдера и не будет: это публичная статическая страница, а секрет, введённый в неё, — секрет опубликованный. Цифры заняты, поэтому вид открывается буквой l.', + }, + }, ] as const export type QueenModule = (typeof MODULES)[number]; diff --git a/apps/website/src/pages/Queen.tsx b/apps/website/src/pages/Queen.tsx index 57562336ff..16ea1f2929 100644 --- a/apps/website/src/pages/Queen.tsx +++ b/apps/website/src/pages/Queen.tsx @@ -73,6 +73,7 @@ const ENGINE_FLAG = typeof window !== "undefined" ? new URLSearchParams(window.location.search).get("engine") : null; import { useI18n } from "../i18n/context"; import { QueenTri } from "../components/QueenTri"; +import { QueenLanes } from "../components/QueenLanes"; import { QueenIdentity } from "../components/QueenIdentity"; import { hashParamsOf, tabAddress } from "../lib/triScreens"; import { @@ -157,6 +158,14 @@ interface ResearchGraph { interface QueenStatus { status: "ok"; + /** Lane occupancy as the swarm reports it. Occupancy, not throughput: a + refused turn holds a lane exactly like a working one (see QueenLanes). */ + workers?: { + capacity: number; + active: number; + idle: number; + utilization: number; + } | null; /** The swarm's own word for its state on the wire (working, idle, …). */ swarmState?: string | null; scheduler: { @@ -339,6 +348,11 @@ const COPY = { projectSources: "sources pinned", triView: "TRI", triHint: "The app inside the game: feed, agent, AI generation, profile and CRM (key r)", + lanesView: "LANES", + lanesHint: "How many bees can work at once, and whether they are working (key l)", + lanesDirective: "LANES", + lanesDirectiveBody: + "Capacity is live provider keys times the lanes each may open. Utilisation counts occupied lanes — a refused turn occupies one too, so it is printed beside the evidence, never instead of it.", triScreens: "App screens", triFeed: "Feed", triAgent: "Agent", @@ -663,6 +677,11 @@ const COPY = { projectSources: "источников закреплено", triView: "TRI", triHint: "Приложение внутри игры: лента, агент, ИИ-генерация, профиль и CRM (клавиша r)", + lanesView: "ПОЛОСЫ", + lanesHint: "Сколько пчёл работают одновременно и работают ли вообще (клавиша l)", + lanesDirective: "ПОЛОСЫ", + lanesDirectiveBody: + "Ёмкость — живые ключи провайдера, умноженные на полосы каждого. Загрузка считает занятые полосы, а отказной ход занимает полосу тоже, поэтому она печатается рядом со свидетельством, а не вместо него.", triScreens: "Экраны приложения", triFeed: "Лента", triAgent: "Агент", @@ -2405,6 +2424,7 @@ export default function Queen({sharedCatalog}:{sharedCatalog?:UniverseAtlas}={}) // Thirteenth, on the letter r (HUD_KEYS[12]; digits spent, t is TOOLS, p is // PROJECT): TRI, the app at app.t27.ai inside the game, one screen per address. { view: "tri" as const, glyph: "△", label: c.triView, hint: c.triHint }, + { view: "lanes" as const, glyph: "≡", label: c.lanesView, hint: c.lanesHint }, ]; const viewLabel = commandItems.find((item) => item.view === view)?.label ?? c.combView; @@ -2830,6 +2850,12 @@ export default function Queen({sharedCatalog}:{sharedCatalog?:UniverseAtlas}={}) projectSources: c.projectSources, }} /> + ) : boardView === "lanes" ? ( + ) : boardView === "tri" ? (