Skip to content

Release 0.230

0.230.67: Tasks grows up: a board, a lifecycle, and somewhere to put finished work (branch claude/handover-tasks-kanban-04d026)

/tasks was a checklist. It is now a project surface: a Kanban board, four states instead of two, a checklist inside each task, and comments from logins, team members and agents.

The board shows three columns, not four

Blocked is a flag on work already under way, not a further stage, and a fourth column cost more width than it earned. Blocked tasks render under In progress with a badge, and you set the flag from the task form.

Reordering a blocked card inside that column no longer clears the flag. The column a card lands in and the status written are two different things now, which they were not before — tidying a column used to unblock tasks as a side effect.

Archive: where a thousand finished tasks go

A Done column grows forever. Archiving files a task away without deleting it: data.archived_at on the node, excluded from every list, count, board and tool unless asked for (?archived=only|all, task_list’s archived). The exclusion lives in taskConds, the one place every query already goes through, so no caller can forget it.

Archive is orthogonal to status: an archived task keeps the status it had. And archiving is metadata — deliberately absent from updateTask’s contentChanged check, so filing a thousand tasks away costs zero embeddings and zero LLM calls.

Smaller things you will notice

  • The checkbox no longer flattens four states into two. Ticking a Blocked task and unticking it restores Blocked, not To do.
  • The comment composer sits above the thread, newest comment first.
  • The task form, the detail view, the task list, the nav rail and the activity column are all resizable, and each remembers its width.

A stored link is permanent, so it has to be right on the day it is written. nodeUrl() mints ${MANTLE_PUBLIC_URL}/n/<id> and hands it to the assistant on every tool result; the assistant writes those links into chat replies, pages, forum answers and outbound email, and nothing ever re-resolves them. But MANTLE_PUBLIC_URL has to be the server origin — /s/<token> share links and the Microsoft OAuth callback are served there — while /n/[id] itself moved to client/web in the v0.200.0 split. On a deployment that gives the owner app its own vhost, every one of those links was a 404, and each one was written into the brain to stay.

/n/* now forwards to MANTLE_CLIENT_ORIGIN, joining the /login, /hub and /team stubs. Keeping the minted link canonical and redirecting at the edge is what makes one stored URL correct under either topology; rewriting the minter to point at the client origin would have broken it the other way. With no client origin configured it explains itself instead of looping, same as its siblings.

Single-host installs — where one hostname fronts both stacks — never saw this, which is exactly why it stayed hidden.

Also: GET /api/assistant/thread takes ?withMessages=0, returning the agent picker list and the resolved active agent without the 100-message thread. The mobile companion needs both at launch — it holds no agent cookie, so the server’s resolution is its default, and that resolution is what now respects agents.assigned_user_id — but it pages its own history from the local cache, so the thread was fetched and dropped on every cold start. Opt-out, so every existing caller is untouched.

0.230.14: Four fonts, one library, every face variable (branch claude/variable-font-refactor)

A typeface library is not a list of decorations. The old one had grown into two registries with different rules: twenty-two display faces for the wordmark, twelve for the interface, most of them chosen to be striking for two words of header. Anything you would actually set a document in was accidental.

There is now one library of sixteen families, and every one is a variable font with at least two axes. One file carries every weight, and where a family has a slant axis, its italic too.

Four things you can set

Settings and Appearance now offers a face and a size for each of the interface, the wordmark, the peer name, and a new one: Pages and Notes. That last is the only typography choice in the product that leaves the browser, because it typesets the PDF export as well as the editor and the share page.

Each row opens the same chooser rather than spilling the whole library down the page: filtered by kind, previewing every face in your own text. The peer name and Pages/Notes default to “same as interface”, so a brain that picks one font still looks deliberate. Sizes gained an Extra small, and the three new ones scale only what they name; Interface size still scales the whole shell.

The ranges are read out of the files, not typed

scripts/fonts-import.mjs parses each font’s own fvar table for its real axis ranges, converts to woff2, installs into both apps with the licence, and prints the registry row. It refuses a face with fewer than two axes.

This is not tidiness. A variable font declared without its weight range makes the browser treat the file as a single regular and fake the bold, which shows up as smeared headings across every screen. Hand-typing sixteen sets of ranges is sixteen chances to introduce that quietly.

Faces stay lazily fetched: a file downloads only when something actually paints in it, so the library costs nothing until you choose from it.

Two things that were already broken

The interface font never reached share links or /print at all. Only the two header faces were stamped into those documents, so a share always rendered in Inter no matter what the brain had chosen. Both surfaces now carry every font the app does, which is what makes the Pages/Notes choice reach a PDF.

Separately, the Appearance screen showed Inter as the selected interface font however you had it set. The face was applied correctly; the attribute the picker reads its state back from was never rendered.

What went away

Bukhari Script and the twenty-two decorative faces are gone. An existing brain that had chosen one falls back to the new default rather than stranding, which is what the registry contract has always promised. The default wordmark is now Bricolage Grotesque, and Mantle’s own mark in the footer follows it.

Only one monospace family survives the two-axis floor (Inconsolata). More can be added at any time: that is now one command and one pasted row.