Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
5 changes: 5 additions & 0 deletions .github/workflows/lazurio-fork-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,11 @@ jobs:
apps/web/src/lazurio/promptDraft.ts
apps/web/src/main.tsx
apps/web/src/routes/_chat.tsx
# The Lazurio shell slot (issue #33): the loader, the rail, the padding and
# Buddy in index.html, and the column head at the top of the sidebar. The
# shell itself is served by the Environment's Launchpad; without it nothing renders.
apps/web/index.html
apps/web/src/components/AppSidebarLayout.tsx
)
unexpected="$(git diff --name-only "$UPSTREAM_SHA..HEAD" |
grep -E '^(apps/(web|mobile|desktop)|packages)/' |
Expand Down
14 changes: 14 additions & 0 deletions apps/web/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -478,15 +478,29 @@
}
</style>
<title>T3 Code (Alpha)</title>
<!-- Lazurio shell (docs/operations/lazurio-fork-release.md "Lazurio shell"): served by the Environment's Launchpad; absent outside Lazurio. -->
<script type="module" src="/.lazurio/shell.js" vite-ignore></script>
<style>
#root {
box-sizing: border-box;
padding-left: var(--lazurio-rail-width, 0px);
Comment thread
agentrozjedemeai marked this conversation as resolved.
}
/* T3's sidebar and its toggle are position: fixed; contain them beside the rail, not under it. */
lazurio-rail:defined ~ #root [data-slot="sidebar-wrapper"] {
contain: layout paint;
}
</style>
</head>
<body>
<lazurio-rail></lazurio-rail>
<div id="root">
<div id="boot-shell">
<div id="boot-shell-card" aria-label="T3 Code splash screen">
<img id="boot-shell-logo" src="/apple-touch-icon.png" alt="T3 Code" />
</div>
</div>
</div>
<lazurio-buddy></lazurio-buddy>
<script type="module" src="/src/bootstrap.ts"></script>
</body>
</html>
27 changes: 24 additions & 3 deletions apps/web/src/components/AppSidebarLayout.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { useAtomValue } from "@effect/atom-react";
import * as Schema from "effect/Schema";
import {
createElement,
useCallback,
useEffect,
useState,
useSyncExternalStore,
Expand Down Expand Up @@ -78,10 +80,10 @@ function readInitialThreadSidebarWidth(): number {
}
}

