Skip to content

kari v0.13.0

Choose a tag to compare

@github-actions github-actions released this 11 Sep 09:15
· 9 commits to main since this release
v0.13.0
9d46868

kari v0.13.0 — built from 9d46868.

This release makes the phone a client you can work from, and the board say
where every card runs.

The phone gets a card you can leave and read. The system Back button closes
the card instead of the app. The card is now a sheet with a Chat page and a
Details page. The board carries the switch for what the nodes do unasked. A
long conversation reads from its end, in pages, on every client.

The board says more about each card. Each machine holds one colour, and a tag
on every card names the machine and the account that pays for it. The quota
bars of two accounts line up in the Mac app, and every window says when it
resets.

Two changes start a card quicker. A #name word in the one-line draft picks
its project. The toast that reports a new card offers Open.

A background run now starts no MCP servers, unless a card asks for them. An
unattended run no longer raises the macOS dialog about the data of another
app. A jump into herdr also opens in the workspace of the project, and not
next to unrelated work.

TL;DR

  • Show the machine and the account on every card, in colour
  • Give the phone card a chat page, and read a long conversation from its end
  • Line up the quota bars, and let every window name its reset
  • Three fixes for the phone: the Back button, the status bar and the automation switch
  • Pick the project of a quick task with a #name word
  • Offer Open on the toast that reports a new card
  • Open a jump in the herdr workspace of the project
  • Let a run start no MCP servers, and let a card overrule the setting

What changed

Show the machine and the account on every card, in colour

The board named the machine in a grey chip, and it never named the account.
On a board with three machines that chip reads like every other chip, so the
reader must stop at each card to find where the work runs. The account lived
only in the quota strip, so a card gave no way to see whose window it spends.

Each machine now holds one colour. A node tag on every card carries that
colour, the machine name, and behind a hairline the account that pays for it.
The same colour marks the chip in the filter bar, the machine in the quota
strip, the head of a queue block and the head of a plan. A colour learned once
holds everywhere. The drawer, the phone card and the conversation window carry
the tag too. The account half appears only when the board spends more than one
account, because a name that never changes tells the reader nothing.

The colour comes from a hash of the node id over eight colours. Nothing stores
it and nobody picks it. The colour survives a restart, and two hubs that see
one node paint it the same.

The quota strip keeps the grid it has. The machine name there takes the colour
of the machine, and the name carries no box and no width of its own, so the
bars of two accounts still start at one place.

State keeps its own colours: the left edge of the card and the state chip. A
first attempt put the machine colour on the top edge of the card as well. Next
to the state edge that reads as an alarm, so the machine colour stays inside
the tag. In Settings and in the Nodes tab the question is whether a node
answers, so the status dot there stays green and grey.

Give the phone card a chat page, and read a long conversation from its end

The card sheet on the phone put the fields, the run log and the last exchange
in one scroll, and the small cross in the corner was the only way out of it. A
conversation of any length was out of reach. Every client also asked the node
for the whole transcript at once, which is thousands of markdown turns on a
session that ran for a day.

The sheet now carries a bar of its own: the way back on the left, and two
pages on the right. Chat holds the conversation. Details holds the facts, the
fields, the run log and the line to resume the session in a terminal. The
prompt box stays at the foot of both. A card that never ran opens on Details,
because a conversation it does not have yet is an empty page. A card that
holds a permission prompt opens at the top of Chat, because the Allow and Deny
buttons sit above the turns: on the newest turn they opened off the screen, on
the one card that is opened to answer something now.

The conversation reads from its end, on every client. It asks the node for the
newest 200 turns, then for 400 more each time the reader asks, and for the
whole transcript when the reader asks for that. A page of older turns lands
above the reader and moves the scroll by the height it added, so the turn on
screen stays where it was. A turn is keyed by its place in the transcript and
not by its place in the list, or a new page would build the list again and
drop the reading position with it. A view that holds the conversation and
nothing else, the phone chat page and the pop-out window, opens on the newest
turn and follows it while the reader sits at the end.

The prompt box is never dead. An offline node, and a job that holds the card,
used to disable it: there was nothing to type into and nothing to keep. The
box now takes text in every state and says what will happen to it. The reply
box on a card in the board and inbox lists opens the same way, and the Send
button is what waits: while a job holds the card and its session does not
answer yet, Send is off and a line says why, so no second run starts in the
same directory. Each card also keeps the text typed for it while the drawer is
open, so a tap on another card no longer throws an answer away.

src/back.ts is rewritten as a stack that takes its history as an argument,
so src/back.test.ts can drive it with a fake one. The behaviour is the
behaviour it had: every open layer holds one history entry, and one step back
closes the layer that pushed it. Two cases that had no test now have one. A
layer that refuses to close takes a new entry, so the next press asks again.
A layer that opens as another closes takes over the entry the closing one
owes, which is what made the sheet need two presses in the first build. The
hook also takes a flag that says whether the layer is open, so a component
that stays mounted can hold a layer. The phone tabs use that flag: back goes
to the board from any other tab, and leaves the app from the board.

