Skip to content

Release/v0.2.0 - #27

Merged
DvGils merged 13 commits into
mainfrom
release/v0.2.0
Nov 6, 2026
Merged

DvGils merged 13 commits into
mainfrom
release/v0.2.0

Conversation

@DvGils

@DvGils DvGils commented Nov 6, 2026

Copy link
Copy Markdown
Owner

No description provided.

DvGils and others added 13 commits November 6, 2026 10:17
…nks read as links

Auth(login=) takes components, with a ui.SignIn(provider id) where each
way in goes, so a heading, a note or a line sits wherever it is wanted.
The page is built before any session exists, so a component with a
handler, or one that only talks to Python, is refused when the app
starts. Left alone, the page draws a line between the provider buttons
and the password form. ui.SignIn works on a public page too.

The Microsoft and Google buttons carry their brand's mark, and
OAuth(icon=) takes a brand, an image under public/ or an icon name.

A link is underlined, faintly, since under the default theme its color is
the color of the text. Links in a sidebar and the header stay plain.

A card's content takes the height a taller neighbor gives the card, so
the footer sits at the bottom of every card in a row.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
A sign-in's lifetime was checked only when a page loaded or a socket
connected, so a tab left open outlived it. A session now carries the id
and expiry of the sign-in it runs on, and when that runs out every session
on it sends its tab away and ends. A tab that comes back on a newer
sign-in follows the newer one.

Auth(on_logout=) gets the User and returns a path, a URL or None for the
login page. It is asked on sign-out and once per expired sign-in. A hook
that fails, is cancelled, takes over ten seconds or answers with
something else is logged and counts as None, so it cannot keep a person
signed in.

The sign-out route now refuses a request from another origin, since it
runs the app's hook and ends its sessions.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…s more

Printing gave one sheet, since the shell is a viewport-high column whose
body scrolls. In print the column and its scrolling boxes run as long as
their content, the sidebar and header are left out, and the dark scheme
steps aside.

A theme's stylesheet is a CSS file under public/ linked after the theme,
watched in debug mode like the theme file.

A dropdown menu is at least as wide as its trigger and grows to its
longest item. An Alert draws its children. The confirm dialog's buttons
and the bundle's labels for screen readers come from the theme's strings.

In the test harness, find and find_all look inside one node with
within=, a type finds its subclasses so ui.Custom finds custom
components, and choose picks an item of a dropdown menu.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ct paths win

Text the framework writes itself now comes from the theme's strings: the
table's row count, the upload box, the chat placeholder, what is wrong
with a form field, and why an upload was refused. A label left at the
framework's default (a form's Save, a Download, a pie chart's Other)
follows the theme, while a word the app wrote stays the app's. Only a
message pydantic wrote itself is replaced, so a validator's own words
are shown as raised.

ui.Button(href=) is a link drawn as a button. A ui.Link in a ui.Sidebar
is drawn as a row to press, so a list of pages needs no classes.

s.print() opens the print dialog after the page was drawn again, and in
print a heading stays with what follows it.

Where a path fits several pages, the one that is exact soonest wins: a
literal segment, then a parameter with a converter, then a bare one,
then the rest of the path. Before, the page added first won.

In the test harness, find and find_all take props as keywords, and a
click on a link or a button with an href takes the session there.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…current

A page, a route or a mount takes allow=, a plain function of the signed-in
user and of the path parameters it names. One place answers it, asked on
a page load, when a tab connects or comes back, on navigation, for a
route and for s.can(path), so a page cannot be reached around its rule,
and a refused page never runs its connect hook. Only True admits.
`millwork routes` lists who may reach what.

Breaking: with auth, app.route and app.mount need a sign-in unless
public=True, and a request that changes something, or a socket, has to
come from the app's own origin. They were open to anyone before.

Auth(recheck=) returns who a signed-in person is now, or None to end the
sign-in, at most once per recheck_every for each identified user. Every
caller waits for the one answer in flight. A session belongs to the user
it started with: when that user changes it ends and the tab loads again,
and a tab that reconnects gets its session back only unchanged. A hook
that cannot answer refuses new access and ends open sessions once the
last answer is twice the interval old.

A session is over before its tab is told, in a task of the framework's
own, so a tab that does not read, a caller that is cancelled or a second
caller cannot leave it half ended. A sign-in's sessions end together.

