Map questions can now be on the world's countries besides Japan's
prefectures: a new question chooses its map above it.
The world is Natural Earth's 1:50m countries (public domain), as they are
on the ground, one region per ISO 3166-1 code (239; Gibraltar and the US
Outlying Islands from the 1:10m layer, which has them), named in every
language a studyset can be in from the Unicode CLDR (Unicode License v3,
its notice in the licence list). tool/maps/world_countries.py builds it;
the outline code it shares with the Japan script moved to outlines.py,
which still writes Japan's file byte for byte. The 1.8 MB file is parsed
off the screen's isolate.
Picking goes in layers on both maps: on the whole map one taps a group
(a continent, or Kantō, Kansai …; chips name them too) and the map flies
there, then one taps the region; Back returns. A group of one (Hokkaidō,
Okinawa) and what is in none (Antarctica) are picked at once. A country
in two groups (Russia, Türkiye, Cyprus) opens the one nearer the tap.
Oceania's frame runs past 180° so the Pacific stays in one piece.
What is still too small to hit gets a ring, 48 dp of reach as Material's
touch target: on the ring it is picked; near it, where the tap could mean
it or its neighbours, the map zooms in rather than guess. Pinching and a
mouse's wheel zoom as before.
Names show in the app's language, beside them the map's own (Japanese for
Japan) or the studyset's (キプロス beside Cyprus in a Japanese studyset).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MMm7usAo6q9ZZRoPkWjoxi
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
The library's top bar had a button each for Import and Refresh. Pulling
the list down refreshes too, and is the quicker way on a phone, so both
now sit in a menu ("Library options"), leaving the bar to Search.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MMm7usAo6q9ZZRoPkWjoxi
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
The menu came in the editor's own white or slate. It now sits on the
surface Material gives menus, as the app's other menus do, with the item
the arrow keys reach tinted as a focused menu item is.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MMm7usAo6q9ZZRoPkWjoxi
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
A studyset opened on its flashcards and slid over to its paths once it
had worked out that one was unfinished, every time it was opened. It now
opens on the tab it was left on, on this device: kept in the person's
folder (studyset_tabs.json), read as their data opens, so at once. The
first time a studyset opens here it still chooses by what it holds
(paths in progress, then flashcards, notes, files), but shows that it is
loading until it knows, rather than one tab and then another; paths that
can't be read count as none.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MMm7usAo6q9ZZRoPkWjoxi
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
Text gets a link from the format bar's new link button or with Ctrl+K
(⌘K): the address for the selection, an empty one taking it away, or at
a caret the text as well, written there linked. An address without its
scheme leads to the web. This replaces the editor's own link menu, which
had no button and spoke English only.
Copying and pasting between the app's editors keeps every format: bold,
links, furigana, formulas, callouts, spoilers. The system's clipboard
holds plain text only (the editor's own copy drops the HTML it makes),
so the blocks copied last stay in memory beside it, and a paste takes
them while the clipboard still holds their text; anything else pastes as
plain text, as before, and other apps get plain text. On phones, which
had no way to copy at all, a bar over selected text copies, cuts, pastes
and selects all (the editor's MobileFloatingToolbar), never over a block
taken whole, whose own buttons a tap is for.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MMm7usAo6q9ZZRoPkWjoxi
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
Finishing a path bursts confetti over the view saying so: pieces in the
palette's brightest colours fall once from the top, turning, and fade
out in under two seconds, so the screen settles. Drawn by hand rather
than with a package; nothing falls with animations switched off on the
device.
Importing paths into a studyset shows its Paths tab at once, and above
the paths, as they come in, how far the import has come: which path of
how many, by its title, and a bar filling with every path and step
stored. Before, the paths appeared one by one in whatever tab showed,
with nothing saying how many were still to come.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MMm7usAo6q9ZZRoPkWjoxi
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
A table keeps each row's height on its cells and corrects it after
layout through a transaction, which a read-only editor never applies:
in a path step, a note or a card, a cell of several lines (る, う, つ in
Tae Kim's conjugation tables) ran over its row's borders. Our fork of
the editor now sizes the rows of a read-only table in place, as it
already did the table's height (81c3006c on halkyon/win32-v6; upstream's
main has the same code).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MMm7usAo6q9ZZRoPkWjoxi
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
The import format gains the blocks lessons need: tables (rows of cells),
callouts (a kind, a title, blocks inside, but no table or callout),
spoilers, quotes and dividers; and runs of text gain a reading shown as
furigana, a colour, underline and strikethrough. A line break is a "\n"
in the text. Still simple blocks turned into rich text here, so a change
of the editor's own format never breaks a file written before. A table's
columns are as wide as their longest line, narrowed alike to fit a
phone, where the cells wrap.
A studyset's Import › Learning paths now takes several files at once,
one path each, stored in the order of their names, so a chapter's
lessons come in with one pick. Every file is read before anything is
stored: one that can't be read is named, and nothing comes in.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MMm7usAo6q9ZZRoPkWjoxi
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
The editor casts a table's sizes (its column width, row height, height,
and each cell's width and height) to decimals, and finds a row's default
height with a null check. JSON written elsewhere may hold whole numbers,
40 where the editor writes 40.0, or no sizes at all: such a table showed
Flutter's error box instead. The Writing System paths in an archive
brought over from elsewhere hold whole numbers. Opening content now gives
its tables their sizes as the editor reads them.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MMm7usAo6q9ZZRoPkWjoxi
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
A study button names the studyset it studies by its id. Copied from a
release or an archive, the studysets get new ids, and a button in the
content kept naming the author's (or the archive's), which the person
doesn't have: it studied nothing. The copier now writes the content after
the studysets, each study button naming the studyset made of the one it
named; only a study button's studyset, as a short id could also be a word
of the content.
A later release taken into a copy still brings the author's ids into its
buttons: the copy updater doesn't rename them yet.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MMm7usAo6q9ZZRoPkWjoxi
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
Your call (2026-10-11): an archive, imported from the Library's Import, to
bring a library from elsewhere as one's own, and later to restore backups
and move studysets between instances before federation.
An archive is a .zip: archive.json says its format; the studyset is laid
out as a release (ADR 0005, rule 3), so the release copier writes it, now
also unlinked from any publication (ReleaseCopier.write), its cards marked
"archive" as imported ones are, handing back each item's new id. A card may
name a dictionary entry instead of its payload: the import makes it from
the instance's dictionary as adding from it does, with today's entry,
examples and tags, and leaves out (counting) one the instance lacks; for
that, DictionaryEndpoint.entries gives entries by their ids, at most 1,000
at once. reviews.json lists reviews of the archive's cards; they go into
the person's review log once the cards are in, so every card's schedule
goes on from them.
The sheet says what the archive holds ("Japanese" holds: 12 studysets,
7,745 flashcards, 3 paths, 20,317 reviews), imports once confirmed, busy
meanwhile, and opens the studyset made. Not an archive, a newer format or
an unreadable one says so and imports nothing. No export into the format
yet.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MMm7usAo6q9ZZRoPkWjoxi
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
On a phone, a plain band ran behind the floating navigation bar, the
screen's content cut off at its top (your screenshot from the Pixel 9a,
Home's activity card). The bar's whole slot was kept from the screens,
painted in their background: the content stopped above it, as said when
the bar came, until the screens were made ready.
The shell now lets the tab screens run on beneath the bar (extendBody), as
iOS 26's tab bar floats over content. Flutter gives each screen what the
bar covers as the body's bottom padding, and every list of the four tabs
(Home, the library, Explore, Profile) ends with that much room, so its
last item scrolls out from under the bar. Home leaves the Ask field's slot
out while asking is off, as an empty slot would take that room from its
body; with the field, its sections scroll beneath both. A floating button
keeps clear of the system's bar by itself, not of this one: the library's
lifts by what the bar covers (FloatingNavigationBar.coverOf). Inside a
studyset, where the bar steps aside, nothing changes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MMm7usAo6q9ZZRoPkWjoxi
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
The deploy reloaded Caddy when deploy/Caddyfile changed, but a reload
reads the file the container sees, and that is the old one: Docker
mounts a single file by its inode, and git writes a changed file anew.
The first deploy of the assistant connector showed it (the container
still had the Caddyfile without the connector's routes). A changed
Caddyfile now recreates the Caddy container, which mounts the new file;
its certificates are kept in their volume, and the instance is away for
a few seconds.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MMm7usAo6q9ZZRoPkWjoxi
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
While the app opens (the instance, the session, the person's data), it
showed a spinner, in three ways. It now shows the kingfisher, and beneath
it "Halkyon" over "Learn", cobalt and copper as the logo has them, in the
app's own Noto Sans (your call, with JetBrains Mono to compare: the
logo's ITC Avant Garde isn't free to ship), and a quiet line of progress.
AGENTS.md now has the rule on text fields' limits, busy sheets and the
launch screen.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MMm7usAo6q9ZZRoPkWjoxi
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
From medium windows on, the kingfisher atop the navigation rail is a
button to Home, as a website's logo leads to its start, with the tick the
rail's own choices make.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MMm7usAo6q9ZZRoPkWjoxi
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
Your calls (2026-10-11): a switch per limit, and steps of 5 with any
number typed.
- A daily limit (new cards, reviews; in a studyset's study options and the
study settings) is a switch, "Limit new cards per day", and its number
while on. Counting down to 0 for no limit had to be guessed, and a goal
has nothing like it. Switched off and on again, the limit comes back as
it was; the first time, at the defaults' 20 new cards, or 200 reviews as
Anki starts them. Stored as before: 0 still stands for none.
- The study goal counts in steps of 5 as the limits do (reviews: 25).
Plus and minus go to the next multiple, so 12 steps to 15 or 10, never
below the first step.
- Tapping the number lets any other be typed, from 1 (CountStepper.onTyped).
Pomodoro minutes and cards per round keep their own steps.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MMm7usAo6q9ZZRoPkWjoxi
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
A sheet can be busy (SheetScaffold.busy): its close button is off and
greyed, and back does nothing until it ends. The Anki import's sheet is
busy while it imports; before, its close button looked as always and
did nothing.
A studyset's screen, while it imports a PDF or a path, greys its back
button and the path's steps and ignores back: leaving would close the
lists' blocs and cut the import off. Importing a path now says so in its
state, as a PDF's did.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MMm7usAo6q9ZZRoPkWjoxi
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
A Pomodoro profile's name could be as long as one liked, and pushed the
Home card's header beyond the window. Of 56 fields, only release notes and
a licence's year had a limit.
Limits by what a field holds (core/widget/text_limits.dart, decided
2026-10-11): a name shown in a menu or on a chip 40 (Pomodoro profiles,
tags, a study button's label); a secondary title or a reading above
characters 60; a title 120 (studysets, notes, paths, steps, PDFs,
callouts, a video's caption, a licence's holder); an answer or item 200
(meanings, readings, glosses, options, categories, gaps, typed answers);
a search 200; a part of a citation 500; a message 2000 (reports, replies,
questions to Claude); a web address 2000; a passage with gaps or a
formula 5000. The public profile and a published studyset's name take the
instance's own limits (display name 30, handle 30, bio 500, links 2000,
name 50), which the app didn't say before. Pasted flashcards to import and
numbers have none. Fields count only from 80 % of their limit.
Text can still be longer where it came from elsewhere (an older app, an
import, a release), so the places showing it cut it short: the Pomodoro
card's header and its profiles. A test shows names beyond every limit on
Home, the library, a studyset's screen with its tabs and topics, the card
browser with its tags, and a session, on a phone and a desktop: none
overflows (it fails on the Pomodoro card as it was).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MMm7usAo6q9ZZRoPkWjoxi
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
From the expanded window on, what can be done to the chosen cards (Move,
Add tags, Delete, …, Done) stood short of the row's end: Select all and
Deselect all were flexible beside a spacer, so Flutter shared the room
between the three and the spacer got a third. Choosing now takes the room
the actions leave, in a row of its own.
On a phone the detail view (a card above a strip) had no way to choose a
card: a tap opened the composer even while choosing. Choosing cards now
shows the grid, where each card shows whether it is chosen, whichever view
was picked, and Done returns to it; the switch between them is off
meanwhile.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MMm7usAo6q9ZZRoPkWjoxi
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
Deeper studysets, on iOS and Android alike (decided 2026-10-11, after
Material 3's and Apple's guidelines, Android's principles of navigation and
Nielsen Norman Group's on breadcrumbs):
- The screens behind a studyset are its path: below "Japanese › Grammar ›
Tae Kim's Grammar Guide" lie Grammar's screen, then Japanese's, then the
library. A topic at any depth opens with the levels between pushed
beneath it; the path's steps go up by popping to that level's page (they
replaced the location before, so back could skip levels); a studyset
opened from elsewhere gets its levels beneath it. Back, Android's
predictive back, iOS's swipe from the edge and the path all lead one
level up.
- The path's steps are targets 48 dp high; a long press on back lists the
levels above, the nearest first and the library last, as iOS's back
buttons do.
- Below expanded windows the topics open in a sheet from "3 topics ▾" at
the end of the path's line, where Vaia puts "All topics"; no drawer,
whose swipe from the edge is iOS's way back and which Material 3
Expressive deprecates. Topics with topics of their own unfold them, at
any depth, in the sheet and the side column alike.
- The navigation bar steps aside inside a studyset (its screen, notes,
paths and dictionary): Material 3 puts it on the top-level screens and
toolbars below, never both. Apple's guidelines keep the tab bar, lest
people forget where they are; the path, starting at Library, says where.
A strip as high as the system's bar keeps those screens clear of it.
- The bar at the bottom keeps Add material and Study.
A trial for review, on its own branch, on top of the floating navigation
bar.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MMm7usAo6q9ZZRoPkWjoxi
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
Below medium windows, the navigation bar (Material 3 Expressive's shorter
one) is detached from the window's bottom edge, as iOS 26's tab bar
floats: a rounded bar 16 dp in from the sides and 8 dp above the system's
own bar, at the navigation bar's elevation. Material 3 has no floating
navigation bar for phones, only in its guidelines for headsets.
Around it shows the background of the screen in view: every screen with a
TopGradient claims its background along with its colour (ScreenTint), and
the shell's scaffold takes it, changing as the frame's colour does. The
screens' content stops above the bar; letting it scroll beneath, as iOS
does, would mean padding every screen of the tabs.
A trial for review, on its own branch, on top of the shorter bar.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MMm7usAo6q9ZZRoPkWjoxi
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
The navigation bar below medium windows becomes Material 3 Expressive's
flexible one, which Flutter's Material library doesn't have yet: 64 dp
high instead of 80, the label of the tab in view in the secondary colour
(the tokens of Compose's ShortNavigationBar). Its pill stays Flutter's,
64 dp wide rather than 56, as Flutter fixes it.
A trial for review, on its own branch, beside the floating one.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MMm7usAo6q9ZZRoPkWjoxi
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
The chips above the assistant's field cut their text at 24 characters
and scrolled sideways out of view, so they helped little. They now flow
onto as many lines as they need (about three before they scroll), each
showing as much of its text as its line holds, the whole of it on
hovering or a long press.
In a study session the button and its field now belong to the card: the
field unfolds across the card's width, inside its edges, where it was
anchored to the window's right edge and overhung the card on wide
windows. The send button keeps clear of the field's ring, more so in the
corner the chips' rounded box leaves.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MMm7usAo6q9ZZRoPkWjoxi
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
The assistant's field now has a switch at its start: a question goes to
Claude or into a web search, and the field remembers which. A web search
opens the chosen engine's results: on Android the device's own web
search by default (ACTION_WEB_SEARCH, answered by its default browser or
search app; Brave Search where nothing answers), elsewhere Brave Search
by default; Settings › Assistant › Search engine offers Ecosia,
DuckDuckGo, Startpage, Qwant, Google, Bing, or one's own address with
%s where the question goes.
Above the field, chips offer what the question is about: a card's
front, back, each hint and its explanation, the studyset's name and
description. None is chosen, so only what the person picks goes along,
each part cut at 600 characters; Claude gets the question and a compact
line per chosen part, a web search the question and the chosen text,
never the studyset's name. A chosen chip alone is enough to send. The
old sentences around every question (where it was asked, the connector)
are gone: they cost tokens for nothing unless the studyset is chosen.
On a studyset's screen the button now stands above the bottom bar, as in
the Library: unfolded inside the bar, its chips lay outside the bar's
bounds, where taps don't land.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MMm7usAo6q9ZZRoPkWjoxi
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
Every screen of a studyset now washes its colour (its own or the nearest
above it), as Vaia does: studying it, its cards, writing a card, its
notes, its paths and steps, its dictionary. The router hands the colour
down from where it resolves the studyset (ScreenColor), so a screen's
TopGradient without a colour of its own takes it.
The dark tone is now a deep shade of the colour, 42 % of it over black
(cerise a maroon like Vaia's #4e0b23, cobalt a navy), instead of a mix
into the background; light mode keeps its tint. The wash reaches about
as far as the header (160 dp; a studyset 180, Home 360). From medium
windows on, the frame fades into the background 60 % of the way down,
and the wash inside its panel is half as strong, as the frame carries
the colour; screens over the tabs (studying, the card browser and
composer, Settings and Connect Claude) sit in the same frame and panel,
without the rail (TintFrame, FramedScreen). The study session's header
lies on the colour with an edge below it, its status bar styled to suit.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MMm7usAo6q9ZZRoPkWjoxi
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
Library, Explore, Profile, Settings and Connect Claude now wash cobalt
down from their top as Home does, as far as their header (200 dp);
Home's wash reaches 360 dp, a studyset's 280.
From medium windows on, the window takes the colour of the screen in
view, as Vaia lays it out: the rail sits on it, transparent and without
a divider, and the screen is a rounded panel inset beside it, with its
own wash at the top. The colour is claimed by the screen's TopGradient
while its tab shows and its route is on top (ScreenTint), so the rail
turns maroon beside a cerise studyset and back to cobalt on leaving it,
in a quarter of a second. The frame is as strong as the rail's labels
allow (WCAG AA on onSurfaceVariant, every palette colour, light and
dark), weaker towards the foot.
Two things the panel's 24 dp less room brought out: a long description
was laid out whole for its first frame, before it was measured, and
overflowed a shorter column; it is now laid out whole but shown no
taller than its preview from the start. And between 840 and about 960 dp
of screen, a studyset's app bar (Study, Add material, Share, options)
didn't fit; Add material is an icon there, its label in the tooltip.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MMm7usAo6q9ZZRoPkWjoxi
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
A connect code, an authorization code and a refresh token are each
consumed by deleting their row and going on only if that deleted it, so
of two requests racing with the same one only one succeeds (as OAuth
requires); tests race them. Registration, open to anyone, takes at most
200 clients an hour (429 after), so a flood can't fill the table before
pruning. The deploy reloads Caddy in place when deploy/Caddyfile changed,
as `docker compose up` doesn't restart it for a file it mounts: without
it the connector's paths wouldn't reach the web server.
Also formats the app's fake assistant gateway, new and so unchecked by
the last commit's check (tool/check.sh formats tracked files only).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MMm7usAo6q9ZZRoPkWjoxi
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
A Claude plan can't drive a chat inside another app, but Claude's apps
(every plan, the phone apps too) can add a remote MCP server as a custom
connector. The instance now is one (ADR 0006): Serverpod's web server
serves the MCP endpoint (/mcp, Streamable HTTP, JSON answers, no
sessions) with five read-only tools over the person's own synced rows
(their studysets, a studyset's cards and notes a page at a time, a
search across cards, a note, their progress), and the OAuth the
specification asks for: protected resource and authorization server
metadata, dynamic client registration, authorization code with PKCE
(S256 only), tokens bound to the resource, iss in the response, refresh
tokens that rotate. Caddy forwards these paths alone to the web server,
which production now runs on 8082.
People connect with a code from the app (Settings › Assistant › Connect
Claude): eight Crockford characters, valid for ten minutes and once,
typed on the authorization page, which names where it returns to, loads
nothing from elsewhere and may not be framed. Anonymous people can
connect too. Codes and tokens are kept only as SHA-256 hashes; an access
token lives an hour, a refresh token 30 days. The app lists what is
connected and disconnects it, which ends its tokens at once; deleting
the account deletes it; a daily job forgets what expired and clients
nobody connected within a week. InstanceInfo says where the connector is
(additive); the migration only adds server-only tables, so devices and
older apps are untouched.
The handlers take plain requests, so the integration tests drive the
whole flow (registration, the code, tokens, refresh, the tools, another
person's content, disconnecting, expiry, pruning, account deletion)
without a web server.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MMm7usAo6q9ZZRoPkWjoxi
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
A device setting, off unless the person turns it on (Settings ›
Assistant), shows a field at the foot of Home and a button that unfolds
into one: above Create studyset in the Library (smaller), in the free lane
on the right of a studyset's bottom bar (floating alone from expanded
windows on), and in the card's lower right corner while studying.
A question is handed to Claude, not answered here: the app opens
claude.ai/new with the question written in, followed by what it is about
(the person's learning on the instance, a studyset by name, or the studied
card's front and back as text), cut at 12,000 characters, for the person
to read and send in Claude. A Claude plan can't drive a chat inside
another app, as Anthropic allows no third party to offer sign-in with it;
Claude is to read the studysets through a connector on the instance
instead (next). Nothing leaves the app until the person sends it there.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MMm7usAo6q9ZZRoPkWjoxi
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
Home starts in cobalt and a studyset's screen in its colour, its own or the
nearest above it (cobalt where none has one), fading into the background
below the app bar, which stays clear above it. The top tone is 60 % of
the colour in dark mode (cobalt a navy, cerise a maroon) and 35 % in light
mode (a pastel tint), less where the theme's text would fall below WCAG AA
on it; a test holds every palette colour to that in both modes. The
status bar's icons follow the tone. Content stays below the app bar, so
nothing scrolls under its title.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MMm7usAo6q9ZZRoPkWjoxi
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
Choosing a destination in the navigation bar or rail plays the platform's
lightest haptic, the selection click (a clock tick on Android), as a
short, weak confirmation. It follows the system's touch feedback setting
and does nothing where the device has no motor.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MMm7usAo6q9ZZRoPkWjoxi
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
A new install asks first which instance to keep the person's studysets
on, as Mastodon's apps do: an address, checked to answer as an instance
of Halkyon Learn, or the build's own (learn.halkyon.eu) in one tap.
Nothing is made anywhere before that, not even an anonymous account. An
install from before goes on with the build's instance without asking.
As Moshidon does, the device keeps an account on each instance it was
used with, each signed in with its own folder and session, one in use at
a time. Settings › Switch account lists them: another one switches at
once, anonymous accounts included; Add account chooses another instance;
removing one not in use deletes what the device holds of it. Signing out
ends only the account in use.
The build's own instance keeps its data where installs kept it before
(`people/`, Serverpod's own session key), so they keep everything; other
instances get folders and keys of their own. What the device shares
across instances (its settings, sounds, the Pomodoro timer's place in the
system) is made once and outlives every switch.
The F-Droid listing drops TetheredNet: the app is no longer tied to the
project's instance.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MMm7usAo6q9ZZRoPkWjoxi
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
Halkyon's website is one site for all its apps, as Proton's is, so
Halkyon Learn's page is halkyon.app/learn rather than a site of its own
at learn.halkyon.app, which redirects there. The snap's and the F-Droid
repository's listings point to it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MMm7usAo6q9ZZRoPkWjoxi
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
Serverpod's rate limit for new anonymous accounts (30 per IP address and
hour) notes every attempt with the address it came from, and deletes
those notes only for email sign-in, which this instance doesn't offer. So
the IP address of everyone who ever started the app stayed in the
database, and in every backup, for good.
The server now deletes them hourly once the limit's hour is past, so an
address is kept for at most two hours. The rows already there go at the
first run after the deploy; backups keep them until they expire (about
three months).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MMm7usAo6q9ZZRoPkWjoxi
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
The Sync card in Settings says how many changes made on this device
haven't reached the server yet: "3 changes wait to sync", in place of
"Everything synced" until a sync takes them, and beside a failure.
The sync engine can't tell, since the server only says what it has at
the start of each sync, so the app counts: every save asks for a sync,
and a sync that goes through takes the saves made before it started.
The count is kept in the person's folder, so it outlives the app.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MMm7usAo6q9ZZRoPkWjoxi
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
Each card's foot now says who made the studyset: "By you", or for a copy
of a published studyset its author's handle. Beside the star, a lock or a
globe says whether it is out on Explore, and opens the Publish sheet.
The instance says which of the person's studysets are published and not
withdrawn (`PublishEndpoint.published`, additive: older apps don't call
it), and the app remembers that in the person's folder, so the library
shows it offline too; publishing, withdrawing and looking one up keep it
current.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MMm7usAo6q9ZZRoPkWjoxi
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
Beside Words and Kanji & kana, a Radicals tab lists the 214 Kangxi
radicals by their strokes, with the forms Japanese writes them as inside
kanji (亻, 氵, 艹, …). One chosen lists the kanji KANJIDIC2 files under
it, in the dictionary's order, to add or to see how one is written; the
tab shares the characters' bloc, so a kanji added there shows as held in
a search too.
The curator labels each kanji with its classical radical (`radical:N`),
so the server's existing label queries serve the tab unchanged. Its
files now say which conversion made them (`.conversion`): an instance
running a curator that converts differently converts again at once,
rather than at its weekly update, so the radicals appear right after a
deploy.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MMm7usAo6q9ZZRoPkWjoxi
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
A studyset with many PDFs took a while to pack, and the window froze
meanwhile. The zip is now made on another isolate (ExportArchiver, an
isolate in the app and in place in widget tests, whose fake clock can't
wait for one), as Anki packages are read.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MMm7usAo6q9ZZRoPkWjoxi
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
A character card's answer lists up to five vocabulary cards of the
studysets the session studies whose front holds the character, with
their reading and first meanings; a tap opens the word's card. The words
come from every card the session reads, whatever tags narrow it, so a
session of kanji alone still shows them, and nothing is fetched: it works
offline.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MMm7usAo6q9ZZRoPkWjoxi
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
The library's and the topics' counts read every card on its front-first
schedule, while a session with Write by hand or Back first on studies
characters and words on schedules of their own. A card written by hand
showed as due when the session found nothing to study, or the other way
round.
The rule for how a card is studied moves out of the session bloc into
card_study_mode.dart (writtenByHand, asksBack, scheduleModeOf), and
ReviewRepository.schedulesAsStudied gives each card the schedule a
session would study it on; the session and the studyset list both use
it. Home's due forecast still counts front to back, as it reads cards
without their content.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MMm7usAo6q9ZZRoPkWjoxi
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
The app's side of stage 4 (ADR 0005, rule 11). The card's Report sheet
offers "Send to the author too" on a card from a release; a copy's menu
reports the studyset as a whole and lists what is reported and
suggested on it, as Explore's summary does; the editors of cards, notes
and steps suggest them to the author as the learner has them, never
under "no derivatives". The Profile tab holds the inbox: the threads on
the person's published studysets and those they wrote, the newest
activity first, with a count on the tab. A thread shows a suggestion
beside the author's own item and its replies; its writer and the author
reply, and the author accepts (the content becomes their own edit, its
files fetched from the writer's), declines, closes, hides or blocks.
Writing needs an identity; reading what is public doesn't.
ADR 0005 gets an addendum for stage 4.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MMm7usAo6q9ZZRoPkWjoxi
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
Stage 4 of the public tier (ADR 0005, rule 6), its server and the app's
data. A learner writes a report or a suggestion on a published studyset,
a card, note or step of it, in their own synced space, so it works
offline; replies and the author's decisions (closed, hidden, accepted,
declined) likewise, in the writer's or the author's space. The instance
gathers them: public to everyone signed in, but for what the author
hides; for the author's inbox and the writer's. What counts is decided
as rows are read, since a modified app could write anything: threads
from people with an identity and a public profile the author hasn't
blocked, replies from the writer and the author alone, decisions from
the author alone, no suggestion under "no derivatives". The author gets
the files a suggestion names from its writer's, and the next release
credits the writers of the suggestions accepted since the last
(Co-authored-by). Deleting a person deletes their blocks and what they
read; what they wrote goes with their space.
In the app, the repository writes reports and suggestions (a card, note
or step as the learner has it, without what is theirs alone, with the
files it names) and takes an accepted suggestion into the author's own
card, note or step.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MMm7usAo6q9ZZRoPkWjoxi
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>