The demo board now names its hub. The fixture carried no hub_id, no
hub_name and no primary flag, and the Nodes tab of the phone reads all
three, so the phone layout of bun run demo showed no Nodes tab at all. The
Settings image is retaken with the three fields, so it shows the pill that
says which device pushes the columns, as a real board does.

Line up the quota bars, and let every window name its reset

The 7-day bars of two accounts stood at different places in the line, and the
reset time of one window ran into the name of the next.

A row held its parts in flex boxes inside a grid cell, each part with a fixed
pixel width. The Mac app draws with WebKit, which gives such a cell less width
than its content asks for, so the boxes were squeezed and the text inside them
overflowed. How far each row slipped depended on the words in it: a row that
said "2h 24m" pushed the second bar 14 pixels further right than a row that
said "14m". The same page in Chromium looked correct, which is why the fault
survived the last change.

Every part of every row is now an item of one grid: the name, the machines,
and for each window the label, the bar, the percentage and the reset time. No
part carries a pixel width of its own. The widest text sets the width of its
column, every bar starts where the bar above it starts, and the rows of a line
share the grid. Measured in WebKit at four window widths, both bars of a column
now start at the same place.

The bar is the one part that gives way. A window too narrow for the row shrinks
both bars to 28 pixels before it touches a word, and both shrink by the same
amount, because one track holds them. Below that the strip scrolls, so two
words can never overlap.

A window with no reset time showed an empty box beside the bar, which reads as
a lost number. Each window now says when it resets, or why it cannot: a window
that Claude Code reports no reset time for says "not started", and a window
with no reading at all says "no data". The tooltip of a meter names the reset
in clock time, the percentage used, and the sample the row was read from.

The phone shows the same. A meter there keeps its box when the window has no
reading, so the bars of two accounts stand in one column.

Three fixes for the phone: the Back button, the status bar and the automation switch

Three faults on the phone, each small on its own, made a card hard to leave,
hard to read and impossible to stop.

The Back button closed kari instead of the card. On the phone the only way
out of a card was the small cross in its corner, and a press of the system
Back button left the app. Finding the card again started from the board. Two
things stood in the way. Tauri's activity refuses back navigation, so a press
never reached the page: scripts/android-back.sh writes the generated
MainActivity.kt again with handleBackNavigation on, and the activity then
hands the press to the web view while the web view has a step to take. The
script runs after every bun tauri android init, next to the icon script, in
both workflows, because the generated project is not tracked. The page also
had no step to take, because a card is not a page. The card and every dialog
now put one entry in the history while they are open, and take it off again
when they close another way. Back closes the innermost layer first and leaves
the app only when nothing is open. A card that holds a prompt you did not send
asks before it closes, exactly as Escape does on the desktop. The history is
matched to the open layers after the current work, not during it, because
React mounts an effect twice in development and answering each step at once
read as a press and closed the card as it opened. The desktop shares the code,
so a back gesture there closes the same layer.

The card sat under the status bar. The card fills the screen and the app
draws under the system bars. The card left room with padding on its own box,
but the close button is placed against that box, not inside it, so a button 10
pixels down sat 10 pixels from the top of the screen, under the clock. The
close button now carries the inset itself, and the header takes a little more
room above the title. The dialogs clear the status bar the same way, and their
foot clears the gesture bar. The inset has a floor of 26 pixels, because a web
view reports zero where the page is not drawn under a bar. The card no longer
pads its foot twice: the composer is its last child, and one inset is enough.

The phone could not stop kari planning. The desktop top bar holds one
switch for what the nodes do without being asked: Off, Ask or Auto. The phone
had none, so a person away from the desk could start a plan by hand but could
not stop kari planning, and could not let it start a weekly-reset plan by
itself. The switch is now on the board, under the column dots. It reads the
same as the desktop one and reports the same "mixed" when the nodes disagree.
The node chips set its scope. Both clients now call one function for the
change, in src/automation.ts, so a wrong tap on the phone offers the way
back that the desktop offers.

Pick the project of a quick task with a #name word

The one-line draft at the foot of a column had no project field. It sent the
card to whatever the filters named, so a task for another project needed the
full dialog, or a move on the card afterwards.

Write #name anywhere in the line to pick the project. The name matches the
project name or the last part of the path, and a few letters are enough:
#docs finds docs-site. The line under the box names the project and the
machine while you type, so the card never goes somewhere unseen. On a board
with more than one node, the tag picks the node as well, because a path lives
on one machine. A node filter keeps the search on that node. "More" carries
the tag into the full dialog.