function SidebarControl() {
function SidebarControl({ lazurioColumnHeadHeight }: { lazurioColumnHeadHeight: number }) {
const usagePageOpen = useLocation({ select: (location) => location.pathname === "/usage" });
const keybindings = useAtomValue(primaryServerKeybindingsAtom);
const { toggleSidebar } = useSidebar();
const { isMobile, toggleSidebar } = useSidebar();
const isSidebarVisible = useSidebarVisibility();
const environmentIdentificationMode = useEnvironmentIdentificationMode();
const stageBackdropVariant = useSidebarStageBackdropVariant(
Expand Down Expand Up @@ -132,6 +134,12 @@ function SidebarControl() {
<div
className="pointer-events-none fixed left-[var(--workspace-controls-left)] top-[var(--workspace-controls-top)] z-50 ml-px flex h-[var(--workspace-topbar-height)] items-center"
data-sidebar-control=""
// Lazurio shell: the open sidebar's top row sits below the column head, and so does its toggle.
style={
isSidebarVisible && !isMobile && lazurioColumnHeadHeight > 0
? { top: `calc(var(--workspace-controls-top) + ${lazurioColumnHeadHeight}px)` }
: undefined
}
>
<Tooltip>
<TooltipTrigger
Expand Down Expand Up @@ -240,6 +248,17 @@ export function AppSidebarLayout({ children }: { children: ReactNode }) {
}
setSidebarWidth(resolveInitialThreadSidebarWidth(null, viewportWidth));
};
// Lazurio shell: height of the column head, 0 while the shell is absent.
const [lazurioColumnHeadHeight, setLazurioColumnHeadHeight] = useState(0);
const observeLazurioColumnHead = useCallback((element: HTMLElement | null) => {
if (!element || typeof ResizeObserver === "undefined") return;
const observer = new ResizeObserver(() => setLazurioColumnHeadHeight(element.offsetHeight));
Comment thread
agentrozjedemeai marked this conversation as resolved.
observer.observe(element);
return () => {
observer.disconnect();
setLazurioColumnHeadHeight(0);
};
}, []);
const [isWindowFullscreen, setIsWindowFullscreen] = useState(() => {
const getWindowFullscreenState = window.desktopBridge?.getWindowFullscreenState;
return isMacosDesktop && typeof getWindowFullscreenState === "function"
Expand Down Expand Up @@ -316,6 +335,8 @@ export function AppSidebarLayout({ children }: { children: ReactNode }) {
onResize: setSidebarWidth,
}}
>
{/* Lazurio shell (docs/operations/lazurio-fork-release.md): the Environment picker and the app switch, above the upstream top row. Undefined, it renders nothing. */}
{createElement("lazurio-column-head", { active: "chat", ref: observeLazurioColumnHead })}
{isOnSettings ? (
<>
<SidebarChromeHeader isElectron={isElectron} />
Expand All @@ -329,7 +350,7 @@ export function AppSidebarLayout({ children }: { children: ReactNode }) {
<SidebarRail onDoubleClick={resetSidebarWidth} />
</Sidebar>
{children}
<SidebarControl />
<SidebarControl lazurioColumnHeadHeight={lazurioColumnHeadHeight} />
<NavigationHistoryShortcuts />
<MainAppLocationTracker />
</SidebarProvider>
Expand Down
100 changes: 54 additions & 46 deletions docs/operations/lazurio-fork-release.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ Admin (Matěj, `immakermatty`) ho jen schválí v GitHub environmentu.
Lazurio Mašiny. T3 vždy běží v kořeni vlastního hostname
(`https://t3code.<vm>.<org>.lazurio.io/`) za TLS reverse proxy. Oficiální
desktop a mobilní aplikace se připojují jako neupravení upstream klienti.
Vzhled a branding webového klienta zůstávají upstream; jediný rozhodnutý zásah
do jeho UI je načtení shellu Lazuria (viz [Lazurio shell](#lazurio-shell)).
Vzhled a branding webového klienta zůstávají upstream; jediný zásah do jeho UI
je slot pro shell Lazuria (viz [Lazurio shell](#lazurio-shell)).

## Kanál aktualizací

Expand Down Expand Up @@ -83,14 +83,15 @@ nabídne normálně.

`main` je přesný upstream stable tag a nad ním jen tyto commity:

| Commit | Proč ho Lazurio potřebuje | v0.0.45 |
| --------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------- |
| `hosted: configurable client session TTL` | `T3CODE_CLIENT_SESSION_TTL` (Machines nastavuje `365d`). | retain: upstream má pevných 30 dní |
| `hosted: serve behind an explicit HTTPS external origin` | `T3CODE_EXTERNAL_ORIGIN`: server na loopbacku za proxy je dosažitelný zvenku, používá Secure cookie `__Host-t3_session` a mutace a WebSocket upgrady autentizované cookie přijímá jen z tohoto originu. | retain: upstream ekvivalent nemá |
| `hosted: explicit environment label` | `T3CODE_ENVIRONMENT_LABEL` pojmenuje kontejnerový Workspace (například `Acme / Management`). | retain: upstream čte jen `PRETTY_HOSTNAME` a hostname, Machines nastavují proměnnou |
| `feat: in-app update from the configured release channel` | `T3CODE_RELEASE_REPOSITORY` a server-advertised `availableServerUpdate`: Mašina nabízí aktualizaci na nejvyšší vydání z nastaveného repozitáře a instaluje ho tlačítkem Update. Navrženo upstreamu. | retain: upstream má pevné `pingdotgg/t3code` a bere první vydání kanálu |
| `release: Lazurio distribution` | Tento dokument, `Dockerfile.lazurio`, `.dockerignore`, kontraktní test a workflow `lazurio-fork-ci.yml`, `lazurio-cli-archives.yml` a `lazurio-release.yml`. | retain |
| `lazurio: unsent prompt draft by link from the shell` | „+ Nový modul“ otevře Chat se zadáním v poli zprávy nového vlákna, neodeslaným ([Zadání z Launchpadu](#zadání-z-launchpadu)). | retain: upstream nemá vstup pro koncept zprávy zvenku |
| Commit | Proč ho Lazurio potřebuje | v0.0.45 |
| --------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------- |
| `hosted: configurable client session TTL` | `T3CODE_CLIENT_SESSION_TTL` (Machines nastavuje `365d`). | retain: upstream má pevných 30 dní |
| `hosted: serve behind an explicit HTTPS external origin` | `T3CODE_EXTERNAL_ORIGIN`: server na loopbacku za proxy je dosažitelný zvenku, používá Secure cookie `__Host-t3_session` a mutace a WebSocket upgrady autentizované cookie přijímá jen z tohoto originu. | retain: upstream ekvivalent nemá |
| `hosted: explicit environment label` | `T3CODE_ENVIRONMENT_LABEL` pojmenuje kontejnerový Workspace (například `Acme / Management`). | retain: upstream čte jen `PRETTY_HOSTNAME` a hostname, Machines nastavují proměnnou |
| `feat: in-app update from the configured release channel` | `T3CODE_RELEASE_REPOSITORY` a server-advertised `availableServerUpdate`: Mašina nabízí aktualizaci na nejvyšší vydání z nastaveného repozitáře a instaluje ho tlačítkem Update. Navrženo upstreamu. | retain: upstream má pevné `pingdotgg/t3code` a bere první vydání kanálu |
| `release: Lazurio distribution` | Tento dokument, `Dockerfile.lazurio`, `.dockerignore`, kontraktní test a workflow `lazurio-fork-ci.yml`, `lazurio-cli-archives.yml` a `lazurio-release.yml`. | retain |
| `lazurio: unsent prompt draft by link from the shell` | „+ Nový modul“ otevře Chat se zadáním v poli zprávy nového vlákna, neodeslaným ([Zadání z Launchpadu](#zadání-z-launchpadu)). | retain: upstream nemá vstup pro koncept zprávy zvenku |
| `feat(web): Lazurio shell slot` | Slot pro shell Lazuria ve webovém klientovi: načtení `/.lazurio/shell.js`, rail vedle aplikace, hlavička sloupce nahoře v sidebaru a Buddy (viz [Lazurio shell](#lazurio-shell)). Mimo Lazurio se nic nevykreslí. | retain: upstream ekvivalent nemá |

Nenastavené proměnné zachovají upstream chování. Commit odstraň, jakmile
upstream nabídne ekvivalent. Klienty, sdílené balíčky a wire kontrakty
Expand All @@ -103,46 +104,53 @@ necommitují.

### Lazurio shell

**Rozhodnuto 2026-10-03 (rozhodnutí o shellu Lazuria), implementace čeká.**
V tabulce overlaye ani v allowlistu zatím není.

V Lazuriu je T3 Code aplikace **Chat** v přepínači Chat · Apps · Automate.
Overlay do webového klienta přidá jen načtení shellu Lazuria: rail
Environmentů, přepínač aplikací v sidebaru T3 a plovoucí bublinu Buddyho.
Shell jsou Web Components se Shadow DOM, takže CSS T3 a Lazuria se navzájem
neovlivní. Servíruje je Launchpad daného Environmentu na stejném originu, za
bránou Environmentu:

- `/.lazurio/shell.js` definuje `<lazurio-rail>`, `<lazurio-column-head>`
a `<lazurio-buddy>` ve verzi Launchpadu na tom Environmentu. Nový rail proto
nepotřebuje nové vydání forku.
- `/.lazurio/shell.json` drží data: přihlášeného člověka, Environmenty
a Organizace, adresy aplikací a barvu Organizace. Fork data Lazuria nezná
a sám nic nenačítá.

Patch bude mít zhruba 20 řádků ve zhruba třech souborech `apps/web`:
`<script type="module" src="/.lazurio/shell.js">` v `index.html`,
`<lazurio-rail>` vedle kořene aplikace, `<lazurio-column-head>` nahoře
v sidebaru místo loga upstreamu a `<lazurio-buddy>` v kořeni aplikace.
Rail je `position: fixed` u levého okraje a místo pro něj udělá odsazení,
ne posun: `#root { box-sizing: border-box; padding-left:
var(--lazurio-rail-width, 0px); }`. `#root` má v upstreamu `width: 100%`
a `body` `overflow: hidden`, takže `margin-left` by pravý okraj aplikace
uřízl; s `border-box` se šířka obsahu zmenší o rail. Každý změněný soubor
přibude do `allowed_upstream_changes` jako vlastní řádek s přesnou cestou
(například `apps/web/index.html`, `apps/web/src/index.css` a soubor sidebaru),
pod jedním komentářem s důvodem na začátku bloku. Kontrola ve forku bude
hlídat, že slot (skript a `<lazurio-column-head>`) existuje.
Při přestavbě na nový upstream tag se pak konflikt ukáže jen v těchto řádcích.
Přepínač i rail vedou obyčejnými odkazy na jiné originy Environmentu
(`launchpad.…`, `mausbot.…`), takže router T3 se nemění.
**Rozhodnuto 2026-10-03 (rozhodnutí o shellu Lazuria); slot je v overlayi od
issue #33.** V Lazuriu je T3 Code aplikace **Chat** v přepínači Chat · Apps ·
Automate. Overlay do webového klienta přidává jen slot pro shell Lazuria: rail
vlevo, hlavičku sloupce (výběr Environmentu, ozubené kolo Nastavení a přepínač
Chat · Apps · Automate) nahoře ve vlastním sidebaru T3 a plovoucí bublinu
Buddyho. Shell jsou Web Components se Shadow DOM (`<lazurio-rail>`,
`<lazurio-column-head>`, `<lazurio-buddy>`), takže CSS T3 a Lazuria se navzájem
neovlivní. Definuje je `/.lazurio/shell.js`, který servíruje Launchpad daného
Environmentu na stejném originu za bránou Environmentu; data má v
`/.lazurio/shell.json` (LazurioPlatform rozhodnutí F36). Fork data Lazuria
nezná a sám nic nenačítá, takže nový rail, nový výběr ani nová data
nepotřebují vydání forku. Fork se smí spolehnout jen na rozhraní v1 z
LazurioPlatform `src/shell/interface.ts`.

Slot jsou dva upstream soubory, každý jako vlastní řádek v
`allowed_upstream_changes`:

- `apps/web/index.html`: modulový skript `/.lazurio/shell.js` s atributem
`vite-ignore` (Vite značku nechá být a nic nebunduje), `<lazurio-rail>` před
`#root` a `<lazurio-buddy>` za ním. Dál
`#root { box-sizing: border-box; padding-left: var(--lazurio-rail-width, 0px); }`:
odsazení, ne posun, protože `#root` má v upstreamu `width: 100%` pod
`body` s `overflow: hidden` a `margin-left` by pravý okraj uřízl. Sidebar T3
a jeho přepínač jsou ale `position: fixed` vůči oknu a samotné odsazení by je
nechalo pod railem. Proto se při definovaném railu stane obal sidebaru
(`[data-slot="sidebar-wrapper"]`) jejich containing blockem
(`contain: layout paint`). Bez shellu pravidlo neplatí.
- `apps/web/src/components/AppSidebarLayout.tsx`:
`<lazurio-column-head active="chat">` jako první prvek sidebaru, nad horním
řádkem upstreamu s logem T3 Code, který zůstává se všemi ovládacími prvky
(rozhodnutí 0179 bod 6: vzhled a branding upstreamu zůstávají). Jedno místo
platí pro sidebar vláken, legacy sidebar i Nastavení. Přepínač sidebaru je
`position: fixed` u horního okraje, takže se při otevřeném sidebaru na
desktopu posune dolů o výšku hlavičky sloupce, s horním řádkem sidebaru.

Kontraktní test hlídá obojí, takže přestavba na nový upstream tag, která slot
ztratí, v CI selže. Přepínač i rail vedou obyčejnými odkazy na jiné originy
Environmentu (`launchpad.…`, `mausbot.…`), takže router T3 se nemění.

Vzhled a branding zůstávají upstream až do stabilních vydání upstreamu: T3
Code nepřebarvujeme ani nepřejmenováváme a dál o něm mluvíme jako o T3 Code.
Mimo Lazurio se nic nevykreslí: bez `/.lazurio/shell.js` (samostatný server,
vývoj upstreamu) zůstanou elementy nedefinované, šířka railu je 0 a klient se
chová jako upstream. Oficiální desktop a mobilní aplikace jsou upstream a shell
nemají.
vývoj upstreamu) zůstanou elementy nedefinované, šířka railu je 0, hlavička
sloupce má výšku 0 a klient se chová jako upstream. Server T3 na neznámou
cestu vrátí `index.html`, takže prohlížeč jen zaloguje neúspěšné načtení
skriptu. Oficiální desktop a mobilní aplikace jsou upstream
a shell nemají.

### Zadání z Launchpadu

Expand Down
49 changes: 49 additions & 0 deletions scripts/lazurio-release-contract.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -268,3 +268,52 @@ NodeTest.test("the prompt hand-off overlay keeps its seam and never sends", asyn
}
NodeAssert.match(ci, /working-directory: apps\/web\n\s+run: pnpm exec vp test run src\/lazurio/);
});

NodeTest.test("the Lazurio shell slot survives a rebuild on a new upstream tag", async () => {
const [page, layout, sidebarUi] = await Promise.all([
read("apps/web/index.html"),
read("apps/web/src/components/AppSidebarLayout.tsx"),
read("apps/web/src/components/ui/sidebar.tsx"),
]);
// Same-origin loader that Vite leaves alone; the Environment's Launchpad serves it.
NodeAssert.match(
page,
/<script type="module" src="\/\.lazurio\/shell\.js" vite-ignore><\/script>/,
);
// Padding, not margin: #root is full width under an overflow-hidden body.
NodeAssert.match(
page,
/#root \{\s*box-sizing: border-box;\s*padding-left: var\(--lazurio-rail-width, 0px\);\s*\}/,
);
// T3's sidebar and its toggle are position: fixed, so padding alone leaves them under the
// rail; with the shell present, the sidebar wrapper becomes their containing block.
NodeAssert.match(
page,
/lazurio-rail:defined ~ #root \[data-slot="sidebar-wrapper"\] \{\s*contain: layout paint;\s*\}/,
);
NodeAssert.match(sidebarUi, /data-slot="sidebar-wrapper"/);
NodeAssert.match(page, /<body>\s*<lazurio-rail><\/lazurio-rail>\s*<div id="root">/);
NodeAssert.match(
page,
/<\/div>\s*<lazurio-buddy><\/lazurio-buddy>\s*<script type="module" src="\/src\/bootstrap\.ts"><\/script>\s*<\/body>/,
);
// The column head is the sidebar's first child, above the upstream top row, for the thread,
// legacy and settings sidebars alike; the toggle moves down with that row.
NodeAssert.match(
layout,
/\n\s*>\n(?:\s*\{\/\*[^\n]*\*\/\}\n)?\s*\{createElement\("lazurio-column-head", \{\s*active: "chat",\s*ref: observeLazurioColumnHead,?\s*\}\)\}\n\s*\{isOnSettings \? \(/,
);
NodeAssert.equal(layout.match(/lazurio-column-head/g)?.length, 1);
NodeAssert.match(
layout,
/<SidebarControl lazurioColumnHeadHeight=\{lazurioColumnHeadHeight\} \/>/,
);
Comment thread
agentrozjedemeai marked this conversation as resolved.
NodeAssert.match(
layout,
/new ResizeObserver\(\(\) => setLazurioColumnHeadHeight\(element\.offsetHeight\)\)/,
);
NodeAssert.match(
layout,
/isSidebarVisible && !isMobile && lazurioColumnHeadHeight > 0\s*\?\s*\{ top: `calc\(var\(--workspace-controls-top\) \+ \$\{lazurioColumnHeadHeight\}px\)` \}/,
);
});
Loading