# Changelog All notable changes to nanoMuse. The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); versions follow [Semantic Versioning](https://semver.org/). Unreleased changes are on `main`. ## [Unreleased] ### Cloud - Fixed a relay without an upstream key holding a place in the account's in-flight count for every chat or clip request it refused with `upstream_unconfigured`: after four such requests the account was answered `too_many_in_flight` until the holds timed out. The key is checked before anything is held. - The `allowance_exhausted` sentence older apps print as it is says "Three ways on", which is what follows (a key of your own, a plan you already pay for, an invitation); it said two. - A clip whose task the app never polled to an end (the app was closed, the phone slept) is now charged all the same: the relay asks the provider itself shortly before the hour's hold on the allowance lapses and once more before the task row is dropped, charges a finished clip once at the price fixed at submission, and lets a failed one go. A poll from the app after that charges nothing more. A handful of tasks a minute, inside the relay process; nothing to configure. - The production relay's self-check (`cloud/deploy/nanomuse-hk/selfcheck.sh`) now also reports a backup problem: no `cloud-*.db.gz` under the backup directory, or the newest one older than 48 hours (`BACKUP_MAX_H`); the directory, the hour limit and the state directory can be set from the environment. The alert line and the deploy script's messages carry no dash. ### Runtime - **A task another device asked for and then stopped now fails with `cancelled`**, as the hub protocol says, instead of being answered `ok` with the conversation's previous reply; the stop button on this computer ends it the same way. Only the device that asked can stop its task: another device's `stop` gets `{stopped: false}` and the run goes on. - **The first feed day is written in the language of your screens.** With the reply language on *Auto*, a batch follows the language the main chat last heard from a client, or the language *Start* was pressed in; the first batch used to come out in English after a first conversation held in Chinese, since it is written the moment the setup ends, before the model has read a word from you. - **A failed model test says what went wrong in plain words.** *Test* under Connections showed the provider's raw error (`AuthenticationError: Error code: 401 ...`); it now shows the sentence a chat turn would show for the same error (the key was refused, the host could not be reached, the model is unknown), translated, with the provider's words one tap away under *Details*. - The first conversation's opening says where your messages go the way the phones do: to the model you configured, and, signed in to nanoMuse Cloud, the conversation also follows you to your other devices; Data controls switches that off. It used to say they go only to the model. - `docs/sentinel.md` describes the decision order the code has: the taint check and the warnings run after a decision is made and can only turn an allow into an ask, a rule's or an always-allow's included. `docs/hub.md` says what the runtime does after `4001`/`4002` (one try a minute, the hub shown as refused); `docs/web.md` names the first conversation's three lines and the block's keys as they are. - **One turn on nanoMuse Cloud, on request.** `POST /api/threads/{id}/send` and the socket's `send` frame take `via: "cloud"`: that one turn runs on the relay's recommended chat model under the account's key, and the next is back on the configured model; `[llm]`, *Use nanoMuse Cloud models* and the conversation do not move. The user bubble, the reply and a failure of that turn carry `model_used`. Signed out it is `401 signed_out` with *Sign in to nanoMuse Cloud first.* - Switching *Use nanoMuse Cloud models* off while the chat model is the account's now says *Add a provider of your own first; with nanoMuse Cloud off, nothing else could answer.* - **Switching the model mid-turn no longer breaks the turn.** *Use as model*, a saved key or a model change while a reply was on its way closed the old client under the running turn, which ended with *Something went wrong: RuntimeError: Cannot send a request, as the client has been closed*. The turn now finishes on the client it started with; the new model takes over from the next turn, and the old client is closed when the run ends. ### Web - **The approval card says why it asks in your language.** *Why it asks* showed the runtime's own notes (`'shell' is in always_ask_tools`, `risk level is 'sensitive' (mode=ask)`); they now read as sentences, in English or 简体中文, with the tool, the risk and the destination kept. The same words appear under a denied step in today's record. The *Stopped.* notice, an oversized upload (*The file is larger than 25 MB.*) and the server's short answers a toast can show (*thread is busy*, *empty file*, *the main chat cannot be deleted* and the like) are translated too. - **Permissions you granted are listed by risk**, the gravest first (sensitive, moderate, low, safe), as on the phone, so a glance says which standing allowances touch sending, paying or the shell. - A failed connection test shows the sentence in your language with the provider's words under *Details*, instead of the raw error. - The community notice no longer calls nanoMuse a preview; 1.0.0 is out. - **Use nanoMuse Cloud this time.** A turn your own model failed (a refused key, a timeout, no connection and the like) offers the button under its sentence when you are signed in and the chat is not on the account: the same words again, one turn on the account's model, the next back on your key, as on the desktop and the phones. The reply says *Answered by nanoMuse Cloud this time (model)* under it. Signed out, the button says to sign in first. - The first run's welcome page says nanoMuse is a non-profit open-source community project, free forever, as the sign-in gate does; a runtime that does not ask for an account showed no such line. - The web's copies of the ideas lists match the Android and desktop copies again (the sandbox wording from #250). ### Desktop - The first page of the first run now shows the app's mark, as the phone does, and offers *Use your own API key instead* under *Sign in*: it goes straight to the models page with no account, and *Skip for now* there with no key saved returns to the first page. - The agent's opening lines in the first conversation no longer carry a dash. - The Chinese copy says 操作屏幕 where it used to say 手 or 动手 for the Hands (the Computer use badges, the permissions rows, the Models page, the first run's source page), and 形象 for the avatar throughout; *Manage routines* now reads 管理例程 like the rest of the Goals page. - The first run's Reach row no longer speaks of pairing, which went away in 0.1.24: the way in is the same account on your phone and your other computers. - The avatar studio's estimate, when it is more than the day's allowance, points to a key of your own instead of to "adding credit"; nothing is sold. - The train-ticket idea no longer speaks of "the phone's Linux sandbox" on a computer (the same list ships on the phones). - A failed call on the Models page is told in plain words, like everywhere else. - The Library room is 资源库 in Chinese, as on the phone, in the docs and in the ideas it shows; the things in it stay 构件, and the folder under `~/nanoMuse` keeps its name. - Switching *Use nanoMuse Cloud models* off while the chat row is the account's and you have no chat model of your own is now refused with one sentence under the switch, which stays on; with an own chat model the row moves there, as before. The same rule as the web console and the phones; the host answers `409 chat_on_cloud`. ### Android - The sign-in page no longer calls a mainland-China number "outside the mainland" while it is still being typed: seven to ten digits that can still become `1xx xxxx xxxx` (with or without `+86`) show no sentence and keep *Send* on; the sentence comes once the number cannot be a mainland one any more. - The welcome page's *Reach* line says how a computer joins today: install nanoMuse Desktop and sign in with the same account (it said "pair", which left in 0.1.24); the Chinese line under the nanoMuse Cloud provider says 服务商 like every other. - The hands notice when they go round in circles: the same action a third time on a screen that did not change gets a line in the history so the model changes tack, and a sixth ends the run as *infeasible* with the action named. Before, a model repeating one tap kept calling the screen model, a screenshot each time, until the thirty-minute limit or *Stop*. - Switching *Use nanoMuse Cloud models* off while the chat model is still the account's now moves the Chat row to your first own chat model (the catalogue's default for that provider), as every client does, and the Models page says *Applies to the main chat and to new chats*; with no chat model of your own the switch is refused with one sentence and stays on, since nothing else could answer. Before, the row kept naming the Cloud model and new chats fell through to whichever provider was used last, without a word. The Cloud provider's own page (Settings → Manage Providers) follows the same rule; a provider of your own switched off there while the chat ran on it hands the chat to the next source, and when none is left a send says *No model can answer right now. Choose one under Settings › Models.* in your language instead of upstream's English line. ### iOS - The Version row in Settings reads the download index the way Android does (the newest release's tag in `releases`), so it finds the latest release from mainland China too, where GitHub is not reliably reachable; before, the index was read in a shape it never had, and the row said *Could not check* whenever GitHub did not answer. A tap on a newer version opens the download page. - The first run's sign-in sheet closes by itself once the sign-in succeeds, so the next setup page is in view at once; before, the sheet stayed on the account page and had to be swiped away. - The Chinese copy uses the same words as the Android app: the agent is 智能体 (繁體: 智慧體) where it was still "agent" in English or 代理, the relay is 中继 (繁體: 中繼) instead of 中转, "Hands" stays in English, and the chat tab is 聊天, not upstream's 闲聊; the account page's usage rows say 对话 for the chat kind. `NanoMuseCopyTests` keeps it that way. - A task another device hands the iPhone (`@iPhone …`) is answered in the language of the device that asked, as the hub protocol's `language` field says (runtime 1.0.0); before, the iPhone's agent went by the text alone. `docs/hub.md` now says what the iPhone does with a step that needs approval: its own card when the app is in front, a declined step named in the answer when it is not. - *Use nanoMuse Cloud models* off while the chat model is the account's now moves the Chat slot and the main chat to your first own chat model (the catalogue's default for that provider); with no own chat model the switch stays on and says *Add a provider of your own first; with nanoMuse Cloud off, nothing else could answer.* under it, on the Models page and on the Cloud provider's own page alike. Before, the switch went off and the chat stopped with an error. The same rule as Android and the desktop. ### Project - `scripts/self-host.sh` and the self-hosting page name the SMS sender the relay actually uses (Alibaba Cloud's 号码认证服务 by default, 短信服务 with a template of your own); the script's sentences no longer carry a dash. - `docs/ios.md` says build 16 is the 1.0.0 build on TestFlight; the *New Contributors* and *Contributors* lines a release generates no longer carry a dash. - The *News* list of the README and its nine translations is newest first again (1.0.0, then the paper, then the first release); the Chinese READMEs and the docs home pages say the TestFlight link is public once Apple's review passes, and the docs home install tables no longer carry a dash. The release recipe in CONTRIBUTING.md says the latest-version line moves to the top. - The release-notes template, `THIRD_PARTY_NOTICES.md` and `SECURITY.md` read without a dash as punctuation; the template's *Community* paragraph says non-profit and no longer calls the version a preview, and its download note no longer says GitHub is the fastest source. The issue forms give `1.0.0 (43)` and `desktop 1.0.0` as version examples. - `CONTRIBUTING.md` and `AGENTS.md` read without a dash as punctuation, and the codename list in the release recipe runs to Keel. - `scripts/release-docs.py` now edits the *News* lists of the ten READMEs itself: the line that names the latest version is rewritten for the new one and moved to the top, the other milestones keep their order, and `--dry-run` shows the result without writing (a test covers it). The 1.0.0 release notes say GitHub's downloads are quick from China in our measurements instead of the fastest. ## [1.0.0] - 2026-10-09 · Keel Keel: the first stable version. What has been a preview since 2026-09-25 is now the set we stand behind: a phone app for Android and for the iPhone, a desktop app for the Mac, Windows and Linux, the web console, and the open-source relay (nanoMuse Cloud) that signs people in and lets their devices talk, all GPL-3.0-or-later and non-profit. One account across every device, Hands on the phone and on the computer, a key of your own for any of eighteen providers or the free allowance. Since 0.1.41: nanoMuse Cloud can be switched off as a model source without signing out, and a phone or a console with a key of its own works signed out; the main chat follows the chat model you pick; the model pickers fold long lists and can be searched; a thinking level for a key of your own on the desktop; the star card has *I already starred*; the desktop waits for a slow Windows start instead of giving up; the reply follows the language of the app; the Sentinel's `auto` mode asks before a tainted send and on a warning; the hub and the relay were hardened (bounded send queues, a sign-in code keyed to the relay, the allowance counted the way the provider bills); and no sentence a person reads carries a dash as punctuation. ### Cloud - **The console's account pages read what they show.** The accounts list ran seven correlated queries per account over the ledger and the keys; it is now one grouped pass per table with the same numbers. The overview, Activity and Data pages labelled their rows from the newest 5 000 (or all 100 000) accounts, decrypting every identifier each time, so an older account showed `?` once the relay passed that many; they now read and decrypt exactly the accounts on the page. The allowance distribution reads the four columns it needs. - A call nobody answers is now failed with `timeout` when the caller's next `ping` arrives, not only when another call does, so a device with one call outstanding hears it within a minute of the deadline, as the hub page says. - Conversation sync keeps at most 2 000 live side conversations per account; a new one past that is refused with `conversation_limit` and stays on the device, and deleting one frees a place. Messages already had a cap, conversations had none. `/v1/sync/state` lists the limit beside the others. - **A stored sign-in code is keyed to the relay.** A code is six digits and the relay kept a plain SHA-256 of it, so whoever reads the database file (a backup left on a share, a stolen copy) tries the million values in a moment and gets back whatever code is live, which signs them in as that person. The hash now takes `CLOUD_SECRET` as its key, the way the hash of a phone number or address already does, so the database alone shows nothing. A code that was sent before the relay updated no longer verifies: the person sees "That code is not right", and after five such tries "Too many tries; ask for a new code"; asking for a new code is the way through. - **A relay that sends codes will not start without `CLOUD_SECRET`.** Without a secret the relay falls back to a published development key, which is only safe while codes stay in its own log. `python -m nanomuse_cloud` refused that combination; a relay built any other way (`create_app(Settings())` under gunicorn, a `Cloud` made by hand) took it, mailing codes while hashing every identifier with a key anybody knows. The check now lives where the relay is built, and it looks at the sender that relay will actually use. - **The allowance is counted the way the provider bills.** The relay charged every prompt token at the full input price and DeepSeek at its daytime price around the clock, so a long conversation drained the free allowance several times faster than it cost the operator. Now the part of a prompt the provider served from its cache (`usage.prompt_tokens_details.cached_tokens`) is counted at the provider's cached rate (10 % of the input price on `deepseek-v4.1-flash`, 20 % on the Qwen models), and a DeepSeek turn charged between 22:00 and 8:00 Beijing time is counted at the night price (¥1 in / ¥4 out per million tokens instead of ¥2 / ¥8); the ledger line says when either applied. The other prices on the menu were checked against Model Studio's page and were right. Two small undercounts were closed the same way: an edit counts the picture sent in (¥0.02) next to the one drawn, and a clip asked for at 720P or 1080P is priced at that resolution instead of 480P. When a provider sends no usage at all, a streamed reply is now estimated by the same rule as a whole one (CJK at a token a character), and an empty `usage` object counts as none rather than as nothing to charge. New per-model fields for `CLOUD_MODELS`: `price_in_idle`, `price_out_idle`, `idle_hours`, `cached_in_rate`, `price_image_in`, `price_second_res`; `/v1/models` carries them under `nanomuse.price_cny`. - Fixed the hub refusing a bad key in the `Authorization` header before the WebSocket handshake, which the apps saw as a network failure and retried for ever; the relay now completes the handshake, sends the `error` frame and closes with `4001` as the hub protocol says. - Fixed a binary frame on the hub dropping the connection; it is answered with `bad_frame` and the socket stays open, as for an unknown frame type. - Moved the threshold rules' e-mail sending off the event loop so a slow mail server no longer stalls every request for up to 20 seconds. - Completed the hub protocol page: every error code, the close codes `4001 account_gone` and `4008`, the `actions` and `ip` fields of a device row, and what a client does on each close. - Fixed the training-set export (`GET /v1/admin/samples/export`) and the console's turn list skipping turns written in the same second as the last one of a page; both now page by time and id (`before_id`), so a busy second loses nothing. - Set the database to `synchronous=NORMAL` under WAL (one fsync per checkpoint instead of per commit) and added indexes for the console's timeline by kind and for the sync tombstone sweep that runs on every push. - Fixed the web console showing the server's English line to a disabled account: its table had the sentence under `disabled` while the relay sends `account_disabled`. Added sentences in both languages for `bad_request`, `too_large`, `no_session`, `device_online`, `sync_off`, `invite_code` and `not_found`; a sign-in made by nanoMuse Web is labelled web rather than code; a failed step in the device chat is marked with a word instead of an exclamation mark. - Removed the em dashes from the consoles' copy, the statistics' explanations, the threshold notice's subject and the `allowance_exhausted` sentence older apps print as it is; the admin page's token hint names `CLOUD_ADMIN_TOKEN` instead of one deployment's file path. - The relay README's error table lists every code the relay sends; `.env.example` names `ALERT_URL` (read by the self-check script) and describes the SMS sender correctly; the deploy README's `admin controls set off` example matches the CLI. - A session key can no longer mint another session key: `POST /v1/auth/session-key` with one answers `403 session_from_session`, so a leaked short-lived key expires when it was meant to instead of renewing itself. - The e-mail a threshold rule sends when a sign-up crosses its line goes out on a thread of its own (the minute timer catches anything left); the sign-in that crossed the line gets its key back without waiting on the mail server, and the switch still flips inside that request. - The request body limit (`MAX_REQUEST_BYTES`, 16 MiB) is enforced while the body arrives: a declared size over the limit is refused before a byte is read, and a chunked body is cut the moment it passes the limit instead of being buffered whole and measured afterwards; the picture sent with an edit request is read only up to one byte past the limit before it is refused, not whole. The README states the Caddy cap (`request_body max_size 20MB`) that sits in front of it. - Hub sends no longer wait on the receiving socket: each connection has a bounded queue (512 frames or 32 MB) drained in order, and a device that stops reading is closed with `4009 slow_consumer` instead of holding the device that was calling it. Documented in docs/hub.md. - Fixed a restart giving the allowance back to an account the operator had set to zero from the console: the one-time seeding now reaches only accounts no 0.15+ relay has seen. - Fixed the invite page's `earned_cny` multiplying today's bonus by the number of invites; it is the sum of what the ledger credited, so changing the bonus later does not rewrite what a person already earned. ### Runtime - **The reply follows the language of the app.** The system prompt named only the script of the latest message, so an English console with a short or mixed message got a Chinese answer about half the time. A client may now send the language of its screens with each message (`language`, a BCP-47 tag, on `POST /api/threads/{id}/send`, the socket's `send` frame and the hub's `task` call); the prompt names that language first and the message's script second. The web console sends its locale; older clients send nothing and get the old behaviour; a fixed *Reply language* still wins. - A tool whose argument check trips on an argument of the wrong shape (a list where a path was expected) now fails that one call, which the model can correct, instead of ending the turn with "Something went wrong". - The hands stop after six steps in a row that moved nothing (an unknown action, the same action again and again) and say so, instead of calling the model until Stop is pressed when no step cap is set. - The hub client waits a minute before reconnecting when the relay closes with `hub_paused`, as the protocol page says, instead of retrying on the one-second backoff. - A model endpoint that answers with no choices is retried and then reported as the provider having trouble, not as an internal error. - A command's output is kept up to 2 MB per stream in memory (the rest drained and counted, with a note saying how much was cut) instead of being read whole; a webhook body over 64 KB is refused as it arrives rather than after. - **nanoMuse Cloud can be switched off as a model source without signing out.** `[cloud] models = false` (and `POST /api/cloud/models`) leaves the account out of the automatic order of the hands, pictures and clips and out of the providers' listing, while the sign-in, sync and the hub stay; signing in no longer adopts the Cloud chat model while the switch is off. Switching off is refused with a plain sentence while the chat model is the account's, since the runtime holds one chat model and nothing else could answer; pick another chat model first. - **Shutdown waits for a cancelled run to finish what it was doing.** `stop()` cancelled every thread's worker, yielded once, then flushed the timelines and closed the stores. A cancelled worker is not finished at that point: it still settles its thread (the stopped notice, the session write, the timeline), and doing that after the stores closed is the `Exception ignored ... during shutdown` of the log. The workers are now awaited, for at most five seconds, so a run wedged in a provider response cannot hold the exit. - **`sync.json` is written aside and moved on**, the way every other state file here is. Losing power mid-write used to leave half a JSON file, which `_load` reads as no state at all: the cursor, the map from threads to conversation ids and the hidden main chats gone, the next pull starting from nothing. - **The vault scrubs short secrets too.** Redaction skipped any secret under six characters, so a four-digit PIN or a short key went into the model's context, the audit log and the app verbatim. Short secrets are now matched as whole words, which redacts `1234` without rewriting half of every word that contains it, and the longer of two overlapping secrets is masked first. A value of one or two characters is not redacted at all, so a label stored by mistake does not blank ordinary words in what the model sees. - **`auto` mode now asks before a tainted send.** Once a conversation has read mail, a calendar, contacts, a file outside the workspace or another private source, a call that would send data to a host outside `sentinel.egress_allowlist` asks in every mode; before, `auto` turned that question into an allow, so an unattended goal pass could read a private file and post it to an unknown host in silence. This is a change of behaviour for Hands-off mode and `--auto`: a background pass that read your mail and then reaches an unlisted host now puts an approval card in the Feed and waits, and `nanomuse daemon`, which has nobody to ask, declines that step with a note and goes on with the rest. The approval card now says the data was read in this conversation rather than this session. Documented in `docs/sentinel.md`. - **Taint is kept per conversation, not per process.** One Sentinel serves every thread of the app and kept a single tainted flag, so a side chat that read a private file made every other chat ask too, and `/reset` or clearing one thread forgot the taint of the chats running beside it. Taint is now recorded per conversation, the way approvals already were; ending a thread drops only its own. As before, it lives in memory only and is gone after a restart; data that crosses conversations through a memory or a workspace file does not carry taint with it, where the old single flag covered that case by accident. - **A thread id cannot name a file outside the threads folder.** The app sends the thread id, and that id names the file the conversation is written to; an id such as `../../somewhere` was taken at its word and read or wrote outside `data/threads`. Ids are now checked where the path is built, and the WebSocket answers such a frame with an error instead of starting a run. - **`auto` mode asks on a warning, as documented.** A call that carries a warning (`rm -rf`, `sudo`, code that reads the environment) was allowed in `auto` mode although step 6 of the policy says it asks in every mode; the gate now keeps it an ask. `nanomuse daemon` declines such a step with a note instead of waiting on a keyboard nobody is at. - **A shell timeout stops the whole process tree.** A command that runs past its timeout, or whose turn is cancelled, is stopped together with everything it started (`sleep 999 | tee`, a server put in the background); the coding runner and the hub's shell action stop their trees the same way. - **Shell approvals see inside substitutions.** The programs in `$(...)`, backticks and `<(...)` are bound too: a permission for `echo` is not one for `echo $(curl ...)`. - **IMAP arguments are quoted.** `read_emails` quotes the folder name and the search text, so a folder or a query with quotes, spaces or non-ASCII letters works and cannot carry a second command; the tool and the mail watcher give up on a silent server after 30 seconds. `send_email` reports a header with a line break instead of crashing. - **`files` reads large files in part**, without loading the whole file first, and lists a match under an extra root by its full path. - **Numbers from the model are read sensibly.** A value passed as text or out of range (`timeout="sixty"`, `limit=-1`) is parsed or clamped instead of crashing the tool; a crash inside a tool is told apart from a bad argument. - **Downloads are capped.** `web_fetch` reads at most 8 MB of a page, and the skill installer reads a SKILL.md only up to its size limit, instead of downloading the whole file first. - **`open_app` refuses commands to the machine** (`shutdown`, `reboot`, `sudo`, a shell); the Sentinel marks such a request sensitive with a warning. - **`memory forget ` is literal and undoable.** `%` and `_` are no longer wildcards, an empty query forgets nothing, and the deletion is logged as one change that `memory restore` brings back. - **Removing a person from the agent's own address book is a moderate step**, no longer a safe one. - **The hands' model shares the chat model's `proxy`** when both are on the same host. - **One answer to "is a model ready".** The Feed, the first feed day, `nanomuse chat` and `nanomuse doctor` agree: a key stored by the app (`{{vault:...}}`), the Cloud key, a ChatGPT sign-in and a local server all count. Before, the Feed said "add a model first" to anyone whose key the app had stored. - **`nanomuse config show` masks every credential**: the image and video slots, the mail password, the search key and the chat channels' secrets, not only the chat and hands keys. - **`[connectors.search] fallback = false`** makes a failed search fail instead of sending the query to DuckDuckGo as well; the docs say when the fallback reaches a second host. - **`deepseek-flash` and `deepseek-v4-flash*` take pictures** (they are served by DeepSeek-V4.1-Flash), so pictures in chat reach them without a first refused request. - **Smaller fixes.** A provider key from the environment was not picked up when `[llm] provider` named a catalogue entry and left `base_url` empty; a non-numeric `NANOMUSE_SERVER_PORT` no longer crashes at start; a non-JSON answer from Telegram, DingTalk, the relay or a ChatGPT token endpoint is reported instead of raising; a malformed point in a phone step makes the model try again instead of ending the task; the hub skips an artefact that turns out unreadable instead of failing the call; background work started by the avatar studio, the browser's download handler and the embedder switch is kept alive and its failures logged; `[sandbox] mode`, `[browser] backend` and `profile`, `[hands] backend` accept only their documented values; the `[gui] max_steps` example reads `0` (no cap), the default. - **Sentences without dashes.** Every line the runtime says to a person or to the model (the CLI, the Sentinel's cards and refusals, the first conversation, the provider sentences such as "Pictures need a provider with image models: …", the tool descriptions, the built-in skills and the example configuration) reads as two sentences, a comma or a colon where it used an em dash, the same wording as the apps; a test keeps it that way. - **A ChatGPT sign-in refreshing in two places at once no longer stalls the app.** When another nanoMuse process holds the token lock, the wait happens off the event loop, so the Feed, the hub and the web app keep answering meanwhile. - **Coding sessions on Windows show their workspace.** The runtime reads a Cursor or Claude Code project folder name back into the path it stands for: on Windows `C-Users-me-app` (Cursor) and `C--Users-me-app` (Claude Code), in either case, read as `C:\Users\me\app`; before, the drive letter was lost and the list showed the folder name instead of the workspace. Unix names read exactly as before. - The sentence shown when the day's share of the free allowance is spent says it comes back tomorrow; it no longer names Beijing midnight, which was only true for a relay with the default day boundary. ### Web - **The star card has *I already starred*.** Next to *Star on GitHub* and *Not now*, the new button says the star is done; it and *Star on GitHub* (the card's, the allowance card's and the community notice's) stop every ask in this browser for good, whatever the cooldown and the asks left. Before, a person who had starred from GitHub itself kept being asked. - The console sends its language with every message, so the reply is written in the language of the screens rather than guessed from the message. - **The console works signed out with a key of your own.** The sign-in page stands only while this runtime asks for an account, none is signed in and no model of your own is ready; a new *Use your own API key instead* button steps past it into setup's own-key path and the browser remembers, with the sign-in waiting under Connections for the Cloud models, sync and your devices. - **A *Use nanoMuse Cloud models* switch on the nanoMuse Cloud card.** Off, nothing runs on the account's models unless you choose it yourself, and you stay signed in; the card says which. While the chat model is the account's the switch answers with the runtime's sentence instead of flipping. - **The model pickers fold long lists and can be searched.** The *Hands model*, *Picture model* and *Clip model* controls on the Connections page became one picker: a button reading `provider · model` opens a panel with *Automatic* first; a provider with a long list (OpenRouter, SiliconFlow) shows eight models at first, its catalogue default for the slot and your current choice first, with *Show {n} more* at the foot of the group; once the rows pass eight in all, a *Search models* field filters every group by model id or name as you type, and *No model matches* says when nothing does. Esc or a click outside closes it. What each control saves is unchanged. - **Every label in Chinese.** Translated the goal cadences *Once a week* and *Once a month* and the calendar's *.ics file* chip, which showed in English on a Chinese console; reworded three Chinese lines (the feed instruction, the goals intro, the provider row) and removed nineteen dictionary entries nothing uses any more. - **No em dashes in what the console says.** Replaced the em dashes in every sentence the console shows (about eighty strings and their Chinese twins, the provider sentences included) with a colon, a comma or a full stop; the sentences the runtime sends keep their English so the Chinese still matches. - **An approval is answered once.** The card's buttons go quiet while the decision is on its way to the runtime, so a second tap cannot approve twice or approve and then deny. - **Lists say when they could not load.** Library, Memory, Skills, Chat apps, the status sheet's Upcoming and Activity show the runtime's sentence and *Try again* instead of a spinner that never stops; Memory no longer flashes *Nothing remembered yet* before the list arrives, and the Chat apps page stops toasting on every missed poll. - **Sending from an idea, a goal or a feed post reports a failure** as a toast instead of failing silently; a reply with a malformed link (a stray `%`) no longer takes the chat down; an HTTP error without a status text reads `HTTP 502` rather than nothing. - **One sign-in form.** The nanoMuse Cloud card under Devices and Connections uses the app's sign-in (code or password, the invite code, the note that SMS codes reach mainland numbers only) instead of a code-only copy of it. - **Keyboard and screen readers.** The chats drawer and the file viewer are dialogs that keep focus inside and give it back on close; the Sentinel mode and proactivity buttons announce which is selected; a skill's switch names the skill. - **The Goals screen loads on its own.** Its life areas, cadence words and due-date line moved to a module of their own (`web/src/goals.tsx`, with tests, one of which checks that every label has a Chinese twin), so the screen is a chunk the first page no longer carries; the leftovers of the removed call screen (state, frame type, styles) are gone; two copies of "3 min ago" for Unix timestamps are one `relativeSeconds` in `util.ts`; a browser with storage switched off no longer throws on the token, the feed watermark or the first-visit notes. - A reminder's or routine's notice in the chat ("Reminder: …", "Routine: …", "Tidied memory") is in the UI language; the person's own words stay as written. - **The chat stays at its latest message.** The timeline pinned itself to the bottom once per event and left the gap that opened when a bubble found its height, an image or a code block arrived after the render, or the window was resized; the view sat short of the bottom and the *Latest* pill showed with nobody having scrolled. The pin is redone whenever the list or its content changes size, a chat opened from the drawer starts at its latest message whatever the previous chat's position was, and the pill of one chat never carries over to the next. - The Chat apps page's Chinese lines live in the shared dictionary with everyone else's, and its calls to the runtime go through the same request helper as the rest of the console (the page had a copy of each). - The first run's model page says the account's model needs nothing set up instead of calling it the quickest start, in English and Chinese; the spent-allowance sentence follows the runtime's new wording. ### Desktop - **A thinking level for a key of your own.** The composer's model picker offered a level only for the Cloud's known models; a model behind an own key showed none. The catalogue now records, per vendor and model pattern, how the vendor's public documentation controls the level (`reasoning` in `providers.json`, with the page it was read from), and the saved row declares those levels to the harness in the vendor's shape: `reasoning_effort` (OpenAI, Kimi, MiniMax, SiliconFlow, Volcengine, xAI, Gemini's OpenAI layer, Groq, Mistral, gpt-oss on Ollama and vLLM), `thinking` plus `reasoning_effort` (DeepSeek, Zhipu), OpenRouter's `reasoning.effort` with the levels its model list gives per model, Anthropic's adaptive thinking with `output_config.effort`. *Provider default* sends nothing, so the vendor's default stands; there is no *Off*, and a model nobody documents a level for, Bailian, LM Studio and a custom endpoint show no control as before. A key saved by an earlier build gets the levels at the next start. The phones read the same catalogue and ignore the new field. - **A slow host start is waited for, not failed.** The shell gave up 120 seconds after starting the host with "the host did not announce its address within 120 s" while the host was still loading; on a fresh Windows install, where the real-time scanner checks each of the app's 20 000 files on first read, the address came seconds after that. The shell now waits as long as the host process runs: after two minutes the loading page says *Still starting* with the host's last lines, after ten minutes a dialog offers *Keep waiting*, *Copy details* or *Quit*, and only quitting stops a host that is still starting. The log notes a start that took over half a minute. - **Settings keeps the way back.** A link on one settings page that opens another (*Add a provider* on Models, the account card on General, *The sandbox* on Files, *Open Devices* on Account) used to leave you on the new page with no way back but the sidebar; the page now shows *Back to {page}* at the top, and a pick in the sidebar starts fresh. - **The file-access rule reads as it works.** The Files page said the default preset was read-only and that a change outside the chat's folder asks; the default is workspace-write, and a change outside the folder is refused unless the agent asks you for it and you allow it once. The Files page and the built-in Files connector say so now, and the docs explain which folders the main chat's `~/nanoMuse` leaves out and why. - **Removing the key new chats ran on no longer breaks every new chat.** When the provider the chat slot pointed at goes away (its key removed, or the account signed out), the slot moves to the account while signed in, else to another key of yours, else to the stock default, instead of staying on a route no adapter serves (*no adapter registered for provider "custom"*). A custom endpoint that does not list its models is refused with *The endpoint did not list its models* before the key is stored, and the form shows a field for the model ids, comma-separated, which are saved as its chat models; a key with a space in it now says so instead of reading as signed out. - **The star card has *I already starred*.** Next to *Star on GitHub* and *Not now*, in the chat header strip and on the cards, the new button says the star is done; it and every *Star on GitHub* (the cards, the allowance card, the Settings and About rows, the Help section) stop the asks on this computer for good, whatever the cooldown and the asks left. Before, a person who had starred from GitHub itself kept being asked. - **The main chat follows the chat model you pick.** Choosing the chat model in Settings → Models, or *Use it* with chat ticked after saving a key, moves the main chat to it as well as new chats; side chats keep their model. The line under the picker says so: *Applies to the main chat and to new chats; a side chat keeps its model.* A main chat on *Use nanoMuse Cloud this time* finishes that turn on the account and comes back to the pick. - **A task handed to another computer answers in this app's language.** The `task` hub call now carries the screen's BCP-47 language; a runtime of 1.0.0 or later replies in it, an older one as before. - **nanoMuse Cloud can be switched off as a model source without signing out.** Settings → Models starts with *Use nanoMuse Cloud models* while signed in. Off, the account's models leave every picker and the automatic order (the hands, pictures and clips fall to your providers), new chats move to your first chat model, the harness's side calls (a chat's title, a compaction) run on that model instead of the relay, and a chat still sitting on a Cloud model is not sent: a card explains and offers another model, a new chat, or *Use nanoMuse Cloud this time*, the only thing that spends the allowance while the switch is off. The sign-in, sync and the devices are untouched; a sign-in while the switch is off changes no slot. - **The model pickers fold long lists and can be searched.** The pickers of Settings → Models and Settings → Media became a button reading `provider · model` that opens a panel: *Automatic* (and *Off* for clips) first, then nanoMuse Cloud with its recommended model marked and first, then one group per provider; a provider with a long list (OpenRouter, SiliconFlow) shows eight models at first, its catalogue default for the row and your current choice first, with *Show {n} more* at the foot of the group; once the lists hold more than eight models in all, a *Search models* field filters every group by model id or name as you type, and *No model matches* says when nothing does. Esc or a click outside closes it. What each row saves, the *Currently* line and the hands' live update are unchanged. - **The installed release is no longer offered as an update.** The daily check compared the latest release with the bundle's labelled version string (`dsh-nanomuse 0.1.40`), which read as 0, so every release counted as newer: About said *0.1.40 is out* on a 0.1.40 install and *Update* downloaded the same build. The check compares the bare number now, and a labelled version is read as its number should one reach the comparison again. - **The ideas catalogue reads without em dashes**, the same file as the phones'. - **Every call to the relay has a deadline.** Sign-in, the models list, the account sheet and the conversation sync gave up never when a connection hung; now a call is given up after 30 seconds (three minutes for a picture made or edited through the relay), and a hung sync no longer stalls every later push and pull. The video model probe gives up after 15 seconds. - **The hub keeps one connection.** A sign-in or a device rename that landed while the previous connection was still reading the key opened a second socket next to the first; the first stayed open and kept receiving. After `hub_paused` from the relay the desktop waits two minutes before it tries again; a device the relay refused (close 4002) stays closed until the next sign-in or rename instead of knocking every second, as the hub page says, and a rate-limit close (4008) waits a minute. - **About offers the right Linux package.** The update row offered the AppImage to everyone; it now offers the `.deb` (the one the docs prefer) and the AppImage only when the running app is one. - **The connectors' sign-in page follows the system language** when the agent has no language yet; it assumed Chinese. - A live stream that failed is reopened only while something on the page still listens to it; the docs now say the update check reads the mirror's index first and GitHub second, as the app does. - **The sign-in fine print and the Legal page no longer link to a page that does not exist.** Both pointed at `docs/terms.md`, which was never written; they now link the privacy policy at `nanomuse.cn/privacy/`, the page every other client links, and the Legal page lost its *Terms* row. - **First run says where the code goes.** The sign-in page of the first run now carries the same line as Settings: a mainland China number gets an SMS, anything else an e-mail. - **Enter while an input method is composing no longer sends.** The coding panel's composer, a chat's rename field, the search box and the connectors' key fields treated the Enter that picks a candidate (Chinese, Japanese, Korean) as a send; they now wait for the text. - **Renaming a chat: Escape discards, Enter commits once.** Escape used to leave the field but the rename still went through when the field lost focus, and Enter followed by that blur renamed twice. - The *Manage permissions* row of the profile drawer opens with Enter and Space; the Help page's *Docs* row opens the desktop page of the docs site instead of a developer page on GitHub. - **The desktop's sentences lost their dashes.** 66 English and 49 Chinese strings, the refusal card's two sentences, the first run's greeting, the video model's activation note and the Linear connector's line now read with commas, colons and full stops; a test keeps dashes and exclamation marks out of both dictionaries from here on. - **87 locale strings nobody read are gone**: the old onboarding slides, the first profile drawer, the first About and leftovers of the feed, ideas and library pages, in both languages; a test now fails on a key the client never uses. - Two Chinese sentences of the Account and Network pages say `nanoMuse Cloud` instead of 「nanoMuse 云端」. - **Windows workspaces read back from the coding agents' folders.** The desktop turned a Cursor or Claude Code project folder name back into the workspace path by reading `-` as `/`, which fits `/home/me/app` and not `C:\Users\me\app`: on Windows the coding panel's sessions of an IDE chat (no `cwd` on record) showed a workspace of `:\Users\me\app`, and the plugin's tests failed on the Windows runner, which broke the 0.1.41 desktop build. Cursor's `C-Users-me-app` and Claude Code's `C--Users-me-app` now both read as `C:\Users\me\app`; Unix paths read as before. - **The hands row says why the hands are off.** Settings → Connectors said *The runtime was not found* whenever a chat ran without `computer_act`, whatever the cause. The row and the runtime row of Settings → Computer use now tell the three apart: no runtime, `NANOMUSE_PY` pointing at a file that is not there or not executable, or a runtime that did not start for the hands, with the start's own error (the host keeps the last mount's outcome and serves it with `hands/runtime`). - **Opening a link on a Windows computer from another device no longer expands `%NAME%`.** `device_open` went through `cmd.exe start`, which read `%USERNAME%` and the like inside a URL as variables; it now goes through PowerShell's `Start-Process` with the target as an encoded literal, and a local file is opened by its path. A `file.put` whose write or rename fails removes its `.nanomuse-part` file. - **Restoring IDENTITY.md or SOUL.md to the stock text asks first**, the way deleting a goal does; the text written there is replaced and kept nowhere. - **A failed call says what happened in plain words, in both languages.** Sign-in, the models list, the account sheet, the avatar studio, the connectors and the other pages that talk to nanoMuse Cloud showed the wire's text as it came (*That did not work: fetch failed*). The relay out of reach, a deadline passed, too many requests, a sign-in no longer valid and a relay in trouble each have one sentence in English and Chinese now; this computer's own host not answering has its own. Anything else keeps the message as before. - **17 locale strings nobody read are gone**: leftovers of the model pickers' first draft (`md*`, `ownKey*`, `frModels*`), kept under an exemption the locale test no longer needs. - The first run's fine print sets the privacy link flush in Chinese (*继续即表示你同意隐私政策*); the English space before the link is part of the English sentence now. - **The installed app knows its own version.** The bundle read its version from the environment at run time, which nothing set in the packaged app, so every install ran as `0.0.0`: the daily update check saw every release as newer (the dot on Settings and the *Update to 0.1.x* row never went away, and *Update* offered the build already installed), and the device told the hub it was `dsh-nanomuse 0.0.0`. The version is baked into the bundle at build time now; a test checks it against the package's. Pre-release tags compare identifier by identifier (`rc.10` after `rc.9`) and build metadata (`+sha`) is ignored, as SemVer says. - **A hub restart fails the calls in flight at once.** A sign-in or a rename while a call to another device was under way left that call waiting its full timeout, and a call made before the new connection was up was written into a socket not yet open; both now fail with a plain reason, and the hub shows as off until the relay's welcome. - **The window stays on the Host's page.** The check that kept navigation inside the app compared the address as a prefix, which `http://127.0.0.1:@other.host/` passed; it compares the parsed origin now, and the microphone permission is granted to the Host's page alone. The Help menu's *About this desktop* opens the docs site's desktop page, as the plugin's About does. ### Android - **The star card has *I already starred*.** Next to *Star on GitHub* and *Not now*, on every card and in the allowance card's star row, the new button says the star is done; it and *Star on GitHub* stop the asks on this phone for good, whatever the cooldown and the asks left. Before, a person who had starred from GitHub itself kept being asked. In every language the app has. - **The main chat follows the chat model you pick.** Choosing a chat model on *Settings › Models*, with *Use it* on the card after a key is saved, or in a chat's own picker set the default for new chats only, and the main chat is never new: a person who added a key of their own saw their side chats answer through it while the main chat kept nanoMuse Cloud under the face. The main chat's binding now moves with the pick, the same write a pick in its own picker makes; a side chat keeps its model; the line under the picker says so in every language (*Applies to the main chat and to new chats; a side chat keeps its model.*). - **The main chat takes the group's thinking level along.** When the main chat followed the chat model you picked, it kept its own thinking level (`off` on a chat that never set one) while a new chat on the same group started at the group's default; the main chat now takes the group's default thinking level as the chat's own picker does. - **A task handed to a computer answers in the phone's language.** The hub `task` call now carries the phone's UI language (`language`, BCP-47), so a runtime from 1.0.0 replies in it instead of guessing from the text; older runtimes ignore the field. - **One rule for "on your own network".** The sign-in screen's *Use a different server* judged a relay address by string prefixes, so `http://10.foo.example.com` passed as a private address while a Tailscale address (`100.x`), a link-local or an IPv6 ULA address was refused; the provider URL field used a second rule that did not know `.ts.net`. Both now share the provider field's rule (`LanOnly`): the address is parsed before it is judged, the carrier-grade range `100.64/10`, link-local and ULA count as one's own network, and so do `.ts.net` names. - **The Devices row says when the operator paused the hub.** The phone used to read the relay's `hub_paused` close as a replaced connection, knock again after 30 s and show *Connecting…*; it now waits two minutes, as the desktop and the iPhone do, and the row reads *Paused by the relay*. - **A notice another device sends says when it could not be shown.** `notify` answered *shown* even when the person had turned nanoMuse's notifications off (Android 13 asks first) or silenced the channel; it now answers that notifications are not allowed, so the asking device can say so instead of assuming the notice was seen. - **Screenshots from the hands are not kept for ever.** Every step on the screen was written as a JPEG under the session's attachments and nothing ever removed one. The ten newest runs keep all of theirs; an older run keeps its trace and the last screen, the one the chat shows; past 200 runs the oldest is removed. The trimming happens as a run starts. - **The model picker keeps its place and speaks to TalkBack.** The search text and the groups opened with *Show N more* survive a rotation and the trip through *Add a provider* (they were reset); the cross that clears the search has a spoken label; *Automatic* and every model row announce whether they are the chosen one, as a radio button does. - **A reach card for a server at an IPv6 address survives a reload.** The line the chat stores for a provider that did not answer kept the host between colons, so an address such as `fd00::1` was read back as a different host and the card lost its words; the host is read as everything between the first colon and the last now. Also: ten edits inside OpenMinis files that carried no `// nanoMuse:` marker have one (nothing changes for the person), and the unused string `nm_hands_model_auto` is gone from all 17 locales. - **nanoMuse Cloud can be switched off as a model source without signing out.** *Settings › Models* has a switch, *Use nanoMuse Cloud models*; off, the Cloud leaves the pickers and the automatic order for every row and no side call runs on it, while sync, the devices and the account page keep working. Deleting the Cloud provider on its provider page signed the phone out, because that instance is the account; the page now says so instead of offering *Delete provider*, and signing out stays under *Settings › nanoMuse Cloud*. - **A phone with a key of its own works signed out.** The first screen required the sign-in before anything else, so a sign-out brought it back as a wall even with providers configured; it now comes back only when neither an account nor a provider with a key is there, and offers *Use your own API key instead* next to the sign-in. Signed out with a key of your own, the password and model-source pages are skipped. - **A chat pinned to a Cloud model stopped at the switch.** A chat whose model was picked from the Cloud group in the chat's menu kept answering on the relay after the Cloud provider was disabled, and so did a remembered Cloud model on a reopened chat; both now fall back to the default the way a group binding does. The *Use nanoMuse Cloud this time* button no longer requires the Cloud provider to be enabled: it is the explicit consent, and the only thing that spends the allowance while the switch is off. - **The model picker stays short with a provider of hundreds of models.** Each group of *Settings › Models* shows eight models (the catalogue's default for the row first, then the one in use, then the rest as the provider lists them) and a *Show N more* row expands it; once the groups hold more than eight models in all, a *Search models* field above them filters every group live by model id or display name, and *No model matches* says when nothing does. - **The shell guard sees through wrappers with options.** `timeout 10 rm -rf …`, `nice -n 19 rm …`, `sudo -u root rm …`, `ionice -c3 …`, `env -i …` and `xargs -I {} rm …` were judged safe because the program was read from the wrong word; each wrapper's options and their values are now skipped and the real program is judged. - **Enter in a field whose hint says Message, Send, Chat, Reply or Comment asks before sending.** The pattern had a typo that made those English hints never match; Chinese hints and the messenger list were unaffected. - **A hands run's Stop takes effect while an approval card is waiting**: the card is denied and the run ends, instead of waiting for the card's answer or its three-minute timeout. The capsule's Allow and Deny answer only the hands' own card, never a shell or browser card of another conversation. - **An approval card whose asker went away is withdrawn.** When the conversation that asked is stopped or closed, its card disappears and the next request in line comes up; before, the card stayed until the three-minute timeout. - **Two `nanomuse-hands run` arriving together cannot both start**; the second is told the hands are busy. - Reasons kept as grant labels (*taps "Pay": looks like a payment*) no longer carry a dash. - **The ideas catalogue reads without em dashes** (the same file the desktop and the web console carry). - **A path the agent or another device names stays inside the sandbox.** The hub's `files` actions, `nanomuse-media` and `nanomuse-pc put` resolved `..` and symlinks literally and fell back to the raw phone path, so a sandbox path could reach the app's own private files; they now resolve inside the rootfs, a bound folder or the session's own folders only, and anything else is "not inside the phone's sandbox". - **The hub waits when the relay asks it to.** A paused hub or a connection replaced by a newer one of the same device (close 4003) is retried after 30 seconds instead of at once, as on the desktop; a key the relay refuses at the handshake (HTTP 401 or 403) is shown as refused under Devices and retried once a minute instead of on the fast backoff forever. The close is acknowledged with a normal code. - **No em dashes left in the Android copy.** Every string in the 17 languages now reads with a comma, colon or full stop where an em dash stood, and a unit test keeps it that way (it also refuses 非营利 for 非营利). - **Chinese copy uses the product's own names.** 简体 and 繁體 strings say `nanoMuse Cloud` instead of 「nanoMuse 云」, 智能体 / 智慧體 instead of the English word "agent", and 中继 / 中繼 for the relay throughout. - **The provider URL field explains a refused `http://` address in the phone's language.** The footer under *Custom API base* that says plain `http://` works only on your own network used to be English in every locale. - **Dates follow the phone's locale.** The computers list, the profile's file cards and the chat drawer format their timestamps with the system's short date and time (the drawer said "now" in English and the computers list used `MM-dd HH:mm`). - **Feed file names, post ids, the profile hash and HEARTBEAT.md keep ASCII digits** whatever the phone's language; a phone set to Arabic or Hindi used to write localized digits into them. - **A large reference photo for the avatar is decoded at a reduced size** instead of in full and scaled down afterwards, so a 48-megapixel picture no longer risks running the app out of memory. - **Links and the share sheet no longer crash a phone without a browser or a sharing app**: the privacy notice on the welcome page, the Cloud help link and the system files' share button fail quietly. - **The connectors' background token refresh runs in one supervised scope**, and two cross-thread flags (the browser hand-over waiter, the profile sync timer) are marked volatile. - Copy buttons use the current Compose clipboard API and the open-in-new icon mirrors in right-to-left layouts; the Android build has no warnings in nanoMuse's code. - CI runs the Android unit tests (`io.github.nanomuse.*`) after building the debug APK. - **The Library shows the signed-in account's files only.** On a phone two accounts share, the Library tab listed the workspaces of the other account's chats as well; it now follows the chat list's rule (the account's own sessions, or the local ones when signed out), and a workspace whose chat is gone is not listed. - **Connector keys and client secrets are masked while typed**, with the eye to show them, like the other key fields in the app. - **The Hands page follows its switch wherever it is flipped**: a change made while the page is open shows at once instead of after reopening it. - **Another device can stop a task it asked this phone to run.** The hub's `stop {call}` / `stop {conversation}` ends the run after the step in flight and the task answers `cancelled`; it used to answer `stopped: false` and let the task run on. Only the device that asked may stop it. ### iOS - **The star card has *I already starred*.** Next to *Star on GitHub* and *Not now*, on the card under the chat header, the account page's card and its allowance row, the new button says the star is done; it and *Star on GitHub* stop the asks on this iPhone for good, whatever the cooldown and the asks left. Before, a person who had starred from GitHub itself kept being asked. In every language the app has. - **A task handed to a computer answers in the phone's language.** The `task` hub call now carries the screen's BCP-47 language; a runtime of 1.0.0 or later replies in it, an older one as before. - **A task on the iPhone can be stopped from the desktop.** The desktop ends a task it started with `stop {call}`, the id of the task frame; the iPhone only knew `stop {conversation}` and answered *not stopped*. Both forms work now, only the device that asked can stop its run, and the task answers `cancelled` as the hub protocol says. - **The Devices row no longer shows a relay sentence while connected.** A relay `error` frame about one frame (`too_large`, `rate_limited`, `bad_frame`) left the row showing the relay's words until the next reconnect although the hub was fine; it is logged and the row keeps saying *Connected*. - **One rule for "on your own network".** The sign-in sheet's *Use a different server* judged a relay address by string prefixes (`10.foo.example.com` passed as private; a Tailscale `100.x`, link-local or IPv6 ULA address was refused) and the Network proxy's bypass used a second list. Both use one rule now: the address is parsed before it is judged, and the private, carrier-grade, loopback, link-local and ULA ranges, a name without a dot and the local suffixes (`.ts.net` among them) count as one's own. - **Larger text reaches the header, the drawer fits its window, the pickers speak to VoiceOver.** The agent's name and status line in the header follow Dynamic Type up to 1.35 times their size instead of staying at 14 and 11.5 pt; the side drawer takes its width from the window rather than the screen, so an iPad window narrower than the screen (Stage Manager, Split View) gets a drawer that fits; the model pickers' rows say *selected* to VoiceOver instead of reading the tick as *checkmark*. - **nanoMuse Cloud can be switched off as a model source without signing out.** *Settings › Models* has a switch, *Use nanoMuse Cloud models*, with one sentence for each state. Off, the account leaves the pickers and the automatic order of the chat, pictures and clips, the avatar studio draws with your own key or says it cannot, and nothing spends the allowance but the explicit *Use nanoMuse Cloud this time*; you stay signed in for sync and your devices. The Cloud provider row cannot be deleted any more (that was the sign-out); its page says why and points to the switch, and the swipe on the providers list no longer offers it. - **A phone with a key of its own works signed out.** The first screen required the sign-in before anything else, so a saved key was of no use without an account. The setup now stands only when there is neither a sign-in nor an enabled provider with a key, and the welcome page offers *Use your own API key instead*, which opens the key sheet and skips the account's pages (password, source). - **A chat pinned to a Cloud model stops at the switch.** A chat whose model was picked from the Cloud group kept running on the account after the provider was switched off; a pinned entry whose provider is off now falls through to the default group like a deleted one, in the chat and in its side tasks. - **The model pickers fold long groups and can be searched.** Each provider group on *Settings › Models* shows at most eight models (the provider's catalogue default for that job first, then the one chosen, then the rest as the provider lists them) and ends in *Show n more* when it has more; once the groups together hold more than eight, a *Search models* field under the Automatic row filters every group live by model id or display name, shows every match, hides groups without one, and says *No model matches* when nothing fits. A key like OpenRouter or SiliconFlow no longer turns the picker into an endless list. - **The Devices row says what the hub is doing.** The hub client now reads the relay's close codes: when the relay refuses the key or the device (4001, 4002) the phone stops knocking and the row under *Settings › nanoMuse Cloud › Devices* reads *Sign in again* instead of *Connecting…* for ever; when the operator paused the hub (`hub_paused`) it waits two minutes and the row reads *Paused by the relay*; a newer connection of the same device (4003) waits 30 s; a dropped network keeps the 1 to 30 s backoff and reads *Reconnecting…*. A link another device asks this iPhone to open is opened only when it is `http` or `https`; a `tel:` number or another app's scheme is refused. - **A one-off routine made after its time runs the next day.** A routine set to *Once* at 18:00 and created at 19:00 never ran and never showed a next time; it now runs the next day at 18:00, and a one-off that has run stays spent. - **Sync tables survive a locked phone.** The per-account sync tables and the chat-owner table were written with the strictest data-protection class, so an app woken in the background while the phone was locked could read them as missing, start empty and write that over them (every conversation pushed again under new ids). They now use the default class, like the chats themselves, and when the file is there but cannot be read the store waits for the next call instead of saving an empty table. - **Pictures and clips go through the Network proxy.** The image and video generators used sessions of their own that ignored *Settings › nanoMuse › Network*; a key of one's own now reaches Model Studio the way a chat turn on the same provider does. Their failures are sentences in every language (*The provider refused this key (HTTP 401)*, *The video took longer than 12 minutes*, *The provider sent no picture*), with the vendor's own words after *The provider says:* when it sent some, instead of English fragments like *HTTP 401: …* or *Timed out after 12 min*. - **Zero compiler warnings in the app's own files.** The scheduler's notification code used completion handlers that the iOS 26 toolchain flags under Swift 6 concurrency; it now uses the async notification-center API, and the background-task id is readable from the registration that runs before launch finishes. A sentence of the avatar flow (*I'll also make four short clips so I can move*) that was missing from the string catalogue is translated in every language. - **A swipe in from the left edge of the main chat opens the drawer** with the side chats, Devices and the settings, as on Android; the round button still does. A side chat pushed over the main one keeps the system's back swipe. - **The main chat follows the chat model you pick.** Choosing a chat model on *Settings › Models* (or *Use it* on the card after a key is saved) set the default for new chats only, and the main chat is never new: on 0.1.41 a person who added a key of their own saw their side chats answer through it while the main chat kept nanoMuse Cloud under the face. The main chat's binding now moves with the pick; side chats keep theirs, and the note under the row says so. - **The pictures and clips pickers say which of your keys they cannot use.** The iPhone draws and films through Alibaba Cloud Model Studio's own endpoints only, so an OpenRouter, OpenAI, Gemini or custom key that the catalogue says can draw was simply missing from *Settings › Models › Pictures*, with nothing to say why. A sentence under the picker now names it, *Not offered here: OpenRouter*, and says where it does work (the desktop app draws through providers that speak the OpenAI images API). - **The ideas catalogue reads without em dashes** (the same file the desktop and the web console carry). - **Rows of chips and buttons wrap on small phones.** The name suggestions of the first conversation and the buttons under the card for an unreachable provider (*Try again*, *Use nanoMuse Cloud this time*, *Network settings*, *Add a key of your own*) sat in one row and were cut off at the right edge on an iPhone SE or with a large text size; they now wrap to the next line. A setup page's fine print is one paragraph with *Learn more* at its end instead of a column beside it. - **Permission prompts in every language.** The iOS prompts for NFC, Bluetooth and Face ID showed English on every phone because the nine `InfoPlist.strings` files lacked them; they are translated now, and the local-network prompt describes this app instead of a virtual machine. - **Words.** In 简体中文 the relay is *nanoMuse Cloud* in every sentence, the name of the Settings row those sentences point to (16 places said *nanoMuse 云* or *nanoMuse 云端*). HEARTBEAT.md shows each routine's cadence in the phone's language, with the Routines list's words, instead of English *daily* / *once* / *every 6 h*. The memory import prompt follows an in-app language change instead of staying in the language of the first launch. - **The build check enforces zero warnings.** *iOS · build check* now fails when the compiler reports a warning in a file under `NanoMuse/`, and its summary lists them; before, the warnings sat in the attached log only. - **No dashes in the words a person reads.** Every sentence of the iPhone's own layer that carried an em dash or an en dash in any of the nine languages was rewritten as two sentences, a comma, a colon or a middle dot: 73 catalogue keys, the helper sentences of the avatar flow, the update check and the relay's refusals, the rows of HEARTBEAT.md and the diagnostics report. Ten keys nothing referenced any more were dropped. The relay is *nanoMuse Cloud* in 繁體中文 too (17 places said *nanoMuse 雲*), and Russian uses Apple's *Эл. почта* for e-mail. A new test, `NanoMuseCopyTests`, fails on a dash, an exclamation mark or a translated relay name in our keys. - **A wrong key is noticed when it is saved.** The vendor sheet tries the key against the provider's model list before keeping it; a 401 or 403 leaves the phone as it was (a provider that already existed keeps its previous key) and says *The provider refused this key (HTTP 401). The provider says: ... Check the key and paste it again.* under the field. Before, upstream's fallback seeded a catalogue list and the sheet closed as if the key were fine. - **The reach card says when a rate limit resets.** `Retry-After` from a plan's 429 now reaches the card, so *Resets in 2 h 5 min* shows for a ChatGPT or Claude plan as it already did for the relay's own answers. - **One entry for *Search models*.** The strings catalogue listed the key twice; the entry that stays carries the Traditional Chinese 搜尋模型 for the search field above the model pickers, and a test now fails when any key in `Localizable.xcstrings` appears more than once. - **Labels that fit.** A post's detail rows size their label column to the longest label instead of 72 pt, so *Geschrieben* and *Написано* stay on one line; the avatar size is a menu instead of five segments, which cut off *Extra large* and *Очень большой* on a 375 pt phone. ### Project - `scripts/android/env.sh` defaults `GOPROXY` to Go's own `https://proxy.golang.org,direct`, as the showcase's Docker build already did, instead of a mainland-China mirror; a `GOPROXY` exported beforehand is kept, and `docs/android.md` says how to point it at `goproxy.cn` on a mainland network. - The Linux `.deb` names the maintainer by the project's GitHub no-reply address (`lgy0404@users.noreply.github.com`) instead of a personal mailbox; electron-builder reads it from `harness/desktop/package.json`'s `author`, which is the only place in the repository that carried the old one. - Rewrote the docs page *One account, all your devices* (`docs/trial.md`, 简体中文 twin too) from the private trial's runbook into a walkthrough of what ships: sign-in on the phone, nanoMuse Desktop, the web console, remote control and its approvals, what follows you, your own relay. - Brought the docs up to 0.1.41: the troubleshooting item for a desktop that does not start now describes nanoMuse Desktop and its `desktop.log`; `hub.md` names the desktop's and the iPhone's hub code and the *Remote control without asking* switch instead of the retired terminal commands; `every-device.md` says the iPhone answers `task` and has had the shape since 0.1.34; `cloud.md` gives *Data controls*, *Conversation sync* and *Allowance* their own sections; `ios.md` says build 14 is the one on TestFlight. The glossary in CONTRIBUTING gained the Models page's terms. - The showcase phone turns itself on, on demo.nanomuse.dev and in the frame on nanomuse.cn alike, and the page presses *Start* once a person is looking (a few seconds in view with the tab in front, or a pointer, key, touch or wheel), never for a scripted browser or before the sign-in it asks for; a line tapped before the Muse exists presses *Start* and waits in the chat. - The showcase's disclaimer names every platform the project ships for (Android APK, iPhone and iPad through TestFlight with where the public link stands, macOS on Apple Silicon and Intel, Windows, Linux, the Docker image with the web console) and points at nanomuse.cn/#download and the latest release instead of one version's files. - Fixed *Allow once* and *Deny* on the capsule over an operated app, which failed with "could not send your answer" because the browser's preflight to the session host was refused; the gateway now answers it. - The phone operator writes the step shown on the capsule in the person's language; the English step sentences on a Chinese phone are gone. The agent now sees an English question as English rather than "Chinese or English", so an English line gets an English answer and English step labels. - The phone operator stops after three taps on the same spot that change nothing, including taps a few pixels apart; it had kept tapping a checkbox for seven minutes on the showcase's 12306 line. - The showcase phone's screenshot no longer copies the apps kept open in the background (the shell hides them with `display: none`, and the capture cloned every one of them): a capture over WeChat went from about five seconds to under half a second, and the phone no longer froze for minutes once Weather was opened after 12306. The capture also keeps a bar hung from a zero-height anchor, so WeChat's tab bar is in the picture again. - The showcase's 12306 line asks for the earliest train rather than the earliest high-speed train; the filter the app offers for that is what kept the operator busy. The `amap`, `train-tickets` and `kuaidi100` skills say not to look for their tools with a shell command, and `amap` that a forecast read from the phone's own Weather app is the whole answer; the showcase's weather and 12306 lines used to end in a pending shell approval. The weather line itself now asks a question the app answers in one screen (will it rain in Beijing tomorrow). - The showcase page's language switch also sets the phone's language and the hosted Muse's reply language, so an English visitor no longer gets a Chinese welcome page, capsule or step labels; the chat model still answers in Chinese now and then after Chinese screen text. - The showcase phone comes back to nanoMuse after the agent has opened or read an app straight from the chat and answered; it had stayed on Weather with the answer out of sight. - The showcase's dark theme is whole: a dark stage, dark guide and dock cards, readable group headings; *Power off* has its own row on narrow screens instead of sitting on the phone's status bar. - The showcase page fits its frame. The panel beside the phone (the headline, the notice with the downloads, the lines to try, the buttons, the fine print) was taller than most laptop windows and scrolled inside the frame on nanomuse.cn, against the page's own scrolling; it is now two columns, the words and the lines, no taller than the stage, so at 1366×768 and up the whole demo is in view at once and nothing inside the frame scrolls (only a window under about 600px of stage scrolls the lines inside their column). The wheel hand-up to the homepage went with it: the browser passes the scroll on by itself when the frame has nowhere to go. On a phone the page is one column, the phone at its own size, and the page itself scrolls. - The phone's welcome, sign-in and capsule texts lost their em dashes and say nanoMuse Cloud by name. - The showcase phone opens its Muse with the access token in the address's fragment (`#token=`), the form the runtime has printed since 0.1.31, instead of the older `?token=` query that reached the gateway; the gateway's log line for a container that does not answer names the path without the query, and a refusal from a session's host carries the CORS headers so the page reads the reason instead of a bare network failure. - A session token no longer reaches the showcase gateway's log at all. uvicorn wrote the request line with its query string, so a page or app still on the older `?token=` form put the token in the log with every WebSocket upgrade and page load, and httpx named the whole URL of what the gateway relayed to the container; the gateway now rewrites any `token=` value in those lines to `[redacted]` before it is written, and a test drives both the socket and the page with such an address and checks every record. The older form is still accepted this release. The showcase README says where the token travels (the socket's first frame, the address's fragment) and that Caddy keeps no access log for the showcase's names. - The release scripts run on macOS too: `release-bump.sh` edits in place with BSD sed as well as GNU, and `release-apk.sh` checksums with `shasum` where there is no `sha256sum`. `release_notes.py` lists a pull request merged by rebase under *What's Changed* (it read merge commits only, and the project merges both ways). `scripts/android/env.sh` looks for the SDK where Android Studio installs it instead of one machine's path. Removed `scripts/gen-avatar.py`, which drew the red panda's Android drawables from a file that left with the red panda in 0.1.23 and could no longer run. - Removed `scripts/rootfs/`, the rootfs builder of the retired Python line (it stays in the history at the tag `pre-openminis`), and added an `.editorconfig` that states the indentation and line endings the tree already uses. - The Code of Conduct says where a conduct report goes (SECURITY.md never had an address), the pull request template lists the current obligations (CHANGELOG line, every locale, docs twin, generated catalogues, DCO), and the issue forms read without dashes. - The READMEs in all ten languages, the whole changelog and the release notes of 0.1.1 to 0.1.40 no longer use dashes as punctuation; a colon, comma, full stop or parentheses stand where they stood, numeric ranges excepted, and Russian keeps the dash its grammar requires. Chinese release notes write 非营利. ## [0.1.41] - 2026-10-07 · Choice Choice: a person with a key of their own now picks which model does each of four jobs, on every client. *Settings › Models* has one row each for *Chat*, *Operating the screen*, *Making pictures* and *Making clips* on Android, the iPhone and the desktop, and the web console's *Connections* page has the same four slots; after a key is saved a *Use it for* card asks what it should handle, a row nobody set follows one order under an *Automatic* entry (the chat model's provider when it can, then nanoMuse Cloud, then the first key that can), and a model of your own that fails never falls back on its own: the card offers *Use nanoMuse Cloud this time* for that one turn. The desktop draws pictures through your own key and picks up a new hands model without a restart, the runtime gained `PUT /api/connections/image` and `/video`, the README opens with the film, the paper is on arXiv, and the docs site has a 简体中文 edition. ### Cloud - **A sign-in for the app store's reviewer.** `REVIEW_ADDRESSES` (e-mail addresses) and `REVIEW_CODE` (six digits), both empty by default: a code request for one of those addresses sends nothing and answers as if it had, and `/v1/auth/verify` accepts exactly that code for it, under the same code lifetime, attempt and rate limits as anyone's. The account is an ordinary one; the admin console tags it *review* in the People table and on its page and leaves it out of the sign-up counts on the Overview and the Stats page. With either value empty nothing changes. - **The web console says why the relay refused**, in Chinese and English: sign-ups paused, service paused, sync paused, device hub paused, account deleted, too many requests under way, rate limited; a paused free allowance is told apart from a spent one; a deleted account signs the page out, and a paused hub is shown as such. - **The console's files are revalidated on every load**, so a deploy reaches the next reload; both console pages set the document language to the one they draw. - **The star card's sentence can be set from the console.** The nudges policy gained `star.text` (English) and `star.text_zh` (简体中文), each at most 200 characters, empty by default; *Settings › Star asks* has the two inputs with a remaining-character count. An app in Chinese shows `text_zh`, else `text`, else its own sentence; other languages show `text`, else their own; only the sentence changes, not the card's title or buttons. Apps built against relay 0.22 and earlier ignore the two fields. - **The visitor's address is read from `X-Forwarded-For` only behind a proxy.** The relay took the header's first hop from any peer, so a relay reached directly (the local compose file with `RELAY_BIND=0.0.0.0`, or no Caddy in front) let a request name its own address and so sidestep the per-address limits on codes and sign-ins. It is honoured only when the socket's peer is a loopback, private or otherwise non-global address (or one named in the new `TRUSTED_PROXIES`); from the open internet the peer itself counts: uvicorn's own `forwarded_allow_ips` is set to the same list instead of `*`. The self-host `Caddyfile` carries the production one's body limit (20 MB, above the relay's 16 MiB), security headers and ten-minute stream timeouts. - **`cloud/.env.example` lists every setting the relay reads** (the code and password limits, the upstream timeout and in-flight cap, the members' catalogue, the geolocation files, the operator-page sources) each commented with its default; the README had called it the full list. ### Runtime - **The sync push delay is read when a push is scheduled.** `ConversationSync.push_soon()` took the two-second default at import time, so a shorter delay set on the module (the tests do this) did not reach the pushes after a sign-in or the switch; it does now. Nothing changes for a person: the delay is still two seconds. - **Stopping ends every request to the relay before the connection to it closes.** The presence note a turn sends once its push is through (`working`) survived being cancelled and went out while the runtime was shutting down, racing the cloud client's close; the hub's background tasks were cancelled but not waited for. Stopping now cancels and waits for the sync engine's, the hub's and the profile's tasks, then closes the HTTP client, and a task that would start on the way out is refused. Nothing changes for a person; the tests' fake relay no longer waits on a half-open connection when it stops. - **`nanomuse --help` rows are whole sentences again** (the `mcp` row had lost the words in brackets); `nanomuse serve` no longer prints "nanoMuse · nanoMuse is ready."; the no-key notice also mentions signing in to nanoMuse Cloud. - **The runtime owns the first conversation** (contract C4, as on the phones and the desktop): `GET /api/firstrun`, `POST /api/firstrun/start` · `pick` · `dismiss`, the `firstrun` socket frame, the state in `firstrun.json`. It tells the model about the ritual only in the chat the conversation is bound to (never in a routine, a feed post or another chat) reads the model's `nanomuse-naming` block when a reply ends, writes what to call you to the profile and to memory (*Call them: …*) and the agent's name to the profile. - **`PUT /api/connections/llm` takes `proxy`** (`http://`, `https://`, `socks5://`, `socks5h://` host and port; `""` clears it) and writes `[llm] proxy`; the connections view shows it with any password as dots. A SOCKS address is refused with the package to install when `socksio` is missing. - **`/api/nudges` keeps the relay's `star.text` and `star.text_zh`** (200 characters at most) for the star card's sentence. - **A file whose name is not ASCII can be downloaded from the web console again.** `/api/files/…?download=1` failed with 500 for a name such as 报告.md; the download header is now encoded. - **`nanomuse config init` writes the `[cloud]` and `[hub]` blocks and `gui.reconnect_grace_s`** with their defaults explained; the configuration page documents them too. - **Every model slot can be chosen through the app layer.** `PUT /api/connections/gui` takes a catalogue id (`bailian`, `openrouter`, ...) as `PUT /api/connections/llm` does, and reports the id a URL stands for; the new `PUT /api/connections/image` and `/video` (with `GET`) write the `[image]` and `[video]` slots: `provider`, `model`, `base_url`, `api_key` into the vault, all empty to clear, a provider without the capability refused with the one sentence. A slot nobody chose follows one order: the chat provider's own model when it can (its hands model when it sees, its picture model when it draws), else the account's when signed in, else the chat model or nothing. `[video] api_key` is sent to the video host; before, the picture host's key went. ### Web - **The first conversation happens in the chat.** The first-run list is *Add a model → Connect (optional) → Start*; the *Meet your nanoMuse* page is gone (the identity form stays under Settings). *Start* opens the chat, where the agent speaks three opening lines in your language, asks what to call you, and a card under its reply offers two names for it (its own suggestions or two from the built-in pool) and *Something else*; the model's `nanomuse-naming` block is never shown, not even while streaming. Its turns do not count as tasks for the star asks. - **A *Proxy (optional)* field** on the own-key model form, for that provider's requests only; nanoMuse Cloud never goes through it. - **The star card's sentence can come from the relay** (`star.text`, `star.text_zh` for a Chinese UI); the title and buttons stay the app's. - **The unit tests no longer depend on the developer machine's language.** - **Chinese copy writes 非营利** (not 非盈利) in the welcome notice and the account page; 89 dictionary entries no key referred to any more were dropped. - **Connections has a *Making pictures* and a *Making clips* row.** Each shows `provider · model` as the runtime resolves it and opens a picker with nanoMuse Cloud when signed in, the chat model's provider when it has the capability, and every catalogue provider that has it under *Add a provider*; a provider without it is not offered. A row nothing covers shows the one sentence and *Add a provider*. The *Pictures and clips* field under the chat model is gone. *Automatic* is the first option of the *Hands model* select and of both media rows, as on the phones and the desktop: saving it returns the slot to the resolution order (the hands to the provider's own hands model, or the account's when the provider cannot see), and a *Currently provider · model* line under the control says what that resolves to today. ### Desktop - **Coding agents page** under *Settings → Coding agents*, also opened from the *Coding agents* chip on a device card: this computer's Cursor, Codex and Claude Code chats first (version, how many run now, recent chats), the account's other computers after; open a chat to read it, send a message and watch the run stream with its tools, stop it. A computer with none of the three installed says so. The desktop now announces and answers the `coding.*` hub actions itself (a phone sees a computer with only the desktop app as a coding computer) with `coding.send` and `coding.stop` behind the same remote-control gate as `shell` and `files`. - **The other device's name in "… is working"** under its prompt, instead of a fixed word. - **The Cloud page is called *nanoMuse Cloud* everywhere**, in the settings sidebar and in every sentence that points to it; the *Upgrade* link left the usage card (nothing is sold). - **Corrected sentences** about where chats live (sync is on by default), where the look is changed and which site the update check reads first; the UI-TARS-desktop operator is credited under Thanks. Chinese wording follows the phone (在线, 快捷聊天, 非营利, 形象工坊, 操作屏幕) and uses 「」 quotes. - **A proxy for the model providers.** Settings → nanoMuse Cloud → Network takes one address (`http://host:port`, `https://`, `socks5://`, `socks5h://`; a `user:pass@` is shown masked). The shell puts it on the host process's environment at the next start, so your own keys, the ChatGPT sign-in, *List models* and the hands' runtime go through it while nanoMuse Cloud and loopback never do; *Restart now* under the row restarts the host without closing the window. - **The Permissions page lists every standing grant by risk tier.** A *Standing grants* section groups what this computer remembers (the remote-control switch, the devices allowed without asking, the hands' per-app grants) under *Runs without asking*, *Remembered from the card* and *Runs, then tells you*, each row with what was allowed, for whom or where, when, and *Revoke*; one host route lists them and one revokes any of them. - **The star card's sentence may come from the relay.** When the nudge policy carries `star.text` / `star.text_zh` (up to 200 characters), the card and the header line show that sentence (`text_zh` in a Chinese UI, else `text`) and the app's own words otherwise; the title and the buttons stay the app's. - ***Report a bug* fills in the issue form's own fields** (where, version, platform, the screenshot's name); the facts had travelled as a plain `body`, which the form drops. - **Settings → Models, right after General**: one row each for *Chat*, *Operating the screen*, *Making pictures* and *Making clips*, every picker listing nanoMuse Cloud first while signed in (its recommended model marked) and then each of your providers' models that can do the job; a row nothing can do names who could and offers *Add a provider*. The two pickers left the nanoMuse Cloud page, which now links here. A row you have not set follows the provider new chats answer through when it is one of yours, then nanoMuse Cloud, then the first of your providers that can; those three rows have *Automatic* as their first entry, the row saying what it gives right now, and picking it drops a choice you made so the row follows that order again. A hands change takes effect at the hands' next step: the plugin now mounts the hands' MCP client itself and restarts `nanomuse mcp` with the new model, no cold restart; a local server without a key is passed to the hands too. Pictures through your own key go straight to that provider (Model Studio, OpenRouter, OpenAI, Zhipu, SiliconFlow, Volcengine, xAI) and are not billed to the account; the Media page's image row became a picker. - **After a key is saved, a *Use it for* card** offers a toggle per thing the key can handle, all on, each naming the model it would get; *Use it* switches those rows, *Not now* changes nothing. When a model of your own fails under a turn, the card offers *Use nanoMuse Cloud this time* while signed in: that one message again through the account, the chat back to its model when the turn ends, the Models page as it was; the same button follows a failed studio round and a failed set of clips. Nothing falls back on its own. ### Android - **The lights while the hands work breathe instead of running:** the comet round the screen's edge and the scan line are gone, the glow breathes 2.4 s in and 2.4 s out, and the capsule's ring and bars breathe at the same pace; all of it holds still under the system's reduce-motion setting. - **The first run's Hands page shows the app's mark.** - **The first conversation says where your messages go with sync on:** to the model, and (signed in) to your other devices through nanoMuse Cloud, which Data controls switches off. - **The star card showed the sentence set in the relay's console** (`star.text`, `star.text_zh` for a Chinese UI) when the operator set one; without one, the app's own line for the moment as before. The title and the buttons stay the app's. - **Chinese copy writes 非营利** (not 非盈利) in the welcome and Cloud notices. - **Settings got a Models page** with four rows, Chat, Operating the screen, Making pictures and Making clips, each showing ` · ` and opening a picker of nanoMuse Cloud's models (signed in, the recommended one first) and the models of your own providers that fit; the card at the top of Settings opens it, a chat pick there or in the chat's menu is the default for new chats, the screen, pictures and clips pickers open with Automatic (what the order gives right now; choosing it forgets a choice made there), and Settings → Hands and the old Image & video models page use the same choices. - **Saving a provider of your own asks "Use it for":** one switch per thing the vendor can do, all on; Use it moves those rows to that provider on the catalogue's default model, Not now changes nothing. When you have not chosen, the screen, pictures and clips follow the chat provider when it is your own and can do the job, else nanoMuse Cloud when signed in, else the first provider of your own that can; and a failed turn on a model of your own no longer falls back on its own: the error card offers Use nanoMuse Cloud this time for that one turn. ### iOS - **The sign-in sheet shows the app's mark**, asks for a mainland-China phone number or an e-mail, says so under the field as soon as a number from elsewhere is typed, and its footer tells what the relay keeps and links the privacy policy. The first run's Notifications page shows the mark too. - **After you pick a new face with a video model set**, the agent says the four clips follow in the background. - **In the account's Devices list** an offline device shows when it was last seen, and a tap on an online one starts a message to it in the chat. - **A fresh install no longer shows the home screen for an instant** before the welcome page; the first conversation says where your messages go with sync on, as on Android. - **The star card showed the sentence set in the relay's console** (`star.text`, `star.text_zh` for a Chinese UI) when the operator set one, as on Android; the title and the buttons stay the app's. - **Chinese copy writes 非营利 / 非營利** (not 非盈利) in the welcome and Cloud notices. - ***Report a bug* fills in the issue form's own fields** (where, version, iOS version and model) instead of a plain `body`, which the form drops. - **Settings › Models showed four rows: Chat, Operating the screen (not on iPhone), Making pictures, Making clips**, each with its provider and model and a picker grouped nanoMuse Cloud first, then your own providers that can do it; picking a chat model changed the default for new chats, for any provider, and the page said so. Pictures and clips followed one order when nothing was chosen: the chat provider's own default, then nanoMuse Cloud, then the first key that can, with the models taken from the catalogue; an *Automatic* entry at the top of those two pickers, showing what it currently resolves to, forgot a stored choice and returned the slot to that order. - **After a key was saved, a *Use it for* card asked which slots it should take**, all switched on, with *Use it* and *Not now*; saving alone no longer switched anything. When a model of your own failed, the card offered *Use nanoMuse Cloud this time* while signed in, retrying that one request on the relay without changing a slot. ### Project - **The README opens with the film:** the 74 s film plays inline at the top of the README and its nine translations (English, or Chinese in the two Chinese READMEs) in place of the four screenshots; nanomuse.cn and the docs home play it too. - **The paper is on arXiv** ([2610.08699](https://arxiv.org/abs/2610.08699), *nanoMuse: An Open-Source Personal Agent for Every Device You Own*). The README and its nine translations carry an arXiv badge, a *News* list of milestones (the paper, the latest version, the first release; a release now replaces one line instead of shifting three) and a *Citation* section with the BibTeX; `CITATION.cff` names the paper as the preferred citation, so GitHub's *Cite this repository* gives it; the docs site's home pages have a *Paper* button and the citation, and nanomuse.cn links the paper. - **nanomuse.cn has a 404 page** of its own, with the site's bar and theme. - **The showcase has an embed mode** (`https://demo.nanomuse.dev/?embed=1`), for the frame on the homepage: no header of its own, the phone waits for the visitor's tap instead of turning itself on, the site's language and theme are followed, and links leave the frame. - **The docs site looks like the homepage:** Home and Try it in the nav, the brand colour and system type, the logo served with the site rather than from GitHub. - **A 简体中文 edition of the docs site** under `/zh/` (every page translated, a language menu in the nav, Chinese sidebar, search and footer) with a *Translating the docs* section and a glossary in `CONTRIBUTING.md`; the nav shows the repository's star count. `calls.md` left the sidebars (its links open the file on GitHub). The Chinese pages follow the audit's changes to the English ones. - **The README has a *News* section**, the browser-demo and website badges and the public TestFlight link, in English and its nine translations, whose literal sentences were rewritten with native readers; the devices figure shows 0.1.40 (the hands' run kept as a trajectory in the chat). - **The maintainers' release recipe** in CONTRIBUTING.md was rewritten, with `scripts/release-bump.sh` and `scripts/release-docs.py`, so a release can be made from the repository alone. - **The privacy page** says that the operator's page shows an address's country, province and city from an offline copy of ip2region, computed when drawn and not stored; `docs/cloud.md` says the star policy sets when an ask may appear, not its words; docs/parity.md, docs/ios.md and docs/roadmap.md describe 0.1.40's behaviour and the paper's roadmap. - **CI runs the relay's `ruff check` and `pytest`** on every change under `cloud/`; it never had. - **`THIRD_PARTY_NOTICES.md` covers the iOS app**, which it said had been removed: iSH, FFmpeg and LAME, rclone, cppjieba, the tiktoken vocabulary, the models.dev snapshot and the Swift packages, with their licenses. `docs/cloud.md` describes how each app is pointed at another relay as the apps do it today; test fixtures use `.example` addresses only. - **One bug form for every surface but Android**: *Where* offers the web console, the desktop app, the iPhone app, the relay, the terminal, the daemon, Docker, the library and the showcase; the repository has the `android`, `ios` and `desktop` labels the forms apply, and the Android form no longer claims to be pre-filled by the app (its link opens the chooser, for bugs and ideas alike). Dependabot watches the packages where they are (`harness/desktop`, `harness/dsh-nanomuse`, `website`); it had pointed at a directory that no longer exists. ## [0.1.40] - 2026-10-06 · Clear Clear: the iPhone's input field is in the clear again, it sat behind the bottom bar, a safe-area inset the chat stopped keeping clear of once the keyboard had come and gone; the bar and the composer are plain rows now, and a page in Settings shows where the app laid them out. Every chat on a phone belongs to the account that made it, so a shared device keeps accounts' chats clearly apart, and signing out asks one question. Every refusal from the relay is one clear sentence, or a card, on every client, and the operator of a relay can close the door with a switch on a *Controls* page, with *Stats* and *Site* beside it. The Mac takes a true picture of the screen with ScreenCaptureKit or says why it cannot, Windows starts again after the update that moved the app, and the desktop's lights, glow and capsule move as the phone's do. ### Cloud (relay 0.22) - **A *Controls* page in the admin console.** Switches for the free allowance, sign-ups, the cloud service, conversation sync and the device hub, applied at once and kept across restarts, each saying what turning it off does; threshold rules that close sign-ups, pause the allowance or sync, or send a notice when the account count reaches a number; and an audit log of who changed what and when. Apps see `403 signup_closed`, `503 service_paused`, `503 sync_paused`, `503 hub_paused` or `allowance_exhausted` with `paused: true`, and `paused` in `/v1/config` and `/v1/me`. `nanomuse-cloud admin controls list|set|audit` flips a switch from the relay's shell. - **The repository's numbers on the *Site* page.** Stars, forks, watchers, open issues and release downloads per day, read from GitHub every six hours or on demand, with table and CSV (`GITHUB_REPO`, `GITHUB_TOKEN`, `GITHUB_COLLECT`). - **A *Stats* page.** Accounts, devices, model calls and tokens, allowance use, sync volume, errors and API calls per UTC day and by category, each with a note on how it is computed, a table and a CSV; the counters behind it survive a restart. - **Full addresses in the console.** The console now shows accounts' phone numbers and e-mail addresses in full in the People table, the top spenders, the events and the account page; logs, e-mails and public pages keep the masked hint. - **Deleting the account forgets all of it.** Deleting the account also forgets its live *working* notes; a test now checks that every table is empty for the account afterwards and that the same address signing up again is a new, empty account. Deleting an account leaves a 90-day tombstone of its keys' hashes (no account id, address or device), so a device still holding one of them is told `401 account_deleted` rather than `bad_key` and can clear its copy; clients that do not know the code see a plain 401 as before. ### Runtime - **Window mode through the Mac helper.** Window mode on a Mac under the desktop app lists and captures windows through the helper's ScreenCaptureKit routes; the runtime's own `CGWindowListCreateImage` capture is used only without the helper, with a log line that says so. A window the helper cannot capture because Screen Recording is off parks the hands on the whole screen with the helper's reason in the observation, and tries the window again on the next look. - **A proxy for own providers.** Added `[llm] proxy` for own-key providers and the ChatGPT plan. - **The ChatGPT plan's failures in one sentence.** The ChatGPT plan reports its failures as one plain sentence each (unreachable, region blocked, signed out, no quota, rate limited) with a code and the time to wait, in English and Chinese. - **The plan's rate-limit windows.** Read from every response and from OpenAI's usage endpoint, shown by `nanomuse chatgpt usage`, the proxy's `/v1/usage` and the web app's `/api/chatgpt/usage`. The Codex request carries `text.verbosity` and `prompt_cache_key` as nanobot's does. - **What the relay refused, in one sentence.** The web app and the desktop's hands runtime now say in one plain sentence what the relay refused: a message too large for the model's window, an invitation-only relay, too many turns at once, a busy provider, the free allowance paused by the operator (not used up), sign-ups paused, and the cloud service, conversation sync or the device hub paused (in English and Chinese) instead of "The model provider answered with an error". ### Desktop - **Every working light breathes.** Every light that said "working" (the chat and status dots, the other device's dot, the thinking dots, the microphone, the glow round the screen) now breathes at the phone's rhythm (2.4 s in, 2.4 s out) instead of running, flowing or rippling, and is a steady light under the system's reduce-motion setting. - **The glow and the capsule are the phone's.** The glow and the capsule while the hands work are now the phone's: a light breathing along the four edges (blue working, amber waiting), the marker (ring, turning cyan arc, dot, the action's name) at the exact point of each click, a dashed path with an arrowhead for a drag, and a pill at the top of the screen with the face, *Step N*, what the hands are doing, *I'll take it* and *Stop*, which moves out of the way when the hands act under it. - **The trajectory in the chat, not a Live stage.** Removed the picture-in-picture Live stage. In its place the chat shows the trajectory of a hands run during and after it: each step's screenshot with the action drawn on it and the agent's words before the step, with previous/next, the arrow keys, *Open large* and *Copy this step*; 40 steps per run, 4 runs and 64 MB of pictures are kept in memory. - **The relay's refusals as cards.** When nanoMuse Cloud refuses a turn, the chat shows a card instead of the relay's reply: the allowance used up (the ways on (your own key, a plan you already pay for, an invitation) and *Try again*), a message too large for the model (*New chat*), a sign-in that expired (*Sign in*), the relay busy or not answering; in English and Chinese. A refusal is no longer retried five times as a rate limit before it shows. The cards know the operator's switches too: the allowance paused for now (the same card with a first sentence that says so), the service, sync or hub paused, one sentence each and *Try again*, never retried as a rate limit. - **Ways on, from the relay.** The ways-on block under the pool in Settings → nanoMuse Cloud reads the relay's guidance (the providers for where you are, the plans, the caveats) instead of a list written into the app, and opens the key rows on the same page rather than the harness's Models settings. - **A heads-up at 80 %.** A one-line heads-up above the composer when 80 % of the allowance is spent, once per pool size, as the phones show it. - **An own-key refusal in one sentence.** A turn on your own key that the provider refused gets one plain sentence by the kind of failure, with the provider's reply folded under *What came back*. - **ScreenCaptureKit on macOS 14 and later.** The Mac helper *nanoMuse Computer Use* now takes the screen with ScreenCaptureKit on macOS 14 and later (`CGDisplayCreateImage` returned nothing on macOS 26); macOS 12.3 and 13 keep the CoreGraphics capture. The helper also lists the windows on screen and captures one window (`GET /windows`, `POST /window`) for the runtime's window mode. - **No substitute picture.** When the helper cannot take the picture the app no longer substitutes an Electron `desktopCapturer` frame: the screenshot fails with the helper's own words (ScreenCaptureKit's error, the missing Screen Recording grant, or that the helper did not start), and the log says `helper screenshot failed (…) — not falling back to desktopCapturer`. - **A truthful Screen Recording check.** The Screen Recording check on macOS 14+ asks ScreenCaptureKit, so `/info` reports *granted*, *denied* or *unknown* truthfully; the status also names the capture source. - **Windows starts again after an update that moved the app.** 0.1.39 installed to `Programs\nanomuse-desktop` where 0.1.30 had been under `Programs\nanoMuse\…`, the profile's link to the plugin pointed at the old folder, and the launch died on `EEXIST: file already exists, symlink …`: the stale junction was being removed through a call that follows the link first and, finding nothing at its end, left it standing. The link is now removed as a link and remade; if that still fails, the message names the path to remove by hand. ### Android - **Every chat belongs to an account** (contract C12). Every chat on the phone now belongs to the account that was signed in when it was made (or to nobody, while signed out), synced or not; the chat list, the search, the home and the push show the signed-in account's only: a side chat or a pre-0.1.39 chat no longer shows under the next account. - **Signing out asks about the chats.** Signing out asks *Keep this account's chats on this device* (off by default): off, the account's chats, memory, feed, goals, routines and face are removed from the phone; on, they are put aside and come back at the next sign-in. Signing in as another account, and *Change server*, go through the same question. *Delete the account* removes all of it with no question. A key the relay refuses does not remove the account's data: the chats, memory, feed, goals and routines are put aside, the sign-in page says *Your sign-in on this phone was ended, sign in again to continue; your chats are kept on this device until then*, and the same account's next sign-in restores them. Only a relay answer that the account was deleted removes them. - **No device backup.** The app no longer takes part in the device backup (`allowBackup="false"`): nothing goes to Google, a reinstall starts empty, and the account's chats come back through sync. - **A plain card when a provider cannot be reached.** Showed a plain card instead of the socket text when chatgpt.com or an own-key provider cannot be reached, when OpenAI does not serve the region, when the ChatGPT sign-in has expired, and when the plan has nothing left in the window; each says what helps and has *Try again*. - **An HTTP proxy for own providers.** Added Settings → Network → HTTP proxy for own providers (host, port, optional user and password; off by default; this device only), used for own-key providers and the ChatGPT plan but never for nanoMuse Cloud or the computer, with a *Test* row. - **Every way on, on the allowance card.** The allowance card now lists every way on: wait, your own key, a model of your own, your computer, invite; the daily cap gets its own line. - **What the relay refused, in one sentence.** Every refusal nanoMuse Cloud sends is now one plain sentence in the phone's language with the right button: a message too large for the model (*New chat*), a key the relay no longer takes (*Sign in*), a private relay or a closed sign-up (*Open Settings*), a busy provider with the wait, the operator's pauses of the service, the sync or the device hub (*Try again*), in all seventeen languages; a `429`, a status code or the relay's JSON no longer shows. When the operator pauses the free allowance, the allowance card says it is paused, not used up, and that what is left stays as it is. - **The ways on, from the relay.** The ways on when the allowance runs low (in the chat card and under Settings → nanoMuse Cloud) list the providers, plans and guide the relay sends for your region (`spend.guidance`), and the bundled catalogue only when the relay sends none. ### iOS - **The input field stays after the keyboard goes.** Four releases reported a chat with no input field; it was there: behind the bottom bar. The bar was a safe-area inset of the home screen, hidden while the keyboard is up, and once it had left and come back the chat no longer kept clear of it: the field showed when the app opened and was gone for good after the first dismissed keyboard. The bar is now a row under the rooms, as on Android, and the composer column (the cards, the tool strip, the input bar) a row under the message list: plain layout, no overlay of the UIKit list, no second host to switch to; the list is given no bottom inset for it, the `/` and `@` popup stands on the column's top edge by layout. The watch stays as the last net and rebuilds a column that detaches or measures 0 at most twice per appearance. - **Settings → Appearance → Composer check.** For a chat whose input field does not show: a switch draws a red frame around the composer column so a screenshot shows where the app laid it out, and a report says the same in numbers, the window and its safe area, the column's frame, every UIKit ancestor with its frame, hidden flag and alpha, the text fields and collection views in the window, the watch's events, with *Copy* for a bug report. The view hierarchy a device can give without a Mac; in nine languages. - **Every chat has an owner** (contract C12). The lists, the Chat tab, the Library, *Today's chats*, the Siri shortcuts and the push show the signed-in account's only; signed out, only the chats made while signed out. - **Signing out is a sheet.** Signing out (here, everywhere, or to use a different server) is a sheet with *Keep this account's chats on this device* (off by default); off, the account's chats, memory, feed, goals, routines and face leave the phone, its sync table with them; on, they are put aside and restored with the account. *Delete the account* removes everything of it on the phone. A key the relay refuses keeps the account's data on the phone, put aside until the same account signs in again, with the same sentence on the sign-in page as Android; only a deleted account removes it. - **The relay's key stays on this device.** The relay's key is kept on this device only (never iCloud Keychain); a fresh install sweeps the device-only Keychain items a previous install left behind. - **A plain card when a provider cannot be reached.** Showed a plain card instead of the connection error when chatgpt.com or an own-key provider cannot be reached, when OpenAI does not serve the region, when the ChatGPT sign-in has expired, and when the plan has nothing left in the window; each says what helps and has *Try again*. - **An HTTP proxy for own providers.** Added Settings → Network → HTTP proxy for own providers (host, port, optional user and password; off by default; this device only), used for own-key providers and the ChatGPT plan but never for nanoMuse Cloud, with a *Test* row. - **The allowance card and the 80 % heads-up.** A turn refused because the free allowance is used up, paused or past today's share pins a card under the chat header (what happened, the ways on (your own key, a plan you already pay for, an invitation), *Try again*) instead of the provider's error text; at 80 % of the allowance one line above the composer says how much is left and where the ways on are, once per pool size. - **What the relay refused, in one sentence.** Every refusal nanoMuse Cloud sends is now one plain sentence in the phone's language with the right button (too large → *New chat*, key refused → *Sign in*, busy with the wait, the operator's pauses → *Try again*), in all nine languages. - **The whole catalogue, and the plans you already pay for.** *Use your own model key* lists the whole catalogue (the region's providers first, what each covers, *Get a key*, the key taken on the vendor's own sheet) and the plans: sign in with ChatGPT (with the line about OpenAI's terms), Claude, Kimi or OpenRouter; a server on a computer of your own (Ollama, LM Studio, vLLM) takes its address. The list follows the relay's guidance for your region when it sends one. ### Project - **`docs/parity.md` re-audited.** Re-audited against the code of the four clients, with rows for the relay's refusals as cards, the 80 % heads-up, the hub tools, voice, share and export, bug reports and the UI language; linked from the docs home. - **`docs/sync.md`.** Every piece of state on the phones, where it lives, whose it is, and what happens to it on sign-out, switching accounts, deleting the account and reinstalling. ## [0.1.39] - 2026-10-06 · Keys Keys: the allowance is a start, your own key or a plan you already pay for is the way on, and every client now says what each gives you, one provider catalogue with capabilities on the phones, the desktop, the web console and the relay, a ChatGPT sign-in that covers chat and the hands and says so, one sentence where a picture or a clip cannot be drawn. A device shows and syncs the conversations of the account that is signed in and nothing of an earlier one's. On Ubuntu the hands' clicks land, on a Mac the Computer Use helper keeps its grants, on the iPhone the crash after onboarding is gone, and the fourteen Android and six iOS languages beyond English and Chinese are complete. The Terminal edition is dropped. ### Cloud (relay 0.21) - **One provider catalogue** (contract C11). `nanomuse/llm/providers.json` lists eighteen own-key providers with what each key covers (`chat`, `vision`, `image`, `video`) their endpoints, key pages, default models and sign-ins (a ChatGPT plan is chat and vision only); every fact checked against the vendor's documentation in October 2026, and the capabilities that could not be confirmed against the runtime's own picture and clip paths left out (video is Bailian only; Volcengine, Gemini and xAI video go through vendor task APIs). `node scripts/providers-json.mjs` validates it and writes the desktop's, the phones' and the relay's byte-identical copies; `--check` runs in CI and is listed in `AGENTS.md`. - **The relay says what each provider covers.** `GET /v1/me` → `spend.guidance` and the `allowance_exhausted` refusal → `guidance` carry the region's providers in order (Bailian first on the mainland, one key for all four; OpenRouter then OpenAI elsewhere) with `covers`, `key_url` and `auth`, the plans a person may already pay for and which clients sign in with them (ChatGPT everywhere; Claude, Kimi and OpenRouter on the phones), the local servers, the docs link and the honest line about the ChatGPT sign-in, in English and 中文. The 0.17 `ways` rows stay, each now with `name`, `name_zh`, `key_url`, `covers` and `auth`; the refusal's one-line `message` names the first picks and the plans. The console at `/app` draws the card from `guidance` (zh, en) and falls back to the two links on an older relay. - **The guide, rewritten.** [docs/own-key.md](docs/own-key.md) is the catalogue in prose: who comes first where, a table of what each provider covers, how to make a key at each, the sign-in flows and which client has which, the ChatGPT caveat, local models, any OpenAI-compatible endpoint. No prices. The release-notes template says *use your own key or a plan you already pay for*. ### Runtime - **Sign in with a ChatGPT plan.** `nanomuse chatgpt login` runs the authorisation flow of OpenAI's own Codex (PKCE, the browser comes back to port 1455 on the runtime's machine, or the callback address is pasted) and keeps the tokens in `/chatgpt.json` (mode 0600), refreshed before they expire; `status` says who is signed in and until when, never the tokens; `logout` forgets it; `proxy` serves the sign-in as a loopback OpenAI-compatible endpoint. `[llm] provider = "chatgpt"` (and `[gui]`) then calls the Codex endpoint in-process, translating Chat Completions to Responses and back, with `gpt-5.6-sol` when no model is named. It covers chat and the hands' vision, not pictures or clips. OpenAI's terms cover using a ChatGPT plan inside OpenAI's own Codex; other apps have had this access cut off before (OpenCode, January 2026), and if it stops working an API key does. The web console drives the same flow over `POST /api/chatgpt/login`, `GET /api/chatgpt/status`, `POST /api/chatgpt/callback {url}` and `POST /api/chatgpt/logout`. - **Pictures and clips from a provider of their own.** New `[image]` and `[video]` slots (`provider`, `model`, `base_url`, `api_key`; `NANOMUSE_IMAGE_*` / `NANOMUSE_VIDEO_*`) say where the avatar studio draws when it is not the chat model's host; `provider` may be a catalogue id anywhere a provider is set (`bailian`, `zhipu`, `openrouter`, …) and brings its endpoint and default model. When no configured provider has a capability, the studio, the settings and `GET /api/providers` say so in one sentence naming the region's providers that would (*Pictures need a provider with image models: Alibaba Cloud Bailian, Zhipu GLM, SiliconFlow or Volcengine Ark*), in English or Chinese, built from the catalogue rather than a raw error. - **Each account sees its own conversations** (contract C10). When another account signs in, the main chat so far is put aside under its owner and that account's own main chat is restored, or a fresh one begins; conversations of another account stay on disk but leave the list and are never pushed into the new account (one made while signed out joins the account that pushes first); signed out, everything local shows again. - **`GET /api/providers`** for self-hosters and clients: the catalogue with which slots use which provider, the union of what that covers, the account's region, the ChatGPT sign-in's state, and the sentence for every capability nothing covers (`?lang=en|zh`, `?region=cn|global`). ### Web - **Your own key, from the catalogue** (contract C11). The spent-allowance card and the 80 % heads-up lead with the region's provider and what one key there covers, fold the rest of the region's list under *Other providers and what each key covers* (from the relay's `guidance` when it sends one, else the bundled catalogue) each with *Set it up* and a key link, and add *Sign in with a plan you already pay for*. Connections names what the chosen provider covers under its tiles; *Automatic* for pictures and clips says which model it means. - **One sentence, never a raw error.** A feature no configured provider covers (pictures, clips, the hands) shows one line naming the region's providers that would (*Pictures need a provider with image models: Alibaba Cloud Bailian, Zhipu GLM, SiliconFlow or Volcengine Ark*, with *How*), in the avatar studio, under *Pictures and clips* and the *Hands model* in Connections, and on the Settings row. The console asks the runtime's `GET /api/providers` for what is configured and falls back to the bundled catalogue on an older runtime. - **Sign in with ChatGPT.** A card under the provider tiles and on the allowance card starts the runtime's Codex sign-in (`POST /api/chatgpt/login`), opens the page, follows `GET /api/chatgpt/status` until the tokens are in, then offers *Use it for the chat* and *Sign out of ChatGPT*; it says the sign-in covers chat and the hands only and carries the line about OpenAI's terms. When the runtime cannot take the callback itself (port 1455 taken, or the runtime on another computer than the browser) the card has a box for the address the browser ended on and hands it to `POST /api/chatgpt/callback`. A runtime without the routes gets the terminal command instead. 40 new strings in English and 简体中文; `web/src/providers.test.ts` checks the ordering, the mapping and the sentences. ### Desktop - **Ways on, from the catalogue** (contract C11). Settings → Account has a *Ways on* block: your account's row first while signed in (what it covers right now), then `assets/providers.json` grouped for where you are: Alibaba Cloud Bailian first in mainland China, OpenRouter then OpenAI elsewhere, then the rest by how much each covers, the servers on this computer and a custom endpoint, every row saying what it covers (*chat · screen operation · pictures · clips*), with *Get a key* and the key taken inline. The key goes to the credential store as `NANOMUSE_KEY_`; `cloud.json` and the model row carry the name only; *Remove* drops the row, its credential and any chat or hands choice that pointed at it. The first run's *Choose models* step is the same rows (the region's group with the ChatGPT sign-in, *More ways* for the rest) with *Skip for now* until a key is saved or a sign-in finished, *Continue* then; it no longer opens the harness's Models page. - **One chooser for chat and the hands.** The two pickers list the account's models and every own row's together, grouped by source; the hands list carries sighted models only and a row with none says so; `/chat-model` and `/hands-model` take the row (`provider`), and `hands.json` for the bundled runtime carries that row's address and key (or `provider: chatgpt` for the sign-in). - **Sign in with ChatGPT.** Under OpenAI's row: *Sign in* starts the bundled runtime's Codex sign-in (`nanomuse chatgpt login --json`), opens OpenAI's page, waits with *Cancel* and *Open the sign-in page*, then reads *Signed in as ChatGPT Plus* and offers *Sign out*; the host keeps the runtime's loopback proxy up as the `chatgpt` provider row (its token a credential, never in `cloud.json`) and brings it back after a restart when the sign-in is still on file. The row says the sign-in covers chat and the hands only (no pictures, no clips) and carries the line about OpenAI's terms; without the runtime it says so and stays disabled. - **Pictures and clips say who could.** The avatar studio, Settings → Media and the pickers show one sentence naming the region's providers with image or video models (*Pictures need a provider with image models: Alibaba Cloud Bailian, Zhipu GLM, SiliconFlow, Volcengine Ark (Doubao).*) with the way to Settings → Account, instead of a sign-in prompt or a raw error; with only the ChatGPT sign-in configured, that pictures and clips are not covered by it. The host's `GET /media` carries `image` (`cloud`, `provider` or `none` with `reason: no_image`) beside `video` (`no_video`). - **Each account's conversations stay its own** (contract C10). A synced conversation remembers the account it came from; signed in as another account, those chats stay on this computer but leave the chats column and the search, are never pushed under the new account, and come back when that account signs in again; a different account than last time starts the pull over from cursor 0 and drops the other account's pulled turns and presence; signed out, everything local shows. - 55 new strings in English and 简体中文. `tests/cloud-providers.test.mjs` drives the host itself (the capability rule, `GET /providers` grouped by region, a key saved by name only, a removed row and the choices that pointed at it) with no relay, no runtime and no network; `tests/providers.test.mjs` covers the gate's sentence choice. - **Ubuntu's hands click again, and the screen is no longer dead after an approval card** (X11). The glow is a full-screen X window whose click-through input region Chromium clears on every change of the window's bounds (on creation (the window is clamped to the work area), on the first map, on every `setBounds` and on the X server's ConfigureNotify that follows) so a click landed on the glow instead of the screen, and the person's mouse was dead while it was up. The glow now steps aside for every pointer action (hidden before the pointer moves, back right after with the marker), the region is set again after every event that can clear it (at once, at 150 ms and at 600 ms), a bounds change from outside is logged and repaired, and a watchdog sets the region again when a real pointer event reaches the glow's page: telling the enter and move Chromium makes up after each show apart from the X server's. Measured in a nested X server with GNOME Shell 3.36 at 3840×2160, scale 2: in 0.1.38 the region was gone for 160–570 ms after the glow's creation and the person's clicks died for good after a resize of the window (0 of 3 landed); now the gap at creation is under 120 ms, every click of the hands landed (10/10, and 5/5 after an approval card), the person's did too (2/2, and 2/2 after a resize), and a region cleared behind the app's back was back within 200 ms with one log line. - **macOS: the hands work through the helper.** 0.1.38's *nanoMuse Computer Use* ran from a translocated copy, the quarantine flag of the download stayed on the nested helper, so LaunchServices started it from a random read-only path and tccd never showed its row in the Screen Recording pane; on a clean Mac Gatekeeper's *could not verify* dialog came up for it too. The app now removes the flag from the bundled helper before the first launch (the log says so), so the helper starts in place, both panes list *nanoMuse Computer Use*, and a granted Screen Recording gives a real picture (verified on macOS 27.0.1, Retina at scale 2). An app run from the disk image or Downloads is refused with *move it to the Applications folder* instead of half working. Fixed with it: a restart asked for while the helper was between processes relaunched the whole app, twice when clicked twice, which left a second copy failing on the host's port and a *bearer token is required* helper; the helper's `/status` reported a scale of 1 on Retina displays; `press`/`release` hold keys down in the helper (shift-click, cmd-click); the helper activates itself before asking for Screen Recording so the request registers from the background. `docs/desktop.md` → *macOS permissions* describes the flow as it really is. ### Android - **A sign-in shows and syncs that account's conversations only** (contract C10). Each synced chat remembers the account it was first pushed to or pulled from; signed in as another account, those chats stay on the phone but leave the drawer, the search, the "working" line and the home, and are never pushed into the new account; chats made while signed out join the account that pushes first. Switching back shows them again, and signing out shows everything. A change of account starts the sync cursor over (a fresh tail pull) and drops the other account's presence and remote-row caches; the Room store migrates in place (`sync_conversations.owner`, version 3). - **Your own key, from a catalogue** (contract C11). The spent-allowance card lists the providers of `assets/nanomuse/providers.json`: the region's first (Alibaba Cloud Bailian on the mainland; OpenRouter and OpenAI elsewhere), each with what it covers (chat · screen · pictures · clips), *Add* opening the pre-filled provider form and *Get a key* the vendor's key page, then the subscriptions that can sign in instead of a key (ChatGPT through OpenAI's Codex OAuth, Claude, Kimi, OpenRouter) through the Providers screen's own sign-in buttons, with the honest line about the ChatGPT sign-in. `minis://settings/providers/add?preset=` opens the form for any catalogue vendor; `?preset=:oauth` opens it on the sign-in. - **Capabilities decide what the app offers.** Pictures, clips and the hands' screen model pick only from providers whose vendor has image, video or vision models (a ChatGPT plan signed in through Codex counts as chat and vision only); when none has it, Settings → Image & video models, Settings → Hands and the avatar studio show one sentence naming the providers that would, instead of failing. - 19 new strings in English, 简体中文, 繁體中文 and the fourteen other locales; six strings of the old two-vendor card and two "no provider" footers are gone. - **Every string in every language.** The fourteen locales beyond English and Chinese (German, Spanish, Filipino, French, Indonesian, Japanese, Korean, Malay, Polish, Brazilian Portuguese, Romanian, Russian, Thai, Turkish) carried only the thirty-seven newest nanoMuse strings and showed the other 840 in English; every `nm_strings.xml` is complete now (the eight name suggestions, the weekdays and the plural forms each language has included) and a check in the tree keeps the files at the same keys. ### iOS - **No crash on launch after onboarding.** Build 9 (0.1.38) died on an iPad every time the chat appeared, with a stack overflow in the chat screen's body: the body is one chain of some sixty modifiers and 0.1.38 added four (the composer's fail-safe host and the presence hooks), enough for the getter's stack frame and the runtime's type instantiation to overrun the main thread's 1 MB. The four are one link each now (`NanoMuseComposerHost`, `NanoMuseChatHooks`) and the composer stack is boxed, so the body is smaller than 0.1.37's; a test keeps its size and depth under a ceiling. - **Your conversations, not the last person's.** Signing in as another account on the same iPhone showed, and pushed to that account, the conversations of whoever had been signed in before. A conversation now belongs to the account that first synced it: the lists and the Chat tab show the signed-in account's own and the ones no account has synced yet, another account's stay on the phone hidden until that account is back (its main chat too), and nothing of theirs ever goes up under the wrong account. A different account than last time pulls afresh and forgets who was working where. Accounts are told apart by the relay's opaque id, never the phone number or e-mail (contract C10). - **Every string in every language.** German, Spanish, French, Japanese, Korean and Russian carried about a hundred of nanoMuse's 718 strings and showed the rest in English; `Localizable.xcstrings` is complete in all nine languages now, and four Traditional Chinese gaps are filled. ### Project - **The Terminal edition is gone.** The `nanomuse-desktop-terminal` packages are no longer built or released (the `desktop/` Python package, its build script and workflow are removed; the 0.1.38 release lost its terminal files); the desktop app is the one desktop shape, and the Python runtime stays for self-hosting and the web console. The install tables in the README, the docs and the homepage list Android, iOS, macOS, Windows, Linux, the browser and Docker. - **The README shows its features as cards** (two columns, the same eight points as before, in all ten languages), replaces the ASCII relay sketch with a picture of the four clients (Android, the desktop, iPhone and iPad, the web app) around one account, and says in the *Any model* card what a provider's key covers and what a plan you already pay for gives you. - **A Discord.** The project has a Discord server for quick questions, showing what you built and talking in English or 中文; the link is in the README badge row and its contribute line (all ten languages), in CONTRIBUTING.md, in the issue chooser, in the release-notes template and in the website's social links. Releases are posted to its `#announcements` by a GitHub webhook; issues, pull requests and discussions go to `#github-feed`. Decisions still land in issues and pull requests. - **Housekeeping.** The related-work links point at PhoneHarness's and ClawGUI's current repositories (the old ones are gone); `CITATION.cff` carries 0.1.38's actual release date; the docs sidebar lists the web console page and no longer calls a design record "Terminal"; the desktop rooms section has an anchor that works on GitHub and on the site alike; the Python line's changelog section has a heading of its own instead of a second *Unreleased*; the repository description names iPhone and iPad. ## [0.1.38] - 2026-10-05 · Loom Loom: the thread from 0.1.37, made to carry its weight. Side chats stay on the device that made them unless you say otherwise, so a new device signing in gets the main conversation's newest three hundred lines in one pull instead of the account's whole history; a device at work says so under its line on the others instead of leaving a prompt that looks interrupted; and the computer-use turn that failed with 413 *Request too large* cannot: the hands keep four screenshots of two megapixels at most in a request, on every client, and the relay accepts sixteen megabytes. The Mac's hands have their own helper app, *nanoMuse Computer Use*, so the Screen Recording and Accessibility grants belong to it and the app no longer restarts to pick them up; the desktop no longer re-reads every session log every two seconds, which is where the slowness of 0.1.37 came from. Ubuntu's hands were run on this machine, under a nested X server, and fixed where they failed: the glow swallowed every click, Shift was dropped for symbols, an approved step came back *Not done*. The iPhone's composer is a native field with a second place to stand; the launch screen is the mark; every client can point at a relay of yours. The README is half as long, `AGENTS.md` says how to work in the tree, the docs are a site, and `scripts/self-host.sh` brings up your own nanoMuse Cloud. ### Cloud (relay 0.20) - **Main first.** `GET /v1/sync/changes` takes `scope=all|main` and, with `since=0`, `tail=K` (K ≤ 500): the newest K messages with their conversations, `cursor` at the account's counter, `more:false`, and `skipped` for what was left out. `scope=main` on an account without a main conversation yet returns an empty page; an unknown scope is 400 `bad_scope`. - **Working.** `POST /v1/sync/working {cid, working, device?}` → 204 (the device from the body or `X-Nanomuse-Device`; 400 `bad_cid`, 404 `no_conversation`, 409 `sync_off`); the hub fans out `working {cid, from, device_name, working, at}` to the account's other sockets. Kept in memory for ten minutes, never in the database; `GET /v1/sync/state` lists the live ones under `working`. - **Sixteen megabytes.** `MAX_REQUEST_BYTES` defaults to 16 MiB (was 6); a 413 says *Request body is N MB; this relay accepts up to M MB* (code still `too_large`). The production Caddy site allows 20 MB. - **Sign-in says it first.** The console's sign-in page says, before asking, that text-message codes reach mainland-China numbers only and to use an e-mail address elsewhere (EN, zh). - **Self-hosting in one command.** `scripts/self-host.sh` asks five questions and brings up a relay with TLS (or `--local` without, on a loopback port, through `cloud/docker-compose.local.yml`); the Caddyfile takes `ACME_EMAIL` for Let's Encrypt and the compose file no longer insists on an `.env` beside it. Proven end to end on this machine: a code, a key, a chat completion, the console. ### Runtime - **Side chats stay home.** `[sync] side_chats = false`: only the main conversation is pushed and pulled (`scope=main`); the first pull after sign-in takes the newest 300 messages (`tail=300`); turning *Also sync side chats* on pulls everything once from the start and pushes this device's side chats from then on. `PUT /api/sync/state {side_chats}`. - **Working, both ways.** The runtime tells the account's other devices when it starts and finishes a turn in a synced conversation and shows theirs: `working` entries in `/api/sync/state` and `hello`, `working` WebSocket frames; an entry goes with the reply, with `working:false`, or after ten minutes. - **Requests stay small.** Screenshots are downscaled to at most 2 Mpx (JPEG q85 past 1 MiB) and only the newest `[agent] max_context_images` (4) image-bearing messages keep their pictures; the older ones carry *(screenshot removed to keep the request small)*. The chat provider logs the request body's size at debug. This is the runtime's half of the 413 of 0.1.37. - **The hands on Linux.** `picture_size` caps the picture at 1600 wide and 2 Mpx (`PICTURE_MAX_PIXELS`) and the operator client asks for the same; a Wayland session raises *the hands are off on this computer* before any capture instead of trying xdotool or pyautogui under XWayland; the MCP bridge verifies the app's approval ticket (above). ### Web - **Also sync side chats** (同时同步旁聊) under Data controls, per device, off by default, with the one-line explanation. - **{device} is working…** (… 正在处理…) under the last message while another device answers; gone when the reply lands or after ten minutes. A message from another device never shows the interrupted/continue UI. - A burst of synced rows (the 300-line tail) renders in one pass. Sign-in says text-message codes reach mainland-China numbers only, in code mode outside the mainland or when a non-+86 number is typed (EN, zh). ### Desktop - **The slowness of 0.1.37, found.** A push with nothing new re-marked every session dirty and scheduled itself again, so the host re-read every session's log every two seconds while idle, on a large account ~50 % of a core and a wait behind every click. Fixed; idle CPU 0.1 %, the UI's p95 from 60–80 ms to 4 ms on a 33-session account. Session titles come from the harness's title projection and a log is read once per change; hub `sync` frames arriving together become one pull a second; the other devices' bubbles no longer scan the transcript on every DOM change. - **No more 413.** The nanoMuse Cloud provider row carries the request image budget (5 MiB of base64 images a request, 2 Mpx a picture), so the harness offloads older screenshots before the relay would refuse the request; an installed profile's row is rewritten on start. - **Also sync side chats** under the sync switch, per device, off by default (contract C9): off, only the main conversation syncs and side rows are ignored; on, this device's side chats go up and the others' come down after one tail pull. The first pull after sign-in asks for the newest 300 messages. - **Working.** The host tells the relay when a turn starts and ends on a synced conversation and shows *{device} is working…* (… 正在处理…) under another device's last prompt until its reply, `working:false` or ten minutes. - **macOS: nanoMuse Computer Use.** The hands' permissions moved to a helper app of their own, `nanoMuse Computer Use.app` in `Contents/Helpers` (Swift; `CGDisplayCreateImage` for the screen, `CGEvent` for the mouse and keyboard, text of any script typed as characters without the clipboard). The Screen Recording and Accessibility panes list it instead of nanoMuse Desktop; switching Screen Recording on no longer needs the app restarted: the app restarts the helper by itself, mid-conversation. The Permissions page, the first-run dialogs and the operator's refusal name the helper; without the helper bundle (an older or local build) everything works as in 0.1.37. Ad-hoc signed until the Developer ID certificate exists, so a new build still starts the grants over: `docs/desktop.md` says how to reset them. - **Ubuntu: the clicks land.** The glow drawn around the screen while the hands work swallowed every click and keystroke on X11: `setIgnoreMouseEvents` sets an input shape the X server forgets each time the window maps or is configured, and the overlay was re-bounded on every action. The shape is re-applied 150 ms after the window maps and after each action, bounds change only when the display does, and the operator waits for the marker before it acts. Ten clicks out of ten land where the log says. - **Ubuntu: `*` is not `8`.** libnut dropped Shift for US-layout symbols; shifted symbols are sent as Shift + key, Tab as Tab, and text outside ASCII keeps the clipboard path with the clipboard restored. - **Approving a hands step runs it.** The app signed the approval ticket over the step's words and the runtime's bridge left them out, so *Allow once* and *Always allow* never verified in 0.1.37 (*Not done*). The bridge accepts the app's form (and the step-less one), with a constant-time compare. - **The ask is named after the window.** The stage's ask and the *always allow* grant read the window title (`计算器 (Gnome-calculator)`), not the runtime's *Done. Screen now:* line; a grant recorded under that misread name is dropped on load. After an approval the confirmed step is re-run in place, so the screenshot it took reaches the model again (the second dispatch had lost its image). Answering on the stage settles the in-chat card and vice versa. - **Screenshots ≤ 2 Mpx on every path.** The operator's `POST /screenshot` takes `max_pixels` (default 2 000 000) and `format`; a 4K root never produces a 413-sized body. - **Wayland is told plainly.** `XDG_SESSION_TYPE=wayland` (or `WAYLAND_DISPLAY` without `DISPLAY`) makes the operator refuse with 503 and the runtime say *the hands are off on this computer* with the Xorg hint, in Settings → Computer use, which on Linux has a Screen row and a Mouse-and-keyboard row now (X11: *nothing to grant*), and on the first attempt in a chat. The Permissions summary names the rows for Linux (EN, zh). - **UI-TARS-desktop's action names** `mouse_move`, `left_single`, `select`, and `press`/`release` for holding keys, on the X11 and Windows route; the Mac helper takes the aliases and reports `press`/`release` clearly until it learns to hold keys. The black-screen notice names the helper when it is in use. ### Android - **No "interrupted" under another device's line.** A line written on another device is never this phone's unfinished turn: the detector, the view-model and the banner all skip remote rows, so *已中断 · 继续* no longer shows under a prompt the other device is answering; *{device} 正在处理…* shows there instead while it does (hidden after ten minutes or once the reply arrives). - **Main first.** *Also sync side chats* as a second card under the sync switch, per device, off by default; off, only the main conversation is pushed and pulled and side rows that arrive anyway are ignored; on, everything, with this phone's side chats backfilled oldest first. The first pull after sign-in is the newest 300 messages, applied in one transaction off the main thread. - **Working.** The phone announces the start and end of its turns on synced conversations and shows the others', from the hub frame and from `/v1/sync/state`. - **Your own relay, in every build.** *Use a different server* under the sign-in form takes the relay's address; *Check* asks its `/healthz` and shows the version; *Use this server* keeps it. `https://` outside your own network, plain `http://` for a private address. Settings → Account shows *Server: host* with *Change*, which signs the phone out first. - **Screenshots in requests.** Tool-result images beyond the newest four are elided to the text placeholder; the person's own attachments are never cut; the hands' loop already sent two 720-px frames. - **Sign-in outside the mainland.** A number with a country code other than +86 is told, before a code is requested, that text-message codes reach mainland-China numbers only and to use an e-mail address; the relay's `phone_region` error maps to the same sentence. - 18 new strings in English, 简体中文 and 繁體中文, and (new files) in German, Spanish, Filipino, French, Indonesian, Japanese, Korean, Malay, Polish, Brazilian Portuguese, Romanian, Russian, Thai and Turkish. ### iOS - **The composer, for real.** The pill's text field is a SwiftUI `TextField` now, not a wrapped `UITextView` (the UIKit host that iOS left empty when the naming card went away is gone with it) and a fail-safe shows the composer from a second attachment point if it still reports no height a second after the chat appears or a message goes out. Return sends when the preference says so, Shift+Return breaks the line, the `/` and `@` popups keep their keys on iOS 17+. Image paste moved to *Paste image* in the plus menu. - **The launch screen is the mark** on the system background, light and dark, no words (iOS caches launch screens per install: reinstall to see it at once). - **Main first and working.** *Data controls → Also sync side chats* (per device, off by default); the first pull fetches the newest 300 messages; *{device} is working…* under a line another device is answering; a line written on another device never shows *Interrupted · Resume*. - **Your own relay.** *Use a different server* on the sign-in page (address → *Check* → *Use this server*; `http://` only for a private address) and *Server: host / Change* in Account, which signs out first. - **Sign-in outside the mainland.** A number with a country code other than +86 is told to use an e-mail address before a code is requested. - 17 new strings in all nine locales. ### Project - **README, half as long.** 110 lines instead of 209, in a plainer voice: one definition, the install table, what it does in eight lines, how it works, one comparison table, self-hosting, how to contribute; the nine translations under `docs/readme/` follow the same structure. - **`AGENTS.md`** (and `CLAUDE.md` pointing to it): the layout, the check commands per area and the conventions, for coding agents and people in a hurry; `CONTRIBUTING.md` gained *Every string in every language*. - **A roadmap for contributors.** `docs/roadmap.md` says what we are catching up with Muse on now (memory, proactivity, the conversation, hands that succeed more often, self-hosting, internationalisation, each with where the code is and a first task), what comes next, and what we look up at; the past releases are one table. - **The docs are a site.** `website/` builds `docs/` with VitePress: a curated sidebar, local search, code links to GitHub; the Pages workflow publishes it at `/nanoMuse/docs/`, and the same build serves [nanomuse.cn/docs](https://nanomuse.cn/docs/). - `config.example.toml` and `docs/configuration.md` document `[agent] max_context_images` and `[sync] side_chats`; `cloud/.env.example` documents `MAX_REQUEST_BYTES`. ## [0.1.37] - 2026-10-05 · Weave One thread, woven: every device shows the account's one main conversation and the same side chats (merged in time, backfilled from the first naming conversation, each turn from another device marked with where it was written) and the muse's name travels with them. The blue mark on the splash, the sign-in and the permission pages; the agent's steps shown by default; Devices as cards. Linux opens again from the icon; the Mac asks for Screen Recording and Accessibility at launch and never hands the model a black screen; the iPad's composer comes back when the keyboard took it. [Release notes](https://github.com/nano-muse/nanoMuse/releases/tag/v0.1.37). ### Web - **Devices as cards.** The Devices page is a settings page: a line on what devices are for and a quiet count ("3 devices · 2 online"); *this device* first: its glyph, its name renamed in place, OS and version, the online dot, what it can do as chips (Tasks, Commands, Files, Screenshot, Open links, Coding agents, Notifications; Hands when they are on), the hub and remote-control switches; then the other devices in a grid of the same cards with *Online · just now* / *Last seen 2 h ago*, a *Type @Pixel 8 in the chat to hand it work* hint for the ones that take tasks, *Ask*, and *Forget this device* behind the dots; only this device, a card on how to add one with the download link. The hands card stays. `last_seen` is read as the relay's Unix seconds (it showed *20711 d ago* before). - **The agent's steps show by default.** Settings → Appearance's *Show the agent's steps* starts on (`nm.show_steps`; a stored off stays off). - **The mark where the app speaks for itself.** The sign-in door, the first seconds before the socket is up and the token gate show the app's own icon (`public/icon.svg`) instead of a face; the face appears once there is a Muse to meet. - **One thread, really.** The main chat is the account's main conversation: the other devices' turns sit between this device's own in time order, each a read-only bubble with *From Pixel 8* under it; a side chat started elsewhere is the same chat here, and the chats drawer no longer badges where a chat was written. A pulled message that is older than the last one on screen slides into its place without a reload (`upsertEvent` orders synced events by time). ### Desktop - **The splash is the blue logo.** `loading.html` shows the N mark on its tile (`resources/logo.png`, from `docs/app-icon.png`) in the loading ring; `resources/icon.png` (the dragon) is gone from the package. - **The app's mark on the first-run pages.** The sign-in pages (number or e-mail, the code, the password) and the permissions page carry the `BrandMark` as their hero (inline SVG, `client/BrandMark.tsx`); the welcome and *Meet* pages keep the face. - **The agent's steps show by default** in the dsh plugin (Settings → General → *Show the agent's steps*, `nanomuse.prefs.showSteps`; a stored off stays off). - **Devices as cards** in the plugin's Devices page and Settings → Devices: the same anatomy as the web's, this computer first with its chips, the remote-control switch and the devices allowed without asking; the others in a grid (two columns from 600 px of page) with *Online · just now* / *Last seen 2 h ago*, the `@name` hint and *Forget this device* behind the dots; only this computer, a card on how to add one with the download link. Computer use stays in its own settings section. - **Linux: the icon opens the app again.** Closing the window used to leave a hidden process behind (the glow and capsule overlays kept it alive), and a second launch only focused a window that no longer existed, so the launcher "did nothing" until the process was killed. Now a second launch opens the window again (or focuses it) and logs which; with the menu bar off, closing the window quits the app, host and runtime included; with the menu bar on, the window closes to the tray as before, and *Help → Quit* (Ctrl+Q) is there for desktops where Electron's tray never shows (GNOME 3.36 / Ubuntu 20.04). The Linux release adds a `.tar.gz` next to the `.deb` and the `.AppImage` for machines without FUSE. `docs/desktop.md` has a Linux notes section. - **macOS: the system asks for Screen Recording and Accessibility at launch.** The app asks through the native TCC calls (`@computer-use/node-mac-permissions`, `@computer-use/mac-screen-capture-permissions`, optional, unpacked from the asar) instead of the 1×1 `desktopCapturer` probe; the Permissions page reads the same native status; `--operator-check` never asks. A black screenshot (what macOS hands an app without Screen Recording) is refused with 403 and the *System Settings → Privacy & Security → Screen Recording* text instead of being sent on as a picture; *Relaunch* is `app.relaunch()` + `app.quit()` (no more second instance); Chromium's ScreenCaptureKit picker is off on darwin (electron/electron#44504). Checklist for a Mac: `docs/tasks/mac-check-0.1.37.md`. - **One thread, really, in the dsh plugin.** The mirrors and *Continue here* are gone: a conversation from another device is a chat here from the moment it is pulled (its session created with its title, listed under *Side chats*), and the main chat is the session the account's main conversation lives in: the first conversation starts in it when the account already has one, and *Make this the main chat* yields to it. The other devices' turns are kept in the plugin's own store (`$DSH_HOME/nanomuse/sync-remote.json`, by session; a dsh session log is append-only and owned by its agent loop: a `user/message` written into it before the first turn broke the log, and the chat rendered nothing for it anyway), shown as that device's bubbles with *From Pixel 8* under them, placed by time among the turns typed here (`client/RemoteBubbles.ts`, the way `FenceCards.ts` dresses the chat; a session with no turn of its own yet shows them above the composer), and handed to the model as one *Meanwhile, on another device* note before its next step (`agent.inject`, the last 40 turns; the note is context, never a row, never pushed back). The person's message goes up the moment it is sent, the reply when the turn ends; signing in or turning sync on pulls first and then sends the whole history, oldest first, 200 a request (a round that did not reach every session goes on). A message deleted elsewhere is hidden here, a chat deleted elsewhere is archived here (`sync/remote`, `sync/archived`; `sync/main` answers with the session the main conversation lives in). A 0.1.36 state with mirrors pulls again from zero, nothing is pushed twice. - **A desktop signing in no longer renames the account's muse.** Sharing this device's connections wrote the whole profile (the default name and the dragon, since the desktop had not pulled yet) so a fresh desktop sign-in turned *Weave* back into *nanoMuse* on every device and took a drawn face off them; the connections now go up alone (`Relay.putConnectors`, a write the relay already kept the look through). A second device joining an account whose main conversation is underway does not begin the first conversation again: no opening, no name to choose, the thread simply continues (`rooms.ts`). ### Runtime - **One screenshot path on a Mac under the desktop app.** When `NANOMUSE_OPERATOR_URL`/`_TOKEN` are set on darwin, `nanomuse.computer` takes screenshots only through the desktop app's operator: no mss, pyautogui or `screencapture` fallback (each would need its own Screen Recording grant and showed the user a second, confusing prompt). The operator's refusal (the Screen Recording text) surfaces as the error; `pick_backend` returns the operator or its reason (`operator_owns_the_screen()`, `tests/test_computer_operator.py`). Linux and Windows keep their fallbacks. - **One thread, really.** The person's message goes to the relay the moment it is sent (`ConversationSync.message_sent`, the push loop coalesces a message that arrives while one is on the wire), the assistant's final text when the turn ends; signing in or turning sync on pulls first and then sends the whole eligible history, oldest first, 200 messages a request. The main thread merges the other devices' turns in time order (a tie keeps the local turn first; the model sees them as *From Pixel 8* lines), and a row of this runtime's own coming back (or a message whose device is this one but whose mapping was lost) is never a second bubble (`nanomuse/sync/engine.py`; `tests/test_sync.py` covers the backfill, the push at send, the merge order and the echo). ### Android - **One thread, really.** The main chat is the account's main conversation on every device: the other devices' turns sit between this phone's own, in time order, each with *From Pixel 8* over the bubble; the drawer no longer badges a chat as another device's: a side chat started elsewhere is the same chat here, continued under the same id. Signing in (or turning sync on) sends the whole history, oldest first, the first conversation included: its scripted opening too, which never had a row of its own. The person's message goes to the other devices the moment it is sent; the reply still goes when the turn ends. A message of this phone's that comes back is never a second row, and the other devices' rows between ours are no longer mistaken for steps of our turns and tombstoned (`io.github.nanomuse.sync`; the mapping database remembers which device wrote a pulled row, migration 1 → 2). Rows that arrive while a turn is running appear when it ends. - **The agent's steps show by default.** Tool pills, the reasoning blocks and the bar over the composer are on unless Settings → Appearance turned them off (`nm.show_steps`; a stored off stays off). - **The app's icon on the account screens.** Sign-in and the first-run door open with the blue mark (`NmBrandMark`), not the face; the face arrives with the conversation. Devices: an offline device says when it was last seen. ### iOS - **The composer that disappeared comes back.** On an iPad the tool strip could end up sitting on the tab bar with no composer under it and no way to bring the keyboard up: the composer's UIKit host had been torn down (a sheet, the keyboard, a background snapshot) and SwiftUI, with nothing in the subtree changed, never rebuilt it: upstream's `[InputBarHealth] STALLED` probe saw it and only logged. A probe view in the composer now reports whether it is in a window and laying out (`NanoMuse/NanoMuseComposerWatch.swift`); when the bar measures zero or the probe is gone for a second (after a `STALLED`, a return to the foreground, or a zero-height geometry sample) the composer subtree is rebuilt under a new identity (`.id(tick)`), once per three seconds, logged under `nm.composer`. The text field keeps a one-line floor so `fixedSize` can no longer collapse it to nothing. - **One thread, really.** Pulled texts are inserted into the chat by time (`ChatStore.mergeRemoteMessage`), deduplicated by `mid`, and shown as the other device's bubble with *From Pixel 8* under it; a reply from another device never folds into this phone's turn. A side chat started elsewhere is an ordinary chat here under the same id, and the drawer no longer badges it. Signing in (or turning sync on) pulls first and then sends the whole eligible history, oldest first, the first conversation included: its scripted opening too, under stable ids. The person's message goes up the moment it is sent; the reply when the turn ends. Rows that arrive while a chat runs a turn wait and appear when it ends (`NanoMuse/NanoMuseSync.swift`, `nanomuse-sync.json` keeps which device wrote a pulled row; the 0.1.36 table still reads). The agent's name follows the account: a rename here reaches the other devices two seconds later (Settings, the naming card), and a pull applies the account's name unless this phone renamed since its last push (`NanoMuse/NanoMuseProfile.swift`). - **The agent's steps show by default.** Settings → Appearance's *Show the agent's steps* starts on (`NanoMuseSteps.defaultValue`; a stored off stays off). - **The app's icon on the first page.** The welcome page opens with the blue mark on its white tile (`NanoMuseBrandMark`, `docs/brand.md`), not the face; the face arrives with *Meet*. ## [0.1.36] - 2026-10-05 · Thread One conversation across your devices: the text of the chats lives on nanoMuse Cloud and every device signed into the account shows the same threads; `@` hands a turn to another device, and the iPhone takes such tasks too. The desktop's hands on UI-TARS-desktop's wheels (the operator in the Electron process, the picture as the coordinate unit, the prediction marker) so clicks land where the model meant. The iPhone level with the phone on voice, composer, header, rename and settings; the Mac face click, the logo splash, Ubuntu icons and the console's Star asks fixed. [Release notes](https://github.com/nano-muse/nanoMuse/releases/tag/v0.1.36). ### Runtime - **The picture is the unit.** The computer hands clicked beside their targets on scaled displays because the model saw a picture scaled to 1,600 pixels wide and was told its coordinates were pixels of the screen. Now `computer_screen` says the picture's size and `computer_act` takes `x`/`y` (or a `box`) in that picture; the runtime maps them once to the hands' own pixels (`nanomuse/computer/coords.py`: UI-TARS's `smart_resize` to Qwen's 28-pixel grid, so the model's own resize is the identity; `Mapping` between picture and screen). `[hands] coords = "norm1000"` for models that answer on a 0–1000 grid. Status and events carry `screen_size` and `picture_size` apart. - **The `desktop` hands backend**: the desktop app's own operator over loopback HTTP (`NANOMUSE_OPERATOR_URL` / `_TOKEN`, `nanomuse/computer/operator.py`), chosen by `auto` when it is there, with `pyautogui` and `xdotool` as the fallbacks; a Wayland session says why there is no operator. - **One conversation on every device.** A signed-in runtime pushes the text of each finished turn to nanoMuse Cloud and pulls the other devices' chats, at start, on the hub's `sync` frame, once a minute (`nanomuse/sync/`: the relay client, the engine with its state in `sync.json`, the `@device` mention). Threads carry the device they came from; `/api/sync/state` (GET/PUT), `/api/sync/delete`, `/api/sync/pull`; `[cloud] sync` and `NANOMUSE_CLOUD_SYNC` for hosted runtimes; the `event_removed` WebSocket kind for a message deleted elsewhere. A message that starts with `@` runs that one turn on another device of the account. ### Web - **Data controls** has *Sync conversations between my devices* and *Delete synced conversations*; synced chats and messages carry *From Pixel 8*; typing `@` in the composer offers the account's devices, with *This goes to …* above the message. ### Desktop - **One conversation on every device, in the dsh plugin.** The other devices' chats appear under *From other devices*, read-only until *Continue here* makes a session of them with the transcript as context; the desktop's main chat is the account's main conversation; later turns from other devices arrive as a note in the open session; Data controls has the switch and the delete (`src/sync.ts`). A dsh session log is append-only and owned by its agent loop, which is why the mirrors are read rather than written into. - **The hands are UI-TARS-desktop's wheels.** The operator moved into the Electron main process (`src/operator.ts`, `operator-server.ts`, `@computer-use/nut-js`), ported from ByteDance's UI-TARS-desktop (Apache-2.0, `THIRD_PARTY_NOTICES.md`): screenshots through `desktopCapturer` at the display's size, move-then-click with a 100 ms rest, drag, scroll in each platform's unit, clipboard typing for non-ASCII text, the hotkey table (⌘ for `ctrl` on a Mac), served to the runtime over a per-launch loopback server with a random token. On a 4K display at scale 2 the click lands within a pixel. The glow draws the prediction marker at the exact point of each action (a turning dashed ring and a dot, the action's name beside it, a trail for drags) from the operator's own coordinates, and steps aside for captures on Linux. `--operator-check` and `smoke.mjs --operator` prove the native addon loads in a packaged build (`asarUnpack`). - **The face takes the click on a Mac.** The chat header is no longer a drag region with holes cut into it, nothing draggable lies under the face at all; one empty strip above the face moves the window, and on macOS the face sits below the title-bar band (`.nm-header-drag`, `styles.ts`). Windows keeps the strip to the left of the caption buttons. Full screen puts the header back where it was. - **A splash that is only the logo.** The app icon in a quiet ring, the wordmark and one status line (`loading.html`); the start screen no longer reads the person's face. - **Icons on Ubuntu.** The GNOME grid showed gears: the Electron package shipped a single 1024 px icon, a size the `hicolor` index does not have, and the terminal package shared its name (`nanomuse-desktop`), one overwrote the other. The Electron package now carries 16–512 px icons (`resources/icons/`); the terminal package is `nanomuse-desktop-terminal`, with its own icon, its own desktop entry (*nanoMuse Desktop (terminal)*) and a lower-priority `update-alternatives` entry for `nanomuse-desktop`, so both can be installed at once. - **A task sheet for a Mac at hand**: `docs/tasks/mac-check-0.1.36.md`: what to build, which permission texts, dialogs, capsule focus, window-mode clicks and the face click to check on a real Mac, and how to send the fixes back. ### Android - **One conversation on every device.** The text of the chats goes to nanoMuse Cloud after each turn and comes back on every device signed into the account: pulled on launch, on return to the foreground, on a hub `sync` frame and once a minute (`io.github.nanomuse.sync`: engine, a mapping database of its own next to the OpenMinis one, the relay client, the transcript rule). Only what a person wrote and the final answer travel, never tool steps, tool results or the system prompt; images and files stay on the device they were made on and travel as names. All devices' main chats are one conversation; a chat deleted on one device is deleted on all; chats from another device carry *From Pixel 8* in the drawer. Routines, goals, the feed and work done for another device stay on the device. Settings → Data controls: *Sync conversations between my devices* (on when signed in) and *Delete synced conversations*. - **Work on another device from the chat.** A message that starts with `@` (an online device of the account; the full name or its first word, case-insensitive) is handed to that device's Muse through the hub, and the answer comes back to this chat; Devices has *Ask this device*, which prefills the composer. ### iOS - **Back from voice to the keyboard.** The voice panel's header has a keyboard button; it keeps the transcript and brings the keyboard back. The tab bar hides while the panel is open, so its bottom row is no longer covered. - **The composer and the bubbles are the phone's.** One-row pill: a bare `+` (camera, photos, files, commands), the field, a bare mic that becomes the send arrow (`NanoMuseComposer.swift`); the person's bubbles are flat grey; the main chat's header floats on a blur over the transcript, side chats get the same translucent bar. - **Rename chat** in the main chat's menu and on a long press in the drawer, with *Delete chat* next to it (`NanoMuseChatRename.swift`). - **Settings aligned**: a *Background & notifications* page (notification state, Background App Refresh, where long tasks belong) and a *Hands* page (why there is no switch on an iPhone, what a computer can do) in the same places as on the phone (`NanoMuseBackgroundSettings.swift`); the Face ID row and the OpenMinis list's *nanoMuse* entry are gone from the shell; the old Cloud welcome banner (`NanoMuseWelcome.swift`) is deleted. - **One conversation on every device**, as on Android (`NanoMuseSync.swift`: a side table in Application Support, no change to the OpenMinis schema; the same transcript rule, the same main-chat adoption, the same switch and delete action in Data controls, *From * in the drawer). `@` and *Ask this device* hand work to another device through the hub directly and show its answer as a turn in the chat; the iPhone's own Muse takes `task` calls from the account's other devices while the app is open. ### Cloud - **Conversation sync (relay 0.19).** `GET/PUT /v1/sync/state`, `GET/POST/DELETE /v1/sync/changes`, `DELETE /v1/sync/conversations/{cid}` (`cloud/nanomuse_cloud/sync.py`): a per-account journal with a `seq` cursor, idempotent pushes of up to 200 rows, one `main` conversation per account (a second one is answered with `main_exists` and the cid to use), text cut at 16 KB, 20,000 messages kept per account, tombstones swept after 30 days, `409 sync_off` while the switch is off; the hub tells the account's other sockets with a `sync` frame after a push that took something. `accounts.sync_enabled` is on by default; switching it off deletes the store. The console has the switch and the delete; the operator's page has counts only (`GET /v1/admin/sync`), never text. The timeline notes `sync.on` / `sync.off` / `sync.deleted`. - **Star asks are saved again.** The console's *Star asks* card enabled *Save* only after a redraw, so a typed change could not be saved; typing redraws now (`admin.js`). ### Docs - Conversation sync in `cloud.md` (the API, limits, refusals), `hub.md` (the `sync` frame), `every-device.md` (the same conversations everywhere; which devices `@` can reach: computers when online, phones while the app is open), `app.md`, `configuration.md`; the privacy page says what is stored, exactly, and the README and the release notes no longer say that messages are not stored by default. The hands' picture-is-the-unit convention and the operator in `gui.md`, `desktop.md`, `desktop-muse.md`, `configuration.md`; UI-TARS-desktop in `THIRD_PARTY_NOTICES.md`. ## [0.1.35] - 2026-10-05 · Accord The iPhone and the desktop brought to the phone's design, screen for screen: the Muse header on every room, the settings as Muse cards, motion clips for a drawn face, the phone's first run and first conversation on the desktop; the face reacts to clicks on a Mac again, Ideas is never empty, the splash has no dragon; when to ask for a star is the relay's policy, read by every client within a day; installed and latest version side by side everywhere. [Release notes](https://github.com/nano-muse/nanoMuse/releases/tag/v0.1.35). ### Runtime - **When to ask for a star is a policy, not a constant.** The relay says when the apps may ask (`GET /v1/nudges`, see Cloud), the runtime reads it once a day and on sign-in and hands it on as `GET /api/nudges` (`?refresh=1` reads it again), with the same defaults built in for when the relay cannot be reached (`nanomuse/nudges.py`). The rule every client follows: a *task* is a turn the person started that got a reply, never the first conversation (until the agent has a name), never a routine, a feed post or a goal check-in; the asks come at the policy's moments (first sign-in, the 3rd / 10th / 30th task, the 7th / 30th day, a goal reached, a new face, the allowance spent), at least seven days apart and at most four times per device. - **Installed and latest.** `GET /api/update` says which version is running and which is the newest release, reading `nanomuse.cn/dl/index.json` first and GitHub's `releases/latest` second, cached a day (`?refresh=1` asks again): `nanomuse/server/update.py`. - **The first run has an end.** `POST /api/onboarded` records it, and with a model configured the Feed's first day is written in the background right then; the Feed's preferences take `daily` / `time` for the daily routine (08:00 by default), which is created the first time the Feed is opened. The macOS screen capture that comes back black says what to switch on and that the app must be relaunched (`BLACK_SCREEN_HINT`). - **macOS window mode has a fail-safe.** `auto` stays the default; when the Quartz layer itself breaks (the window list or the capture raises), the hands stay on the whole screen for the rest of that target and say so once (`WindowLayerBroken`; `status()["window"]["reason"]` carries the why); a new target tries again; an explicit `window` mode keeps trying. ### Web - **Star asks at the policy's moments** (`nudges.ts`): the gate counts only person-started turns after the first run, keeps the cooldown and the lifetime cap per browser, and asks on sign-in, after tasks, on a new face, when the allowance is spent, on the 7th / 30th day and when a goal is reached, never in the first conversation. - **Two version lines on Settings**: *nanoMuse 0.1.x · installed* and *Latest 0.1.x: you have it* / *0.1.x is out · Update* / *Could not check · tap to try again*. - **Ideas and empty states.** The curated ideas catalogue (`web/src/ideas/`, byte for byte the phone's, with a test); the Feed opens on the intro card with *Write it now*; Memory, Skills and Devices say what will appear and how; the Hands card says why window mode parked (*On the whole screen for now: …*). ### Desktop - **The phone's first run, kept by the host.** Welcome, sign-in, password, which model answers, models, macOS permissions (with the test screenshot and the mouse move), Meet: recorded in `firstrun.json` as *done*, never skipped on a fresh install (`src/firstrun.ts`, `FirstRun.tsx`). The first conversation is the phone's: the app speaks first (three scripted lines, no tokens), asks what to call you, and the model's `nanomuse-naming` fence becomes the naming card; the `take_name` / `ask_user_question` detour is gone. - **The face reacts to clicks again on macOS and Windows.** Chromium reads `-webkit-app-region` from the untransformed layout, so a header centred with a transform was all drag bar; the header is centred with margins and the island is an explicit no-drag region. - **Ideas were empty** when the bundle ran from `lib/chunks/`: the `import.meta.url` of a split chunk resolved `../assets/` one level too deep; package assets are found from the package root now (`packageAssetsDir`, with a test). - **A splash without the dragon.** Wordmark, a quiet loader, the person's own face when there is one (`harness/desktop/resources/loading.html`, `splashFace()`); the bundled dragon frames are gone. - **Motion clips for drawn faces.** Four 4-second clips (idle, working, waiting, happy) drawn from the face by `wan2.2-i2v-flash` through the relay or an own Bailian key (`src/video.ts`, `src/motion.ts`), stored under `avatar/motion/`, served from this computer and played in the header, the sidebar and the capsule; *Animate the avatar after a change* in Settings → Media (on by default) with *Make / Redo clips*; the cost estimate counts them. - **Feed, Goals, Library follow the phone.** The intro card and *Write it now*, a daily routine ensured at first open, the first day written when the first conversation ends; the phone's copy on the empty states. - **Installed and latest** as two lines in About and on the Settings row (`nanomuse.cn` first, GitHub second). Star asks from the relay's policy, with the moments above. - **macOS permissions re-audited.** TCC attributes the bundled runtime to the app, so only *nanoMuse Desktop* has to be switched on under Accessibility and Screen Recording: the words say so now; a restart dialog when Screen Recording flips on; a black-screen notice with *Relaunch*; *Try it* rows (a test screenshot, a mouse move) and the runtime row under Settings → Computer use and on the first run's permissions page. ### Android - **Star asks from the policy** (`community/Nudges.kt`, the `StarPrompt` gate): the naming conversation, routines, the Feed and goal check-ins never count as a task; the asks come at the 3rd / 10th / 30th task, the 7th / 30th day, a goal reached, a new face, first sign-in and when the allowance is spent: seven days apart, four per phone; the policy comes from `/v1/nudges` once a day and from `/v1/me`, with the defaults built in. - **A Version row** in Settings: *Latest 0.1.x, you have it* / *0.1.x is out (tap to update* / *Could not check) tap to try again* (`community/UpdateCheck.kt`: `nanomuse.cn/dl/index.json`, then GitHub). - **The first feed day** is written the morning after the first conversation; the Feed's intro card and *Write it now*; the goal sheet's button is *Start*. ### iOS - **The phone's chrome.** The Muse header (face disc, name pill, live status line, round drawer and menu buttons) on the chat and on Feed, Ideas, Goals and Library (`NanoMuseChrome.swift`); the side drawer after Android's; assistant replies in Muse's grey bubble; the agent page's toolbar as round buttons; room menus; the status line says *needs approval* and *drawing a clip* when that is what is happening. - **Settings rebuilt** as Muse cards in the Android order (`NanoMuseSettingsHomeView`): Image & video models, Avatar, Computers, Appearance (avatar size, model under the name, steps, theme, `NanoMuseAppearance.swift`), Notifications, Account, Coding, Scheduled tasks, Shared folders, Chat files, System files, a Version row with installed and latest (`NanoMuseUpdateCheck.swift`) and the Cloud / Account pages in Android's order. - **Motion clips.** `NanoMuseVideoGen.swift` (DashScope's async API through the relay or an own Bailian key, the phone's prompts word for word) and `NanoMuseAvatarMotion.swift` (idle, working, waiting, happy; 4 s; per device under `avatar/motion/`); the face plays them (`AVQueuePlayer` + `AVPlayerLooper`, suspended in the background), with bundled clips for the dragon; *Image & video models* lists the models and the *Animate the avatar after a change* switch, *Make / Redo clips*; the studio's estimate counts the clips. - **First run and Feed.** A Notifications page in the first run (three dots); the Feed's intro card inline, the empty state, the first day written after the first conversation; a goal reached, a new face, the 7th day and the tasks are the star asks' moments (`NanoMuseNudges.swift`, the relay's policy with the defaults built in). - **Words.** 104 more nanoMuse strings in 简体中文 and 繁體中文; *Password* and *From* were English-only. ### Cloud - **Cloud 0.18.0: the nudges policy.** `GET /v1/nudges` (public, `Cache-Control: public, max-age=3600`) and `nudges` in `/v1/me` say when the apps may ask for a star: `star.enabled`, `url`, `moments` (`signed_in`, `tasks`, `new_look`, `exhausted`, `days_used`, `goal_done`), `cooldown_days`, `max_asks`; `GET` / `PUT /v1/admin/nudges` (`X-Admin-Token`) read and set it, every field validated, the version bumped by the server, `{"reset": true}` goes back to the defaults; stored in the relay's `settings` table (`nudges.py`). The console's **Star asks** card edits it. ### Docs - A native-speaker pass over the nine README translations and the website's text (the notice block, the News, the versions table); the README says the iPhone app ships (TestFlight) and 0.2.0 is the App Store. `docs/parity.md` records the round: the iPad keeps the Muse shell (no split layout of its own), `auto` window mode with the fail-safe, the star rule as policy C1. ## [0.1.34] - 2026-10-05 · Turns The hands are handed over and wait, on every client, and approvals are answered where you are; chat apps (飞书, 钉钉, 企业微信, Telegram); two model settings and a region-aware way on; connections shared across the account's devices; the iPhone and the desktop catch up with the phone. [Release notes](https://github.com/nano-muse/nanoMuse/releases/tag/v0.1.34). ### Runtime - **Holds: the hands are handed over, and wait.** A *hold* belongs to one chat and one kind of hands: the browser, this computer, the phone. The person opens one with *Take over* (`POST /api/holds`), the agent opens one itself with the new `hand_over` action of `browser`, `computer_act` and `phone_act` (a `reason` is required: *sign in*, *the code from your SMS*, *confirm the payment*), and the person's **Done** closes it (`POST /api/holds/{id}/done`). While a hold is on, every action of that tool for that chat waits instead of failing; the agent's own hand-over waits up to ten minutes, then goes on without them. Each change is a `hold` event in the timeline and the open ones ride in the hello state as `holds`, so the chat, the phone's capsule, the desktop's stage and the web's browser viewer show one card with one Done. The browser's `take_over` / `handed_back` map onto it. The system prompt says when to hand over and that the person's work must be looked at, never redone. (`nanomuse/agent/holds.py`.) - **Two model settings: chat and hands.** With a nanoMuse Cloud account the chat opens on `deepseek-v4.1-flash` and the hands see the screen with `qwen3.8-27b`; the relay's `/v1/models` says which model is `for` which (`nanomuse.for`, `nanomuse.recommended_for`), and the runtime reads that before its own defaults. `[gui]` is independent of `[llm]`. The vision rule is narrower: a DeepSeek id is sighted only when it says `v4.1`, `vision` or `ocr`; `qwen*-vl` and `qwen3.8-*` are. - **Where you are decides the way on.** The account carries the relay's `region` (`cn` = mainland China) through sign-in, `/api/cloud` and `/api/cloud/me`; the own-key presets are Alibaba Cloud Bailian and OpenRouter. - **Connections travel with the account.** The remote MCP servers this device connected go into the profile's `connectors`: a name, an address without its query string, how it signs in; never a key, and the other devices' entries come back as `shared` on `GET /api/connections`, each with `here` saying whether this device has it too. A change on one device pushes; the hub's `profile` frame pulls. - **macOS window mode.** On a Mac the hands can work in one application's window while the person keeps the mouse (`[hands] mode = auto | screen | window`, `auto` = window mode as soon as a target application is set): the window is captured and the events are posted to its process, Retina coordinates mapped back, long text sent in chunks, the accessibility path taken when it is there. The screen's first line says `window` when that is what the picture is; the `window` block of the hands state says whether it can run here and what it is working in. Each application asks once: *Let use ?*, with once / this conversation / always (`computer_app:` grants, revocable in Permissions). When the window goes away the next look is the whole screen, with a notice. Hands events carry `x`, `y`, `x2`, `y2`, `mode` and `window` for the stage. (`nanomuse/computer/mac_window.py`, pyobjc in the `hands` extra on darwin.) - **Chat apps.** Your nanoMuse answers in Feishu, DingTalk, WeCom and Telegram over the vendors' long connections, no public address needed (`nanomuse/channels/`, after nanobot's design; see `THIRD_PARTY_NOTICES.md`). One conversation per chat (`channel--`), replies stream in place where the vendor allows editing, tool work as a status line, approvals answered with 允许 / 拒绝 or a button, files both ways. The first message from an unknown person gets a six-character pairing code; approve it in the app or with `nanomuse channels approve`. Allowlists, group policy (mention / open), and *deliver here* chats that receive background results, pending approvals and questions. `nanomuse channels status | pending | approve | deny | login feishu | test`; `login feishu` creates the bot from a QR code. `/api/channels`; secrets go to the vault and are never read back. Optional extras `nanomuse[feishu]`, `[dingtalk]`, `[wecom]`, `[channels]`; the package imports with none of them installed; the Docker image has them. - **A picture with the avatar request.** *Change your look like this* with one picture attached draws the candidates from it, as the phone does (`avatar` events carry `reference`); more than one picture, or a file, goes to the agent as before. - **Ideas have a kind.** Each idea is a chat, a routine (`time`) or a goal (`category`), as on Android. - **Fixed: adding a remote MCP server that is down.** `streamable_http_client`'s task group cancelled the caller's task when the server did not answer, a 500 on the Connections page and *Attempted to exit cancel scope in a different task* in the log. Each attempt has its own exit stack, closed in the same task; SSE is tried after streamable HTTP; the error says the real cause (a 502). - **Fixed: a message typed during a background pass** (a goal's check-in, say) was filed as that pass's work, it left the chat for the Feed, and so did the avatar card. `user` and `avatar` events never carry `source: "background"` now; the studio's notices say `source: "studio"`. - **Fixed: unknown `/api/*` paths** returned the app's HTML with a 200; they are a 404 in JSON. ### Web - **A browser you can drive.** The browser viewer takes over (*Take over* / *Done*), clicks, types, scrolls, opens a URL, goes back, and says *paused, waiting for you* while the agent waits; hold cards in the chat for both kinds (*Your turn: Done*, *You have the browser, Done*). - **Chat model and Hands model** are two settings on Connections; the Bailian and OpenRouter presets fill both. - **The way on follows where you are.** Sign-in, onboarding, the allowance card, the account page and the presets' order put Alibaba Cloud Bailian first for a mainland account (zh-Hans UI, a phone number, or the relay's `region`) and OpenRouter first elsewhere, with one line that says why (*Alibaba Cloud Bailian only signs up accounts from mainland China …*). - **Connections from your other devices** are listed under the catalogue (*Connected on : sign in here to use it on this device*), with the local sign-in prefilled. - **Where the hands work** (Devices: *Auto / One window / Whole screen*) and the per-application grants in Permissions. - **Settings → Chat apps.** One card per app with its switch, credentials, group policy, allowlist, paired chats with their *deliver here* switches, a test send, the setup steps and the vendor's console link, Feishu's QR sign-in, and the pairing codes waiting for a yes at the top. - **The Muse page** gets the avatar share sheet (five cards, drawn on a canvas, shared or downloaded) and the phone's empty-state words; the look card shows the reference picture; ideas open a chat, create a daily routine or start a goal. - **Plainer words.** The community notice is *Free, open source, non-profit* and says who pays and what is kept; the star asks say what a star does (*a star on GitHub tells the people building it that it helped*) and no longer *your support is what keeps us going*; 你, not 您. ### Desktop - **The face opens the agent page**, as on the phone: *Change avatar* (the phone's words prefilled) / *Edit name* / *Avatar studio…*, the online line and what it is doing, a share card, and the four panels: Activity, Approvals (with the per-application *Always allowed* grants and *Revoke*), Daily, Soul & memory. - **Change the look from the chat.** The phone's `AvatarFlow` rules, word for word (第 N 个, *option 2*, *the first one*, *regenerate* …): the request is caught before it reaches the model, a cost note, four numbered candidates, a pick by click or by words, *Regenerate*, then the agent confirms in a sentence and the new face shows as a card; the same memory line as on the phone is written (`POST /nanomuse/rooms/avatar/adopted`). - **Goals, Feed and Ideas follow the phone.** *Create goal › category* shapes the goal in the chat with up to three questions, one at a time; `nanomuse-goal` and `nanomuse-goal-update` fences become goals, check tasks (the schedule bundle, on by default) and progress; `nanomuse-feed` posts to the Feed; the four fences render as cards in the chat. Ideas is the phone's list (`ideas.{en,zh}.json`, byte for byte, with a test): *Send to chat*, *Create routine* (daily, at the idea's time), *Start goal*; the model-generated ideas are gone. - **Hands: allow, hold, glow.** **Allow once / Always in / Deny** on the live stage (always = a `computer_app:` grant, revocable on the agent page and in Settings → Computer use); holds: the agent's `hand_over` shows *Your turn, , Done* and the tool waits, *I'll take it* pauses the agent before its next step; a transparent, click-through, always-on-top **glow** around the screen with the face at the pointer (amber while a hold is on), and a **capsule** on top when the main window is not in front, with the approval or the hold and its buttons. Every overlay, and the window itself while the hands run, is kept out of the screenshots (`setContentProtection`). On macOS the permissions are walked through in order (Accessibility, then Screen Recording) for the app and the bundled `nanomuse` runtime both. - **The version, and a real check for updates.** *nanoMuse Desktop · harness * in About, in Help and in the application's About box; the check reads GitHub's releases and falls back to `nanomuse.cn/dl/index.json`, compares versions, picks the asset for this platform, once a day; a newer one puts a dot on the ••• menu and the Settings row and an *Update to 0.1.x* entry; *About nanoMuse* is in the menu on every platform. (The old check was a timer that opened the releases page.) - **Chat model and hands model** rows (`deepseek-v4.1-flash` / `qwen3.8-27b`; the hands model goes to `$DSH_HOME/nanomuse/hands.json`, 0600, read at the next start); connections listed across devices with *Connect*; Bailian or OpenRouter first by region; the star asks in the plainer words. ### Android - **Two model settings.** The Cloud group opens on `deepseek-v4.1-flash`; *Settings → Hands → Hands model* defaults to `qwen3.8-27b` (the relay's, or the same model under your own key) and says why a model was picked. DeepSeek models are read as sighted only from `v4.1` on, or when the id says `vision` / `ocr`. - **Connectors across devices** through the relay profile: the entries, never a token; *Connectors* lists what is connected on your other devices with a one-tap way to sign in here. - **The way on follows where you are.** When the allowance is spent, Alibaba Cloud Bailian comes first in mainland China and *Sign in with OpenRouter* first elsewhere; OpenRouter joined the own-key presets (`preset=openrouter` opens its OAuth sign-in). - **Words.** Every nanoMuse string in English, 简体中文 and 繁體中文: plainer star asks, the community notice, no exclamation marks, 你 not 您; the Cloud row says *对话用 DeepSeek,动手用 Qwen*. ### iOS - **The face opens the agent page**, as on Android: Activity, Approvals, Daily, Soul & memory, and a share sheet (`NanoMuseAgentPage.swift`, after `AgentProfileScreen.kt`). - **Change the look from the chat.** The Android `AvatarFlow` rules, EN and 中文: *change your avatar to …* is caught in the app, four candidates above the composer, a pick by tap or by words (第二个, *the first one*), *Regenerate*, *Generating options / Finalizing avatar*, then the face is set and pushed to the profile, the same image path and cost note as the studio (`/v1/estimate`; the relay, or your own Bailian key). - **Goals, Feed and Ideas are real rooms now.** The phone's fence protocol (`nanomuse-goal`, `nanomuse-goal-update`, `nanomuse-feed`) read back from the chat; goal creation with up to three questions; the Feed written by a daily routine; Ideas by kind: *Send to chat*, *Create routine*, *Start goal* (*not scheduled on iPhone yet* is gone). A scheduler runs them: the routines are caught up in the foreground (on activation and every minute), a `BGAppRefreshTask` (`io.github.nanomuse.app.scheduler`) asks iOS for a run in the background, and a local notification at the set time says *Check-in: , open to run it* and opens the room. - **First run and settings as on Android.** The four-page first run (the same *needed / stage* logic); a nanoMuse settings page (shell and header switches, account, coding agents, scheduled tasks, shared folders, chat files, system files) reachable from the chat's ••• menu and from Settings; a coding-agents page over the hub (`coding.*`); the system files browser. - **Models, region, connections (contracts C4, C3, C5).** The chat opens on `deepseek-v4.1-flash` and the pick sticks (`followPick`); the relay's `nanomuse.for` / `recommended_for` are read; DeepSeek models are text-only unless `v4.1` / `vision` / `ocr`; own-key presets for Bailian and OpenRouter; when the allowance is spent, Bailian first in mainland China and *Sign in with OpenRouter* first elsewhere. This phone's connectors go to the profile (never a header, a query string or a key) and the other devices' show as *Connected on : sign in here to use it on this phone*. - **The iPad runs the Muse shell too** (the same layout, larger); upstream's split layout is gone. - **Words.** 249 nanoMuse strings in English, 简体中文 and 繁體中文, the 中文 taken from the Android strings where the same line exists; interpolated strings that were never localised (`AppLocalized("\(x)…")`) are now; 量身打造 and 开启 are gone. ### Cloud - **Cloud 0.17.0: lanes, connectors, region.** Every chat model on the menu says what it is `for` (`chat`, `gui` or both) and where it is recommended (`nanomuse.for`, `nanomuse.recommended_for`); the menu opens with **DeepSeek V4.1 Flash** for the chat (reads pictures, thinks before it answers; ¥2 / ¥8 per million tokens at 0.7×) and keeps **Qwen 3.8 27B** for the hands; `CLOUD_MODELS` takes `for` and `recommended_for`, and the relay logs when a lane has no, or more than one, recommended model. `enable_thinking` and `reasoning_content` pass through untouched both ways; reasoning tokens are paid for as completion tokens whichever way the provider reports them. The profile carries `connectors`: merged per device, at most 64, never a credential (`400 no_secrets_in_profile`). `/v1/me` says where the person is (`region`: `cn` / `intl` / `unknown`) and lists the ways on in that order (`spend.ways`); the *allowance used up* refusal points mainland accounts to Bailian and everyone else to OpenRouter (`OPENROUTER_URL`). Only DeepSeek V4.1 and later are assumed to read pictures by name (the probe still has the last word). The admin page shows each menu model's lane next to its price; the console's allowance card and refusal copy follow the region. ### Showcase - **The phone's capsule decides on the spot.** **Allow once** / **Deny** on the capsule itself (`POST /api/approvals/{id}`), and the agent's holds as cards: *Your turn, Done*, *You have the phone, Done*; only questions still open the app. The bridge restores holds from the hello state and re-syncs when the Phone switch is flipped. - **Gateway: two lanes.** `MAIN_MODEL` defaults to `deepseek-v4.1-flash`, `GUI_MODEL` is a fixed `qwen3.8-27b` (no longer derived from the main model); `NANOMUSE_LLM_VISION=off` only for a text-only main (DeepSeek ids without `v4.1` / `vision` / `ocr`); a visitor's own OpenRouter key gets `qwen/qwen3.8-27b` for the hands. The page's notice is *Free, open source, non-profit* and names Bailian and OpenRouter by region. - **The phone's operator had no model on the showcase.** `.env.example` ships the four `GUI_` lines empty, and the gateway read an empty line as a value: an operator lane without a model, which it answered `404 no_lane` for, so every phone task on demo.nanomuse.dev failed at its first step and the chat model (DeepSeek, which takes no images) was left to drive the phone blind. An empty line now means what the comment says: the main model when it is sighted, Model Studio's `qwen3.8-27b` on the same key when it is not. And a text-only chat model is told so (`NANOMUSE_LLM_VISION=off`): on Model Studio's compatible mode a message with a screenshot in it came back as an empty reply rather than an error, twice, and the Muse fell silent after the operator's report. ## [0.1.33] - 2026-10-04 · Steps The words under the face are the step's own, on every client; the iPhone gets the Muse shell; the browser is handed over on Android; the connectors that need an app of your own say so; and three fixes from a day's use of the desktop. [Release notes](https://github.com/nano-muse/nanoMuse/releases/tag/v0.1.33). ### Runtime - **Every tool takes `step`.** One more argument on every tool, in the model's own words for the person watching (*打开携程网站*, *Check the login page*) stripped before the tool sees its arguments and shown under the face while the step runs and as the pill's title in the chat. The prompt asks for it on every call; `nanomuse mcp` exposes and strips it the same way, so the desktop's hands get it too. (Issue [#67](https://github.com/nano-muse/nanoMuse/issues/67)'s other half: the status line used to read *Running: ``*.) - **A black screenshot is an error, not a picture.** A capture that comes back all black: macOS without Screen Recording granted to this process, or granted after it started; a Wayland session on Linux, used to go to the model and the live stage as if it were the screen. `take_screenshot` now raises `BlackScreen` with what to do (*allow Screen Recording for nanoMuse … then quit and reopen the app*), the computer tools and the hub's `screen` action pass that on, and no black frame reaches the stage. - **`?token=` is refused.** Announced in 0.1.31, kept one more version in 0.1.32: a token in a query string is now a 401 that says so (`LEGACY_TOKEN_MESSAGE`); a socket that opens with one gets `{"kind": "error", "code": "legacy_token"}` and a 4401 close. The bearer header and the socket's first frame are the only ways in. ### Desktop - **The words under the face are the step's own.** The host reads the model's `description` / `step` of the running tool first (*打开携程网站*) and falls back to *kind · brief* (*Terminal · ls -la*) only when the model wrote none; the system prompt asks for the words on every call, in the person's language. - **A way out of the trace view.** The tabs of the conversation (*Chat*, *Trajectory*, …) are hidden in the Muse chrome, which left *Inspect* on a step with no way back. When any tab but the chat is active the strip shows as a segmented pill above the conversation, and **Esc** returns to the chat. - **The typing dots are centred** (they sat on the left edge of their bubble). - **The face opens the studio.** The face in the profile drawer is a button into the avatar studio, and the drawer's menu has *Avatar studio…*. The menu it opened was also invisible: a later `.nm-menu` rule made it `position: fixed` at *100 % + 6 px*, below the window, which is why a click on the face looked like nothing; the drawer's rule now wins. The studio says *sign in first* in the person's language instead of the relay's English when there is no account. - **The app remembers.** The host's port was picked afresh on every launch, and the window's origin (`127.0.0.1:`) is the key to everything the browser side keeps (the preferences, the star asks' *never again*, the stage's place, the harness's own settings) so every launch forgot all of it. The port used last time is tried first (`/port`), then 38421, then any free one. - **The trace view's way out sits at the top right.** The segmented *Chat · Trajectory* pill was centred, on top of the face's status line; it is at the top right now, where the tabs were. - **Theme-colour swatches are gone** from Settings → General (the accent follows the avatar, as on the other clients). - **Permissions read back right.** One hook behind the onboarding slides, Computer use, the Permissions summary and Dictation: the system's word is re-read on a timer, when the window gets focus and when the page becomes visible again, so a switch flipped in System Settings shows as a check the moment you come back; *Allow* becomes *Open System Settings* once it has been pressed (a second press cannot bring the system's dialog back); Linux and Windows read *not needed* and show the check. **Screen Recording** has two fixes of its own: the first capture attempt is made before the pane opens, so nanoMuse is actually on the pane's list, and when it is granted while the app runs a notice says macOS applies it only to freshly started apps, with *Restart now*. - **The live stage moves and resizes.** Drag the frame anywhere over the window, resize it from the bottom-right corner; where you put it is remembered (`prefs.stage`), and it stays inside the window when the window shrinks. - **Connectors: six more, and the ones that need an app of your own say so.** Attio, GitLab, Miro, QuickBooks, YNAB and Oura join the catalogue (75 services). Eight services (GitHub, Slack, Discord, HubSpot, Render, Bitrise, PagerDuty, Box) do not register clients by themselves; *Connect* used to fail there with a one-line error. They are marked in the catalogue, and connecting walks through making an OAuth app at the vendor's developer page with our callback address (shown, copyable) and pasting its client id; the id is kept like a registration so the next sign-in does not ask again. A server connected by address without dynamic registration gets the same sheet. - **Star asks, as on the phone.** *Your support is what keeps us going* with *Star on GitHub* / *Not now*; asked once after the tenth task and once after a new look from the avatar studio, besides sign-in, the first task and the spent allowance. A run the host started on its own (the first meeting, a goal's check-in) is not counted as the person's task any more (the *first task* ask used to come right after the introduction). - **The preset's routes are admitted like the harness's own.** `/nanomuse/cloud`, `/nanomuse/connectors`, `/nanomuse/rooms` and `/nanomuse/assets` were mounted raw on the harness's web server, so any local process could read the account's status or post a sign-out without the browser session (a page on another origin was already refused). They now go through the harness's `connection` admission (the Host/Origin fence and the session cookie the launch token mints) and answer 401 / 403 as `/api` does (`src/admit.ts`). ### Android - **The status line keeps the step's title.** While the model streams and between steps the header shows the running tool's `tool_title` (*打开携程网站*) and keeps the last one until the next step, instead of *Writing the reply*. - **The browser is handed over, not described.** A page that needs the person (a login, a verification code, a payment, a CAPTCHA) used to end the turn with a sentence telling them to do it themselves, and nothing to tap. `browser_use` has a `hand_over` action: the agent's own tab (same WebView, same session) opens in the browser sheet with the agent's hold released, a **Your turn** card above the composer says what the page asks of them with *Open the page* and *Done, continue*, the sheet carries the same line and button, and the tool call waits (up to fifteen minutes) until *Done*, then the agent goes on from the page as it is, told never to ask for or type credentials. - **Hands approvals are answered on the capsule.** A tap whose label says send, post or delete used to put *Waiting for your approval: Open* on the floating capsule and send you back into nanoMuse to answer. The capsule now has **Allow** and **Deny** itself, answering the same request the chat card and the notification show; money still goes through the card (it confirms with the screen lock), and the capsule offers *Open* for that one. - **Connectors and MCP are one entry.** Settings → *Connectors* is the only row; the full MCP editor (by address, by command, imported JSON) is *Your own servers* at the end of the Connectors page. The catalogue grows by the desktop's six, and the eight services without dynamic registration ask for an OAuth client id (and secret) with the developer page and the callback address to copy, instead of failing. - **Star asks.** The new copy, the tenth task, the new look from the avatar studio. ### Web - **The step's title on the pill and under the name.** Tool pills show the model's `step` words; the status under the name reads them from the runtime. - **Star asks:** the new copy, the tenth task, the new look. ### iOS - **The Muse shell, on the iPhone.** `NanoMuseRoot` takes the root from upstream's `ContentView` on the iPhone (the iPad keeps the split layout): the chat's title is **the face, the name and a status line**, *waiting for you* while a question or an approval is up, then the running tool's own words (*打开携程网站*), *writing the reply*, *On it: …*, the model's name when idle, with the face in its five moods (the drawn one from the account, the dragon otherwise) breathing, bobbing and popping as on the other clients; a tap on it opens the avatar studio. A bottom bar carries the rooms: **Ideas** (bundled, *Send to chat*) and **Library** (the sessions' files, QuickLook, share, *Open conversation*) are real, **Feed** and **Goals** are empty states until the phone has something to run them with. The drawer holds the sessions, search, a new side chat, *Pin as the main chat*, and *All chats* / *Settings* into upstream's layout. New files under `NanoMuse/`; the upstream edits are three, each marked `// nanoMuse:`. - **The avatar studio.** The Android studio's styles and prompts, four candidates drawn in parallel through the relay, a cost sheet first (eight pictures against what is left), the pick adopted and posed into the moods, then pushed to the account's profile so every device changes with it; the profile is pulled on start and on the hub's `profile` frame. - **Connectors.** The desktop's catalogue (75 services) from the bundled `connectors.json`, the MCP authorization flow (discovery, dynamic registration, PKCE through upstream's `MCPOAuthController`) key and open services, the client-id ask with the callback address to copy for the eight services that register no clients. The Settings row that was *MCP Integrations* is **Connectors**, with *Your own servers* at its end. - **Data controls** (the relay's switch, the kept-turns count, deletion) and **Reach** (a sheet per device of the account: open a link, send a note, a shell line, a screenshot). - **Star asks** after the first and the tenth finished task and after a new look, with the new copy, as a card under the header; 145 strings added to `Localizable.xcstrings` (EN, 简体, 繁體). ### Cloud - **Cloud 0.16.0: a pool set to any figure.** `POST /v1/admin/pool` sets one account's pool to `left_cny` or `grant_cny` (the allowance machinery only ever grew pools; a wrong credit could not be taken back), `POST /v1/admin/pool/batch` does it for a list or for everyone, both on the operator's page; negative credits are accepted; events `pool.set` / `pool.set.many`. Deployed to cloud.nanomuse.cn. ## [0.1.32] - 2026-10-04 · Union The four clients, evened out to the union of what each could do: the account, the connectors, the star asks, the agent's steps. What is platform-specific or still open is in [docs/parity.md](docs/parity.md). ### Android - **Settings → Connectors.** The desktop's catalogue of 69 remote MCP servers, on the phone: grouped as the desktop groups them, one line about each, the vendor's page a tap away. Open servers add with a tap; a key service asks for the key and keeps it in the entry; an OAuth service signs in with the account you already have: the phone runs the MCP authorization flow (protected-resource discovery, the authorization server's metadata, dynamic client registration, then upstream's PKCE flow in a Custom Tab) and writes the access token into the entry's `Authorization` header, which is how the in-guest MCP client reads it; tokens about to lapse are refreshed when the app starts and when the page opens. A connected service is an MCP server entry under the catalogue id, so *Settings → MCP* shows and manages it too. The catalogue is one file shared with the desktop (`scripts/connectors-json.mjs` writes the asset; `--check` fails when it is stale). - **Fixed: a finished tool step left the Computer sheet stuck ([#67](https://github.com/nano-muse/nanoMuse/issues/67)).** The sheet of a step that had ended still read *nanoMuse is using Memory*, paged *1 / 1* between two arrows that went nowhere, and the floating step card stayed over the composer after the run. The title now follows the step's state (*nanoMuse used Shell*, *nanoMuse read File*, *Memory failed / stopped / timed out*) the arrows appear only when there is more than one step and the centre says *Done*, *Failed* or *Stopped* for a single one; the floating bar is drawn only while the run is on (and a second and a half after, so the last step can be read), and the sheet closes on ×, swipe, Back and the scrim through one animated dismissal that always ends in the sheet being gone. - **The agent's steps are off by default.** The tool pills, the reasoning blocks, the Computer sheet and the floating step bar are gone from the chat unless *Settings → Appearance → Conversation → Show the agent's steps* is on; a turn that was nothing but steps leaves no empty bubble. What stays is the line under the avatar, which now names the work and never a state of mind: the step under way (*nanoMuse is using Shell*), then *Writing the reply*, and between steps *On it: 〈the request, briefly〉*, no *Thinking…* anywhere. The home screen's line follows the same rule. - **A star, asked for at three moments.** When the free allowance is claimed (the account page after signing in), after the first task the agent finishes, and when the allowance is used up (a third way on the card, with the key and the invitation). Each is a card where it happens, shown once; none comes back after you have been to the GitHub page. - **The amounts are the relay's.** *¥10 of use to start*, *+¥5 for each of you* and the exhausted-allowance message now print what the relay says (the account's `invite_bonus_cny`, the page's figures) so the operator can change them without an app update. ### Web - **Show the agent's steps.** Settings → Appearance has the switch, off by default: the chat keeps to the conversation and the line under the name says what the agent is on; on, every tool it uses is a chip in the chat. This device only (`nm.show_steps`). - **The same three asks for a star** (account page after signing in, first finished task, allowance used up), the same once-and-done rule in `localStorage`; **the allowance and invite figures come from the relay** (`GET /api/cloud/config` → relay `/v1/config`, with fallbacks of 10 and 5 for an older relay). - **No more *Thinking…*.** The status under the name says what the agent is on: the step's label from the runtime, *Waiting for you*, or *On it: 〈the request, briefly〉*; the runtime no longer sends a *Thinking…* detail between steps, leaving each client to word the pause in its language. The fallback copy for `allowance_exhausted` no longer hardcodes *+¥5*. ### Desktop - **The whole account, as the phone has it.** Settings → Account now shows the pool in yuan with the 80 % heads-up and, when it runs low, the ways on: your own key (→ Models, with the how-to), an invitation, and a star once; the invite code and link with what they earned; usage by kind (today / all time) and by model; the password: set, change, remove; every device holding a key, *Sign out* on each and *Sign out of every device*; the account's timeline; *Delete account*. The host passes the relay's routes through (`/nanomuse/cloud/me`, `/sessions`, `/sessions/revoke`, `/account-events`, `/password`, `/sign-out-all`, `/delete-account`, `/config`); nothing of it is kept on the computer. - **Invite code at sign-in, amounts from the relay.** Settings → Account and the welcome take a friend's code with the six digits (*Have an invite code?*), and the welcome prints what the relay gives on sign-up (`/v1/config`) instead of a fixed number. - **Usage in yuan.** Settings → General's usage card reads *¥x of ¥y left* when the relay sends the pool; *Nearly out* and *The ways on* lead to the account page; the token figures stay for an older relay. - **A star, asked for at the right moments.** Once as a card on the account page after signing in; once in the header line when the first task runs to its end (*First one done. Liked it?*, with *Star on GitHub* and *Not now*); and in the usage card when the allowance is spent. *Star on GitHub* ends every ask for good (`nm.star.*`). - **Show the agent's steps.** Settings → General → *Conversation* has the switch; off (the default, as before), the chat keeps to the conversation and the Activity tab has the steps; on, every tool row is unfolded in the chat. The words under the face between steps are *On it: 〈the request〉* instead of *Gathering thoughts*; *Star nanoMuse on GitHub* is a row under Help & support and under About, and a line under the usage bar when the allowance is spent. - **nanoMuse Desktop, laid out as the Muse desktop is.** Measured against the Mac Muse screen by screen, the harness bundle (`dsh-nanomuse`) now has: the **rail** with the face as the agent's state, Chats · Search · Feed · Ideas · Goals · Library, a recent document and the hamburger (Settings, Report a bug); the **chats column** with the main chat, the side chats and their row menus; the **conversation** in Muse's dress: centred face and status line (*Working*, *Waiting for your answer*, *Needs approval*), bubbles, the running dots, tool rows folded away except approvals, presented files and questions, a mic in the composer, hover actions (quote, copy); the **profile drawer** with Activity · Approvals · Reminders · Identity and the identity cards as Markdown documents; the **rooms**: a room opens full width with the rail only, and a chat it starts (a goal's planning chat, a Feed discussion, an idea, a Library creation) opens *beside* it in a split, with a toggle at the room's top left and "☰ Chats" in the chat's header to come back; Goals with Muse's six categories plus *Other* and a planning chat per new goal; the Library as *All content* with a *Recent* row and *Create* that opens a chat prefilled for a document, web page, image, video or podcast; Ideas in sections. [docs/desktop-muse.md](docs/desktop-muse.md). - **Settings, Muse's sheet and order.** General (account card, usage with the plan's bar, language, appearance with the theme-colour swatches, app behaviour, shortcuts with *Quick Chat*, about with *Check for updates*), Connectors, Computer use, File system access, Dictation, Wallet, Secure storage, Permissions, Message channels, Devices, Data controls, Help & support, Legal, Sign out; the account, the models, the presets and the harness's own rows under *Advanced*. Wallet, Secure storage and Message channels say what is true here (no payment method is held, which files hold what on this computer, which channels reach the agent) rather than imitate pages for things the desktop does not do. - **Connectors as a catalogue.** Settings → Connectors has a search, *Connected* and *Available* with *Connect* at the end of a row, and a detail per connector (what it can do, what gates it). The inventory is what the agent can really reach: the hands, the mailbox, the calendar, the address book, your other devices, the web, files, the terminal, the rooms, the schedule and any MCP server in the preset. *Connect* on the mailbox, the calendar or the address book opens a consent sheet that says what the agent gets, who decides, where the credentials live, and the exact steps (`nanomuse vault set …`, the `config.toml` lines, a restart), with copy buttons and a button that reveals the runtime folder; there is no sign-in button on purpose, since the credentials stay in your own vault. - **Seventy services to connect, by signing in.** The Connectors page now has a catalogue of remote MCP servers: Notion, Linear, Atlassian, Asana, ClickUp, Todoist, Airtable, HubSpot, Intercom, GitHub, Sentry, Vercel, Netlify, Cloudflare, Supabase, Neon, Stripe, PayPal, Square, Figma, Canva, Webflow, Dropbox, Box, Hugging Face, DeepWiki, Context7 and the rest, about seventy, in Muse's categories (*Work*, *Talk*, *Files*, *Developer*, *Data*, *Design*, *Money*, *Search*, *Infrastructure*), each with its brand mark, one line on what it does and the vendor's page. *Connect* asks the server how it lets a client in: an open server is connected on the spot; one that speaks **MCP authorization** opens the vendor's sign-in page in your browser (protected-resource and authorization-server metadata, dynamic client registration, PKCE, `resource`, refresh before the token lapses, a loopback callback on `127.0.0.1`), the sheet waiting with *Open again*, *Try again* and *Start over*; one that wants a **key** asks for it and says where the vendor hands it out. The search field also looks up the public **MCP Registry**, and *Connect by address* takes any Streamable HTTP URL, with a key or a pre-registered client under *More options*. The agent's tools arrive as `mcp____` the moment a connection is made, and go when it is cut, no restart; a connection's detail lists its tools with **one switch each**, and a switched-off tool is hidden from the model and refused if it asks anyway. Tokens and keys live in `connectors.json` on this computer, never reach the model, and are put on the wire by a loopback proxy the agent's client talks to; a refresh the vendor refuses turns the row to *Needs sign-in* with *Sign in again*, and a key is replaced in place. Not here: Muse's per-action permission dropdowns (the permission preset and the tool switches gate ours), and services whose server wants a client registered with the vendor beforehand (Zapier, Heroku), those are key or address rows. [docs/desktop-muse.md](docs/desktop-muse.md#settings). - **The face moves.** The dragon's four states (idle, working, waiting, happy) are the phone app's short clips, looped silently wherever the face is drawn 44 px or larger (the header, the profile panel, the stage's cursor marker) with the still as the poster; the still alone below that size or under the system's reduced-motion setting. A drawn face has stills only (the relay keeps WebP, not video), so it and an emoji move with CSS instead: a slow breath while idle, a sway while the agent works, a small hop when it waits for you. - **Your own quick-chat key.** Settings → General → Shortcuts shows the combination that brings the window up (⌥ Space on macOS, Ctrl+Alt+Space elsewhere) with *Change* (press the next one, Esc to cancel) and *Default*; the shell registers it at once and keeps it in `desktop.json`, and says so when another app already holds the keys. The Shortcuts card used to show *Alt+Space* on Windows and Linux while the shell listened for Ctrl+Alt+Space; it now shows what is registered. - **Fixed: a remote server 250 ms away "failed to fetch".** Node's dual-stack connect gives each address family 250 ms before trying the next, which is less than one round trip to a server on another continent, so a connector that `curl` reached fine failed from the harness. The bundle raises that attempt window to 1.5 s for the whole host process. - **`nanomuse mcp` carries the connectors.** The runtime's MCP bridge used to serve the hands alone; the connectors `config.toml` turns on (`read_emails` / `send_email`, `calendar`, `contacts`) now ride along, so a mailbox set up for the runtime is a connector of the desktop agent too, and the Connectors page lists it as connected once it appears. [docs/harness.md](docs/harness.md). - **The first run as the recording shows it.** After signing in, three pages: *Allow nanoMuse to use your computer?* (Accessibility, Screen Recording, each with *Allow* turning into a check), *Allow nanoMuse to access your files?* (the working folder, and what it may touch under the default permission preset), *Turn on voice input* (the microphone), with *Skip* under the card that becomes *Continue* once everything on the page is allowed, a dots pager and arrows in the corner; where the system gates nothing (Linux, Windows) the rows are already checks. At the end the host opens the **main chat** on the agent's own folder (`~/nanoMuse`, a workspace) and posts the introduction into it: the agent says hello, asks what to call it (three names to pick from or one of your own, through `ask_user_question`, then `take_name`), mentions the connectors and asks one small question, once per install. Help & support has *See the first run again*. ### iOS - **The account, whole.** nanoMuse Cloud's page has what the phone and the desktop have: the password as the other way in (*Sign in with a password*, *Use a code instead*), a friend's invite code with the six digits, the free amount from the relay above the form, and (signed in) the allowance in yuan with the 80 % heads-up and the ways on when it runs low (your own key, an invitation, a star once), the invite code with *Copy* and *Share the link*, usage by kind (today / all time) and by model, the password (add, change, remove), the signed-in devices with *Sign out* on each and *Sign out of every device*, the account's timeline, *Delete account*. The relay's `allowance_exhausted`, password and invite error codes read as sentences. - **Sign in to nanoMuse Cloud: free.** The start screen's first entry, above *Add a Provider*: a phone number or an e-mail, a code, and the model is yours, the Cloud page as a sheet; signing in makes the provider and a model group, so the steps below are done. - **What it is doing, not that it is thinking.** The typing line reads *〈name〉 is on it*. - **Show the agent's steps.** Settings → Chat → Steps, off by default: a finished message keeps to the conversation; the steps of the message still running stay visible (the phone has no status line under a face yet). This device only. - **On TestFlight.** The first iOS build, 0.1.31 (2), was archived, signed and uploaded by the *iOS · TestFlight* workflow, processed by App Store Connect and handed to the internal testers. The archive is signed **manually** (the team's Apple Distribution certificate from the repository secrets and *App Store* provisioning profiles fetched from the account at the start of every run) because Xcode's automatic signing signs an archive with a development profile first, and a development profile needs a registered device, which a team that only ships through TestFlight has none of. The test information for an external group (the beta description, *What to Test*, the links, in English and Simplified Chinese) is filled in and the external group exists without a public link; Apple's beta review is the step after the maintainer's smoke test on a phone. The certificate was made without a Mac, with the App Store Connect API, and the page says how to make it again. [docs/ios.md](docs/ios.md#testflight). ### Cloud - **Cloud 0.15.0: the allowance is set at runtime.** `ALLOWANCE_CNY`, `INVITE_BONUS_CNY` and `SIGNUP_OPEN` can be changed while the relay runs (*Settings › Runtime* on the operator's page, or `POST /v1/admin/settings`) kept in a `settings` table, in force on the next request, back on the environment's default when cleared; nobody updates an app, the apps print what `GET /v1/config` (new, public, a minute's cache: version, sign-up, allowance, bonuses, links, `REPO_URL`) says. Each account remembers the allowance it was created under (`accounts.allowance_uy`), so *Apply to existing accounts* (`POST /v1/admin/allowance/apply`) credits every non-member below the current figure exactly the difference, once (`from: allowance` in the ledger), and a lower figure never shrinks a pool. *Credit everyone* (`POST /v1/admin/credit-all`, ≤ ¥100) is the one-off present to every enabled non-member account. `GET /v1/admin/settings` reports the values in force, the environment's, the overrides and the count below the allowance. - **Cloud 0.14.0: the operator's page in pages.** One long page became eleven views behind a side navigation (*Overview*, *People*, *Places*, *Money*, *Activity*, *Demo*, *Data controls*, *Site*, *Models*, *Health*, *Settings*) each fetching its own numbers when first opened, the view and its filters kept in the hash so a view is a link. *People* filters the accounts by region (country → province → city, from the latest address), channel, status (member, disabled, locked, password or not, *Help improve* on, pool used up, a device online, new this period), last activity, lifetime spend and last client, with a search; the charts above the table (sign-ups by day, where they are (a country opens its provinces, a province its cities), spend buckets, activity, channel, client) are the same filters drawn, one click applies one and a second removes it; the table sorts by any column and exports the current selection as CSV (hints masked). *Places* ranks countries and provinces by accounts, new accounts, sign-ins, requests or demo visitors and a row opens *People* with that region chosen; *Money* has kind chips and a share bar; *Activity* searches the timeline; *Demo* filters sessions (running, signed in, anonymous, own key) and charts demos a day, why they ended and where visitors came from; *Models* searches the catalog; *Health* is `/v1/admin/health` on the page, refreshed every 30 s. [cloud/README.md](cloud/README.md#operators-page). ## [0.1.31] - 2026-10-03 · Locks The twenty-seven open points of the audit that followed 0.1.30, decided and done. Nothing new to learn; a number of things that were loose are now tight. ### Security - **No token in any URL.** The runtime's access token used to ride in the WebSocket address (`/ws?token=…`) and in every inline file and screenshot link, where proxies and servers log it. Now the socket is opened bare and the token is the first frame (`{"kind": "auth", "token"}`, within ten seconds, or the socket closes with 4401); inline bytes (`/api/files/*`, the browser frames) open through **signed links** the client computes from the token (`?exp=&sig=`, HMAC-SHA256 over the expiry and the path, good for the next six-to-twelve hours and for that path only; a logged link opens one picture for a while and nothing else); and the pairing link puts the token in the fragment (`http://host:port/#token=…`), which a browser never sends. The web app, the phone module and the showcase follow; `?token=` is still accepted this release and goes in the next. The retired Electron tray's *Open in browser*, which handed such a link to the system browser, is gone with the app it belonged to. [docs/app.md](docs/app.md#authentication). - **The target device agrees to remote control.** Another device of your account that wants to run, read or write something on this one over the hub (`shell`, `files`, `file.get`, `file.put`, `open`, `screen`, `coding.send`, `coding.stop`) is approved by the person *at* this device first (the usual card, *once* or *always for that device* (a `remote_control:` grant under Permissions, revocable there)) instead of only at the asking end. On nanoMuse Desktop the *Remote control* switch is off by default and means "each device asks here". [docs/hub.md](docs/hub.md), [docs/every-device.md](docs/every-device.md). - **Reading another device's files asks.** `device_files` and `device_get` were *safe*: the model could list and read files on your other devices without a card. They are *moderate* now, with *for this conversation* on offer. - **Approvals *for this conversation*, on every side.** The runtime's middle scope was *for this task*, the phone's *for this chat*; both are now *for this conversation*, as long as the chat the call came from exists (deleting or clearing it ends the grant; nothing persists across a restart), the same on Android, the web app, the desktop and the runtime. On Android a conversation's answer is per target (app or address), not per risk class. The Harness approval card lost *Always allow* until the Permissions page can list and revoke such answers. - **Enter is a send.** In a messenger's message field, Enter (or the keyboard's send action) is what a tap on *Send* is: the Hands step is approved one at a time as an outbound action when the focused app is a chat app or the field's hint says send or message. Taps keep their word list. - **The coding CLIs get a scrubbed environment.** Codex, Claude Code and Cursor used to inherit the runtime's whole environment, keys included. They now get the same scrubbed set the shell tool gets plus their own variables (`OPENAI_*`/`CODEX_*`, `ANTHROPIC_*`/`CLAUDE_*`, and the cloud credentials when `CLAUDE_CODE_USE_BEDROCK` / `_VERTEX` says so: `CURSOR_*`). - **`confirmed` on the MCP bridge is a ticket, not a word.** A call arriving over `nanomuse mcp` with `confirmed: true` skipped the approval, whoever wrote the `true`. The bridge nanoMuse Desktop starts now carries a secret (`NANOMUSE_MCP_CONFIRM`, one per launch, shared with its plugin), and `confirmed` must be the plugin's ticket over those exact arguments, produced after the person said yes on the permission card: a `true` the model wrote on its own is refused. A bridge added to some other host by hand has no secret; there the host's own approval policy is the gate, as before. - **`python_execute` without a sandbox is *sensitive*.** On macOS, Windows and a Linux box without bubblewrap the static check is the only wall, so every script asks at that level; inside a working sandbox plain computation stays *moderate*. - **`{{vault:NAME}}` is for connectors only.** The docs said a shell command could carry a vault placeholder for the Sentinel to fill; no production tool ever opted in, so the literal text went to the subprocess. The shell and Python tools now refuse such a command with the reason, and the docs say what is true. - **Android.** `WebAppActivity` is no longer exported (only the app's own scheme reaches it); plain `http://` is allowed only for addresses on your own network (`10.x`, `172.16–31.x`, `192.168.x`, `.local`), checked at the provider URL field too (`io.github.nanomuse.net.LanOnly`); the relay and the hub are TLS only. The calendar and contact connector fields in the web app say what they do with the address (*nanoMuse fetches whatever address you put here, from this machine) only paste links you trust*, since `file://` and private addresses stay allowed there on purpose. - **nanoMuse Cloud (relay 0.13.0).** Requests started together used to pass the allowance check one by one and overshoot it together: each request is now **reserved while it runs and settled when it is over** (a picture or a clip at its price, a chat at a typical turn's worth of its model, a clip still being made held until the task is seen done) and at most `MAX_IN_FLIGHT` (4) requests of one account run at once (`429 too_many_in_flight`, `retry_after`). The hub limits every socket to 60 frames and 8 MB a second sustained (twice that in a burst): frames over it are dropped with one `rate_limited` error a second, a socket that keeps flooding is closed with 4008. `GET /v1/admin/health` answers with aggregates only (requests under way, the hub's counters, the last hour, the database), and `deploy/nanomuse-hk/selfcheck.sh` reads it every ten minutes from a systemd timer, logging to the journal and posting to `ALERT_URL` once an hour per problem. `POST /v1/auth/session-key` turns a key into one that lapses on its own (`ttl_s`, 90 days at most; `via: "session"` and `expires_at` under `/v1/me/sessions`). [cloud/README.md](cloud/README.md). - **The showcase.** A kept Muse (nanoMuse Web) that sleeps is woken only for a request that proves the account's token (the bearer header, the socket's first frame, a signed link) not for anyone typing its address; a browser arriving with the address alone gets a small page that takes the token from the app's storage and asks for the wake, so a bookmark still works. The container is started with a **session key** from the relay (`WEB_KEY_TTL_S`, 30 days) and the gateway keeps no key at all any more (the standing key the sign-in produced is signed out at once; once the session key has lapsed, signing in again recreates the container around the same volumes). A visitor's own provider (BYOK) is resolved and checked once and the session's calls are **pinned** to the addresses found then (the name travels as SNI and `Host`, the certificate is checked against it as usual) so a name cannot be re-pointed inside our network later (DNS rebinding). The gateway reaches Docker through **`docker-proxy`** (`tecnativa/docker-socket-proxy`: containers and a look at the networks, nothing else, no exec, images, volumes or build) and no longer holds the socket. [demo/showcase/README.md](demo/showcase/README.md). - **The web app's `postMessage` names its target** (the showcase page) instead of `*`. ### Changed - **nanoMuse Desktop on Windows** draws the system's caption buttons over its frameless window (`titleBarOverlay`, in the theme's colours); Linux keeps the window manager's bar. **Signing out** (or *Reset*) takes the window back to the welcome sheet, the way a fresh install starts. - **繁體中文** on Android says 你, not 您, in the strings inherited from OpenMinis (marked ``). ## [0.1.30] - 2026-10-03 · Rooms ### Added - **The desktop's rooms: Feed, Ideas, Goals, Library.** The four rooms of the Muse desktop are on nanoMuse Desktop's rail, each one `main` panel of the harness's layout, kept by one host service in `~/.nanomuse/desktop/nanomuse/rooms.json` and streamed to the window. *Feed*: posts the agent writes for you in a hidden chat from your feed instructions, your goals and the profile (a batch when the room is empty and then every few hours) with a picture when the page it read had one, ♡, *Discuss* (a side chat on the post) and the instructions sheet behind the sliders icon; a dot on the rail for posts newer than your last visit; `feed_post` from any chat. *Ideas*: suggestions in groups, each a card with what it includes, how it works and *Start now*. *Goals*: Muse's categories, each goal a chat of its own that the agent names and keeps a one-line status for (`goals_room_update`), *In progress* automations from the harness's schedule plugin (now part of the app's profile), *Check in*, a timeline grouped by day. *Library*: shelves (documents, web, images, videos, podcasts, system files), *+ Create…* (a brief → a chat that writes the file under `~/nanoMuse/Library` (`构件` in Chinese) with the workspace-write preset), a card grid, a viewer and a Markdown editor; everything delivered with `present` lands here, `library_add` lists a file without delivering it. [docs/desktop-muse.md](docs/desktop-muse.md#the-rails-other-rooms--feed-ideas-goals-library). - **The live stage.** While the agent uses this computer's screen (Hands) or looks at a phone through Reach, the screen it is working on is shown picture-in-picture over the chat, as in Muse: the latest screenshot, dimmed while it works, a caption with what it just did (*clicked "Save" · Finder*, *typed "hello" · WeChat*, *pressed ⌘ S · Pages*, *looking at the screen*), the agent's face as the cursor marker where it last clicked, × to put it away, *Expand*, and *Take over* while a step runs (it stops the turn). The host keeps one frame in memory, taken from the hands' own MCP results, and drops it ten minutes after the last step. - **Memory on the desktop.** The agent keeps one-line facts about you with a `remember` tool when you tell it something you will expect it to know next time; every chat's prompt carries the list back. The profile panel's Memory tab lists them, adds one, forgets one, and has Muse's *Import memory* sheet: paste what another assistant knew, one line per memory (lines already known are skipped). Memory stays on this computer. - **App behavior.** Settings → General, under the desktop shell: *Open at login* (a login item; an autostart entry on Linux), *Show in the menu bar* / *system tray* (an icon with *Open nanoMuse*, *New chat*, *Quit*), *Quick chat with ⌥ Space* (Ctrl+Alt+Space on Windows and Linux: the window comes up with a fresh chat and the composer focused; pressed while it is in front, it steps aside). - **Four more Settings pages.** *Connectors*: the built-ins (Hands, Reach, the rooms, Schedule) and the MCP servers behind the preset's tools with their tool lists, *Add a connector* opening the agent preset; *File system access*: the folders the agent uses, each opening in the file manager, the rules, Full Disk Access on macOS; *Dictation*: the harness's local voice input (switched on in Plugins; audio stays on the computer), the microphone permission, the system's dictation; *Permissions*, one page that says what the agent may touch and where each switch is, with the recent approvals. - **Data controls, the local part.** *Import memory*; *Download your agent data*: a zip in Downloads with the account snapshot (no sign-in token), the look, the rooms, memory and the chats as the harness keeps them, shown in the file manager; *Reset*: memory, the rooms and the local look go and the account signs out; the chats stay. - **The avatar studio on the desktop.** *Change look → Draw one* in the profile panel (or "change your avatar to …" in a chat, which opens it through `draw_new_look`): describe the character, pick a style, see the cost against today's allowance, and four candidates come up in a 2×2 grid as in Muse; pick one and the other poses are drawn from it, then the face is written to the account and worn on every device. The pictures come from the account's image model through the relay with the runtime's prompts; the stills are squared to 512 px WebP in the browser. - **Report a problem with a screenshot.** The hamburger's *Report a problem* under the desktop shell saves a screenshot of the window to Downloads and opens the GitHub issue page with the build's facts filled in; a toast says which file to drag in. ### Changed - **One desktop app, built on DeepSeek Harness.** The desktop built on the harness (`harness/`, shipped as *nanoMuse Harness* beside the older app in 0.1.28 and 0.1.29) is now **nanoMuse Desktop**: the installers are `nanoMuse-Desktop--win-x64.exe`, `-mac-arm64.dmg` / `-mac-x64.dmg` (and `.zip`), `-linux-x64.AppImage` / `.deb`, built by `.github/workflows/desktop-app.yml`, and the app carries the application id of the one it replaces (`io.github.nanomuse.desktop`) so it installs over it. The Electron shell around the Python runtime and the web app (`desktop/app`, 0.1.19–0.1.29) is retired; the runtime still rides inside the new app for the hands (`nanomuse mcp`), and the terminal binary stays the zero-install fallback. The app's home is `~/.nanomuse/desktop` (`NANOMUSE_DESKTOP_HOME`); a home left by nanoMuse Harness under `~/.nanomuse/harness` is taken over once, account and chats included. The bundle alone, for a DeepSeek Harness Desktop someone already runs, is `dsh-nanomuse-.tgz` (was `nanoMuse-Harness-.tgz`). [docs/desktop.md](docs/desktop.md), [docs/harness.md](docs/harness.md). ### Fixed - **The model chosen in Settings reaches the chats already open (Android).** A member who changed the nanoMuse Cloud group from the recommended model to `deepseek-v4.1-flash` kept talking to `qwen3.8-27b`: the open chat (the main chat above all, whose view-model lives as long as the app) had its model resolved once and only re-resolved when the model's provider was switched off, and a chat pinned to a model from the ⋯ picker never followed the group at all. Now a member taken out of the bound group re-resolves every open chat on the spot, and a chat pinned to one of the Cloud's models follows the Cloud group once that model leaves it. Signing in again no longer adds a second *nanoMuse Cloud* group with the recommended model back when the person's own group has another. - **A wrong model-provider content check is said plainly** (relay 0.12.0). Model Studio's refusal of a prompt (`DataInspectionFailed` / `data_inspection_failed`, native and OpenAI-compatible spellings) comes back as `400 content_rejected` with *The model provider's content check declined this request; try different words*, in the app's language, instead of a bare upstream 400; the same for a picture prompt. - **Hands approvals read the screen too.** A tap's approval card no longer trusts only the label the screen model reported: the accessibility tree's words under the finger are read next to it and the stricter class decides, so a pay button the model calls "Next" still stops, and a blank target is judged by what is there. Remembering a payment is offered only on a phone with a screen lock (the lock is what confirms it); without one the card says so and allows once. Backups and device transfers leave `grants.json` on the phone. - **The runtime's guard rails, tightened.** The Python reach check names dynamic imports and string attribute lookups (`__import__`, `importlib`, `getattr(os, …)`, `exec`/`eval`); `browser.fetch` checks every redirect hop against the private-address guard, not only the first URL; a path the files tool cannot resolve counts as outside the workspace; `files.search` stops after 50 k entries; a configured MCP server starts with the library's minimal environment plus its own variables rather than every credential of the runtime; a shell run from another device over the hub sees the scrubbed environment as well; the web user agent carries the real version. - **nanoMuse Harness.** The Invite dialog always met a 404 (`call()` POSTed a GET route); the code and password views have a way back to the number or e-mail, a wrong code clears the boxes and refocuses them, and Settings → Account can sign in with a password and resend a code like the first run; sign-out failures are shown instead of swallowed; Computer use links to both the Accessibility and Screen Recording panes; the bug-report link opens through the bridge; the device reports the package version. The Electron shell only allows `file:` navigation to its own pages, checks permission kinds against the known panes, releases the keep-awake blocker on quit and uses one http(s) rule for external links. - **Web app and nanoMuse Desktop.** Sheets and the first-sign-in steps keep keyboard focus inside and give it back on close; an events load superseded by a newer one is dropped instead of landing in the thread the user moved to; the sidebar's online dot has a spoken label; dates on the Account and Coding screens follow the UI language. The desktop app opens only http(s) links in the system browser, the stage socket stops reconnecting on 4401/4404, and the quick-chat listener can be removed. - **Relay 0.12.0.** Wrong passwords from one network address are capped per hour across all accounts (`LOGIN_FAIL_PER_IP_HOUR`, default 30: a list of numbers tried once each never tripped the per-account lock); a relay that sends real codes refuses to start without `CLOUD_SECRET`; hub pending calls are keyed per account; `upstream stream error` lines name the exception. - **Showcase gateway.** A streamed refusal is not charged to the session; the image gate is released during retry pauses so one visitor's 429 does not hold another's picture; Caddy caps request bodies at 64 MB; the ended-session WebSocket line is debug. - **Docs.** The approval scopes as each app has them (*this task* in the runtime, *this chat* on the phone); the Hands row in the READMEs; `cors_origins`, `NANOMUSE_CLOUD_*`, `NANOMUSE_HUB_NAME` and `NANOMUSE_CODING_HOME` documented; the release-notes template carries the Harness installers and the DeepSeek Harness notice. ## [0.1.29] - 2026-10-03 · Likeness ### Changed - **nanoMuse Harness is laid out like the Muse desktop, screen for screen.** The first run is full-window and follows Muse's sheets: welcome with one *Sign in* pill → *Sign in or create an account* (phone or e-mail) → six code boxes that verify themselves (or a password, `/v1/auth/login`) → a spinner → the permissions carousel with ‹ ›: *Allow nanoMuse to use your computer?* on macOS, where *Allow* asks the system for Accessibility and Screen recording through the desktop shell and turns into a check once granted; *Allow nanoMuse to access your files?* with the workspace folder and a native picker; *Your other devices*: → *ready*. The window: a rail in Muse's order (Chats with a dot while the agent works, Search, Devices, Schedules when installed, the hamburger at the foot); a chats column with *Search*, **Main chat** and **Side chats** with *+* (pin, inline rename, archive, *Make main chat* on each row's menu; a blank chat reads *New chat*); the face and name pinned at the top centre with a status chip that tints while it works or waits, *Stop* beside it, *Invite* at the top right (the account's code and link, from `/v1/me/invite`); your messages in accent-toned bubbles, the agent's in grey ones, timestamps and message actions on hover; the composer as one pill (*+*, *Message*, the send disc) docked at the bottom with no greeting over an empty chat; the harness's approval card restyled into Muse's permission card (shield, headline, *Allow once* in blue, *Reject*). The face opens a profile panel beside the chat: the avatar with a pen (*Change look*, written to the account and worn on every device; *Edit name*), the connection, and four tabs: Activity, Approvals (every answer on a card, recorded here), Schedule, Memory. Settings has Muse's pages: General (with *Show DeepSeek Harness controls* under Developer, which brings the model picker, the modes and the workspace browser back), Account, Models, Agents, **Computer use** (the macOS permissions with *Open System Settings*, *Keep the screen awake while it works*), Devices, **Data controls**, **Help & support**, **Legal**, Advanced, Sign out. On macOS the shell's window has no title bar (the traffic lights sit over the rail) and its base colour follows the theme, so nothing flashes white in the dark ([docs/desktop-muse.md](docs/desktop-muse.md), [docs/harness.md](docs/harness.md)). ### Fixed - **The macOS builds of nanoMuse Harness package again.** electron-builder names the bundle after `executableName` (`nanomuse-harness.app`), which `scripts/desktop-app/package-mac.sh` did not look for; it now takes the bundle electron-builder left and renames it to the product's name before signing, so the zip and the dmg carry `nanoMuse Harness.app`. The bundle's shell test compares real paths, so the macOS temp dir under `/private/var` passes. Both found by the first macOS run of `harness-desktop.yml`; the 0.1.28 installers were built from `main` with these fixes. ## [0.1.28] - 2026-10-03 · Harness ### Added - **nanoMuse Harness: the desktop built on DeepSeek Harness, as installers.** Until now the Muse on the harness was a tarball for people who already ran DeepSeek Harness Desktop; `harness/desktop` is a desktop app of our own (Windows (`nanoMuse-Harness--win-x64.exe`), macOS (`-mac-arm64.dmg`, `-mac-x64.dmg`, and `.zip`), Linux (`.AppImage`, `.deb`)) with the harness, the nanoMuse bundle and the runtime for the hands inside, so nothing is installed at first launch and no Node, pnpm or Python is needed on the machine. The shell starts the harness's Host as a child process with its own Electron binary in Node mode (the way DeepSeek Harness's desktop does; Electron pinned to `44.0.0`, the version the harness's `require-builtin` addon accepts), against a profile of its own under `~/.nanomuse/harness` that names the bundle and links it from the copy inside the app; it loads the Host's URL in a window titled nanoMuse, opens external links in the browser, and when the Host does not come up puts the reason and the log's tail on the clipboard for an issue. `.github/workflows/harness-desktop.yml` builds all of it per platform on a release tag, boots each packaged harness once in Node mode as a check, and attaches the installers to the release. Unsigned for now, like the other desktop app ([harness/README.md](harness/README.md), [docs/harness.md](docs/harness.md)). - **The admin page says where people are** (relay 0.11.0). Every address on the operator's page (in the accounts table, the account drawer, the sign-ins, the statement, the timeline, the showcase's visitors) is named with its country, province and city, looked up in an offline copy of [ip2region](https://github.com/lionsoul2014/ip2region)'s database (Apache-2.0; city level in China, country and state elsewhere) that the relay fetches once after start into its data directory (`ip2region_v4.xdb`, 11 MB) and reads in memory: no third party is asked about a visitor, nothing more is stored. The admin answers that carry addresses add `places` (`{ip: {country, code, province, city, isp, text}}`), the address drawer says where the address is, and a *Where from* panel (`GET /v1/admin/places?days=`) counts accounts by their latest address, new accounts, sign-ins, requests and demo visitors by country and province. `CLOUD_GEOIP=0` switches it off; `CLOUD_GEOIP_DB` / `CLOUD_GEOIP_URL` name the file, `CLOUD_GEOIP_V6_URL` adds the IPv6 file; the overview's `geo` block says whether the file is there, being fetched, or failed, and the panel says so. - **The relay checks each catalog model instead of guessing from its name** (relay 0.11.0). After the provider's list is read, every chat model on it is asked, in the background and three at a time, to reply with one word and then to name the colour of a small magenta square (a colour no model guesses): a model the provider refuses (retired, say) is left off the list, `vision` is what the model answered (DeepSeek V4 on Model Studio reads pictures, and its name does not say so) and `verified: true` marks an entry the probes have confirmed; the answers are kept in the database (`model_probes`) for a week (`CLOUD_CATALOG_PROBE_TTL_S`; `CLOUD_CATALOG_PROBE=0` goes by the names). `/v1/models` carries `input_modalities` from it and `nanomuse.catalog.probing`; `GET /v1/admin/catalog` and a *Model catalog* panel on the admin page show which models answer, which see, which were refused and with what words. ### Fixed - **A phone task survives a dropped connection.** When the phone's socket went away in the middle of a task (the phone module in a browser tab reconnects with backoff after a drop, the Android app after a network change) the operator failed the task on the first request that got no answer, and the capsule on the phone was left on its last step. The operator now waits up to `[gui] reconnect_grace_s` (30 s) for a phone to be connected again and goes on from the screen it then sends; the link keeps the task meanwhile, so the returning phone's hello sets its capsule right; a phone that does not come back ends the task with a plain message, and the capsule still hears `end`. The showcase gateway logs each session socket's open and who closed it with what code, by session id, so a stuck phone can be read from the log. - **The web app stops reconnecting to a session that has ended.** Inside the showcase's phone (and in nanoMuse Web) the runtime web app reconnected every few seconds, for hours, after the gateway had closed its socket with 4404 (*this session has ended*): the production gateway log showed thirty to forty-five accepts a minute. The app reads 4404 as *gone*: no more retries, and a quiet banner says the demo has ended (or that this nanoMuse has stopped, outside the showcase). - **The capsule on the phone comes down with the run's last word.** In the phone module, the chat run's final message (the hands have returned by then) now ends the task's capsule and brings the phone back to nanoMuse, whatever became of the task's own `end`; a second net under the one from 0.1.27 for the capsule left on "step 6, looking" over a finished conversation. - **Natural Chinese where it still read like a translation**: the web app's dictionary, the Android app's strings, the Chinese README, the DeepSeek Harness bundle and the site. - **A model picked in the Android app stays picked, and the hands use it.** The chat's picker bound a choice to that one chat, and every new chat went back to the default group (the Cloud's recommended model) so a member who chose `deepseek-v4.1-flash` found the next chat on `qwen3.8-27b` without a word; a pick among the Cloud's models now moves to the front of the Cloud group (new chats follow) and a line says so. Hands picked its screen model with a name test (`vl`) over every model the account could name, which is how a member was billed for `qwen-vl-max` without ever choosing it: the order is now the model chosen in Hands settings, the chat model you picked when it can see, the default group, the Cloud menu's own screen model, the Vision Group, and a model from the Cloud's catalog only when you picked it. The Hands page says which model and why. In the picker the Cloud card opens expanded, menu first, with the catalog under "More models on your account" and a note on how they are billed; the chat's "•••" menu shows the model answering now. - **A thinking level set in an app no longer makes the Cloud model refuse** (relay 0.11.0). The relay's shipped `CHAT_DEFAULTS` adds `enable_thinking: false` to save reasoning tokens, and Model Studio refuses that next to a `reasoning_effort` other than `none`, so a member who turned thinking on in the Android app got a 400 on every message. The relay now reads what the request says about reasoning (`enable_thinking`, `thinking`, `reasoning_effort`, `thinking_budget`) and sets the default accordingly, and makes an explicit "off" consistent. - **An upstream error is written down with the model and the provider's words.** The `upstream.error` timeline entry said `chat 400`; it says `chat 400 qwen3.8-27b: 'reasoning_effort' must be 'none' when …` (the model and the first line of the provider's message, never what was asked) so the operator reads why a request failed. The devices hub no longer logs a traceback when a device's socket closes under its receive loop. ## [0.1.27] - 2026-10-02 · Ledger ### Added - **Data controls** (relay 0.9.0). *Settings → Data controls* on every app (the phone, the web and desktop apps, the dsh plugin, the console at `cloud.nanomuse.cn`) holds one switch, *Help improve nanoMuse's AI models*: while it is on, the relay keeps the text of the account's chats with the Cloud models for the community's own open model. What it keeps is narrower than the co-creation programme kept: what you wrote, what the model answered and the tool calls it chose, with the model, the token counts, the app and its language; not the system prompt (memory, SOUL, instructions), not what tools returned, not pictures, audio or clips, and never next to who you are. The page says how new accounts start on that relay (`IMPROVE_DEFAULT`; on `cloud.nanomuse.cn` it is on for accounts created from relay 0.9 on, and accounts from before keep what they had chosen), counts the turns kept, links the privacy policy and deletes everything kept with one tap. `/v1/me` carries it under `contribute` (`on`, `samples`, `default_on`, `keeps`, `privacy_url`), the account page shows how it stands and leads to Settings, and the timeline names every change (`contribute.default`, `contribute.on`, `contribute.off`, `contribute.deleted`). [docs/privacy.md](docs/privacy.md) says all of it in one place. - **The operator sees the data on the admin page.** A *Data controls* panel from `GET /v1/admin/data?days=`, how many accounts have the switch on (and the share), how many turned it off by hand, the turns kept over the period and in all, the tokens in them, a day-by-day line for each of samples, accounts, switched on/off, default-on and deleted, the turns by model and by app (Android, web, Windows, macOS, Linux), the thirty most recent turns in short with a masked account hint, and an export. - **The relay records where and with what, and the operator reads every line** (relay 0.10.0). Each sign-in, request, event and device now carries the visitor's network address (the first hop of `X-Forwarded-For` behind Caddy, else the socket's peer) and the client software (`User-Agent`, read as *Android 0.1.27*, *runtime on Linux*, *browser*); the account keeps its first and last address, its last client and when it was last seen (`accounts.first_ip/last_ip/last_ua/last_seen_at`, `ip`/`ua` on `api_keys`, `ledger`, `events`, `ip` on `devices`, added by migration). The admin page shows it all: a *Client / IP* column in the accounts table (the search box finds an address too); in the account drawer the last-seen line, an *Addresses / IP* section with how often, first and last and the platforms behind each address, the address and client on every sign-in, device, statement line and timeline entry; the statement and the timeline read on page by page to the first line (`GET /v1/admin/accounts/{id}/ledger|events?before=&limit=`, with the totals), not the last forty; and an address opens the accounts seen from it (`GET /v1/admin/address?ip=`), with a way back. The *Data controls* panel adds a *By account* table (every account with turns kept, how many, their tokens, first and last, the models) opening the account, where the kept conversations are listed in full: each turn expands to every message and every tool call with its arguments, untruncated, with an export of that one account's turns (`GET /v1/admin/samples/export?account_id=`). The kept turn's `meta` carries the address for the operator; the training export (`samples/export`) drops it. The page's foot and *What is recorded* say what is kept, and [docs/privacy.md](docs/privacy.md) and the privacy page say the same. - **The macOS desktop build signs and notarizes itself once the Apple credentials exist.** `scripts/desktop-app/package-mac.sh` reads a *Developer ID Application* certificate (`MAC_CERT_P12_BASE64`, `MAC_CERT_PASSWORD`) and an App Store Connect API key (`APP_STORE_CONNECT_KEY_ID`, `APP_STORE_CONNECT_ISSUER_ID`, `APP_STORE_CONNECT_KEY_P8`; `APPLE_TEAM_ID` checked against the certificate) from the environment (`.github/workflows/desktop-app.yml` passes the repository secrets of those names) and then signs the app under the hardened runtime with `desktop/app/resources/entitlements.mac.plist` in a keychain of its own, notarizes the app and the dmg with `notarytool` and staples both; without them it is ad-hoc signed as before and macOS asks for *Open Anyway* once. The certificate alone signs without notarizing. Until an Apple Developer account is behind the project the releases stay ad-hoc. - **nanoMuse on DeepSeek Harness ships as a preview.** The bundle in `harness/dsh-nanomuse` (the account as the *nanoMuse Cloud* provider, the Muse window, the first run, Hands over MCP and Reach both ways on [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness)) comes packed with every release as `nanoMuse-Harness-.tgz` (and `SHA256SUMS-harness.txt`), for people who run DeepSeek Harness Desktop 0.2.0-rc.2: start the app once, quit it, `dsh plugin --profile desktop add nanoMuse-Harness-.tgz`, start it again ([harness/README.md](harness/README.md)). `.github/workflows/harness.yml` builds, typechecks, tests and packs the bundle on every change under `harness/`, installs the tarball into a fresh dsh profile to be sure it loads, and on a release tag attaches it to the release; the release recipe checks the bundle's version with the others. It is a preview next to the desktop app, not instead of it: our own build of the dsh shell (name, icon, About, installers) is the next step in [docs/harness.md](docs/harness.md#phases). The bundle's version now follows the release's. - **A member picks from the models under the Cloud key; nothing to type** (relay 0.10.0). The relay reads the provider's own `/models` under the operator's key (once an hour, `CLOUD_CATALOG_TTL_S`; `CLOUD_CATALOG=0` switches it off), sorts the ids by their shape into chat and picture models: spoken, heard, embedding and rerank models are left out; video models are not on the compatible list, and lists them in a member's `/v1/models` after the menu, each `catalog: true`, `listed: false`, with `priced_as` and `vision` (whether the chat model reads pictures); `nanomuse.catalog` says how many and names the provider's error when the list could not be refreshed, in which case the last one stands. The runtime's `/api/llm/models` for the Cloud preset hands the pickers the list in groups (`menu`, `catalog`, `image_catalog`, `video_catalog`, `vision`), and the web and desktop apps' Connections → Model shows a member one list in two groups, *Menu* and *More models on your account* (the ones that read pictures say so), with *Type a model id…* still there; the picture and clip pickers get the same second group. The phone app and the dsh plugin read the relay's list already, so they get the models with no change. A guest sees the menu alone. - **The phone in the browser keeps its visitors' whereabouts, and the admin page shows them** (showcase gateway 0.3.0). The visitors' book records, per visitor, the address and browser string of the first and the latest sign-in or demo and how many sign-ins, and one *visit* row per demo started, when, from which address, with which browser, whether it brought its own key, and when it ended with what it used (requests, tokens, pictures, clips) and why. `GET /api/demo/admin` (header `X-Admin-Token` = `SHOWCASE_ADMIN_TOKEN`; `?account=` for one visitor; 404 without the token) hands it over, and the relay passes it through as `GET /v1/admin/demo` (`WEB_ADMIN_URL`, `WEB_ADMIN_TOKEN`) and into each account's drawer: a *The phone in the browser* panel with the visitors (sign-ins, demos, first, last, client / IP), the demos running now and the period's, and the account's own demos under its devices. The gateway now forwards the visitor's browser string to the relay with the sign-in, next to the address it already forwarded, and the production showcase calls the relay on the shared Docker network (`WEB_RELAY_URL=http://nanomuse-relay:8787`) so those headers arrive as sent; through the public name Caddy replaced them with the gateway's own address. - **Members may set any model the provider has** (relay 0.8.0). The menu is the menu for everyone; an account on the operator's list, or made a member on the admin page, may name any model under the operator's key by its id: a chat model for chat, an image model for pictures, a video model for clips, never across (`CLOUD_ANY_MODEL_MEMBERS=0` switches it off). The id goes upstream as typed; the ledger prices it as the dearest menu model of its kind and says so (`priced_as`). `/v1/models` and `/v1/me` carry `any_model` for the account, `GET /v1/models/?kind=chat` checks a typed id, and the runtime remembers `member` / `any_model` on the account view. In the web app and the desktop app, Connections → the account's model says so and lets a picture or clip model be typed (*Other model…*); on the phone, the provider's *Add custom model* and the media screen's *Other model* field already did. It is how a member tries a model (DeepSeek's, say) before it goes on the menu. - **The phone in the browser is drawn the way the Android app draws it.** The simulated phone (MobileGym and the showcase) shows the web app in lite mode, and that mode now follows the Android app screen by screen rather than its own older layout: the face on a disc with the name tag and status under it (silent when idle, *思考中…* while it works), round corner buttons, a plain bottom strip of five glyphs with the current one filled, the left drawer with the main chat, the devices, the coding assistant and the side chats; the tab pages: the Feed with its day heading and cards, Ideas as rows of emoji and pitch with a sheet per idea, Goals with its Tracking section and the categories, Library with its two-way control and 46px tiles, and the Android words (聊天, 点子, 资源库, 主要聊天, 旁聊); every screen reached from the drawer under Muse's page bar (the back glyph on a disc, the title centred); the chat as one grey bubble per block of a reply without tails and the ••• menu with 编程助手 / 记忆 / 设置; Settings as the phone's list of white cards and rows (model, agent, behaviour, app, about) each opening a page of its own on a narrow window and the sectioned page on a wide one; the agent page behind the face with its four panes (Activity, Approvals, Daily, Soul & memory) and the pen menu; the approval card with its grey disc, the *Exactly what will run* block, *Allow once* as the blue pill and the longer grants behind *for longer…*; and Devices as the phone's section, this device's two switches with their captions and its name, the other devices as rows with an online dot (*Ask* on the ones online), the hands card on a computer. The list pieces the phone draws with (card, row, divider, switch row, caption) are one component, `web/src/components/MuseList.tsx`. The wide layout is unchanged. - **The harness preview draws the desktop the way Muse does** (packed with this release as a preview: above). The bundle switches the harness's stock sidebar and general-settings row off and occupies their seats itself: an icon rail (the agent's face, Chats, Search, the harness's global panels, Devices, a corner menu for Settings, the shortcuts, Plugins and reporting a problem) beside a chats column that hosts the harness's own workspace browser, with the same child seats declared so every other plugin's contribution mounts where it did; the agent over the conversation with one status line (connected, thinking, the Hands step, waiting for you) and Stop while a turn runs; a Devices page; Settings as a grouped nav (General, Account, Models, Agents, Devices, then every page the harness's other plugins register under *Advanced*, Sign out at the foot) with the harness's own rows (permissions, language, appearance, font size, shortcuts…) mounted in our General page; and a first run the way the Muse desktop opens: the face, the slogan and what it can do, sign in with a phone number or e-mail (free), use your own key, or later, then three short cards (Hands, the workspace folder, your other devices) and a ready page. One stylesheet rebinds the harness's design tokens to the Muse palette. [docs/desktop-muse.md](docs/desktop-muse.md) writes the Muse desktop down screen by screen, interface only, with what the harness build takes, leaves out or does its own way; [docs/harness.md](docs/harness.md) gets the window as its fifth slice and the shell as dsh's desktop app with the bundle in its profile. ### Changed - **An invitation credits both sides, and the co-creation bonus is gone.** Each new person who signs up with your code adds ¥5 to your allowance and ¥5 to theirs (relay 0.9.0: `invite.invitee_bonus_cny`, a `credit` ledger row `from: invited` on the new account, `INVITE_BONUS_CNY` for both). The co-creation programme (its name, its one-time ¥10 and its step after sign-up) is replaced by the data controls above, which never change the allowance either way; the starter ¥10 is as it was. The exhausted-allowance message, the heads-up, the account pages and the first-sign-in steps (password, then which model answers) say so on every app, and the relay's `CONTRIBUTE_BONUS_CNY` is no longer read. - **The fine print says what the relay keeps and where to read it.** The sign-in pages, the community notice, the welcome and the ten READMEs said messages are not stored unless contributed; they point to the privacy policy and *Settings → Data controls* instead, and the apps' privacy links open `nanomuse.cn/privacy`. - **The three hosts promise HTTPS for a year.** `nanomuse.cn`, `demo.nanomuse.dev` and `cloud.nanomuse.cn` send `Strict-Transport-Security: max-age=31536000` (this host only; subdomains make their own promise), next to the `nosniff` and referrer headers they already sent. The showcase `Caddyfile` is in `caddy fmt`'s shape, so Caddy stops warning about it at start. - **One licence in every label.** The runtime, browser, showcase gateway and showcase Caddy images said `MIT` in their OCI labels, the gateway's `pyproject.toml` too, and the desktop app's `package.json` said `GPL-3.0-only`; all now say what the repository's LICENSE says, `GPL-3.0-or-later`. The showcase gateway is 0.2.0 in both places it carries a version (it had 0.1.0 in one and 0.1.1 in the other). - **The Android app reads optional JSON strings through one helper** (`JSONObject.stringOrNull`). `optString(key, null)` on Android gives back the *text* "null" for a JSON `null`, and the goals, their check-ins and the ideas the model proposes carry explicit nulls; a goal's session, task and last note, a check-in's status and note, an idea's time and category now come back as nothing instead of "null". Eight strings nothing referred to are gone from the three `nm_strings.xml` (742 each), and the deprecated Compose APIs the app still used: the mirrored icons (`Feed`, `Logout`, `OpenInNew`, `TrendingUp`, `LibraryBooks`, `InsertDriveFile`), `LocalLifecycleOwner` from `lifecycle-runtime-compose`, `using` in the avatar's keyframes, an opt-in instead of a suppression on the home bus: are replaced; 31 compiler warnings in nanoMuse's own code are 5. - **The desktop app is on a supported Electron.** Electron 33 left support a year ago; the shell is on Electron 44 now (Chromium's current security fixes with it), built by electron-vite 5 on Vite 7 and packed by electron-builder 26: `npm audit` of the desktop app goes from sixteen advisories (one critical, in `tar` under the old packer) to none. Electron 44 has no `openAsHidden` any more: *Start with the computer* registers the login item with a `--hidden` argument instead (macOS says so through `wasOpenedAtLogin`), and the app comes up in the tray without a window when it was started that way. - **The web app builds on Vite 8.** Vite 5.4 carried a path-traversal advisory in its dev server and `@vitejs/plugin-react` 4 warned at every test run under Vitest 4; the app is built by Vite 8 with plugin-react 6 now (`npm audit`: none), in under a second, with the shared helpers in a chunk of their own (`util-*.js`) next to the screens that were already split. Same screens, same strings, same look. ### Fixed - **The Android unit tests compile again.** `ReachTest` still called the address parser of the local-network pairing that went with 0.1.23, so `:app:testDebugUnitTest` failed to compile since then; the test now covers Reach as it is: a computer is a hub device, and `nanomuse-pc` names every verb in its help (73 tests under `io.github.nanomuse` pass). The chat view model's `nmDismissAllowance()`, called by nothing since the allowance card began clearing itself, is removed. - **The capsule on the simulated phone no longer stays on its last step, and the phone comes back to nanoMuse when the hands are done.** On the showcase phone a task could end in the chat while the pill at the top kept saying *第 6 步 · 正在看屏幕…*: the capsule relied on the server's `task end` alone, and that one message can be lost, the socket dropping just then, the operator giving up on an error, the chat run cancelled. Three things now hold it: the operator reports the end (or the question) whichever way the task finished, in a `finally`, shielded from the cancel; the phone's state in every socket's `hello` carries the task under way (`phone.task`), so a phone that reconnects sets its capsule from the server's word (back up if the task is still on, down if it ended meanwhile) and a socket closed for good takes it down at once; and a capsule left "working" with nothing from the server for three minutes comes down by itself. A send on a socket that closed under a request is a device error like any other now, not a crash of the task, and an `end` arriving after Stop no longer turns the red *Stopped* into a green tick. Then, as the Android app brings itself to the front when its hands are done, the simulated phone resumes the nanoMuse app a moment after the tick (after Stop, and when the capsule comes down by itself over a lost task that had gone somewhere): the report is there, not in the app that was operated. [docs/gui.md](docs/gui.md) writes the contract down for other devices. - **nanoMuse is first in the dock beside the phone, and the sign-in fields stay above the keyboard.** The dock on the showcase phone's right edge is MobileGym's State Builder (WeChat, Alipay, SMS, 12306, Weather) and nanoMuse was not in it; its launcher icon now sits at the top of the dock, a shortcut that brings the app to the front (or turns the phone on) and is lit while nanoMuse is on the screen (`site/compose.mjs`, `page.js`). And on the app's sign-in page, typing a password (or anything in a field) had the simulator's keyboard shrink the page while the pill's column kept its room, so the field slid under it and out of sight: with the keyboard up the hero, the mode switch and the fine print step aside and the field being typed in is scrolled into view, so the phone number, the code or the password and the *Sign in* pill are all on the screen (`demo/mobilegym/apps/nanoMuse/pages/SetupPage.tsx`; the own-server form and the own-key form as well). - **The relay no longer logs a traceback when a client disconnects mid-request** (relay 0.7.2). A phone changing networks or a tab closed while its body was still arriving raised `ClientDisconnect` inside the chat route, which the server logged as `Exception in ASGI application` with forty lines of stack: seven times a day. It is a quiet `400 client_disconnected` now, to a caller that is not there to read it. - **The docs say what nanomuse.cn/web is now.** The Install paragraph of the README in its ten languages, the *every device* page, the design record of the web app and the roadmap still described the kept Muse per account; they describe the demo on the simulated phone, and keep the kept-Muse lane as what `WEB_ENABLED=1` runs. - **The archived design record of the Python-line Android app links to its neighbours again.** - **Dependabot watches every package.** The desktop app's npm packages, the relay's and the showcase gateway's Python packages and the four Dockerfiles were outside its config, which is how the desktop app sat on an end-of-life Electron; it covers them now, with Electron majors as PRs of their own. - **`CITATION.cff` names the current version.** It had stayed at 0.1.11; the release script now checks it with the other version fields, so it cannot fall behind again. `docs/archive/android-python-line.md` moved into `archive/` with its relative links unchanged; they point one level up now. ## [0.1.26] - 2026-10-02 · Window ### Added - **The showcase is a page around the phone.** `demo.nanomuse.dev` opens on a simulated phone (MobileGym, at `/phone.html`) with nanoMuse brought to the front and a private Muse started for the visitor; beside it, lines to try (a new look for the Muse, the apps on the phone operated for you, memory and reminders) each a tap that puts the text in the chat on the phone, and the session's status. The nanoMuse app on the phone exposes `window.__NANOMUSE__` for that page (`demo/mobilegym/apps/nanoMuse/host.ts`) and hands drafts to the web app over `postMessage`; the web app takes `?ui=lite` (the chat alone, no first-run setup) and tells its parent frame when it is up. The gateway draws the new look: the OpenAI images calls a session's avatar studio makes are answered on Model Studio's native endpoint with the demo key (`IMAGE_MODEL`, `IMAGE_PER_SESSION`, `DAILY_IMAGES`; `gateway/showcase_gateway/images.py`), and the container learns the model through the new `NANOMUSE_LLM_IMAGE_MODEL` / `NANOMUSE_LLM_VIDEO_MODEL` overrides. A visitor's Muse no longer asks for a Cloud account (the gateway sets `NANOMUSE_CLOUD_REQUIRED=0` for the session: the sign-in gate used to stand in front of the demo). With the kept Muses off (`WEB_ENABLED=0`), `/web/` redirects to the phone, so `nanomuse.cn/web` stays the web entry when the sign-in version is taken down. - **The phone in the browser takes the web version's place, and asks who is trying it.** nanomuse.cn/web now leads to the showcase (the simulated phone with a private Muse on it) and the kept Muses per account (`WEB_ENABLED=1`) are off in production. Before the phone starts a Muse, the visitor signs in to nanoMuse Cloud on the phone's own pages, the Android app's door (*Sign in: free*: a mainland phone number or an e-mail and a six-digit code, or the account's password; the relay's refusals in the phone's language; a first sign-in creates the account, with its free allowance, so the app is already theirs the day they install it). The gateway puts the sign-in to the relay (`POST /api/demo/signin/code`, `/verify`, `/login`, `/signout`, `GET /api/demo/me`), keeps the account's opaque id, the relay's masked identifier and the channel in `VISITOR_DB` (never the identifier, never a message) revokes the device key the relay issued (the demo Muse talks to the showcase's model, not to the account's allowance) and hands the browser a ticket (`VISITOR_TTL_S`, thirty days) that `POST /api/demo/session` wants as a bearer (`401 signin_required` without one). One account is one person wherever it signs in from: `PER_ACCOUNT_ACTIVE` Muses at once, `PER_ACCOUNT_DAILY` a day; the session log names the visitor by the masked identifier and `GET /api/demo/info` → `signin` counts them. `DEMO_SIGNIN_REQUIRED=0` keeps the old way: a Muse on the first tap. And the page says what it is: a card beside the phone, *a demo in a simulator*: the phone and its apps are MobileGym's re-creations, the nanoMuse in it the lite web app, a long way from the Android app, with *Get the Android app* and *Desktop* to the download section, the same line under the pill on the phone, and the status line tells a visitor who is not signed in what the pill does. The welcome page on the phone also gained the Android app's notice card (free, open source, non-profit, a tap opens the project's page) and its meet page says *Meet nanoMuse*, the app's name, as the Android app does. `demo/showcase/gateway/showcase_gateway/visitors.py`, `relay.py`; `demo/mobilegym/apps/nanoMuse/pages/SetupPage.tsx`. - **The phone in the showcase behaves like the Android app, and keeps MobileGym's own controls.** The nanoMuse module for MobileGym now announces itself as a device with a capsule, and shows the one the Android app has: while the Muse operates the phone, a pill at the top with its face, the step in progress (*第 3 步 · 点击「查询车票」*) and **Stop** (the action in flight fails with `nanomuse:stop`, the agent asks what to do next); a ring with the action's words marks the target before the tap lands, a trail each swipe, a chip what is typed; a password or code field is never typed into: *Your turn*, the person fills it in and taps *Continue*; an approval the agent waits for, and a question it has, become a card with *Open*. The screen it sends is the Android app's: a 720×1600 picture (twice the phone, so the model reads small text; taps scaled back) and a list of up to 120 elements read off the DOM. The module speaks 简体中文 and English, following the simulator's language (`res/strings.ts`, MobileGym's `useAppStrings` convention): the setup page, the hosted-Muse card, the notifications and the capsule. The web app's `?ui=lite` keeps the phone layout and the Android app's tabs at any width (only the first-run setup, the sidebar and the desktop hints are off). The page around the phone is composed at build time (`site/compose.mjs`) from MobileGym's own `web/index.html`, stylesheet and scripts: the Gesture Guide (Back, Home, Recents), the State Builder dock and drawer (snapshots, phone language, device time / battery / location, WeChat, Alipay, SMS, 12306, Weather, patched live), *Power off* (nothing cut down) with our lines to try beside the phone. And a new look moves there: the gateway's clips lane (`gateway/showcase_gateway/clips.py`) stands in for Model Studio's storage so a container without internet can animate its face (the policy, the frame, the task, the polling and the finished MP4 all go through `/llm//main/api/v1/*`) counted per session and per day (`VIDEO_MODEL`, `CLIPS_PER_SESSION`, `DAILY_CLIPS`); the runtime learns where the video API lives through `[llm] video_base_url` / `NANOMUSE_LLM_VIDEO_BASE_URL`. - **The showcase on one screen, and the phone in it the Android app's.** The page fits the window: the phone is scaled to the height left under the header (a real bezel around it, the screen clipped to its corners), the panel beside it holds the lines to try and nothing more (the paragraph that explained what a phone and a Muse are, the per-group blurbs and the folded "how it works" are gone (one link to the README stays)) and nothing scrolls. The nanoMuse app on the phone opens on the Android app's welcome page (`FirstRunSetup.kt`'s: the dragon's face, *Welcome to nanoMuse*, the one line, the three rows (chat, Hands, Reach) and a pill), where the pill starts the visitor's Muse; *I have my own API key* opens the three fields, *Connect your own nanoMuse* the form for a server of one's own; the cards that explained the operate-this-phone switch and `nanomuse serve` are gone. Its launcher icon is the Android app's: the one-stroke N on a white tile (`res/mark.tsx`, the path of `assets/brand/nanomuse-mark.svg`), its colours the web app's palette with the Android app's action blue, its words the Android app's (`nm_welcome_*`, `nm_setup_*`, the step captions `nm_hands_fx_*`: *回主屏*, *打开 微信*, *等 2 秒*, *向上滚动*), and the strips above and below the web app's frame follow the scheme the web app reports (`{type: "nanomuse:theme", theme}`, with `nanomuse:ready`) instead of the browser's. The site build takes the phone's media from `/cdn` (the gateway's `CDN_DIR`) or, with `MOBILEGYM_CDN_BASE=https://cdn.mobilegym.dev`, from MobileGym's CDN: a build without either had the launcher's widgets failing and the media apps empty. Two hints that said the obvious are out: the Library's *Tap one to open it here* (web) and *Tap a picture to choose it* in the avatar studio (web, Android). - **nanoMuse on DeepSeek Harness: an internal preview, not in any build.** The start of the next desktop as a set of plugins on [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) instead of our own shell around the Python runtime: a bundle in `harness/dsh-nanomuse/` that signs in to the account and hands its models to the harness's model layer as the *nanoMuse Cloud* provider, puts the dragon and the name in the sidebar and the hero, adds the *nanoMuse account* section to Settings, and declares an agent preset with nanoMuse's voice over the harness's standard tools, plus the Hands: the new `nanomuse mcp` command serves this computer's `computer_screen` and `computer_act` over MCP on stdio (screenshots as images; a step the Sentinel would ask about is refused until the call carries `confirmed: true` after the person agreed), and the preset mounts it, so the harness's model looks at and operates the screen with the runtime's own tools. Verified against a local relay on a scratch install; where each part of nanoMuse goes and in what order: [docs/harness.md](docs/harness.md). The desktop app in `desktop/` is unchanged. - **The harness preview gets the Muse style and Reach** (still internal, not in any build). The first run meets the agent instead of asking for a DeepSeek key: the face, the line under the name, what Hands, Reach and the Muse style are, then *sign in with a phone number or e-mail (free)*, *use my own API key* or *later*. The name, face and moods come from the account, as on the phone: the host pulls the relay's profile when the hub says it changed and the sidebar and hero wear it (the dragon, an emoji on its colour or a drawn face) *working* while a session runs and *waiting* while one asks; a rename on another device is on the desktop within a moment, and the model answers to that name. The desktop joins the account's device list (a hub client in TypeScript; it answers `info` and `notify`, the latter as a toast) and a second plugin, `dsh-nanomuse/reach`, brings the tools the phone has: `devices`, `device_screen`, `device_shell` and `device_open` (the harness's approval card first), `device_files`, `device_notify`, `delegate` (the other device's Muse runs the task; when it asks for an approval the card appears here and the answer travels back). While Hands or Reach work, a pill at the top shows the face, the step, what it is doing and *Stop*; Settings → *nanoMuse account* lists the devices with online dots, renames this computer and forgets offline ones. Reach runs the other way too: the desktop answers the phone's `shell`, `files`, `file.get`, `file.put`, `open` and `screen` with the same shapes, limits and error codes the runtime gives, behind a *Remote control* switch under *this computer* in that section (off, other devices only see it and can notify it), and each call shows as a toast after it happened; pressing *Stop* on a delegated task now stops the job on the other device as well. And the phone's `delegate` lands here: a task from another device runs in a harness session named *From * (resumed for the next one, across restarts), its steps stream back to the asker, any approval it needs appears on the asking device and is decided there, and `stop` ends it. - **The desktop lays itself out the way Muse's does.** On a wide window (the desktop app, a browser at full width) the left side is a rail of icons with the agent's face and name on top (a tap opens its activity, the approvals, what it may do and what comes next) and the five sections, the devices (a green dot when another of yours is online) and, at the bottom, Account, Settings and a small *More* menu: *Report a bug* (the issue form with the version and the platform filled in), *Developer tools* and the version. While the chat is open the chats sit beside the rail in a column of their own (a search box, the *Main chat*, the *Side chats* with a +) and the content takes the middle with the agent's face and status pinned over it, as before. Settings on a wide window has its sections down the left, a click away each (the shape of Muse's settings) and the same one page on the phone. The developer side is behind one switch, Settings → *Developer* → *Developer tools* (this device only): the *Coding* screen in the rail and the runtime's address and where its token is, with links to the command line and to the DeepSeek Harness bundle; `#coding` still opens the screen. The desktop app gains Settings → *Desktop app*: *Start with the computer* (Windows and macOS; the login item opens in the tray), a line on the tray icon, and the shortcuts, a **quick chat** on ⌥ Space (Mac) or Ctrl+Shift+Space (Windows, Linux) brings the window up with the cursor in the composer, also in the tray menu; the window on ⌘⇧M / Ctrl+Shift+M and Stop on ⌘⇧Esc / Ctrl+Shift+Esc as before. `web/src/components/Sidebar.tsx`, `web/src/devtools.ts`, `desktop/app/src/main/index.ts`. - **The avatar studio is a screen on the web and the desktop, the phone's.** The pen on the avatar in the profile sheet opens the phone's three choices (*Change the look* (the chat, with the request started for you), *Edit the name*, *Avatar studio…*) and the studio screen is the phone's page: the face at the top, cycling through its moods, with what it is and in which style; *Describe a new look* with the seven style chips and *Draw four*, drawn by the image model named under the button (*Alibaba Cloud Bailian · qwen-image-3.0*, with *Change model*); *Pick one* on a 2×2 of candidates, *Use this one*, and the poses then the clips drawn in the background with a progress line (*Drawing the poses… 3/5*; *The new look is on; making the clips… 2/4*), all without a card in the chat. The ··· menu has *Name & style*, *Image model*, and for a drawn face *Redraw the poses* (`POST /api/avatar/moods`) and *Back to the built-in look*. Settings' avatar row has the studio tile where the describe box was; the chat still takes "change your avatar to a corgi". The runtime keeps `avatar//face.json` with each face's description, style and model, reports the studio's progress on the socket (`{"kind": "studio", "current"}`) and `/api/avatar` says which face the profile wears and what it is. `web/src/screens/AvatarStudioScreen.tsx`. ### Changed - **The showcase talks with DeepSeek V4 Pro.** `MAIN_MODEL` defaults to `deepseek-v4-pro` on 阿里云百炼 (the 27B Qwen was not up to the longer tasks). Hands reads a screenshot at every step and DeepSeek takes no images, so the operator lane falls back to the sighted `qwen3.8-27b` on the same host and key when the main model is a text-only family; `GUI_MODEL` still picks it by hand. - **The line under the name is "An open-source personal agent for every device you own."** *Fully* and *Muse-style* are out of the slogan: the first sentence of the README still says it is an agent like Meta's Muse, and the promise of no closed component stands. Every README opens on the cover (`assets/brand/nanomuse-cover.png`, with the SVG beside it: the mark, the name, the line) where the icon, the heading and the sentence were; the same line on the Android welcome screen and About page, the iOS About view, the desktop package's synopsis, `CITATION.cff`, the roadmap and the website. - **The README in ten languages, and a header of two lines.** 繁體中文, Español, Français, Bahasa Indonesia, 日本語, 한국어, Русский and Tiếng Việt join English and 简体中文 (`docs/readme/README_.md`: the nine translations live there, so the repository's front page lists one README; the notice, what nanoMuse is, why, how to install, what it does, how to contribute, the disclaimer and the licence; the news and the version table stay in the English one, which each translation names as the reference). The top of every README is two lines: the ten languages, then six badges (stars, downloads, *Test Suite* (GitHub's own badge for the `ci.yml` workflow, renamed from *CI*), *Try in the browser*, *Website* and the licence) each checked to load; the three rows of release, platform and Docker badges are gone. - **nanomuse.cn/web's sign-in page fits a landscape window too.** From 900px the words: the brand, *Try nanoMuse*, four short points (nothing to install; a model with a free allowance; it keeps everything; the same account everywhere) and the two notices: sit on the left and the form on the right, both centred, as the desktop app's page does; a phone keeps the single column with the points as a small grid under the form. The first screen used to be one long column with everything in it. - **A device keeps the face it drew when another device renames** (relay 0.7.1). A rename on the phone used to come back to the desktop as "a face from the account": the same pictures downloaded again into `avatar/sync-/`, worn under that name, and the clips the studio had made and the face's description left behind (so *Redraw the poses* said there was none), and the phone did the same the other way. The relay now names the pictures it holds (`face_id`, the hash of the idle still), a device whose worn face has that hash takes the name and nothing else (an older relay is checked against the downloaded still), and the face's description and style travel with it, so the other devices show what it is and can redraw its poses with their own image model. Two clips at a time, and a clip the video provider refuses as too many (429) is submitted again after a pause: four at once used to lose two of them. - **The README says where the project is.** A short section before *Contribute* in every README, and the same words in the release-notes template, the roadmap and the site: the 0.1 versions are a preview, with rough edges we know about and more we do not, so what breaks and what is missing is the most useful thing to send; the apps are for anyone and the developer side is there when turned on; the product and the skill and plugin interfaces keep moving quickly for a while and settle as they go; what we are after is a personal agent that belongs to the person who runs it, built in the open on infrastructure anyone can reuse, worked out with users and developers everywhere. - **Commit subjects follow Conventional Commits.** `type(scope): subject` (`feat(android): …`, `fix(relay): …`, `docs(readme): …`) on every commit of a pull request; the *Commits* check (`.github/workflows/dco.yml`, which already wanted a `Signed-off-by`) fails on a subject that does not fit, merge commits excepted, and Dependabot's are prefixed `chore(deps)`. [CONTRIBUTING.md](CONTRIBUTING.md) has the types and the scopes. - **GitHub releases in the shape people know.** A release page is a short story, then *Highlights*, *Upgrade Notes* (the APK, the un-notarised installers, what an operator or an upgrader must know, where to get it) and *Community*, followed by *What's Changed* (every pull request merged since the previous tag, `title by @author in url`), *New Contributors*, *Contributors* and the *Full Changelog* compare link: the last four generated by `scripts/release_notes.py`, which `scripts/release-apk.sh` runs when it publishes (`` in the notes says where; the Chinese block stays at the end). `docs/release-notes-template.md` has the new shape and 0.1.25's page was rewritten in it, assets untouched. ### Fixed - **The Linux desktop app works again.** Every request to the 0.1.25 Linux runtime died with `No module named 'tkinter'`: `pyautogui`'s dependency `mouseinfo` *exits the interpreter* when tkinter is missing on Linux, and the frozen runtime leaves Tk out, so the first look at the hands (part of every state push) took the request down with it, and the window stayed on the welcome. The runtime now registers a stand-in `mouseinfo` before importing `pyautogui` (nothing ever opens the MouseInfo window), a backend whose set-up fails in any other way merely counts as unavailable, and `xdotool` is the fallback as before. - **Windows: a machine that intercepts its own loopback is told so, or served over `::1`.** On Windows, asyncio emulates its self-pipe with a TCP connection to 127.0.0.1 and waits for it without a timeout; on a computer where a proxy client routes every connection (Proxifier, Clash/V2Ray/Surge in TUN mode, a game accelerator) or security software swallows local connections, `nanomuse serve` sat there forever: alive, listening on nothing, the desktop app giving up after two minutes with "did not answer on /api/health". The runtime's `socket.socketpair` now waits three seconds, falls back to the IPv6 loopback when only IPv4 is taken, and otherwise stops with an explanation that names the usual culprits and what to set (`loopback blocked:`, `nanomuse/loopback.py`); the desktop shell makes the same test before it starts the runtime, over `::1` when 127.0.0.1 is taken, with the window pointed there, and shows the explanation at once instead of probing for two minutes. ## [0.1.25] - 2026-10-01 · Mirror ### Added - **One name and one face for the account** (relay 0.7.0). The agent's name and look (the dragon, an emoji on a colour, or a face drawn in the avatar studio with its five stills) now live with the account (`GET` / `PUT /v1/me/profile`, last writer wins, a `rev` that grows on every write) and follow you to every device: a rename or a new face on the phone is on the desktop within a moment, and the other way round. The relay tells the devices on the hub (`{"type": "profile", "rev"}`), each fetches the new rev and wears it; the device that wrote it skips its own echo, a rename does not resend the pictures, and the first device to sign in seeds an empty profile with what it has. Nothing else travels (keys, providers and settings stay where they were entered) and an emoji look, which the phone cannot draw, is the dragon there. `nanomuse/hub/profile.py`, `io.github.nanomuse.cloud.ProfileSync`; the privacy note, [docs/cloud.md](docs/cloud.md) and [docs/hub.md](docs/hub.md) say what the relay now keeps. - **A stop button on the desktop stage.** While the hands work the computer, the pill at the top of the screen has *Stop* (a click ends the run the way the shortcut does) and the stage lets clicks through everywhere else, so the desktop stays usable underneath; Linux gets the same through the window's shape. - **The avatar studio draws in seven styles on the web and the desktop** (3D toy (Muse's), flat, 3D clay, watercolour, pixel, line, sticker) the phone's list, same words to the model, so a face described on either device comes out alike. *Settings → Draw a new one* shows the chips and starts the studio straight away (`POST /api/avatar/begin {description, style}`); the request from the chat keeps the 3D toy look. - **Appearance.** Every surface (the web and desktop app, nanomuse.cn/web's sign-in page, the relay's console and the site) is light by default; *Settings → Appearance* has *Light*, *Dark* and *Follow the system* (this device only), and the site's footer has a toggle. Before, a system set to dark made every page dark whether or not you wanted it. ### Changed - **Fewer words on every screen.** The Devices, Account, Connections and Settings screens of the web and desktop app, and the phone's welcome, Cloud, Devices, PC, Hands, coding and media pages, keep every control and lose the paragraphs around them: one line under each heading, a plain *Rename this device*, the Sentinel modes without their ids, a *Connections* row where the list of tool names was, avatar tiles the size of a thumb, a name field without its hint and counter, and Cloud fine print that says what the relay keeps (an account id, a masked identifier, usage counts and the agent's name and look) in one sentence. - **The runtime opens its port first and finishes starting behind it.** uvicorn used to run the whole start-up (tools, the cloud account, the hub) before the socket was bound, so a slow step left `/api/health` unreachable and the desktop app waiting for a port that never opened; the services now start in the background after a short grace (`[server] start_grace`, 4 s) and `/api/health` says `starting: ""` while they do. If the app is still not serving after 20 s and 60 s a watchdog writes where every thread is to the log, so a hang on one machine can be read instead of guessed at. The desktop shell's error box tells the two apart: a runtime that answered but never came up (the log has the stack; security software or a firewall on 127.0.0.1 is the usual cause), a `config.toml` that does not parse (only when the log really says so: "config.toml 读不出来" was shown for every failure before), and a Linux runtime refused for its glibc (install 0.1.24 or newer; those are built in Debian 11). `netstat` on Windows is read with a 64 MB buffer (`ENOBUFS` on machines with many sockets) and without a console window. - **The relay's certificate is checked with a bundle the bundled runtime has.** The desktop runtime on macOS (PyInstaller's Python, no system CA store on its path) could not verify `cloud.nanomuse.cn`'s certificate; `ssl.SSLCertVerificationError` is a `ValueError`, so the hub filed it as *refused* and the Devices page said the account had turned this computer away. The runtime now points Python at certifi's bundle when the system has none to offer (`SSL_CERT_FILE`, `nanomuse/certs.py`), a certificate failure is reported as a disconnect with the reason, and the Devices page shows the reason for a disconnected hub too. - **The sign-in page fits a landscape window.** In the desktop app and a wide browser the words, the perks and the community notice sit on the left and the form on the right, both centred, instead of a phone column in the middle of a wide window; a phone keeps the single column. The form says less: the label already says *Mainland China phone number or e-mail*, so the SMS-or-e-mail line under it and the `cloud.required` footnote are gone, and the password hint is one line. - **Your own key is a step after sign-up, and a row in Settings.** After a new account the web app asks the third question the phone asks: *Which model answers?*, the account's own, or *I have my own API key*, which opens Connections with Alibaba Cloud Bailian preselected; and while the account's model answers, *Settings → Model* shows *Use my own API key* under the model row. Before, the way to a key of one's own on the desktop and the web was Connections → Model → a provider chip, unnamed as such. - **Pictures are queued, not refused** (relay 0.6.3). The image provider allows an account only a couple of pictures at once and answered the studio's four with `429 Requests rate limit exceeded`; the relay now draws for everyone through a gate of two and retries a 429 or a 5xx with growing pauses (honouring `Retry-After`) before answering `429 provider_busy` with `retry_after`; the runtime's studio draws two at a time and waits a 429 out three times; the phone already did. The card says *The image provider is busy right now: try again in a minute* when it still fails. `IMAGE_CONCURRENCY`, `IMAGE_RETRIES`. - **A number the SMS sender cannot reach is told so at once** (relay 0.6.1). 号码认证 sends codes to mainland China numbers only; a Hong Kong, Taiwan or overseas number used to get a code that never came and "try again in a moment". The relay now answers `400 phone_region` before making a code: *Codes reach mainland China numbers only for now; elsewhere, sign in with an e-mail address*, and the phone, the web app, the desktop, the web version's sign-in page and the relay console show that sentence; the sign-in field says *Mainland China phone number or e-mail*. - **The relay's web console takes a phone number too** (relay 0.5.4): the sign-in field says *Phone number or e-mail*, switches to the number keypad when you start with a digit, and the refusal names both; the docs and the docstrings that still said "sign in with an e-mail" say a phone number or an e-mail. ## [0.1.24] - 2026-10-01 · Signal ### Added - **Sign in with a phone number.** A mainland China mobile number is a first-class way in on every platform: the phone, the web and desktop app, and nanomuse.cn/web, next to an e-mail: the six-digit code comes by SMS (relay 0.5.3 sends it through Aliyun's 号码认证服务 `SendSmsVerifyCode`, `ALIYUN_SMS_API=dypns`, with the service's ready-made signature and template; `dysms` keeps the older 短信服务 way), and a password set under Account works for either. The identifier box switches to the number keypad when you start with a digit; on the phone the code box is marked as a one-time code so the system can fill in the SMS it just received. The gateway's sign-in page gains the password way (`POST /api/web/login`) and says, first, that the browser version is for a first try: the phone app and the desktop app are the everyday ones, with the same account. - **The face moves on the web and the desktop.** The dragon plays the four short clips the phone has (a head shake at rest, headphones and a laptop while working, a crystal ball while waiting, a star when pleased) wherever it is drawn at 44 px or more (the chat header, the welcome, the sheet, onboarding); small sizes, lists, a system set to reduced motion and a clip that fails to load show the still, as before. The avatar studio makes the same four clips for a new face once its stills are on, through the video model where the endpoint has one (the relay's `wan2.2-i2v-flash`, or Wan on Alibaba Cloud Model Studio, by the asynchronous video API the phone uses (`[llm] video_model` overrides)) and the cost card counts them ("8 pictures and 4 clips"); a face from an OpenAI-compatible provider without a video API keeps its stills. `avatar//.mp4`, the `clips` field of the `avatar` event, the `animating` stage. ### Changed - **A signed-in runtime with no model key uses the relay.** A desktop or web runtime whose account is signed in but whose model still pointed at a provider with an empty key (the DeepSeek default) answered every message with a 401; at start-up the hub now makes the relay the model in that case, and leaves a local server (Ollama, LM Studio, a LAN address) alone. - **When the runtime is slow to start, both sides say where they are.** The runtime starts its parts in named steps with time limits (tools, then the cloud account and hub, then the scheduler) and logs each; `/api/health` says `starting: ""` until it is done; the welcome banner is printed once the app answers, and the console is UTF-8 on Windows. The desktop shell probes health with Node's own HTTP client, and every ten seconds while it waits writes the probe result, whether the port is open and who listens on it to `desktop-app.log` (the same file its own lines go to now) and quotes them in the error box. - **The Linux desktop runtime runs on older systems.** It is built in a Debian 11 container (glibc 2.31), so Ubuntu 20.04 / 22.04 and their kin no longer stop with `GLIBC_2.35 not found`. - **The hands are findable when they are off.** On a computer with the hands off, the agent's briefing says so and names the switch (*Devices → Hands on this computer*), so "open the calendar app for me" gets the way to it instead of a shrug; the Hands card tells Mac users about the Accessibility and Screen Recording permissions macOS asks for. ### Removed - **The local-network host script.** `host/nanomuse_host.py` and *Pair over the local network* on the phone are gone; a computer joins by installing nanoMuse Desktop and signing in with the same account, on any network. *Settings → Computers* lists the account's computers, says which account this phone uses (a computer that is missing has nearly always signed in with another) and a tap tells whether it answers. `nanomuse-pc` and the agent's briefing speak of the hub only. ### Fixed - **Long contributed conversations are kept whole** (relay 0.5.2). A turn over the size limit was cut mid-character and then failed to parse, which broke the operator's samples list and export with a 500. Oversize turns are now cut by whole messages from the middle (the system prompt and the last exchange stay), then the longest text is shortened with a marker; rows cut the old way still load, marked `truncated`. ## [0.1.23] - 2026-09-30 · Welcome ### Added - **The avatar studio on the web and the desktop.** What the phone has had since 0.1.20, now run by the runtime so both share it ([docs/avatar.md](docs/avatar.md)): 「换个形象:一只橘猫」 or "new avatar: a robot owl" in the chat (or *Settings → Draw a new one*) opens a card that says the cost first (the relay's `/v1/estimate` against your allowance; the count alone with your own key), draws four candidates to pick from (tap, or 「第二个」 / "the first one"; *Redraw*), then the four poses from the one you chose, and the new face is on everywhere. Pictures come from the chat model's host: the relay and any OpenAI-compatible provider through the images API, Alibaba Cloud Model Studio through its native endpoint. `[llm] image_model` / `video_model`, `GET/POST /api/avatar/*`, the `avatar` timeline event. Without an image model the chat says so instead of trying. ### Changed - **The web app wears the blue N**: `icon.svg`, `mark.svg`, the PNG icons and the manifest colour are the mark the phone, the desktop and the site use. The red panda drawn live and the six knitted dolls are gone; the dragon is the only drawn face (a profile that still names one of the old faces wears it), next to a face from the studio and the emoji. - **The desktop app lays itself out like one.** In the Electron shell the sidebar is there at every window width and the content runs across the window: the phone column with borders is for a phone-sized browser tab only; on a Mac the sidebar starts below the traffic lights and that strip drags the window. Connections → Model gains *Pictures and clips*: the picture and clip models the studio draws with, listed from the endpoint (the relay says what each model does; other hosts by name), *Automatic* by default. - **When the runtime does not start, the desktop app says something useful.** The wait is longer (120 s on Windows, where the first launch of the bundled runtime is slow behind Defender), the quoted log is this session's only (the file is appended to across runs, and a failure from last week was being read as today's) and the error box has *Copy details* (message, versions, the session's log tail, ready for an issue) and *Open the log folder* instead of a lone OK. - **A computer joins by signing in, not by script.** *Settings → Computers* on the phone leads with the way that needs nothing typed: install nanoMuse Desktop, sign in with the same account, and the computer is under *Account → Devices* within seconds, on any network, with notifications and whole tasks, and folds the local-network host script away as the fallback for a computer without the account; the agent's own briefing says the same. The desktop app says once, when it first reaches the hub, that the phone can now operate this computer, with the way to Devices. [docs/android.md](docs/android.md), [docs/hub.md](docs/hub.md). - **The stage, the same on both.** The light that runs round the screen while the hands work is one comet in three hues (blue into violet into cyan, a whitish head) on the desktop stage and the Android stage alike, an 8 s lap with a longer, fainter tail and the haze 30 % quieter; amber while the hands wait for you (the desktop stage now turns amber too, when the operator stops to ask). The phone's capsule is the desktop pill: dark and translucent with a hairline edge, the face in a slowly turning three-hue ring, four activity bars, a two-weight title and detail, a low-saturation Stop, sliding in from the top and out again. - **A download mirror in reach of China.** The site's server keeps the newest two releases under [nanomuse.cn/dl/](https://nanomuse.cn/dl/) (`demo/showcase/mirror/release-sync.py` on a fifteen-minute timer, every file checked against the SHA-256 GitHub records for it, `latest` pointing at the newest tag) and the site's download list has a source switch, GitHub or the mirror, that a browser in Chinese starts on. Both READMEs point to it. - **The operator sees the whole picture.** Two panels on the relay's admin page: the relay's own numbers by day (sign-ins, new and active accounts, sign-ups through an invite, co-creation joins, calls, refusals) with the devices by kind and system, the invite funnel and nanoMuse Web's counts; and the site's visits and downloads (page views, visitors, crawlers, downloads per file from the mirror next to GitHub's own counts, stars, referring sites) counted by day from Caddy's access log by `demo/showcase/mirror/traffic.py` (`nanomuse-traffic`, every ten minutes; the log itself lives a week). No addresses are stored: a visitor is a salted hash for the day. `docs/privacy.md` says so. Relay 0.5.1. - **The samples export downloads again.** The operator's "Export JSONL" came back as "Failed to fetch": the training set was streamed and re-encoded on the way. It is now one whole response with its length, Caddy leaves that path uncompressed, and when something does go wrong the page says what (connection, HTTP status, a token that expired) instead of the browser's one line. - **The repository's front.** GitHub Discussions is open (Q&A, ideas, show and tell); the description and topics say every device rather than Android alone; the README badges now cover Web, Android, Windows, macOS, Linux and Docker, the release download count, CI, the desktop and Docker builds. - **Coding agents, three small things.** `running` counts on macOS and Windows too (`ps`, `tasklist`; only Linux's `/proc` was read before). The record of runs survives a restart of the runtime (`/coding/runs.json`, the last 50). A Cursor chat made in the IDE is marked as what it is (not resumable from the command line) and a message to it goes straight out as a new chat in the same workspace with the last exchange quoted, instead of trying, failing and then doing that; the apps say so above the composer. - **One pool, for good** (relay 0.5). The free allowance is **¥10 per account for its lifetime** instead of ¥15 a day: it does not reset, and nothing is refused at midnight or for a count of clips. It grows by **¥5 for each new person who signs up with your invite code** and by **¥10, once, for joining the co-creation programme** (*Contribute conversations*, renamed and still off by default). Members have no limit. Existing accounts keep what they had spent and start with ¥10 on top. `ALLOWANCE_CNY`, `INVITE_BONUS_CNY`, `CONTRIBUTE_BONUS_CNY`, `OWN_KEY_DOCS` on the relay; `DAILY_CAP_CNY`, `VIDEO_CLIPS_*` and the credit ledger are gone. `/v1/me.spend` says `grant`, `left`, `warn` (the 80 % heads-up) alongside the old names for one version; `/v1/estimate.affordable` is `cny <= left`. - **When it is gone, three ways on.** The relay refuses with `429 allowance_exhausted` and a structured body (what is left, the pool, the invite link, whether the co-creation bonus is still open, the guide's address); the web, the desktop and the phone show a card under the refused turn and on the account page: your own key (Alibaba Cloud Bailian first, the provider form pre-filled: `minis://settings/providers/add?preset=bailian` on the phone, Connections with the preset chosen on the web; and a step-by-step guide at [nanomuse.cn/own-key](https://nanomuse.cn/own-key), [docs/own-key.md](docs/own-key.md)), an invitation, or the co-creation programme. Switching to your own key never touches the sign-in or the devices. - **Two short steps after a new account.** A code sign-in that creates the account is followed, on the web and the phone, by *Set a password* (so the next device signs in without a code) and *Join the co-creation programme?* (+¥10 once), both skippable, both under Account later. The relay says `created: true`; the runtime passes it on from `/api/cloud/verify`. - **The notice, said as a slogan and put first.** *Free, open source, non-profit. Open source, built together: a personal agent for all* / 「免费 · 开源 · 非营利,开源共建,做属于所有人的个人智能体」, closing with *Report a bug, ask for a feature, send a pull request, every one brings a personal agent within everyone's reach* / 「欢迎反馈问题、提需求、贡献代码,每一次参与,都在让个人智能体离所有人更近一步」. It sits in a box right under the tagline in both READMEs, in a band under the site's nav, and its title opens the site's notice from every app card. Every mention of ¥15 a day and ¥3 an invitation now reads ¥10 / +¥5 / +¥10 / your own key. ## [0.1.22] - 2026-09-30 · Commons The release that makes the project easier to trust, cheaper to run and simpler to join. The notice (free, open source, non-profit) is the first thing in the README, on the site and in every app, said in two sentences. The daily allowance is ¥15, the models behind pictures and clips cost a fifth of what they did, and the app tells you what a new face will cost before it draws. Invite a friend and both of you gain. Sign in with an e-mail. Calls are gone; a microphone is back. And, only if you switch it on, the conversations you have with the Muse can go towards a model the community trains itself. ### Added - **Invitations** (relay 0.4). Every account has an invite code and a link (`https://nanomuse.cn/web/?invite=…`); a friend who signs up with it gives you ¥3 of credit (spent after the day's ¥15 is used up, never expiring) and four more video clips; the friend starts with the ordinary allowance. *Account → Invite a friend* on the web and the phone; the sign-in form has a field for a code, and the web app reads `?invite=` from the address. `INVITE_BONUS_CNY`, `INVITE_URL`, `VIDEO_CLIPS_FREE`, `VIDEO_CLIPS_PER_INVITE` on the relay; `POST /v1/admin/credit` for the operator. - **A cost check before a face is drawn.** On the phone, changing the look (from the chat or from *Avatar*, pictures or an animated face) first shows what it will cost (`GET /v1/estimate`), what is left today and how many clips remain, and waits for a tap. `wan2.2-i2v-flash` (¥0.10 a second, 5-second clips) replaces MiniMax-H3 (about ¥0.5 a second) as the default video model and `qwen-image-3.0` (¥0.18) replaces `qwen-image-3.0-pro` as the default picture model; phones on 0.1.21 that still ask for the `-pro` id are answered by the plain one. Four clips per account (one animated face), more with invitations; members have no limit. - **Contributing conversations, off by default.** *Account → Contribute conversations* on the web, the phone and the console: while it is on, the relay keeps each chat turn (what was sent (pictures replaced by a marker), what came back, the token counts and the app's platform and language) for a training set for the community's own open model. Turn it off and delete what you gave at any time; deleting the account deletes it too. The operator sees these turns only for contributing accounts and exports them as JSON lines without account ids (`/v1/admin/samples`, `/v1/admin/samples/export`). [docs/privacy.md](docs/privacy.md) says exactly what is and is not kept: no location, no IP addresses. - **Voice input on the web and the desktop.** A microphone next to *Send* that uses the browser's own speech recognition where it exists (Chrome, Edge, Safari); the words land in the box. Nothing is sent by nanoMuse and no model is involved. The phone keeps the voice input it had. - **The community notice, up front.** README and README_zh open with it and the tagline links to it; the sign-in gate, *Settings → About* on the web and the account page on the phone carry it with *Star on GitHub* and *Report a bug or ask for a feature*; the site's hero and the console footer too. Two sentences: it is free, non-profit and open source; issues, feature requests and pull requests are welcome, let's build it together. - **A running light round the stage.** The edge glow the hands work behind is no longer static: one light runs clockwise round the screen, a bright head with a long tail, bending round the corners, the Electron stage (still four thin strips, nothing full-screen animates) and the Android stage (a `PathMeasure` segment stroked three times). ### Changed - **¥15 a day** per account (was ¥25), across chat, pictures and clips, plus invitation credit. Members still have no cap. - **E-mail only.** New accounts sign in with an e-mail address; the phone-number option is no longer shown (accounts made with a number keep working, and the operator's allow-list still accepts numbers). The relay's messages, the apps' forms and the docs say "e-mail". - **Calls are gone.** The voice and video calls of 0.1.20–0.1.21 (Qwen Omni real-time) are removed from the phone, the web app, the desktop, the runtime (`/ws/call`, `[cloud] realtime_*`) and the relay (`/v1/realtime`, the omni models, `REALTIME_*`). Messages marked "said on a call" and ledger rows of kind `realtime` still display. [docs/calls.md](docs/calls.md) is a note now. - **The desktop app installs.** macOS: the app is ad-hoc signed and shipped as a `ditto` zip and an APFS dmg, so dragging it out no longer fails with Finder error -36 and Gatekeeper offers *Open Anyway* instead of "damaged". Windows: the runtime keeps its workspace under the data folder and the shell starts it there with `PYTHONUTF8`, so it no longer stops with "the data folder is not writable" right after installing (it had tried to create `workspace` in System32). The failure dialogs speak the system language. - **Every app follows the system language**: the phone, the web app, the desktop shell and its dialogs, the console and the site pick 中文 or English from the device; a language can still be set by hand. - The relay is 0.4.0; the runtime's `/api/cloud/verify` takes `invite`; `/api/cloud/contribute` and `DELETE /api/cloud/samples` are new. Android versionCode 23. ## [0.1.21] - 2026-09-30 · Footing A release with nothing new to demonstrate and a great deal to trust: a pass over every part of nanoMuse (the runtime, the relay, the web app, the desktop shell, the phone, the documentation) with the failures said plainly, the money counted once, the secrets kept where they belong, the loops kept free, and the apps able to pick themselves up. ### Added - **Failures in one sentence.** A run that fails no longer surfaces as "Something went wrong: "; `nanomuse/server/failures.py` turns provider and relay errors (401/402/429/404, broken streams, timeouts) into a sentence with a code, the detail folded under *Details* in the card. Cloud endpoints answer with the relay's status and an `X-Nanomuse-Code` header. The relay itself says `upstream_auth` / `upstream_model` / `upstream_busy` / `upstream_` in its own words instead of forwarding the provider's ("Incorrect API key" was the relay's key, never yours). On the phone, hub and coding failures are sentences in English, 简体中文 and 繁體中文 (`HubErrors`) instead of exception text. - **nanoMuse Cloud as a named model.** When signed in, the provider form offers *nanoMuse Cloud* in its own *Your account* group: the chat models the relay serves, the recommended one first, no key or URL field; saving it keeps the account key in the vault (`/api/cloud/use-as-model`). Summaries read "nanoMuse Cloud · model" instead of the relay's host. - **Desktop: About, updates, recovery.** The tray has *About nanoMuse* (version, runtime, shortcuts, the non-profit and trademark lines) and *Check for updates* (GitHub Releases; nothing downloads on its own). When the runtime the shell started dies, the shell starts it again once and says so; a second time it offers *Restart* / *Open the log* / *Quit* with the log's tail. Startup failures quote the log and name the usual causes. The window opens where it was closed. - **Runtime: a newer release is noticed.** `GET /api/update` asks GitHub Releases at most every six hours and *Settings → About* shows "0.1.22 is out" with a link when there is one. `server.update_check = false` or `NANOMUSE_NO_UPDATE_CHECK=1` turns it off; a hosted web session never checks (its operator updates it). Nothing is downloaded, nothing about the install is sent. - **Calls on the phone:** the camera can be switched on and off during a call (the model is told), and the screen stays on while one is up. - **Docs:** `docs/android.md` describes the app people download, OpenMinis-based, one arm64 APK, the account first, Hands / Reach / the hub / Calls / Coding, where the secrets live, how to build and release; the Python-line description moved to `docs/archive/android-python-line.md`. `app.md`, `gui.md`, `device.md`, `local-runtime.md` and `launch-checklist.md` carry a design-record banner; `brand.md` and `design.md` say the dragon is the default face; `troubleshooting.md` gains the desktop and the refused-hub entries. ### Fixed - **Relay (0.3.2).** A stream the provider refused before the first token was charged the estimated prompt tokens; a refused or dead-on-arrival stream now costs nothing (a stream that broke off midway still pays for what arrived). A retried video poll could bill a clip twice; the task upsert keeps `charged`. `X-Admin-Token` is compared in constant time like every other secret. The picture URL the image provider returns is fetched only from the provider's own hosts, so the relay cannot be steered at its own network. A hub call nobody answered within its TTL is dropped with a `timeout` error to the caller instead of being left to its own clock. `CODE_SENDER=log` on a public relay with open sign-up logs an error at startup instead of quietly serving codes nobody receives. - **Runtime.** Timelines are written off the event loop, a burst of events coalesced into one write (`flush()` on stop); `/api/coding/*` run their CLI probes and folder scans in a thread; the browser tool's fetch refuses loopback, LAN and link-local hosts like `web_fetch` does; `nanomuse serve` explains a bad `config.toml` (TOML or field errors) and an unwritable data directory in a sentence instead of a traceback; MCP and browser fallbacks are logged. - **Web.** After a sign-in the hub view is fetched directly, so the required-account gate no longer waits on the socket; a failed timeline load shows the empty chat with a toast instead of nothing; a failed send puts the draft back; a REST 401 opens the token gate; a fresh hello clears half-finished streams; "Failed to fetch" became "Cannot reach your nanoMuse right now." and the first screen says *Connecting…* (or the error, with *Try again*) until the runtime answers; onboarding steps that could not load offer *Retry*; a call ends when the allowance or the length limit is hit; every screen but chat and feed is a lazy chunk; the document title no longer reads "nanoMuse · nanoMuse"; server sentences go through `t()` so the Chinese UI stays Chinese. - **Android.** Reach pairing tokens moved from plain `SharedPreferences` into the encrypted store, and the backup and device-transfer rules exclude every secret store. `HubClient.stop()` shuts its executors and connection pool down; a 4001/4002 close (bad key / bad device) stops reconnecting and the Devices row says *Refused by the relay: sign in again*; turning the hub on asks for `POST_NOTIFICATIONS` on Android 13+ so the background service can stay up. `CallEngine` shares one HTTP client, cancels the socket on hang-up, drops a stale error when hung up on purpose and folds unknown provider errors into "unreachable". `AlarmReceiver` stays exported for `BOOT_COMPLETED` but behind a signature permission, so other apps cannot post alarm notifications through it. - **Docs and site.** `every-device.md` and `desktop/app/README.md` no longer say the desktop app is unpackaged; the README no longer says the desktop and the web "come next"; the legacy site page links the release page instead of a file that does not exist. ### Changed - Vite dev proxy target is configurable (`NANOMUSE_API=127.0.0.1:8799`). - Workflows that still pinned `checkout@v4`, `setup-python@v5` and `setup-node@v4` use the versions the other workflows already run on. - Android versionCode 22. ## [0.1.20] - 2026-09-30 · Presence The Muse on the line: call it, by voice or with the camera on, and it answers in its voice while you are still talking. Your coding agents (Cursor, Codex, Claude Code) seen and steered from any device of the account. The account at the front door of every app, with a password, its sign-ins and its history; usage by kind and by model everywhere, the operator's page included. And one design language (the phone's) for the web, the desktop, the console and the operator's page, with a finer stage over the screen while the hands work. [docs/calls.md](docs/calls.md), [docs/coding-agents.md](docs/coding-agents.md). ### Added - **Calls** (`nanomuse/call.py`, `web/src/call/`, `io.github.nanomuse.call`). A voice or video call with the Muse over Alibaba's Qwen Omni real-time model (`qwen3.5-omni-flash-realtime`): the microphone at 16 kHz, the answer at 24 kHz played as it arrives, talk over it and it stops, one camera frame a second on a video call. The runtime's `WS /ws/call` bridges the browser and the desktop to the relay's `WS /v1/realtime` (signed in) or to the provider with your own Bailian key, opens the session with the Muse's name, personality and memories, records what was said in the main chat flagged `via: "call"`, and reports the cost after every answer and why a call ended. The phone dials the same two routes itself (OkHttp, CameraX). A dark stage with rings that breathe with the voice, captions, the phone controls; the handset in the chat header, *Video call* in the menu. - **Coding agents** (`nanomuse/coding/`, `nanomuse/tools/coding_tool.py`, `web/src/screens/Coding*`, `io.github.nanomuse.coding`). The runtime reads the Cursor, Codex and Claude Code sessions on its computer from disk, starts the agents' own CLIs for a new message and streams the run (text as it comes, tools as they are called, stop at any time). `/api/coding/*` for the apps, `coding.*` hub actions so the phone or another computer looks and steers, the `coding_agents` tool for the Muse itself. The phone has a Coding screen behind the drawer and Settings (`nanomuse://coding`); the web and the desktop have one from the sidebar. - **The account, first.** Every app starts with the sign-in (a code or, once set, a password) and asks right after which model answers: the account's own or a key of your own (`[cloud] required`, default on; `NANOMUSE_CLOUD_REQUIRED=0` for self-hosters). Signing out brings the setup back. - **Passwords, sign-ins, history** (`cloud/` 0.3.1). `POST /v1/auth/login`, `POST /v1/auth/password` (set, change, remove; scrypt; a lockout after repeated failures that a fresh code sign-in resets), `GET /v1/me/sessions` with `DELETE /v1/me/sessions/{prefix}`, `POST /v1/auth/sign-out-all`, `GET /v1/me/events`: an account timeline of sign-ins, password changes, refusals, upstream errors and calls ended, never message content. The *Account* screen on the phone, the web, the desktop and the web console: who is signed in and since when, the password, the devices holding a key (each revocable), the history, and the three ways out. - **Usage by kind and by model.** The ledger's four kinds (chat, pictures, video, calls) with the token split of a call charged at the provider's audio and picture rates; `/v1/me` carries `usage {today, total} by kind` and `by model`; every Account screen shows today and all time, per kind and per model, with ¥. - **The operator's page** (`cloud/nanomuse_cloud/console/admin/`). The dashboard from `/v1/admin/overview`: accounts (with a password, members, disabled), active today and over 7 / 30 / 90 days, spend today / this week / over the period by kind and by model, stacked bars by day, the top spenders, today's signals, the timeline with a kind filter; each account opens in a drawer (`/v1/admin/accounts/{id}`) with its spend by kind / model / day, sign-ins, devices, recent requests and timeline. Tables show the masked hint; the identifier is decrypted for the drawer only. - **One design language.** The phone's (a grey canvas, white cards with hairlines, rows with a tinted icon tile, one action blue) on the web app, the desktop and the web console (`/app/`, which now signs in with a code or a password and has the Account sheet). - **A finer stage.** The desktop overlay (`desktop/app/src/renderer/stage/`): target rings with a turning cyan arc and paired ripples, a comet from the previous point, a travelling dot and arrowhead on drags, chevrons on scrolls, keycaps for key presses, a sliding gradient rim with a sweep for screenshots, and a pill that ends green or red: cheap without a GPU: no full-screen animation or blur, the canvas sized to what is drawn, the loop running only while something is on it. The phone's `HandsStage` draws the same picture on an Android Canvas. ### Changed - The avatar flow no longer says the options are ready the moment the request is accepted; it says it is drawing and reports back when the pictures (or the clip) have landed. - The served runtime runs uvicorn on the standard asyncio loop: under uvloop a child process's helpers (Cursor's worker server) inherited our pipes and a run never ended. The coding runner also finishes on the agent's terminal event, not on pipe EOF. - Android versionCode 21. ## [0.1.19] - 2026-09-30 · Ensemble Every device is a Muse: the computer gets the same shape as the phone (a face, one main chat and side chats, Devices, approvals in tiers, hands on its own screen) and the devices of one account give each other work. And a Muse needs no device of yours at all: nanoMuse Web runs one for you at [nanomuse.cn/web](https://nanomuse.cn/web/). [docs/every-device.md](docs/every-device.md). ### Added - **nanoMuse Web** (`demo/showcase/gateway/showcase_gateway/accounts.py`). Sign in at nanomuse.cn/web with an e-mail or a phone code and the showcase gateway starts you a container of your own: kept, with volumes, signed in to your Cloud account as the device *Web*, its model the Cloud, stopped after six quiet hours and woken on the next visit. `WEB_*` settings; the relay's `/v1/me` now carries an opaque account id; both compose files join the `nanomuse-web` network. Tests with a fake relay and a fake runner. - **Desktop installers with the runtime inside** (`scripts/desktop-app/`, `desktop/app/electron-builder.yml`, `.github/workflows/desktop-app.yml`). PyInstaller builds `nanomuse` as one folder, electron-builder wraps it with the window: `nanoMuse-Desktop--win-x64.exe`, `-mac-arm64.dmg`, `-mac-x64.dmg`, `-linux-x64.AppImage`, `-linux-x64.deb`, attached to the release by CI on every `v*` tag. The terminal binary's workflow attaches its packages to the same release. - **Android follow-ups.** The phone reads the runtime's `tool`, `tool_result` and `approval_result` stages, relays its own Sentinel approvals to the device that asked (`approval` events, `approve` calls into RiskGate), and shows a *Devices* row in the drawer with how many other devices are online. versionCode 20. - **Hosted runtimes start signed in.** `NANOMUSE_CLOUD_KEY`, `NANOMUSE_CLOUD_BASE_URL`, `NANOMUSE_HUB_NAME`, `NANOMUSE_ONBOARDED` seed the vault, the Cloud account, the hub name and the first-run flag (`nanomuse/hub/service.py`, `_seed_from_env`). - **The runtime on the hub** (`nanomuse/cloud.py`, `nanomuse/hub/`). `nanomuse serve` signs in to nanoMuse Cloud with an e-mail code (the key in the vault, optionally as the model provider), keeps one socket to the hub, answers `info`, `shell`, `files`, `file.get`, `file.put`, `open`, `screen`, `notify` for the account's other devices (`[hub] remote_control` turns that off), and runs an incoming `task` in a visible side chat *From * whose approvals travel back to the asker. Outgoing: `devices`, `device_shell`, `device_files`, `device_open`, `device_screen`, `device_notify`, `delegate`, the model reaches the other devices with the same tools it has for this one. Progress frames both ways: `tool`, `tool_result`, `approval`, `approval_result`, `text`, `image`, `file`. Settings `[cloud]`, `[hub]`; `/api/cloud/*`, `/api/hub/*`. Tests with a fake relay. - **Hands on this computer** (`nanomuse/computer/`). The phone operator loop over a computer: `mss` for the screen, `pyautogui` or `xdotool` for the mouse and keyboard, the active window's title, Qwen's `computer_use` dialect; tools `computer_screen`, `computer_act`, `computer_task`, judged by the Sentinel like everything else; `[hands]` settings, off by default; live `hands` events for the stage. - **The web app in the Android shape** (`web/`). A Devices page (this device's name, *Join the hub*, *My other devices may operate it*, the other devices with online dots, *Ask*, *Forget*; Hands with its driver and Stop), side chats addressed to a device with an *on * chip, relayed tool chips and approval cards with a device pill, a live Hands card, the Cloud sign-in in the first run and under Connections, the dragon as the default face. On screens 1024 px and wider a sidebar takes over from the tab bar and the chats sheet: the agent and its status, the sections, the chats and the devices a chat can be addressed to, Settings. - **nanoMuse for the desktop, the window** (`desktop/app/`, Electron via electron-vite). Attaches to `nanomuse serve` or starts it, loads the web app from `127.0.0.1` with the token, keeps the runtime alive from the tray, registers `Ctrl/Cmd+Shift+Esc` as a global Stop, and draws the **stage**: a transparent, click-through window over the display with the ring and ripple where the hands are about to click, a drag line, and a pill with the step and how to stop (UI-TARS-desktop's ScreenMarker, Android's `HandsStage`). `--screenshot` and `--stage-demo` for checks without a person at the screen. ### Fixed - A remote approval answered from the web app over the WebSocket went to the local gate instead of the hub; it now travels back to the device that asked, and the card closes on both sides. - A delegated task's final answer was lost when the target's chat finished before the caller read it; the relay now keeps the last assistant text of the run. - `.webp` avatars were served as `application/octet-stream` on systems without the type registered. - A Cloud key kept in the vault showed as *missing* under Connections. ## [0.1.18] - 2026-09-29 · Open Sign-up is open to everyone, and the app says what it is before asking for anything: a fully open-source, non-profit community project that will never charge, an e-mail address and a code give about ¥25 (≈ $3.5) a day of model use, paid by the developer. This version also carries everything built since 0.1.16 that had not shipped: nanoMuse Cloud itself (drafted as 0.1.17 "Doorstep", never released), the hub that lets the devices of one account meet, nanoMuse Desktop, video through the relay, and the iOS tree brought back to compiling. ### Added - **Open sign-up with a daily allowance in money** (`cloud/`). `SIGNUP_OPEN=1` lets anyone sign up; identifiers in `ALLOWED_IDENTIFIERS` are *members* with no cap; everyone else has `DAILY_CAP_CNY` (default ¥25) a day, the day starting at `DAY_OFFSET_H` (default 8: Beijing midnight). Every charge carries its cost in micro-yuan from a per-model price list (qwen3.8-27b ¥3/¥12 per million tokens in/out, qwen3.8-flash ¥0.8/¥2.7, qwen-image-3.0-pro ¥0.25 a picture and ¥0.5 above 1400 px, MiniMax-H3 ≈ ¥0.5 a second at 768p); chats are checked before and priced after, pictures and clips are refused up front when they would cross the cap. `/v1/me` answers a `spend` block (today, total, cap, ¥→$ at `USD_CNY`, when the day resets); `/v1/models` lists each model's kind, multipliers and prices; `/v1/admin/accounts` shows spent today and in all per account with a member toggle (`POST /v1/admin/unlimited`), `/v1/admin/usage` rows carry cost. The admin console shows ¥ and $ beside tokens and the price list and rate. Tests cover members, the cap, prices and the day boundary. - **The notice** (free, open source, non-profit, what signing in gives, what is and is not kept) on the welcome page, the sign-in page, the account page, the README and the website. - **A first run in four pages** (`io.github.nanomuse.ui.onboarding.FirstRunSetup`). Welcome: the face, one line on what it is, three feature rows, the notice, *Sign in with e-mail: free* and *I have my own API key*. Models: own-key path only, skippable. **Hands**: the accessibility service and the overlay permission asked up front, each row showing where it stands and opening the right settings page, re-read on return, the Hands switch flipped on when both are in place, and *Skip for now*. Meet the agent: the hand-off into the first conversation. Page dots, slide transitions. - **Approvals in three tiers** (`io.github.nanomuse.guard`). `RiskTier`: *runs, then tells you* (installs), *asks first, may be remembered* (deleting, sending: for this chat or always for X), and *highest* (money): asked at the moment of paying every time, unless the user picks **Remember and run next time in X**, one app or site, confirmed with the phone's screen lock (`DeviceCredential`: the framework `BiometricPrompt` with device credential on API 29+, the Keyguard intent below), never a chat-wide grant. Every payment that then runs on the grant is reported to the model, which says so in one line; after any "always" the model points to *Settings → Permissions*, where remembered approvals are now grouped by tier (payments first). Hands, the browser and Reach pass the gate's notice through to the model. `RiskPolicy` and the Hands prompt say that buying, ordering, booking and trading are fine when the user asked for exactly that, and that nothing unlawful or harmful is done however it is worded. - **Money in the app.** *Settings → nanoMuse Cloud* shows today's spend in ¥ and $, the cap or *Member · no daily cap*, the total paid by the developer and when the day resets; the Settings row shows today's ¥. - **nanoMuse Cloud in the app** (`io.github.nanomuse.cloud`, drafted as 0.1.17). `NanoMuseCloud` speaks the relay (`/v1/auth/code`, `/v1/auth/verify`, `/v1/me`, `/v1/auth/sign-out`, `/v1/auth/delete`) and provisions the result as an ordinary OpenAI-compatible `ProviderInstance` on the relay's base URL: the issued `nm_…` key in the encrypted key store, the models fetched with the same `/v1/models` refresh every provider gets, a *nanoMuse Cloud* model group with the recommended chat model (the default only when there is none), the relay's picture and video models for the avatar (only when none is set). `CloudSignInScreen` (e-mail or phone, code with a 60-second resend countdown, the relay's error codes as sentences) and `CloudAccountScreen` (masked hint, spend, provider settings, sign out, delete the account). - **The relay** (`cloud/`, Python, FastAPI + SQLite, version 0.2.0): codes by e-mail (SMTP) or SMS (Aliyun) with per-identifier and per-IP throttles; identifiers stored as HMAC-SHA256 hashes with a display hint; `nm_…` keys stored hashed; an OpenAI-shaped proxy: `/v1/models` with modalities, `/v1/chat/completions` streaming with usage forced on, `/v1/images/generations` and `/v1/images/edits` translated to Model Studio's native image API, video synthesis and task polling proxied: a ledger per request; the hub (`/v1/hub`, WebSocket) and `/v1/devices`; the web console at `/app/` and the admin console at `/app/admin/`; Dockerfile, Caddy compose, a deploy script with daily backups, `.env.example`, tests against a fake upstream. [docs/cloud.md](docs/cloud.md), [docs/hub.md](docs/hub.md), [docs/trial.md](docs/trial.md). - **The hub.** Devices signed in to one account see each other over the relay and can give each other work; the phone joins (info, open, notify), the web console lists devices and can forget an offline one. [docs/hub.md](docs/hub.md). - **nanoMuse Desktop** (`desktop/`): the computer's Muse, a terminal chat with hands on the machine (shell with a process-tree timeout, files, browser, a look at the screen) and on the hub; installers for Windows, macOS and Linux out of `scripts/build-desktop.py` and the `desktop` workflow. [docs/desktop.md](docs/desktop.md). - **iOS**: the iOS half of OpenMinis 1.13 restored under nanoMuse's name with the Cloud sign-in and the hub client; it compiles on a Mac runner (an unsigned build check, no Apple credentials needed). Runtime still unverified. [docs/ios.md](docs/ios.md). ### Changed - **Hands has no step cap.** The operator runs until the task is done, infeasible or stopped (30 minutes at most); `--max-steps N` on `nanomuse-hands run` still sets one, `0` means none. The desktop phone operator follows (`[gui] max_steps = 0`). - **Models that never declared their modalities are no longer read as text-only.** The provider's own `architecture.input_modalities` now wins over the models.dev catalogue; when neither says anything, a name heuristic (`VisionFamilies`) decides whether pictures are sent and the system prompt stays silent instead of stating "text only": qwen3.8-27b no longer tells the user it cannot see the picture it was given. The bundled models.dev snapshot is refreshed. - **Cloud group provisioning** rebuilds entries from the relay's model list, reuses an empty *nanoMuse Cloud* group instead of leaving it at 0 members, and makes sure the default group has members. - The sign-in page puts e-mail first (phone still accepted); the daily-cap error says when the allowance starts over and that an own key has no cap. - versionCode 19; installs over 0.1.16 without losing data. ## [0.1.16] - 2026-09-26 · Palette The image and video models are picked the way the chat model is (from what the key can actually use) and drawing on Alibaba Cloud Model Studio works again. ### Fixed - **Avatar and pictures on Alibaba Cloud Model Studio answered HTTP 404.** `ImageGen.generate` sent Model Studio hosts to `compatible-mode/v1/images/generations`, which does not exist there (the public host answers 404 to every model; a dedicated `maas` host happens to serve it). Generation now takes the native `multimodal-generation/generation` path the poses already used (`callDashScope`, shared by generate and edit): a text-only turn, `size` as `W*H`, `watermark` off, `prompt_extend` off for qwen-image. Verified against qwen-image-3.0-pro, qwen-image-2.0-pro, qwen-image-max and wan2.7-image-pro. - **Default image model vs. the page's hint.** `suggestedModel` took the catalogue's first image entry (wan2.7-image-pro) over the host's recommended model; it now prefers `recommendedModel` (qwen-image-3.0-pro on Model Studio) whenever the provider lists it, else the first model that draws, else the recommendation alone. ### Added - **Image and video models listed from the key** (Settings → Image & video models). `ImageGen.availableModels`: the provider's model entries (the same `/models` fetch the chat models come from, refreshed once when the provider has none and on *Check again*) filtered to models that draw by catalogue modality or by name (`looksLikeImageModel`; edit-only models and, on Model Studio, models outside the native endpoint's qwen-image / wan-image are left out), dated snapshots hidden behind their alias, the recommended model first. `MediaModels.checkVideoModels`: Model Studio's `/models` says nothing about video, so `VideoGen.KNOWN_DASHSCOPE_MODELS` (MiniMax-H3, wan2.6 / 2.5 / 2.2 i2v and t2v) plus anything on the list named like a video model are probed with `VideoGen.probe` (an empty task, answered 404 "Model not exist" or accepted and failed at once, nothing billed) and remembered per provider for a day. Both sections show the result as choice rows with a *Recommended* mark, a checking line and *Check again*; the free-text field stays for any other name. Strings in en / zh / zh-TW. - **Wan video models.** `VideoGen` builds the body per family: MiniMax keeps `media[first_frame]` / `resolution 768P` / `ratio` / `duration 4–15`; Wan gets `img_url`, `resolution 720P` or a pixel `size`, and a `duration` only where the model takes one (2–15 on wan2.6, 5 or 10 on wan2.5, none on wan2.2). A Wan `-i2v` / `-t2v` pick is swapped for its sibling when the job is the other kind. ### Changed - zh / zh-TW avatar strings say 形象 where they said 脸 (`nm_avatar_settings_subtitle`, `nm_avatar_describe_title`, `nm_avatar_footer`). - versionCode 17; installs over 0.1.15 without losing data. ## [0.1.15] - 2026-09-25 · Stage What the hands do is now something to watch, and the capsule no longer gets in their way. ### Added - **The stage** (`io.github.nanomuse.hands.HandsStage`): a second full-screen overlay window that never takes a touch, drawn on an Android Canvas after UI-TARS-desktop's ScreenMarker (`ScreenMarker.ts`, `setOfMarks.ts`; Apache-2.0): a glow breathing along the screen's edges for as long as the run is on (blue while the hands work, amber while they wait for you; a 5 s cycle); at the point the model chose, a red dashed ring turning once a second with a dot at its centre and the action's name beside it (*Tap “Search”*, *Hold*, *Double-tap*) shown 260 ms before the finger lands, then a ripple as it lands; a long press fills a second ring for as long as it is held; a swipe or scroll sends the ring along its path with a trail and an arrowhead; typing, Enter, Back, Home, opening an app and waiting write their name where the last ring was. Hidden, like the capsule, for the instant of every screenshot, so the model never sees any of it. Labels in en / zh / zh-TW (`nm_hands_fx_*`). ### Fixed - **The capsule caught the hands' own taps.** The model cannot see what is under the capsule (it is hidden from the screenshot), so a tap it asked for could land on the capsule: on *Stop*. For the length of every injected gesture the capsule's window now lets touches through (`FLAG_NOT_TOUCHABLE`, applied and confirmed before the gesture, put back after), and when the target is under it, it first moves to the other end of the screen so the ring there can be seen. - **App-icon quick actions on a phone** (long-press the icon → New chat / Voice / Camera) mounted the NavHost straight into the upstream OpenMinis chat screen, cold or warm. They now open the draft inside the home shell, which consumes the pending voice or camera action as before. The draft's id is decided once per launch (`remember`), so a recomposition no longer mints a new one. ### Changed - versionCode 16; installs over 0.1.14 without losing data. ## [0.1.14] - 2026-09-25 · Home Coming back to the app always lands in its own home again. ### Fixed - **The OpenMinis chat screen on top of the home.** On a phone, every "open this session" entry point (the notification for an approval or a finished task, a tap on the tool capsule, Hands bringing the app back after a run) went through the upstream deep-link path, which pushed `chat/{sessionId}` on the back stack: the OpenMinis chat screen, without the Muse header and tabs, sitting over the home shell until you pressed back. It read as the app falling back to an older look in the middle of a task. `HomeShell.openSession` now takes those requests to the shell (pops back to it if something sits on top, mounts it if the graph started elsewhere) and shows the session there; wide windows keep the upstream list/detail route. The *Launch session → New chat* preference no longer pushes the upstream chat on a warm return either: the shell opens on the main chat by itself, as it already did on a cold start. ### Changed - versionCode 15; installs over 0.1.13 without losing data. ## [0.1.13] - 2026-09-25 · Reach The phone drives your computer. A small companion on the PC (one Python file, standard library only) is paired with the app by a six-digit code on the same network; from then on a sentence on the phone runs there: a command in its shell, a file fetched or dropped, a page opened in its browser, a look at its screen. The results come back to the phone, and so do the approvals: a command for the computer is judged by the same `ShellGuard` as the phone's own shell and waits for the same card before it is sent. One way: the phone drives the computer, never the other way round. ### Added - **`host/nanomuse_host.py`**: the companion. `python3 nanomuse_host.py` prints the computer's LAN address and a pairing code (six digits, ten minutes, one phone, five wrong tries lock it) and serves JSON over HTTP: `POST /pair` (code → bearer token; only the token's SHA-256 is kept in `~/.nanomuse/host.json`), `GET /info`, `POST /shell` (`command`, `cwd`, `timeout` ≤ 15 min; exit code, stdout and stderr capped at 200 KB, `timed_out`), `GET /files?path=`, `GET /file?path=` (≤ 50 MB), `PUT /file?path=`, `POST /open` (`http(s)`/`file` URLs only, the default browser), `GET /screen` (mss + Pillow when installed, else `screencapture` / `grim` / `gnome-screenshot` / `spectacle` / `import` / `scrot` / PowerShell; 501 when nothing works). `--forget` drops every paired phone; `--no-pair` starts without a code. `tests/test_host.py` (9 tests) drives it end to end on Linux, macOS and Windows in CI. - **`nanomuse-pc`** (`io.github.nanomuse.reach.ReachOffloadHandler`): `status` (which paired computers answer), `run "" [--on ] [--cwd] [--timeout]`, `ls []`, `get [--name]` (into the chat's attachments; pictures come with a `markdown` line), `put [--force]` (never overwrites quietly: an existing file needs `--force`, and `--force` needs the card), `open `, `screen` (a picture of the computer's screen into the attachments). Exit codes 0 ok · 1 failed · 2 usage · 3 no computer / refused · 4 the user said no. When the host does not answer, the reply says so and points to the setting. - **Approvals on the phone** (`GuardKind.COMPUTER`): a command for the computer goes through `ShellGuard.assess` and `RiskGate` exactly like one for the phone; the card and the notification add *On the computer “desk”, not on this phone.* Grants are kept apart from the phone's (`pc:` targets), so *always allow* for a folder here never covers the same folder there. - **Settings → Computers** (`io.github.nanomuse.ui.reach.ComputersScreen`, `minis://settings/computers`): the paired computers with system, address and when each last answered, tap to check, *Forget* to drop the key; *Pair a computer* in two steps (run the script, enter the address and the code) with the errors the host returns in plain words; *How it works* in four lines (judged like the phone's shell; runs as you; local network, no encryption yet; the computer never reaches into the phone). The Settings list shows the count. - **The agent knows its computers** (`Computers.promptParagraph`): which are paired, the verbs, the one-way rule, and (with none paired) the one-time pairing it should explain instead of pretending. - `Computers` (store + OkHttp client, tokens in the app's private preferences); `ReachTest` (4 tests); en / zh / zh-TW strings (`nm_pc_*`, `nm_risk_desc_on_computer`, `nm_risk_preview_computer`); CI lints and formats `host/` with the Python line. ### Changed - The Hands settings page removes its lifecycle observer when it leaves the screen. - versionCode 14; installs over 0.1.12 without losing data. ### Known issues - No TLS between the phone and the computer in this version: the token travels in clear on your local network. Use a network you trust; `--forget` when a phone is gone. Certificates pinned at pairing are the plan for the next Reach step. - The address is typed, not discovered: when the computer's IP changes (DHCP), forget and pair again, mDNS discovery is not in yet. - `put` reads the whole file into memory on both sides; fine for documents and pictures, not for gigabytes. - The computer's screen is a picture for the agent to look at, not yet a hand: mouse and keyboard on the PC (the UI-TARS-style operator) come later, on the same host. ## [0.1.12] - 2026-09-25 · Hands The phone's screen as a hand. For the apps that have no API (12306, 微信, 支付宝, 美团) the agent can now use the phone the way you do: it looks at a screenshot, decides one action, taps, types or swipes, and looks again. Perception is the screenshot alone; no accessibility tree is read. The accessibility service is only the hand: it takes the screenshot, performs the gesture and types into the field that has the cursor. A capsule at the top of the screen shows each step with *Stop*; logins, passwords and codes are handed to you; a tap that pays, sends, posts or deletes waits for the same approval card as the shell and the browser. Off by default, under *Settings → Hands*. ### Added - **`nanomuse-hands`** (`io.github.nanomuse.hands.HandsOffloadHandler`, registered in `MinisApp` next to `nanomuse-media`): `run --task "" [--app ""] [--max-steps 25]` blocks for the whole task and answers one JSON object (`outcome` done / infeasible / stopped / needs_user / failed, `answer` or `message` or `question`, `steps`, `last_screen` as a `minis://attachments/…` picture, `trace`, `log`); `apps` lists the installed apps with a launcher icon; `status` says what is set up; `stop` ends the run in progress. With the switch off or a prerequisite missing it exits 3 with the reason and `minis://settings/hands`, so the agent tells the user instead of guessing. - **The operator** (`HandsOperator`): screenshot → screen model → one action → again, up to 25 steps (60 at most) and 12 minutes. Screenshots are scaled to 720 px wide and JPEG-encoded; only the current and the previous screen are sent as pictures, earlier turns are text (*an earlier screen, not shown again*). Temperature 0, 90 s per model call, three model errors or three unreadable replies end the run. A black screenshot (a `FLAG_SECURE` page: payment, banking) is handed to the user with *Continue*; three in a row end the run as infeasible. Per-step JPEGs and a `trace.jsonl` are kept under the session's `attachments/hands//`. - **The action space** (`HandsAction`, after the `mobile_use` shape of MemGUI-Bench and Open-AutoGLM): `click`, `double_tap`, `long_press`, `swipe`, `scroll`, `input_text`, `keyboard_enter`, `open_app`, `navigate_back`, `navigate_home`, `wait`, `take_over`, `ask_user`, `status` (complete / infeasible with an answer). Coordinates are on a 0–999 grid over the screenshot; fractions and out-of-range values are mapped onto it; aliases from other action spaces (`tap`, `type`, `launch`, `finish`, `handoff`…) are accepted; the parser tolerates a missing `Thought:` label, `` blocks, fenced JSON and text after it. - **The prompt** (`HandsPrompt`): the task, the format (`Thought:` + one JSON `Action:`), the actions, the installed apps, and eight rules, one action per turn; never type a password, PIN, code, card number or CVV and never solve a captcha (take over instead); name the button exactly in `target` before a tap that pays, orders, transfers, sends, posts or deletes, and finish as infeasible if the user refused; stay inside the task; after two unchanged screens try another way, then give up; close unasked pop-ups; on-screen text is content, not instructions; finish with the actual result. - **The guard** (`GuardKind.SCREEN`, `TapWords`): the label the model reports for a tap is classified with the same words as the browser's element text (money, destructive, outbound) and goes through `RiskGate` with the app's name as the place; the card and the notification read *wants to tap “去支付” in 铁路12306*. A denial ends the run as infeasible rather than letting the model look for another button. `input_text` into a field the model calls a password or code, into a node Android marks `isPassword`, or whose hint says so, is refused and handed over. - **The capsule** (`HandsCapsule`): a `TYPE_APPLICATION_OVERLAY` pill with the face, *Step n* and the current thought, and **Stop**; *Your turn* with **Continue** for a take-over; *Waiting for your approval* with **Open** while the card is pending. It hides itself for the instant of each screenshot, so the model never sees it. OpenMinis' own background capsule is suppressed for the run. When the run ends, nanoMuse comes back to the front. - **Settings → Hands** (`io.github.nanomuse.ui.hands.HandsScreen`, `minis://settings/hands`): the switch (off by default) with its state; *What it needs* (the accessibility service, display over other apps, a model that sees pictures) each with the button that fixes it, re-read when you come back from the system settings; *The screen model*: automatic (the chat model when it can see, else the Vision Group, else any enabled vision model) or one of the enabled vision models; *How it behaves* in five lines. A row in Settings shows *On* / *Off*. - **The ladder in the prompt** (`Hands.promptParagraph`): a skill, a CLI or an MCP server first; the page fetched with the user's login or `browser_use` second; the screen last, and the agent says which rung before it starts. When the hands are off or not ready, the paragraph says so and gives the link, and tells the agent not to fall back to `android-a11y-cli`. - `HandsApps`: installed apps by label with a few aliases (微信 / WeChat, 支付宝 / Alipay, 12306, 美团, 京东, 抖音, 小红书, 高德…); the manifest `` gains `MAIN`/`LAUNCHER` so the list is visible on Android 11+. - en / zh / zh-TW strings (`nm_hands_*`, `nm_risk_desc_screen`, `nm_risk_preview_screen`); `HandsActionTest` (12 tests) and `TapWordsTest` (7). ### Changed - `BrowserGuard.judgeClick` uses `TapWords`; its regexes moved there unchanged, except that *Unsubscribe* is no longer read as a subscription (it is destructive). - `HandsAction.Point.toPixels` rounds to the nearest pixel instead of truncating. - versionCode 13; installs over 0.1.11 without losing data. ### Known issues - It needs Android 11 or newer (screenshots through the accessibility service), a vision model, and the two permissions. Apps that set `FLAG_SECURE` are black to it and handed to you. Typing goes through `ACTION_SET_TEXT` on the focused field, which some custom keyboards and web views ignore: the model is told to tap the field first and try again. The x86 emulator cannot run the arm64 APK; the flow was built and unit-tested before release and driven on a phone after it. ## [0.1.11] - 2026-09-25 · Hatch A face of its own, and a first conversation the model runs. The bundled default avatar is now a small pale-yellow dragon: drawn with qwen-image-3.0-pro, posed for the five states with the same model, and shipped with four looping clips from MiniMax-H3, so a fresh install moves the way a custom face does. The first conversation no longer reads the user's reply with regular expressions: the chat model decides what "what should I call you?" was answered with, keeps the thread when the answer is something else, and proposes the agent's own names in the user's language. The image-and-video settings recommend one Alibaba Cloud Model Studio key for all three models while keeping a different provider per model possible. ### Added - **The built-in dragon** (`res/drawable-nodpi/nm_avatar_{idle,working,waiting,happy,error}.webp`, 1024², ~170 KB together; `res/raw/nm_motion_{idle,working,waiting,happy}.mp4`, 768², 4 s, ~1 MB together). `AgentMood` carries the still and the clip for each state; the header, the studio preview and the notification icon use them, and the built-in face fills the disc like a custom one. The vector red panda is gone. `docs/avatar-moods.png` and the screenshots in `docs/screenshots/` are redrawn with the dragon. - **Model-driven naming** (`io.github.nanomuse.onboarding.FirstConversation`, `nanomuse-naming`). While the first conversation waits for the form of address, the system prompt asks the model to decide what the user meant: an address → confirm it, ask what to call the agent and end with a `nanomuse-naming` block (`user_address`, two `suggest`ions); "nothing in particular" → the same with `null`; anything else → help with it first and bring the question back, no block. While the chooser is up, a name typed, "call you 豆丁" or "the first one" is reported as `agent_name` in the same block; other messages are answered and the chooser stays. The block is parsed in `nmAfterTurn` and rendered as nothing; the address goes to MEMORY, the name to SOUL.md; the phase is kept across restarts. Suggestions come from the model (two-character Chinese names in the spirit of 豆丁 or 小满 when the user writes Chinese, short English names like Pip or Wren otherwise) and never an existing assistant's name (Siri, Alexa, Cortana, Jarvis, Muse, Gemini, Copilot, 小爱, 小度, 小艺, 天猫精灵, 豆包, 文心, 通义, 阿福); the built-in fallback pools follow the same rule (豆丁, 小满, 团团, 叮叮, …; Pip, Wren, Juno, Remy, …). `FirstConversationTest` (6 tests). - **One key for three models.** The *Image & video models* intro and the welcome screen's provider step say it plainly: one Model Studio key covers the chat model, `qwen-image-3.0-pro` and `MiniMax/MiniMax-H3`; each model is still chosen on its own, so any of them can come from another provider on another key. The video model follows the image model's provider when that provider is on Model Studio and nothing else was chosen; *No video model* is remembered as a choice. The image footer explains that qwen-image-3.0-pro both draws and poses. The avatar announcement mentions the clips being made when a video model is set. ### Changed - `ImageGen.suggestedModel` for Model Studio is `qwen-image-3.0-pro`; `editDashScope` calls the 3.x models with their own parameters (`size`, `prompt_extend`, `watermark`) and keeps `qwen-image-edit-max` for older ones. - `MediaModels.imageEndpoint` only reports an endpoint with a model name, so the model-driven explanation runs instead of the fixed fallback when the name is blank. - The media page's status rows read `model · provider`; *Back to the built-in face* replaces *Back to the red panda*; the default description in the studio describes the dragon. - versionCode 12; installs over 0.1.10 without losing data. ### Removed - `FirstConversation.extractAddress`, `extractAgentName`, `interceptWhileChoosing`, `onUserNameReply`, `ChatViewModel.nmBeforeSend`: the heuristics the block replaces. ## [0.1.10] - 2026-09-25 · Motion The face moves, and the three models are named. Muse has its image and video models built in; nanoMuse runs on three of your own (the chat model, an image model, a video model) and now says so in one place, in the settings and in the conversation. With a video model set, the avatar gets a short looping clip for each state: a head shake at rest, a crystal ball while it waits for you, a star when pleased, a laptop while it works. The agent can also make pictures and short clips on request through the same two models, and when one is missing it explains what to set up instead of pretending. ### Added - **Settings → Image & video models** (`io.github.nanomuse.ui.media.MediaModelsScreen`, `minis://settings/media`). One page for the three models: the chat model (the default group, a row to its picker); the image model, a provider among those that can draw, the model name with the catalogue's quick picks, *Ready* / *Not set* / *Type a model name below*, and what stops working without it; the video model: opt-in, a provider on Alibaba Cloud Model Studio and `MiniMax/MiniMax-H3`, the *Animate the avatar after a change* switch, and a row for the current face's clips (n of 4, *Make* / *Redo*, the stage while it draws). The Settings list shows the row with *Not set* while no image model is usable; the avatar studio's *Image model* entries open this page. - **The moving avatar** (`io.github.nanomuse.avatar.AvatarMotion`, `io.github.nanomuse.media.VideoGen`). After a new face is adopted and its poses are drawn, four 4-second clips are made in the background, one per state from that state's pose as the first frame, sequentially, through Model Studio's asynchronous video API (temporary upload → `video-synthesis` task → poll → download). The header and the studio play the current state's clip in a loop, muted, inside the same circle (`AgentAvatar.LoopingClip`: `TextureView` + `MediaPlayer`, first frame faded in, paused with the app), and fall back to the still pose where there is no clip. The status line reads *Animating 1/4…* meanwhile. Clips are cleared with the face they belong to. - **`nanomuse-media`** (`io.github.nanomuse.media.MediaOffloadHandler`): a sandbox command for the agent. `image --prompt … [--from ] [--size WxH]` draws or edits through the image model, `video --prompt … [--from ] [--seconds 4-15]` makes a clip through the video model, `status` prints what is configured; results land in the session's attachments with a `markdown` line the agent puts in its reply so the file shows inline. When the model needed is not set, the command exits with a `tell_user` message and the link to the setting. - **The agent knows its three models** (`MediaModels.promptParagraph`, appended to the system prompt): which are set, how to use `nanomuse-media`, and (when the image model is missing) to explain that changing its look or drawing needs one, that unlike Muse this is something the user sets up, and to link *Image & video models*. A "change your avatar to …" request with no usable image model goes to the chat model with a one-turn note (`SessionAddenda`), so the answer comes from the agent in the user's language rather than from a fixed string. - en / zh / zh-TW strings (`nm_media_*`, `nm_avatar_status_animating`); `MediaTest` (6 tests: argv parsing, DashScope host detection, task-failure wording, motion prompts, the missing-model note, the CLI help). ### Changed - `MediaModels.imageEndpoint` counts an image model as set only when it has a provider with a key *and* a model name; a provider the catalogue does not know shows *Type a model name below* instead of *Not set*, and the avatar flow no longer starts and fails with "set an image model first". - The image-model sheet in the avatar studio is gone; its entries lead to the new page. - versionCode 11; installs over 0.1.9 without losing data. ## [0.1.9] - 2026-09-25 · Portrait The face, Muse's way. Changing the avatar is a sentence in the chat ("change your avatar to a corgi", with a reference picture if you like) answered by four takes in a 2×2 card; tap one or say "the second one", and the agent announces its new look while the poses are drawn. Behind the face is the agent's own page. The name pill under the face is re-measured against Muse's, and the face comes in five sizes. ### Added - **Avatar change in the chat** (`io.github.nanomuse.avatar.AvatarFlow`, `ChatViewModel.nmInterceptAvatar`). Requests in Chinese and English are recognised before the model sees them: 把/将 … (虚拟)形象/头像 换成/改成/变成 X, 换个形象:X, 变成 X; change/switch/set (your/the) avatar to X, new avatar: X, become X. The first image attached goes in as a reference (`ImageGen.edit`). Four candidates are drawn in parallel into a 2×2 card (`AvatarOptionsCard`: numbered tiles, a breathing placeholder, per-tile retry, *Again*, *Keep current*); the pick is a tap or a typed choice (第二个, 2, "the second one", 左上, "last"; "again" redraws). The chosen face is adopted at once, the announcement is persisted with a `nanomuse-avatar` fence that renders as the frozen card (chosen tile highlighted), the poses land in the header as they finish, and a line goes into MEMORY. A draft main chat gets its session row first, and the reply is persisted with the request so the transcript never ends on an unanswered turn. - **House-style prompts** (`AvatarStudio`). New default style *3D toy*: soft matte collectible-vinyl render, full body, facing the viewer, centred on pure white, square, one character; the four takes vary colouring, a lighter and a darker breed with an accessory, a playful outfit. Poses follow Muse's fixed set: headphones and a laptop (working), a crystal ball (waiting), a five-pointed star (happy), a sweat drop (error). - **Share** (`io.github.nanomuse.avatar.AvatarShare`, `AvatarShareSheet`, `AvatarShareCard`). After the new look lands, a card offers to share it; the sheet shows five pastel 1080×1350 cards (the face on a rounded white card with a soft shadow, a speech bubble, the wordmark and tagline) and hands the chosen one to the system share sheet through the `FileProvider`. Also from the agent's page. - **The agent's page** (`io.github.nanomuse.ui.profile.AgentProfileScreen`, `minis://settings/profile`), opened from the face or the name on any home tab: ×, share, the face with a pen badge (→ *Change avatar* / *Edit name* / *Avatar studio*), the name, *online*, and four panes: today's and yesterday's activity from the sessions (title, tools used or the reply, time; tap to open), approvals kept as *Always* with a link to Permissions, the daily routines with a link to Scheduled tasks, and the SOUL and Memory gradient cards with an *Edit* row for the name. *Change avatar* returns to the chat with "Change your avatar to " / "把虚拟形象改成" already typed and the keyboard up (`HomeBus.PrefillComposer`, `ChatViewModel.nmPrefillComposer`). - **Avatar size** (`io.github.nanomuse.ui.avatar.AvatarSize`; *Settings → Appearance → Avatar size*): Small 44dp, Medium 56, Large 66, Extra large 76 (default), Hidden, the header follows live. - Status lines *Generating options* / *Finishing the new look* while the flow draws; the face shows the working mood meanwhile. - en / zh / zh-TW strings (`nm_avatar_*`, `nm_profile_*`, `nm_appearance_avatar_size*`); `AvatarFlowTest` (5 tests), `AvatarStudioTest` updated for the new prompts. ### Changed - **Name pill** (`MuseHeader.MuseNamePill`), measured against Muse's screens: white, 14dp radius, 2dp shadow, 12×4dp padding, the name 14sp *regular* (it was 15sp SemiBold) and the status as an 11.5sp grey second line inside the pill instead of a separate row; the pill overlaps the face by 10dp. - The setup is marked done the first time the home is shown, so an empty main chat (a draft with no session row) can no longer bring the welcome screen back after a trip to another page. - versionCode 10. ### Fixed - `AvatarStudio` (since 0.1.6) updated its four candidate slots with a read-modify-write from four coroutines; one result could overwrite another and leave a tile loading forever. The updates are atomic now (`MutableStateFlow.update`). ## [0.1.8] - 2026-09-25 · Polish The shell, brought level with Muse's. Same features underneath; the type, the home composer, the header and every settings page now follow Muse's own screens rather than OpenMinis' iOS-style chrome. ### Changed - **Type.** Muse renders in the phone's system face (MiSans on Xiaomi, Roboto elsewhere), so nothing is bundled; what changed is the scale. `MinisTheme` now builds its `Typography` from a Muse-like scale (zero tracking, 16/24 body, 14/20 and 13/18 secondary, SemiBold titles (17/22 for page titles)) instead of Material's defaults (`Theme.kt`, `museTypography()`); the user's text-size setting still scales it. - **Home composer** (`ChatScreen.kt`, `ChatComposerWidgets.kt`). In the home shell the composer is Muse's one-row pill on a flat grey capsule (`MuseTones.bubble`, 26dp radius, no shadow, 16dp margins): a bare "+", the text with the placeholder *Message*, a bare mic; a blue send arrow replaces the mic once there is text or an attachment, and the stop button while the agent works. The "/" commands moved into "+" → *Commands*. Voice mode, recording and message editing fall back to OpenMinis' two-row card, which is unchanged elsewhere. The one `BasicTextField` is declared once and placed in either layout. - **Header.** The rows under the agent's name (model group, provider · model) are off by default on the main chat, as on Muse, which shows only the name; the bar is 116dp instead of 136dp. *Appearance → Home → Show the model under the name* turns them back on, live (`AppearanceScreen.kt`, `KEY_NM_HEADER_MODEL`, `NmHomeChrome.rememberHeaderModelShown()`); with them off, ••• → *Model* opens the picker. The round header buttons lose their outline in the light theme and get a soft shadow, like Muse's discs. - **Replies.** In the home shell the agent's text arrives in grey bubbles, one per paragraph, with no "✦ name" label above each turn: the face in the header says who is talking (`NmAssistantBubble`, wrapping `AssistantText`, `AssistantMarkdownBlock` and legacy content; a 6dp gap stands in for `AssistantHeader`). Code blocks, tables and HTML stay bare so they keep their width. - **Settings** (`SettingsScreen.kt`, `SettingsComponents.kt`, `io.github.nanomuse.ui.muse.MuseChrome`). Muse's page: a round back disc, a centred 17sp title, white 16dp cards on the grey canvas, rows of one bare ink glyph + label + chevron, hairline separators, no section headers or subtitles. The main page opens with the model card (default group, provider · model, *Change*) where Muse has its plan card, then *Manage Providers* and *Token Usage*; the agent's card (Soul, Avatar, Memory, System files, Skills, MCP Integrations, Environment Variables); the phone's (Permissions, Background & notifications, Storage, Shared Folders, Mount External Folders, Backup & Restore); the app's (Appearance, Logs); About, Privacy Policy, Feedback; the version at the foot. Every entry OpenMinis had is still there. - **Every settings sub-page.** `SettingsScaffold` and the 24 screens that draw their own bar now use `MuseTopAppBar`, a drop-in for Material's `TopAppBar` / `CenterAlignedTopAppBar`. `SettingsSection` labels are sentence-case grey text instead of small caps; cards are 16dp; `SettingsRow` draws its glyph bare in ink (22dp) instead of on a coloured tile; `SettingsChoiceRow` shows Muse's radio: a hollow ring, or an ink disc with a white check. - `docs/screenshots/chat-approval.png` (and the web copy) retaken on this build. - versionCode 9. ### Kept on purpose - Every OpenMinis feature and setting; the two-row composer for voice, recording and editing; the status line under the name ("Needs approval", "Working…") and the naming card of the first conversation. ## [0.1.7] - 2026-09-25 · Welcome The first run is back, in Muse's shape. A fresh install opens on a welcome screen (the mark, three steps, one blue button) instead of a chat that cannot answer: add a model provider, pick from the models it actually serves, meet the agent. The steps are OpenMinis' own provider and model screens. ### Fixed - On phones, 0.1.3's home replaced the session list and with it the three-step setup OpenMinis shows before the first conversation (`OnboardingLanding` in `SessionListScreen`). A fresh install of 0.1.3–0.1.6 opened straight into the main chat with no provider; the reply to the greeting failed with "No provider configured" and the only way to a provider was Settings in the drawer. `NanoMuseHome` now shows `FirstRunSetupScreen` until the setup is done, and step 2 is OpenMinis' `OnboardingModelSelectionScreen`, which refreshes each enabled provider's catalogue and lists it. - The main chat's `ChatViewModel` is not built before the setup is over; one built earlier resolved its model when no default group existed and kept whichever entry it found first. ### Added - **Welcome screen** (`io.github.nanomuse.ui.onboarding.FirstRunSetup`): the N mark, "Welcome to nanoMuse", the three steps as rows with done / current / locked states, a blue *Continue* that performs the next step (*Start* on the last), *Skip for now* on step 2, the fine print that the key stays on the phone with *Learn more*, a gear to Settings. Gate: shown when there is no provider, or for a brand-new install (no sessions) until *Start* or *Skip for now* sets `setup.done` in the `nanomuse` prefs; loading is gated on `ProviderRepository.configLoaded` and the session list so a returning user never sees it flash. - en / zh / zh-TW strings (`nm_setup_*`); `FirstRunSetupTest` (4 tests). ### Changed - versionCode 8. ## [0.1.6] - 2026-09-25 · Avatar The agent gets a face of your choosing. Describe it in a sentence, your own image model draws four, you pick one, and the app poses it for every state the agent can be in, then the face on the disc breathes, bobs, tilts, pops and shakes with what the agent is doing, the way Muse's does. Around it, the small motions that make an app feel finished: pages that cross-fade, feed cards that settle in, a heart that pops. ### Added - **Avatar page** (`io.github.nanomuse.avatar`, `ui/avatar`; Settings → Avatar, or tap the face on any header). Muse's layout: the face on its disc with the agent's name and what it is doing right now, the preview cycling through the moods; *Describe a new face*, one sentence, a row of style chips (flat, 3D clay, watercolour, pixel, line art, sticker) and a blue *Draw four*; *Pick one*: a 2×2 grid of candidates with a shimmer while they draw, a check on the chosen one, and *Use this one*. Failed tiles say why and retry on tap. The ⋯ menu holds *Name and style* (the soul editor), *Image model*, *Redraw the moods* and *Back to the red panda*. - **Bring your own image model.** Generation goes through the providers you already configured, no new key: any enabled OpenAI-compatible, OpenRouter or xAI instance, with the catalogue's image models offered as chips and a sensible default per host (`qwen-image-3.0` on Alibaba Bailian / DashScope, `gpt-image-1` on OpenAI, `grok-2-image` on xAI, `google/gemini-2.5-flash-image` on OpenRouter). Candidates use OpenMinis' `images/generations`; the four moods are image *edits* of the picture you chose (*working* wears headphones at a laptop, *waiting* looks up with a question mark, *happy* hugs a star, *error* has a sweat drop) through `images/edits` on OpenAI-style hosts and DashScope's native `qwen-image-edit-max` on Alibaba hosts, so the character stays the same. Two calls run at a time; rate limits are retried twice with a growing pause. - **The face moves.** `AgentAvatar` cross-fades between moods (260 ms) and animates them: a slow breath at rest (2.6 s, +1.8 %), a quicker breath and a 2.5 dp bob while working, a ±4° tilt while it waits for you, a spring pop when a turn ends well, a 420 ms shake when it failed. A custom face fills the whole disc; the built-in red panda keeps its inset. The afterglow only reads outcomes from the run that just ended (a plain reply after an old failure smiles) and a run you stopped ends quietly. - **Pictures stay on the phone**: `minis-global/nanomuse/avatar/{base,working,waiting,happy,error}.png` (512 px), `candidates/0–3.png` and `avatar.json` (prompt, style, model, time). Candidates survive restarts until you draw again; *Back to the red panda* removes the face and its moods but keeps the candidates. - Motion elsewhere: home pages cross-fade (160 ms in, 120 ms out; the chat underneath fades a touch slower so the switch reads as one), new feed posts settle in and deleted ones fade out (`animateItem`), the like heart pops with a spring and its colour eases. ### Changed - The header's face opens the avatar page; the name pill still opens the soul settings (name, style). - Deep link `minis://settings/avatar` (also `/face`). - versionCode 7. ### Kept, on purpose - The built-in red panda is still the default and one tap away; the launcher icon is untouched. Provider instances, keys and OAuth are OpenMinis' own: the avatar page only reads what you configured there. ## [0.1.5] - 2026-09-25 · Memory The agent starts to keep things: a feed it writes for you every morning, the files that make it what it is (who it is, what it knows about you, what it remembers, when it wakes) readable and editable in one place, and a way to bring over what another assistant already knew. Long tasks keep the screen on and ask "continue?" instead of wrapping up early. ### Added - **Feed tab** (`io.github.nanomuse.feed`, `ui/feed`). A fifth glyph in the bottom bar. A built-in routine: *Write the feed*, daily at 08:00, visible under Goals → Routines with its switch, editor and run records like any other, asks the agent to read GLOBAL.md, the last seven days of diary, USER.md and the goals, and write three to six short posts. Each post is a fenced ` ```nanomuse-feed ` JSON block in the feed's own side chat ("Feed"); the app turns every block into `minis-global/nanomuse/feed/YYYY-MM-DD/NN.md` (front matter: title, type, emoji, source, created, liked) and the tab renders them as Muse's cards on a grey canvas: weekday + part of day as the section title, a 44 dp emoji tile, title, Markdown body, heart · *Discuss* · ⓘ. *Discuss* opens a side chat titled after the post with its text as the opener; ⓘ shows type, time, sources, the file path, and a delete. The first visit shows Muse's *About the feed* card with the steering sentence and *Edit* / *Got it*; the round sliders button at the top right opens the sheet: the sentence (`feed-preferences.md`, quoted in the feed's system prompt), the daily switch, "Daily at 08:00 · tap to change" (opens the routine editor), *Write it now*. Twelve posts a day at most; thirty days are kept. - **System files** (Settings → System files; also in the ⋯ menu of every tab and the drawer). Muse's list: an `MD` badge, the file name, "MD · 358 B · 03:13", sort by name or by last modified, ⋮ to copy or share. Opening a file gives Muse's page: round back button, the name, a pill with the pencil and ⋯, an italic *About this file* quote, and the file rendered as Markdown. Five files: `SOUL.md` (the persona (saving refreshes the cached soul), `USER.md` (new) what the agent knows about you), `GLOBAL.md` (the memory; ⋯ jumps to OpenMinis' memory pages for the diary), `feed-preferences.md`, and `HEARTBEAT.md` (a read-only view of every routine and goal check with schedule and last run, with a ♥ badge). Edits use a monospace editor with a hint, Cancel and a blue Save. - **USER.md in the system prompt.** When the file has content it is appended after the memory fragments, with one paragraph telling the model to keep it current from its shell (`/var/minis/memory/USER.md`) and never to put secrets in it. The file lives next to `GLOBAL.md` and `SOUL.md` in `minis-global/memory/` so the agent can reach it: the outline had it one level up. - **Import memory** (System files → ⋯ → Import memory). One page: the prompt to give your other assistant ("gather everything you remember about me into bullet points…") with a copy button, a *From* field, a paste box, and *Add to memory*, which appends `### · ` and the text under a `## 导入` / `## Imported` section at the end of `GLOBAL.md` (created if missing, recognised in either language). - **Finished browser steps** keep their picture: the last live frame of the in-app browser is remembered per tool block (`BrowserFrames`) and shown when the step saved no screenshot of its own, and the floating status bar reads "Open test page · Done" for a completed `browser_use` step. The two levels of status stay as they were: the running tool's title under the face, the action chips on the cards. - **Screen stays on** while the agent drives the in-app browser or another app through the accessibility service (`KeepAwake`, hooked in `ChatScreen` next to `isStreaming`; the a11y CLI handler stamps every command). Ordinary chat and shell work let it time out as usual. - **"Still going after 200 steps".** When the agent loop reaches `MAX_AGENT_TURNS` it no longer writes an error into the chat; a card above the composer asks whether to keep going: *Continue* resumes from where it stopped, *Stop here* leaves OpenMinis' Resume banner in place. The card is cleared when you send, retry, clear the chat or switch sessions. - `nanomuse-feed` blocks render as a small "Added to the feed" card with an *Open* button in the conversation where they were written. ### Changed - `nmAfterTurn` also hands every completed turn to the feed parser; `buildSystemPrompt` appends the USER.md paragraph and, in the feed's session only, the feed protocol and a digest of the memory files and goals (≤ 7 000 characters). - nanoMuse log categories no longer double the `nanoMuse.` prefix. - versionCode 6. ### Kept, on purpose - OpenMinis' memory pages (`GLOBAL.md` + diary) and the soul editor are unchanged; the system files pages open them for the parts they cover. The feed routine is an ordinary scheduled task: pause, edit or delete it like any other. ## [0.1.4] - 2026-09-25 · Guardrails Before it deletes your files, sends something out or pays, the agent stops and asks, in the shell and in the browser. What you approve can be remembered per chat, or for good per recipient / host / folder. Passwords and verification codes are never typed by the agent; the browser is handed to you instead. ### Added - **Shell guard** (`io.github.nanomuse.guard.ShellGuard`). Every `shell_execute` command is classified before it runs: *destructive* (`rm` outside scratch dirs, `find -delete`, `shred`, `truncate`, `git reset --hard` / `clean -f` / `checkout --` / `branch -D`, `gh repo delete`…), *outbound* (`git push`, `curl`/`wget` with a body or a writing method, `ssh`/`scp`/`rsync`/`nc`, mail clients, `lark-cli` write verbs, `gh` writes, cloud CLIs, package publishing, anything named like a sender), *money* (an outbound command that mentions paying, ordering, transferring…), *install* (`apk`/`apt`/`pip`/`npm i`/`cargo install`…) and *safe*. Pipelines, `&&`/`;` chains, `sudo`/`env`/`nohup` wrappers, heredocs and inline `sh -c` / `python -c` scripts are judged part by part; deleting under `/tmp`, `/var/tmp`, `/dev/shm` is free. Each classification carries an *object*: the folder (`/var/minis/workspace`, `/var/minis/shared`, or the first two path components), the host, the remote, the chat id, the recipient. - **Approval card** (`RiskApprovalHost`, `RiskApprovalCard`). Destructive, outbound and money commands suspend the tool call and slide a card in above the composer, Muse's way: icon, "Allow Spark to delete in the workspace?", one line of what it means, the command in a grey preview box, and the buttons *Allow once* · *Allow for this chat* · *Always allow for the workspace* · *Deny*. The chat behind it dims; the header status reads *Needs approval*. Alarming shapes (wiping a whole tree, `dd`/`mkfs`, `curl | sh`, `chmod 777`, force push, fork bombs) get a warning card with only *Allow once* and *Deny*. Payments are asked about every time; there are no remembered approvals for money. Three minutes without an answer count as a denial. - **Remembered approvals** (`Grants`, `minis-global/nanomuse/grants.json`). *Allow for this chat* covers the same class for the rest of that conversation and is dropped when the chat is cleared or deleted; *Always allow* is persisted per class + object. Settings → Permissions gets a *nanoMuse remembered approvals* section listing each standing grant with its date; tapping one revokes it. - **Browser guard** (`BrowserGuard`). Before `browser_use` clicks or types, the target element is described in-page. A tap whose text or label reads like paying, ordering, sending, deleting, publishing or confirming a purchase asks first: the card shows the button text and the page URL, and *Always allow* binds to the host. Typing into a password field, a one-time-code field (`autocomplete`, name, id, placeholder, label, a numeric 4–8 digit code box, CVV) is refused outright: the tool result tells the model the field is the user's to fill, and the in-app browser is brought to the front on that page. - **Background approvals.** If the app is in the background when a card is pending, a high-priority notification carries the same title and description with *Allow once* and *Deny* actions; tapping it opens that chat. The notification is cancelled when the card is answered from anywhere. - **Policy paragraph in the system prompt** (`RiskPolicy`): what the app stops for, that the model should not ask twice, that a denial must not be retried or routed around, that passwords and codes are never typed, and that installs run with a one-line mention afterwards. - 23 unit tests for the shell and browser classifiers (`app/src/test/java/io/github/nanomuse/guard`). ### Changed - `executeShellCommand` runs the gate first; a denied command returns the denial to the model as the tool result and marks the tool card failed. Installs run without asking and append a one-line notice to the result. - versionCode 5. ### Kept, on purpose - OpenMinis' own confirmation for `minis-config` settings changes (`ConfigConfirmationGate`) and its per-category tool permissions are untouched; nanoMuse's grants sit above them on the Permissions page. ## [0.1.3] - 2026-09-25 · Home The app opens on a conversation, not a list. One main chat with the face at the top, side chats in a drawer, and a bottom bar with Ideas, Goals and Library: the shape of Muse on a phone. Everything OpenMinis had is still there; it is reached from these pages instead of the old session list. ### Added - **Home.** In compact windows the start route renders `NanoMuseHome`: a four-tab shell (chat, ideas, goals, library) whose chat pane stays composed under the other tabs, so switching tabs keeps the scroll position, the composer draft and a running stream. Tablet and landscape widths keep OpenMinis' two-pane scaffold unchanged. Deep links and notifications still open a full-screen chat on top. - **Main chat.** The first session becomes the main chat and is remembered (`nanomuse` prefs); it is what the app opens on and what the chat tab returns to. Its header is Muse's: the face on a pale disc, the name pill hanging off its chin, a round menu button on the left and a round ⋯ on the right. Side chats get the compact header (title, menu, ⋯) and take their title from the first exchange, as before. - **The drawer.** Agent name, "Main chat", the side-chat list with search, an archive glyph that opens the full session list (OpenMinis' `SessionListScreen`, now at `nanomuse/all_chats`, handing the pick back to Home), settings, compose. Long-press a side chat to make it the main chat. - **Goals.** A page with *Tracking* (goals with a checkbox, the latest note, cadence, next check and progress; ⋯ for open conversation / check now / pause / delete), *Routines* (OpenMinis' scheduled tasks: switch, schedule summary, run now, run records, editor, "All routines"), and *Create a goal* with seven categories. Creating one is a conversation: the sheet's "Let's go" sends an opener to the main chat with a two-turn system addendum; the model asks its questions and ends with a fenced ` ```nanomuse-goal ` block that the app turns into a goal card and a `goals.json` entry. Each goal gets its own session and a hidden interval `ScheduledTask` (`hidden`, `goalId`, `intervalMinutes` added to the model) that appends a one-line check to that session; the goal's context and the reporting protocol travel in that session's system prompt, and the model's ` ```nanomuse-goal-update ` block becomes a progress card and updates the row. - **Ideas.** Twenty-four starters in six sections from `assets/nanomuse/ideas.{zh,en}.json`: emoji, pitch, what it does, and a sheet with one primary action, send to chat, create a routine (prefilled scheduled task, editor opens), or start a goal in a category. - **Library.** *Artifacts* and *Media* segments listing what the agent wrote under `minis-sessions/*/workspace` and `minis-global/shared`, newest first, with type icons and image thumbnails, the conversation it came from, preview through OpenMinis' file preview, share via the file provider; ⋯ opens shared folders and the main chat's files. - `nanomuse-*` fenced blocks render as cards in both the streaming and the static Markdown renderers. ### Changed - The first-conversation and goal hooks now also run after *retry*, *resume* and queued prompts, not only after a plain send. - The scheduled-task list hides goal checks; they are managed from the Goals page. - Muse neutrals for nanoMuse pages (`MuseTones`): white surfaces, a warm disc under the face, grey fills, OpenMinis' iOS-grouped scheme is left as it is for its own screens. - versionCode 4. ### Kept, on purpose - Session list, scheduled tasks and their editor, shared folders, file browser and preview, terminal, model groups and every settings page are unchanged and reachable from the new pages. ## [0.1.2] - 2026-09-25 · Identity The agent gets a face and a name, and the first conversation is where you meet it: the shape of Muse's first run, on top of OpenMinis's setup cards, which are unchanged. ### Added - **The red panda in the chat header.** Above the name, 36 dp, drawn from the same shapes as the logo (`scripts/gen-avatar.py` derives five VectorDrawables from `web/src/components/redPandaShapes.ts`). Its mood follows the session: idle, working (breathing, narrowed eyes) while the model streams, waiting (round mouth) when an approval, a permission or a config confirmation is pending, happy for three seconds after a turn, error for four after a failure. Tap it (or the name pill under it) to open Settings → Soul (`minis://settings/soul`). - **Muse's header.** The name sits in a capsule under the face; while the agent works the model rows give way to a status line (the running tool's title, "Thinking…", or "Waiting for you" in the accent colour) and come back when it is idle. - **The first conversation.** With no sessions yet and the default name still in place, the first chat opens with a scripted greeting, how the agent works and "what should I call you?"; the reply is saved as the user's form of address in `GLOBAL.md` (`## About the user`), the model confirms in one sentence and asks for its own name, and a chooser card appears under that message, two suggestions and "Something else…", the composer's placeholder turning into "Write a name here". A pick or a typed name goes straight into `SOUL.md` (no `minis-config` round-trip, no approval gate); the header, the message labels and the placeholder rename at once, and the model's next reply, one line about the name, three concrete things it can do here, "what first?": is steered by a system-prompt addendum that exists only for those two turns. The opening and the card are virtual UI rows: never in the database, never in the history a provider sees (Anthropic rejects a transcript that opens with the assistant). The opening is drawn again when the first session is reopened. - **The name everywhere.** Notifications carry the face as their large icon and the Soul name as sender: the foreground service (" is working"), background results, scheduled tasks, alarms, config confirmations, and the notifications the agent sends itself; the browser banner reads " is browsing". Both happen through `scripts/rebrand.py` (`notification_faces`, the `%1$s` placeholder), so an upstream pull keeps them. - **CI**: `.github/workflows/android.yml` builds a debug arm64 APK on ubuntu-24.04 for pushes and pull requests that touch the Android tree, and uploads it as an artifact. No signing, no secrets. - `-Pnm.abi=x86_64` builds a chat-only test APK for the x86_64 emulator (the sandbox payloads are arm64-only). ### Changed - The chat top bar no longer shows the session title; "Rename chat" moved to the ⋮ menu, and the Appearance switch "Show chat title" is gone (its `minis-config` key remains, inert). - Copy in nanoMuse's voice for the welcome card, the onboarding subtitle and the service notification (en, zh, zh-TW; other locales keep upstream's wording). The Korean locale had 30 strings still reading "Minis": `rebrand.py` now catches the particle-suffixed form. - versionCode 3. ### Kept, on purpose - The provider list and its OAuth sign-ins (Claude, Codex, Kimi, OpenRouter) and the three setup cards stay as in OpenMinis 1.13; there are no vendor presets yet. ## [0.1.1] - 2026-09-25 · Foundation The first version of the Android line: [OpenMinis](https://github.com/OpenMinis/OpenMinis) 1.13 as nanoMuse, functionally identical to upstream. Pre-release; arm64 APK signed with the project key that every later version will use. ### Changed - **The app is OpenMinis 1.13, imported with `git subtree` under `android/`** (unsquashed, 38 upstream commits in the history). The iOS half, the iSH submodule and iOS-only scripts are removed; `deps/proot` is a submodule pointing at our fork [nano-muse/proot](https://github.com/nano-muse/proot), whose `loader-info.awk` no longer needs gawk. - **Identity**: application id `io.github.nanomuse.app` (the Kotlin package stays `com.openminis.app` for upstream merges), name nanoMuse in all 17 locales and in every user-facing string, the agent's default name and 🐾 header, the one-stroke N as adaptive launcher icon (white tile, brand gradient; a dark skin; Android 13 themed-icon layer; a flat status-bar mark for notifications), the brand blue `#015CFB` / `#58A6FF` instead of iOS blue and the teal Material scheme, one accent hue in chat (links, thinking, inline code, blockquotes, the send button, the user bubble). - **Where things point**: update check on `nano-muse/nanoMuse` releases; About shows the licence, "Based on OpenMinis 1.13" and the Meta trademark note; Feedback opens a pre-filled GitHub issue form; the privacy policy is [docs/privacy.md](docs/privacy.md). The Telegram group and the mailbox are gone. - **Licence**: the repository is GPL-3.0-or-later ([NOTICE](NOTICE), [THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md)); contributions need a DCO sign-off. - **Build**: `scripts/rebrand.py` and `scripts/gen-android-icons.py` are idempotent and re-run after every upstream pull; `scripts/android/build-natives.sh` builds proot, the Alpine root file system and `rclone.aar`; `scripts/release-apk.sh ` builds, signs (`android/keystore.properties`, debug key otherwise), names and publishes. JDK 21, SDK CMake 3.22.1, NDK r27c, Go 1.26. ### Frozen - The Python line (`nanomuse/`, `web/`, `demo/`, `site/`) at tag `pre-openminis`, the base of the web and desktop phases. What it does is recorded below, under the tag's own heading. ## [pre-openminis] - 2026-09-24 · The Python line The first release: an open-source personal AI agent inspired by Meta's Muse. One agent that does the work while a Sentinel decides what may run; it reaches a service through its API, an MCP server, a command-line tool, a browser or (with the *Phone* switch on) the screen of the app on your phone. This release is the first phase of the plan: the phone, with the agent on your own machine and no cloud VM; the web on a per-user VM and the desktop come after ([docs/roadmap.md](docs/roadmap.md)). ### The agent - **Tools**: web search (DuckDuckGo out of the box; Brave, Tavily or a SearXNG instance with a key or URL), web pages, files and pages in a workspace, shell and Python (each call in its own [bubblewrap](https://github.com/containers/bubblewrap) sandbox on Linux), mail, a browser view with take-over, calendar and contacts connectors, any [MCP](https://modelcontextprotocol.io) server. - **Memory** you can read, edit and forget; recall by keyword and, with any OpenAI-compatible `/embeddings`, by meaning; a tidy-up with undo. - **Goals** worked on over weeks while the app is closed, with check-ins on a schedule; **reminders and routines**; **triggers** that start work when mail arrives, an event is near or a webhook fires. - **Skills** in the [Agent Skills](https://agentskills.io) format (`SKILL.md` folders), eleven built in, among them five for Chinese services that need no screen: `feishu` (飞书 through the official lark-cli: agenda, messages, events, tasks, documents), `tencent-meeting` (腾讯会议 through Tencent's tmeet CLI: meetings, invitations, recordings and minutes), `amap` (高德地图 through its MCP server: places, routes, distance, weather), `kuaidi100` (parcels through the 快递100 MCP server) and `train-tickets` (trains through the community 12306 MCP server, timetable, seats, prices, connections; booking on the phone's screen only when told to). [docs/services.md](docs/services.md) lists what has been run inside the phone's root file system, what has not, and what we will not recommend. - **MCP servers** take `{{vault:NAME}}` placeholders in `url`, `args` and `env`, resolved at connect time; a call with no arguments sends an empty object (servers built on zod reject a missing one); the **sandbox** has `share` / `share_read_only` so a CLI and its login can be brought into the box (mirrored under the box's `$HOME`) while the rest of home stays out. - **A feed** written for you from what the agent knows and what you asked it to follow; **ideas**; a **library** of everything it made. - **Any OpenAI-compatible model**: Chat Completions or the Responses API, streaming, native or prompt-based tool calling; 阿里云百炼, OpenAI, OpenRouter, Ollama, vLLM, a gateway with its own headers. Pictures attached in chat go to models that take images. ### Sentinel - The agent never touches a tool directly. A policy decides **allow / ask / deny** per call (by risk level, by rules you write, by taint (private data read earlier holds later egress to stricter rules)) and every decision lands in an audit log. - **Approvals you scope**: once, this task, always; revocable from the app. Sends and payments on the phone are only ever *once*. - A **credential vault**: secrets referenced as `{{vault:NAME}}` reach the tool, never the model. ### The phone - **Operating the phone** (`[gui]`, off by default; *Connections → Phone*; `NANOMUSE_GUI_*`). Three tools when it is on: `phone_screen` (the screen as a picture with a caption), `phone_act` (one action by position, with a `label` saying what is under the finger), `phone_task` (a goal for the *phone operator*). - **The operator** is a port of the `mobile_use` loop from [MemGUI-Bench](https://github.com/lgy0404/MemGUI-Bench) (MIT): one screenshot per step, coordinates on a 999×999 grid, `Thought` / `Action` / one tool call, the history as one sentence per step, temperature 0. It never types passwords or codes and never confirms a payment it was not told to make; it asks instead. It works from the picture (no per-app integration) so it works in any app; a device that has an accessibility tree (the Android app) sends the elements along as a second input, for small text and for knowing a password field when it sees one. - **The ladder**: the screen is the fourth rung, after a skill / CLI / MCP server, a fetch with the user's login, and the in-app browser. The system prompt says so, the agent announces a phone step before it takes it and asks first when it is climbing on its own; a skill declares its rung with `channel:` in `SKILL.md` (`api`, `cli`, `web`, `browser`, `gui`, `mixed`) and a `gui` skill is marked *on the phone's screen* in the agent's index; every audited tool call records its `channel`, and the Activity view shows how many of a session's steps were on the screen. - **Sentinel on the screen**: reading a screen taints the session; a tap whose label names 确认支付, 转账, 提交订单, 发送, 删除 … (`sensitive_words`) or a step that types and submits at once is SENSITIVE with a warning and asks, once. - **Traces**: every phone task is a JSONL file under `/phone-traces/`; `nanomuse phone traces` lists them, `nanomuse phone trace -o trace.html` renders one with every screen and every tap drawn on it. - **Devices**: a phone connects over the app's WebSocket, announces its apps and screen size, answers `screen` and `act`; the protocol and the finger-overlay spec are documented for other executors. The first device is the [MobileGym](https://github.com/Purewhiter/mobilegym) simulated phone: in-page screenshots, a ripple for every tap, a caption saying what Muse is doing. - **Android operates its own screen** (Android 11+): the app's accessibility service takes the screenshot (downscaled to 720 px wide, taps scaled back), performs the gestures, presses Back / Home / Recents, opens apps, types through `ACTION_SET_TEXT` or the clipboard and refuses password fields outright; it sends the element tree with every screen. While a task runs a **capsule** sits over the operated app: the red panda, the current step and a **Stop** button; one tap ends the action in flight (`nanomuse:stop`), the operator finishes with `stopped` and the agent asks what to do instead of carrying on. When the agent needs you the capsule grows into a card with the question and *Open*. The finger is drawn as in the spec (rings, lines, typed text, caption) on a layer the screenshot never sees. *Connections → Phone → This phone* shows whether the service is on, opens the Accessibility settings, and explains Android 13's *Restricted setting* and the service being switched off by the system. ### The apps - **The web app** (`nanomuse serve`): Chat, Feed, Ideas, Goals, Library, Connections, Permissions, Activity; approval cards and questions as they happen; artifacts with previews; push notifications with the app closed; English and 简体中文. - **First run and identity**: setup is a three-item checklist (meet your nanoMuse, add a model, connect mail / calendar / contacts (optional)) with *Start* locked until a model is saved and the ticks kept across a reload. The name comes first (1–20 characters, six suggestions, empty means nanoMuse), then the avatar, a tagline, a tone (formal / casual / playful / concise), how much it says (short / detailed / bullet points), anything else in your own words, and what it calls you; each is its own paragraph of the system prompt. The name runs through the app, the approval and permission copy, Settings, the CLI banner and the MobileGym bridge. The model form groups providers by protocol with vendor subtitles (DeepSeek, Kimi, Qwen, GLM, 豆包, MiniMax, OpenAI (Chat Completions and Responses), OpenRouter, Ollama, any OpenAI-compatible endpoint) with a masked key, a *Get a key* link per vendor, `/v1` added to a bare host, no key for local endpoints, and the endpoint's own model list (`POST /api/llm/models`, catalogue fallback, a typed model never replaced). Bring your own key; there is no account with us and no OAuth login. - **The red panda**: the agent's face is an SVG drawn live that changes pose with its state, idle (breathing, blinking, glancing about), working (typing at a laptop), waiting for you (a raised paw and a "!"), done (a bounce), failed (a shake), offline (asleep). Drawn simplified with soft 2.5D shading above 28 px and flat below (three levels of detail from one geometry, `redPandaShapes.ts`), it is also the logo: the web icon, the README cover, the Android launcher (flat, with a monochrome silhouette for themed icons and the status bar) and the launcher icon of the MobileGym app, all generated, none drawn twice. Six plush dolls and an emoji remain as alternatives. - **The Android app**, two flavours. `nanomuse.apk` runs nanoMuse **on the phone itself**: a small Alpine Linux with Python, `nanomuse` and Node inside the APK (under 70 MB compressed), unpacked on first start and run under [PRoot](https://github.com/termux/proot) (a user-mode chroot, no root) with the WebView on `127.0.0.1`. A foreground service starts it, restarts it, holds a wake lock only while a task runs, and comes back after a reboot; your data lives outside the root file system and survives updates. `nanomuse-connect.apk` is the remote for a server on your computer: scan the QR code `nanomuse serve` prints, then the same app in a WebView with notifications. Both signed on every release ([docs/local-runtime.md](docs/local-runtime.md), [docs/android.md](docs/android.md)). - **Keeping it running**: the scheduler and the phone share the schedule. The Python side knows the earliest moment anything is due (`next_wake_at` in `/api/upcoming`, a `schedule` event when it changes) and the local runtime sets one alarm for it: exact when Android allows it, inexact when it does not (Android 14 denies exact alarms to a fresh install), and `POST /api/tick` when it fires. *Settings → Keep it running* shows the battery, overlay and exact-alarm state with a button to Android's own page for each, a *Start after a reboot* switch, and the extra step vendor Android needs (小米, 华为 / 荣耀, OPPO, vivo, 三星, 魅族, with a shortcut to the auto-start page). Crashes are written to a file on the phone and nowhere else; *Export logs* zips them with the app's recent logcat and the runtime's log for the share sheet. - **The browser, on either side**: one browser tool with two backends, Chromium through Playwright on the server, with a persistent profile so logins survive a restart, or the phone's own WebView inside the app, offscreen on a private virtual display so pages run at full speed while the app is in the background. Same actions, same numbered elements, same frames in the chat; `mobile` / `desktop` / custom profiles; a `fetch` action that carries the browser's cookies for logged-in requests without a page. *Take over* in the app slides the real page up as a sheet (no reload), **Done** hands it back. A `wait` action for pages that draw themselves after `load`; the browser's own profile files never show up as artifacts. The device protocol is documented for other executors ([docs/browser.md](docs/browser.md)). - **The phone's own capabilities** ([docs/device.md](docs/device.md)): on the phone the app runs a small MCP server on `127.0.0.1` that `nanomuse serve` picks up as the server `device` with no configuration, clipboard (read only while on screen, as Android has it), notifications, calendars (read, create, update, delete), contacts (read-only), location, alarms and timers through the clock app, photos through the system Photo Picker only. Each tool has its own Sentinel default (`DEVICE_TOOLS`); MCP servers in general take per-tool `tools.` overrides now. Every permission is Android's own dialog (shown at once when the app is on screen, through a notification to tap when it is not) and the model is told whether the user declined or nobody answered. Reading notifications is not offered. - **The workspace in the Files app** (local build): a `DocumentsProvider` shows the agent's workspace as a root in the system Files app and in open/save dialogs. **Share into nanoMuse**: text, links and files from any app's share sheet become a new conversation with the files uploaded and the text as a draft, waiting for what to do with them. - **The CLI bridge**: inside the phone's sandbox, `nanomuse-device`, `nanomuse-browser` and `nanomuse-open` let any shell command or script use the phone's capabilities and the in-app browser through the server (`/api/bridge/*`) with a one-command token, as nested tool calls under the same Sentinel, on the same timeline. `nanomuse-open` is the box's `BROWSER`. The Python side knows when it runs on a phone (`nanomuse.runtime`) and describes its sandbox honestly. - **The showcase** (`demo/showcase/`): a gateway that starts a private nanoMuse per visitor next to the simulated phone, with metered model access and bring-your-own-key; images published from CI. - **Website**: `site/`, one static page in English and 中文 with the red panda playing a task end to end, published to GitHub Pages from `.github/workflows/pages.yml`. - **The film**: a 66-second promo (`site/media/nanomuse-promo.mp4`) rendered frame by frame from `site/promo/storyboard.html` with Playwright and ffmpeg, the same markup and mascot as the app, so it can be re-cut by editing HTML. ### Command line `nanomuse chat`, `run`, `serve`, `daemon`, `goals`, `reminders`, `triggers`, `calendar`, `contacts`, `skills`, `memory`, `vault`, `phone`, `audit`, `config`, `doctor`.