The tag is cut out of the title. Two rules protect the text. A word of digits
alone, such as #1234, is an issue number and is never read as a tag. A word
that matches no project also stays in the title, and the line says that
nothing answered to it. A line that holds the tag and nothing else keeps it,
because an empty title is worse than a title that repeats the project.

The hint under the box gets its own line, and that line wraps. The word that
says a tag missed shared a line with the project and the machine, so WebKit
cut it off at 147 pixels and the reader saw no reason for the miss. The Mac
app draws with WebKit, so this was the app, not one browser. While the box is
empty, the same line says "#name picks a project", so the way to pick one is
there when the reader looks at an empty box, and gone as soon as text is
typed. The line above keeps the ellipsis for a long path.

Offer Open on the toast that reports a new card

Before this change, a new card gave the toast "Task added" and nothing
more. To open the card that was just added, the user had to find it in
its column. The board holds many cards, and a new card can land under a
filter or below the fold, so the card was hard to find.

The toast that reports a new card now carries the card. The toast shows
an Open button, and a click anywhere on the toast opens the card. This
holds for the add dialog on the desktop and on the phone, and for the
one-line add at the foot of a column.

The action runner took the card as a fixed value. A new card has no id
before the action answers, so a fixed value cannot name it. The card
argument now also takes a function of the result, in the same way as
undo already does. An action that fails reports no card, because no
card exists.

A toast that offers two buttons now takes the longer of the two lives,
which is ten seconds. The toast for a new card carries both Open and
Undo, and the user must be able to read both.

Open a jump in the herdr workspace of the project

Jump in asked herdr for a tab and said nothing about a workspace. herdr then
put the tab in the workspace that had focus, so a card opened next to
unrelated work, and the workspace for that project kept none of its own
sessions.

A herdr workspace carries no directory of its own. kari now reads the
directory of every pane and takes the workspace that already sits in the
project cwd. When more than one workspace sits there, the workspace with the
most recent agent state change wins, and the place on the workspace bar
breaks a tie.

When no workspace sits there, kari creates one with workspace.create, names
it after the directory, and renames its first tab to the card title. That
workspace holds one tab, so close_herdr_tab_on_done removes the workspace
with the card.

The choice is a pure function over the three lists that herdr answers with,
so the tests cover it with no herdr running.

Let a run start no MCP servers, and let a card overrule the setting

Every background run started the MCP servers of the Claude Code
configuration of the user. A run is unattended, so a server that needs the
user is no use to it, and one that reads the data of another app is worse
than useless: macOS guards the data of an app, and it charges the request to
the responsible app, which is kari. The dialog "kari.app would like to
access data from other apps" then appears for a run that nobody watches, and
again for the next run, because the answer covers the process that asked and
not the one after it. A password manager server did this about once a
minute.

Settings holds the choice for every card, and it starts no server by
default. A card can say something else, in both directions, with the MCP
servers field of the drawer. A run that starts none is given
--strict-mcp-config with an empty configuration, which is the pair the
summarizer already used.

The choice is an enum of three words rather than an optional flag, so a
patch can put a card back on the setting without a nested option. An older
settings record holds no key for the setting and an older card row holds no
value in the column, and both read as they must: no server, and follow the
setting.

The argument list of a run is now built by a function of its own, so a test
can read what a run is given. That is the whole bug class here: which flags
a run gets.

Install

Download the .dmg for your Mac: aarch64 for Apple silicon, x64 for Intel.
The app is not signed. After you copy it to Applications, run:

xattr -dr com.apple.quarantine /Applications/kari.app

On Windows, run kari_0.13.0_x64-setup.exe. It installs for the current user,
because kari reads the Claude Code state of whoever is logged in. That installer is
not signed either, so SmartScreen asks once: More info, then Run anyway.

An installed kari updates itself to this release: it checks on start and every six hours,
writes the new version beside the running one and offers a restart. Settings, Updates has the switch.

Every host runs the headless node, so that its sessions stay on the board while no window is open.
A Mac takes kari-node-v0.13.0-aarch64-apple-darwin (or the x86_64 one),
a Linux host kari-node-v0.13.0-x86_64-unknown-linux-gnu.tar.gz,
a Windows host kari-node-v0.13.0-x86_64-pc-windows-msvc.zip.
Run kari-node service install to keep it running at login. One engine runs on a host at a time:
open the app and the node steps down, quit the app and it takes the host back.
A node updates itself only when asked: kari-node update, or serve --auto-update.

An Android phone installs kari-latest.apk. Add this repository to Obtainium: the asset name never
carries the version and the signing key never changes, so an update installs over the previous build.
The phone reaches the nodes over a private network, such as a VPN, and pairs with the code from
Settings, Nodes on the desktop. See TOUR.md.

See the README for requirements and setup.