Skip to content

korekore Phase 2d: account, own keys and making a Character - #196

Merged
longzhi merged 58 commits into
korekore/phase-2c-onboardingfrom
korekore/local-on-main
Oct 26, 2026
Merged

longzhi merged 58 commits into
korekore/phase-2c-onboardingfrom
korekore/local-on-main

Conversation

@longzhi

@longzhi longzhi commented Oct 26, 2026

Copy link
Copy Markdown
Collaborator

Phase 2d of the korekore UI (ADR-0021): the account, own keys and making a Character, which main gained in the current UI only, now in the korekore look, on the Mac and on Linux. Also the Dock icon, and a fix to Make your own look. Behind VIBEBUDDY_UI=korekore; without it both apps behave exactly as before.

Stacked on #177 (2c) → #176 (2b) → #175 (2a) → #173 (Phase 1).

This branch merges main (717de01), so the diff against 2c also shows main's commits since 2c branched (#160 to #193). This PR's own change is the commits after that merge. The merge had four conflicts, all where both sides added fields or strings; both sides are kept: Status.swift and desktop/src/status.rs (korekore's activities and rolledBack beside main's account, voiceKeys, making and relayRefused), AppModel.swift (both new methods) and Localizable.strings (both sets).

What it does

  • Account and own keys open the General page: who is signed in, Sign out, Delete account (a korekore confirmation), the plan, this month's allowance and today's use, then the ElevenLabs and Volcano Engine keys (typed once, then only SET and Clear). The design has no Account board and no sidebar tile for one, so this is a section of General, not a page of its own.
  • Making a Character on the Character page: made Characters are cards in the grid (Use, Resume, Delete), a dashed Make a Character card opens the form, and the progress, the reason it stopped, or "is made · Write to box" shows above the grid. The form: start from, name, look, voice (each with ▶), and the lines by occasion. On the Mac it is a sheet; on Linux it takes the page's place, as the current Linux UI does.
  • The Relay's refusal (signed out, needs the Paid plan, can't check) shows on the Device page under Link while the box is away.
  • Make your own look opened its card below the grid, out of view, so it seemed to do nothing. The page now goes down to the card.
  • The Mac's Dock icon, with the switch on: Kore on sun with an ink outline, the General page's badge at icon size, set each time the Dock tile appears (the App shows in the Dock only while a window is open). The bundle's own icon stays the robot until the current UI goes. Linux has no counterpart: its launcher icon comes from the installed icon theme, and a Wayland window can't change it while running. Its tray already shows Kore.

What each part does is the current UI's, copied (AccountView, OwnKeyRow, MadeCharacterCard, MakingRow, MakeCharacterSheet; on Linux account(), own_keys(), making_row(), make_form()).

Screenshots

Mac, the real window drawn off screen in a debug build with sample data (a sample signed-in account):

General Character
Dock icon

Linux, drawn headless by iced_test on Omarchy (a sample account and a made Character):

General Character
Make a Character

Where it departs from the canvas

There are no boards for these parts, so there is nothing to match pixel for pixel. They use only the design's components and tokens: cards, rows, pills, pixel tags, the dashed card of Make your own look.

New files

  • app/Sources/VibeBuddy/Korekore/Pages/AccountCard.swift: the account and own keys
  • app/Sources/VibeBuddy/Korekore/Pages/MakingCharacter.swift: made Character cards, the making row, the Make a Character sheet
  • desktop/src/korekore/account.rs: the same on Linux
  • docs/design/korekore/screenshots/phase-2d/*.png

Changes to existing files

After the merge:

File Lines What
app/Sources/VibeBuddy/AppDelegate.swift +2 with the switch: the Dock icon when the Dock tile appears
app/Sources/KorekoreUI/MenuPanel.swift +37 KorekoreAppIcon
app/Sources/VibeBuddy/Korekore/Pages/CharacterPage.swift +64 −30 made cards, Make a Character, the making row, the scroll to Make your own look
app/Sources/VibeBuddy/Korekore/Pages/GeneralPage.swift +2 the account section
app/Sources/VibeBuddy/Korekore/Pages/DevicePage.swift +13 the Relay's refusal
app/Sources/VibeBuddy/Korekore/KorekoreSnapshots.swift +8 a sample account and the Dock icon
desktop/src/korekore/pages.rs +50 −31 the same three pages; dashed() shared by both new-item cards
desktop/src/korekore/mod.rs +2 −1 the new module
desktop/src/korekore/snapshots.rs +29 a sample account, a made Character, the form
desktop/src/main.rs +24 −14 with the switch: go down to the look's card after drawings are picked; a nested if from 2c collapsed for clippy
app/Localization/zh-Hans.lproj/Localizable.strings +2 the new strings
docs/design/korekore/README.md +1 the Phase 2d note
LESSONS.md +2 anchor pkill -f patterns over SSH

Tests

  • just test (Swift build and SelfTest): passed.
  • tools/check-localization.py: 576 keys, OK.
  • cargo clippy -p vibebuddy-desktop --all-targets (macOS): no warnings. cargo test -p vibebuddy-desktop: 58 passed on macOS, 60 on Omarchy (Linux). Clippy is not installed on Omarchy.
  • Mac: installed in /Applications with the switch on and used; the Dock icon checked by eye.
  • Linux: the debug App run on Omarchy with the switch on, General and Character pages checked. That daemon is 0.5.0, which has no account, so the signed-in states are checked only in the snapshots.
  • Not tested: signing in, saving a key, and making a Character from start to end against a real account.

🤖 Generated with Claude Code

longzhi and others added 30 commits October 21, 2026 17:21
…ADR-0018)

Brings ADR-0018 and the Account and Daily allowance terms onto main, and
points ADR-0017's account line at it: voice is free, with a daily
allowance per account. The API document is docs/api.md.

Closes #159

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
docs: accounts ADR-0018 onto main
How the daemon signs in through the browser with a loopback redirect and
PKCE, exchanges the code for a session, and uses it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
docs: the account part of the VibeBuddy API; GitHub sign-in first
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
docs: the voice part of the VibeBuddy API
POST /v1/account/sign-in returns the service's sign-in URL with a PKCE
challenge and a loopback redirect to the daemon's own
/v1/account/callback, which exchanges the code for a session and keeps
it in config_dir()/account (0600). Sign-out revokes the session and
forgets it even offline; deleting the account forgets it only once the
service has deleted it. /v1/status gains the account, with the GitHub
login looked up for display.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Mac and Linux alike: signed out, Sign in opens the browser on the URL the
daemon gives and waits for the status stream to say signed in; signed in,
it names the GitHub login (or the id, or the email) with Sign out and
Delete account, which asks first. A build that can't sign in says so,
and an absent daemon points to Advanced.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Recognition and synthesis go to /v1/voice/* with the session. A 401
forgets it, which signs out; a 429 is passed to the box as voice.error
allowance or global_cap, and no session as signed_out. With the voice
service, voice is offered only while signed in, and signing in or out
tells the box again.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…-cloud#8, #10)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The box was to call the voice service itself over the Relay, with a
voice token. That needs an HTTP client and a second TLS session in the
firmware, a token endpoint and a token handout, and a box that follows
every change to the voice API, to save one hop of a few hundred
milliseconds; the Relay carries only ciphertext since ADR-0015.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The box sends its recording over the Relay too, as it does over USB and
the local network, leaving 8 places in the Relay's outgoing queue to its
other lines so VOICE END isn't lost behind audio; the daemon offers voice
and takes the turn there as anywhere but the UART bridge.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign in to the VibeBuddy account from the Apps
Voice through the account: the daemon uses voice v1 with the session
docs: the global cap in the VibeBuddy API
The daemon keeps today's use from the service (read at sign-in and when
the App opens the tab, POST /v1/account/refresh), counts each command
the voice service recognizes, and remembers an allowance or global-cap
refusal until the UTC day ends; /v1/status carries both. Both Apps show
"Voice today: 7 of 100", or why voice is refused until midnight UTC.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
We charge only for what runs on our servers. Free keeps everything
over USB and the local network, and making Characters with the user's
own key, which stays on the computer.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
For vibebuddy-cloud#17.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
For vibebuddy-cloud#18.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
For vibebuddy-cloud#19.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The user's own key for a speech provider (ADR-0020) is kept by the daemon in
config_dir()/voice-keys.json (mode 0600) and set with PUT /v1/voice-keys. A
reply whose voice is from a provider with an own key goes straight to that
provider and never to vibebuddy-api: ElevenLabs as mp3_24000_48, Volcano
Engine's V3 stream as 24 kHz MP3 with its leading ID3 tag dropped. A 401 or
403 marks the key refused in /v1/status and leaves that reply silent, with
no fallback to the service. The status says only which keys are set.

The Mac and Linux Account tabs get "Your own keys": a field for each, a link
to each provider's key page, "Set" in place of the value, and Clear.

Closes #182

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The daemon reads the account's plan from GET /v1/account wherever it
reads today's usage (sign-in, POST /v1/account/refresh), and on the
Paid plan counts each reply spoken through the service against the
month, in UTF-16 code units as the service does, until the limit is
crossed. /v1/status `account` gains `plan`. Both Apps show "Free" or
"Paid plan", and on Paid "This month: 12,300 of 100,000 characters ·
renews 24 Nov"; the daily line stays.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
POST /v1/characters makes a Character's lines in a voice from the new
characters/voices.tsv (GET /v1/voices): with the user's own key for the
voice's provider, 16 kHz PCM straight from ElevenLabs or Volcano Engine;
otherwise from the voice service with "for":"character". Lines with
{address} are made once per form of address, named and laid out as
tools/make-character.sh does. Each line is trimmed, normalized to -1 dBFS
and packed with the box's ADPCM encoder into pack.bin and
address/<form>.bin under config_dir()/characters/<id>/, byte for byte as
tools/character_pack.py packs them. A refusal stops the job and keeps the
lines made; making the same id again resumes. Progress is `making` in
/v1/status.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…sal (#181)

The computer's hello carries the account session when signed in. After
the Relay refuses with signed_out or plan (close 4006 or 4007, at hello
or on its hourly check), the daemon doesn't try it again until the
account signs in or out or a read of the account finds the Paid plan;
unavailable (close 1013) backs off like any refusal. /v1/status reports
why as device.relay_refused, and both Apps' Device tab says "Sign in to
use the Relay.", "The Relay needs the Paid plan." or that it can't check
the account right now, while the box is away. docs/relay-protocol.md
describes the session, the three refusals and what the daemon does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ve a panic

GET /v1/characters lists the Characters made here from their manifests,
with whether their packs are made. A box wearing a made Character's pack
gets spoken replies in the voice it was made with, not Jessica. A job
that panics ends as failed with reason "internal" instead of making
forever.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
longzhi and others added 26 commits October 25, 2026 16:15
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
DELETE /v1/characters/<id> removes config_dir()/characters/<id>/: 404 for an
id that isn't valid (so nothing outside characters/ is reached) or wasn't made
here, 409 while its job runs. Both Apps get Delete… on each made Character,
asking first, and off with a hint for the one on the box.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ADR-0020: one Paid plan carries the Relay and a monthly voice allowance
docs/api.md: plans, the monthly allowance, and Character lines
Show today's voice use in the Account tab
Paid plan: show the plan and the monthly allowance in the App
Paid plan: the daemon presents the account to the Relay; the App explains a refusal
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Own keys: ElevenLabs and Volcano Engine keys for spoken replies
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Making a Character: the daemon makes the line audio
Making a Character: in the Mac and Linux Apps
GET /v1/voices/<id>/sample returns one line in that voice as a WAV file:
the first done line without {address} of the preset it comes from,
decoded from the preset's pack (now compiled into the daemon) as 16 kHz
mono; Jessica's is the firmware's built-in done line, 24 kHz stereo.
404 for anything not in voices.tsv.

The make-a-Character form lists the voices as rows, each with a play
button: AVAudioPlayer on the Mac, pw-play/paplay/aplay on Linux as the
Character previews already do. One plays at a time.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The voice picker's play button now plays the preview of the preset the
voice comes from, with the card's player (Jessica's is the robot's), and
hides itself when this App has no such preset. GET /v1/voices/<id>/sample
and the packs compiled into the daemon go: the daemon is as it was.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Making a Character: hear a voice before picking it
# Conflicts:
#	app/Localization/zh-Hans.lproj/Localizable.strings
#	app/Sources/VibeBuddy/AppModel.swift
#	app/Sources/VibeBuddyCore/Status.swift
#	desktop/src/status.rs
The merge of main brought the Account tab, own keys and Making a Character
to the current UI only. The General page now opens with the account and
own keys, the Character page shows made Characters, Make a Character and
the making progress, and the Device page says why the Relay refuses.
Make your own look scrolls to the card it opens below the grid.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The design hands over no app icon, so the Dock shows the General page's
badge: Kore at a whole-pixel scale on a kore-pink tile with an ink outline.
The bundle's icon stays the robot until the current UI goes. Snapshots
gain the icon and a signed-in account with own keys.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The App shows in the Dock only while a window is open, and the tile then
takes the bundle's icon, so the icon set at launch never showed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Linux counterpart of the Mac change: the General page opens with the
account and own keys, the Character page shows made Characters, Make a
Character with its form in the page's place, and the making progress, and
the Device page says why the Relay refuses. After drawings are picked for
Make your own look, the page goes down to the card it opens below the grid.
Snapshots gain a signed-in account, own keys and a made Character.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Copilot AI balanced review requested due to automatic review settings October 26, 2026 03:58

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has insufficient remaining quota.

An organization owner can choose the repository organization as the billing source for reviews requested by members with a paid Copilot license. Learn about Copilot code review billing.

@longzhi
longzhi merged commit 652a40c into korekore/phase-2c-onboarding Oct 26, 2026
2 checks passed
@longzhi
longzhi deleted the korekore/local-on-main branch October 26, 2026 06:03
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.

2 participants