A page is found by its path as the browser spelled it, decoded once, on
a page load and in the tab alike. Something an app mounts sees the app's
own sign-in.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
A tab that spoke while the app was still being asked where a sign-in's
tabs should go was told a generic word and never sent there. It is now
left on its socket until there is something to say, and so is a tab
whose session ended while it was still connecting. A sign-in that ran
out during a connect ends the session before anything of the page is
sent, and a straggler of a sign-in that has just ended goes where the
others went without the app being asked again.

A page's close hook runs in a task of the framework's own, so it reaches
its end whatever becomes of the handler or navigation that was leaving
the page, may end the session it is closing, and its being cancelled
from inside does not stop a shutdown. The end waits for a close that is
already under way.

Work started on a session after it ended is cancelled at once, a task
the end cancelled does not come back through on_done, navigation is one
of the session's own tasks, and a page whose connect finishes after the
end is closed straight away. Making room for a session and signing out
no longer wait on close hooks.

A task cancelled in the moment it was started never ran a line and so
never said that it ended: it stayed running and its awaiters waited for
good. It now ends as cancelled.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
It drives the app through its ASGI surface the way browsers do, page
loads and sign-ins over HTTP and each tab over a websocket that speaks
the tab's protocol, on a virtual clock so a grace period or a slow hook
costs nothing to wait for. A scenario is a seeded list of small actions:
a click, a dropped connection, a role taken away, a tab that stops
reading, time passing, the server stopping.

The app writes down what its own code did and each tab what it was sent,
and the oracle reads that record against what the manual promises:
nothing runs on a session that is over, a session keeps its user, a
guarded page is never acted on or sent past its rule, a change reaches
a session within the recheck interval, a page is closed once, handlers
take turns, no tab is left on a session it was not told has ended, and
nothing is left behind. A failing scenario shrinks to the few actions
that cause it.

The test runs 250 scenarios and guards the promises that hold. The five
that do not hold yet are listed in it, so the list can only get shorter.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The app under test now holds a table with row actions, a form, an input,
a chat that streams, an upload, a download, a custom component, a question
with a form in it, and tasks that are delayed, failing or run in a thread.
Sign-in hooks run in threads in some scenarios. A worker thread takes turns
with the virtual loop, so a scenario with threads still runs the same way
every time.

The guard test now knows each failure by what the record says about it, so
a new way of breaking a known promise fails the test.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
A request and its cancel in one read, a chat message and its stop in one
read, a handler that waits on a task when its session ends, a connect hook
that waits on a task, a page loaded again or duplicated in the middle of
things, a session that says more than it keeps for a tab that is away, and a
large download. A tab now holds what it has to say until it knows its
session, as the bundle does.

Today's code breaks a promise in 3,147 of 10,000 scenarios. The guard test
knows each of the twelve kinds by what the record says about it.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
A scope owns everything that waits on someone's behalf and gives back what
a child was handed, however the child ended. A turn is held by one handler
at a time and is never gone on without. An outbox takes what a session has
to say without waiting and keeps it for replay. A connection follows an
outbox onto one socket and carries nothing but a goodbye once closed.

Nothing in the package uses them yet. Each has tests of its own.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
A session's state changes only in steps, none of which waits, and
everything that waits for a session is a child of its scope or its own
life. The life alone opens and closes pages. The session never holds a
socket: it posts to an outbox, and a connection carries that to the tab.
A sealed outbox, a cancelled scope and a closed connection are the gates
that hold after the end.

session.py holds the steps and has no async in it. work.py holds the life,
the children and the awaited half of the API. The websocket endpoint is
the life of one connection. The app has a scope for its sessions' lives
and one for its background work, admits a session in one step, and ends a
sign-in in one step with one remembered destination. Auth has a scope for
its questions. A task is started through its session's scope and knows
whether it has started. The test harness drives a real connection.

Measured by the lifecycle fuzzer, the old code broke a promise in 3,147 of
10,000 scenarios; this breaks none in 30,000. tests/test_rules.py reads
the design's rules off the source. Two reviews of the code found eleven
holes in the core and four unintended differences, all fixed here.

What an app can notice is in the changelog: nothing after a handler's own
end runs, a cancelled connect gets no close, a close hook has thirty
seconds, a page is drawn only once its connect has returned, a request or
a chat message cancelled as it arrives is forgotten, and ending a session
from outside returns at once with session.gone to wait on.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Four jobs, each on seeds the date picks, so every night runs scenarios
nobody has run before and a failure is found once and replayed by its
seed. When one fails, the job shows the smallest scenario of each
failure.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@DvGils
DvGils merged commit 977a98c into main Nov 6, 2026
5 checks passed
@DvGils
DvGils deleted the release/v0.2.0 branch November 8, 2026 10:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant