From 7fafe5f5152d4af6a91b8327b8714934c9bb90bb Mon Sep 17 00:00:00 2001
From: jaylfc
Date: Sat, 13 Jun 2026 16:15:54 +0100
Subject: [PATCH 01/57] fix(store): rkllama model install when backend runs but
is unregistered (#843)
* fix(store): model install works when rkllama runs but is unregistered
The model-store Install button failed for rkllama models on the Pi with
'backend install failed: script not found: scripts/install-rkllama.sh'.
Two compounding bugs (found by live test of the store route on the Pi):
1. get_device_capability built installed_backends only from the registry,
so a running-but-unregistered rkllama was treated as not installed and
every model install took the install_chain branch. Now it also live-probes
rkllama (new rkllama_is_running() checks the taOS + legacy ports) and adds
it, so the resolver returns action=use and goes straight to the model pull.
2. The rkllama backend manifest's install.script pointed at a non-existent
scripts/install-rkllama.sh; repointed to the real scripts/install-rknpu.sh
as a safety net for when the chain is genuinely needed.
This is the real symptom behind the #783 store-UI caveat; the rkllama
connection logic itself was already correct (probes 7833 then falls back to
8080). Adds rkllama_is_running tests.
* fix(store): offload the rkllama liveness probe to a thread
Addresses a Gitar review note: rkllama_is_running() does blocking socket
I/O, and get_device_capability is async, so run it via asyncio.to_thread
to avoid stalling the event loop during a model install.
* fix(store): drop the manifest script repoint from this PR
Qodo flagged that scripts/install-rknpu.sh exit-0-false-succeeds in a
non-interactive shell without TAOS_RKNPU_SETUP, so the install_chain would
mark rkllama installed when it did nothing. That is worse than the original
loud 'script not found'. The chain path is not the bug this PR fixes (the
live-probe handles the running-rkllama case), so revert the manifest change
and track the chain fix separately.
* fix(store): log the rkllama runtime-detection fallback instead of swallowing it
---
tests/test_rkllama_installer.py | 25 +++++++++++++++++++++
tinyagentos/installers/rkllama_installer.py | 12 ++++++++++
tinyagentos/routes/store_install.py | 14 ++++++++++++
3 files changed, 51 insertions(+)
diff --git a/tests/test_rkllama_installer.py b/tests/test_rkllama_installer.py
index c217c229..2d743802 100644
--- a/tests/test_rkllama_installer.py
+++ b/tests/test_rkllama_installer.py
@@ -11,7 +11,32 @@
from tinyagentos.installers.rkllama_installer import (
parse_hf_resolve_url,
resolve_rkllama_url,
+ rkllama_is_running,
)
+from tinyagentos.installers import rkllama_installer
+
+
+class TestRkllamaIsRunning:
+ def test_true_when_taos_port_responds(self, monkeypatch):
+ monkeypatch.setattr(
+ rkllama_installer, "_port_responds_with_rkllama",
+ lambda port, timeout=1.0: port == rkllama_installer._DEFAULT_RKLLAMA_PORT,
+ )
+ assert rkllama_is_running() is True
+
+ def test_true_when_only_legacy_port_responds(self, monkeypatch):
+ monkeypatch.setattr(
+ rkllama_installer, "_port_responds_with_rkllama",
+ lambda port, timeout=1.0: port == rkllama_installer._LEGACY_RKLLAMA_PORT,
+ )
+ assert rkllama_is_running() is True
+
+ def test_false_when_nothing_responds(self, monkeypatch):
+ monkeypatch.setattr(
+ rkllama_installer, "_port_responds_with_rkllama",
+ lambda port, timeout=1.0: False,
+ )
+ assert rkllama_is_running() is False
class TestParseHfResolveUrl:
diff --git a/tinyagentos/installers/rkllama_installer.py b/tinyagentos/installers/rkllama_installer.py
index 5513dc34..73aea082 100644
--- a/tinyagentos/installers/rkllama_installer.py
+++ b/tinyagentos/installers/rkllama_installer.py
@@ -59,6 +59,18 @@ def _port_responds_with_rkllama(port: int, timeout: float = 1.0) -> bool:
return False
+def rkllama_is_running() -> bool:
+ """True if a live rkllama server answers on either the taOS or legacy port.
+
+ Used so a running-but-unregistered rkllama is treated as an installed
+ backend during model resolution (skips a needless reinstall chain).
+ """
+ return (
+ _port_responds_with_rkllama(_DEFAULT_RKLLAMA_PORT)
+ or _port_responds_with_rkllama(_LEGACY_RKLLAMA_PORT)
+ )
+
+
def default_rkllama_url() -> str:
"""Return the best local rkllama base URL.
diff --git a/tinyagentos/routes/store_install.py b/tinyagentos/routes/store_install.py
index a6460374..3e5cc5dd 100644
--- a/tinyagentos/routes/store_install.py
+++ b/tinyagentos/routes/store_install.py
@@ -9,6 +9,7 @@
"""
from __future__ import annotations
+import asyncio
import logging
from dataclasses import asdict
from urllib.parse import urlparse
@@ -104,6 +105,19 @@ async def get_device_capability(request: Request, target_remote: str | None) ->
installed_backends = tuple(b for b in _KNOWN_BACKENDS if b in ids)
except Exception: # noqa: BLE001
installed_backends = ()
+ # A backend that is actually running but missing from the registry must
+ # still count as installed, so the resolver uses it (action="use")
+ # instead of trying to (re)install it. rkllama runs as a bare process on
+ # the Pi and is often not registered, which made every model install
+ # take a broken install_chain path (issue #783 follow-up).
+ if "rkllama" not in installed_backends:
+ try:
+ from tinyagentos.installers.rkllama_installer import rkllama_is_running
+ # Offload the blocking socket probe so it never stalls the loop.
+ if await asyncio.to_thread(rkllama_is_running):
+ installed_backends = installed_backends + ("rkllama",)
+ except Exception as exc: # noqa: BLE001
+ logger.debug("rkllama runtime detection skipped: %s", exc)
return DeviceCapability(
device_id="local",
targets=targets,
From e8810f4a7e949ad3fdefab8b1a73a65f5c6334bc Mon Sep 17 00:00:00 2001
From: jaylfc
Date: Sat, 13 Jun 2026 16:50:09 +0100
Subject: [PATCH 02/57] docs: rename brand to taOS + brand contact to
info@taos.my (#847)
* docs: brand contact details to info@taos.my and add taos.my website link
Swaps the personal gmail for the branded info@taos.my across the README, LICENSE
commercial-contact, code of conduct enforcement, CONTRIBUTING, and getting-started
docs, and surfaces taos.my as the project website. Leaves the gitea-migration
runbook config value (infra, not public contact) untouched.
* docs(readme): rename the TinyAgentOS brand to taOS
Replaces the long-form 'TinyAgentOS' display name with 'taOS' throughout the
README, keeping one bracketed mention up top ('taOS (short for TinyAgentOS)')
so first-time readers learn what taOS stands for. Lowercase tinyagentos in
URLs, paths, and the package/repo slug is unchanged.
---
CODE_OF_CONDUCT.md | 2 +-
CONTRIBUTING.md | 2 +-
LICENSE | 2 +-
README.md | 35 ++++++++++++++++++-----------------
docs/getting-started.md | 2 +-
5 files changed, 22 insertions(+), 21 deletions(-)
diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md
index 4befe6ce..5d43e3c2 100644
--- a/CODE_OF_CONDUCT.md
+++ b/CODE_OF_CONDUCT.md
@@ -60,7 +60,7 @@ representative at an online or offline event.
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported to the community leaders responsible for enforcement at
-jaylfc25@gmail.com.
+info@taos.my.
All complaints will be reviewed and investigated promptly and fairly.
All community leaders are obligated to respect the privacy and security of the
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 7a6668c7..66e5b0f0 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -233,4 +233,4 @@ Routes are registered in `app.py`. Route modules access stores via `request.app.
## Contact
-Questions not suited for a GitHub issue? Email jaylfc25@gmail.com.
+Questions not suited for a GitHub issue? Email info@taos.my.
diff --git a/LICENSE b/LICENSE
index fb05adda..60338ad9 100644
--- a/LICENSE
+++ b/LICENSE
@@ -49,7 +49,7 @@ free of charge and is not a Commercial Purpose, provided the Software (and
anything built on it) is not offered, sold, hosted, or otherwise made available
to any third party.
-To obtain a commercial license, contact JAN LABS LTD at jaylfc25@gmail.com.
+To obtain a commercial license, contact JAN LABS LTD at info@taos.my.
4. Trademarks
diff --git a/README.md b/README.md
index 6b71e9e7..9df79226 100644
--- a/README.md
+++ b/README.md
@@ -18,11 +18,11 @@
-Self-hosted AI agent platform that runs on whatever hardware you have. An old laptop, a Raspberry Pi, a gaming PC, an SBC gathering dust, or all of them at once. TinyAgentOS turns your spare hardware into a distributed AI compute cluster.
+Self-hosted AI agent platform that runs on whatever hardware you have. An old laptop, a Raspberry Pi, a gaming PC, an SBC gathering dust, or all of them at once. taOS (short for TinyAgentOS) turns your spare hardware into a distributed AI compute cluster.
A full web desktop environment with 36 bundled apps, 108 catalog apps, 47 MCP plugins, 16 agent frameworks, a curated local model catalog of 112 manifests covering LLMs, vision, embeddings, audio, and image generation (including RK3588 NPU variants via c01zaut/happyme531), plus 167k+ searchable models from HuggingFace, agent deployment, training, image/video/audio generation, and full system monitoring, all from a single web dashboard. Supports Apple Silicon (MLX), NVIDIA, AMD, Rockchip NPU, Raspberry Pi, Android phones, and more.
-**Framework-agnostic by design.** TinyAgentOS owns everything that matters: your agent's memory, files, communication channels, model access, and configuration. The agent framework is just a replaceable execution engine. Switch from SmolAgents to LangChain to OpenClaw and your agent keeps its entire history, all its Telegram/Discord/Slack connections, its trained LoRA adapters, its files, and its API keys. No migration, no data loss, no reconfiguration. This is possible because TinyAgentOS manages the full agent lifecycle outside the framework.
+**Framework-agnostic by design.** taOS owns everything that matters: your agent's memory, files, communication channels, model access, and configuration. The agent framework is just a replaceable execution engine. Switch from SmolAgents to LangChain to OpenClaw and your agent keeps its entire history, all its Telegram/Discord/Slack connections, its trained LoRA adapters, its files, and its API keys. No migration, no data loss, no reconfiguration. This is possible because taOS manages the full agent lifecycle outside the framework.
**[taOSmd](https://github.com/jaylfc/taosmd) — Framework-agnostic AI memory system.** 97.0% **end-to-end Judge accuracy** on [LongMemEval-S](https://github.com/xiaowu0162/LongMemEval) — retrieve → generate → judge-with-LLM-grader, 500 questions across 50+ sessions each. For context, the most-cited open comparators — MemPalace (96.6%) and agentmemory (95.2%) — publish **Recall@5** retrieval scores on the same dataset, which measures only whether the correct session lands in the top-5 (no generation, no judge). The metrics aren't apples-to-apples until one of us re-runs end-to-end; ours is the stricter measurement. Per-category on our hybrid-plus-query-expansion config: knowledge-update 100%, multi-session 98.5%, single-session-user 97.1%, single-session-assistant 96.4%, temporal-reasoning 94.0%, single-session-preference 90.0%. Everything runs on a £170 Orange Pi 5 Plus with no cloud dependencies. The stack: temporal knowledge graph with validity windows + contradiction detection, hybrid semantic+keyword vector search with cross-encoder rerank and LLM-assisted query expansion (the "Librarian" layer), zero-loss append-only archive, automatic fact extraction, intent-aware retrieval routing, multi-layer context assembly. Any agent framework can read/write through the HTTP API.
@@ -72,7 +72,7 @@ Open `http://your-host:6969` (or `http://taos.local:6969` with mDNS). The root U
## Web Desktop Experience
-TinyAgentOS ships with a full browser-based desktop environment. Open it at `http://your-host:6969/` and you get a window manager, dock, launchpad, notifications, widgets, and 36 bundled apps, no native install required. On phones and tablets it automatically swaps to a widget-first home screen with swipeable pages, a persistent dock, and desktop-style app windows with close/minimise title bars, installable as a fullscreen PWA from the browser's "Add to Home Screen".
+taOS ships with a full browser-based desktop environment. Open it at `http://your-host:6969/` and you get a window manager, dock, launchpad, notifications, widgets, and 36 bundled apps, no native install required. On phones and tablets it automatically swaps to a widget-first home screen with swipeable pages, a persistent dock, and desktop-style app windows with close/minimise title bars, installable as a fullscreen PWA from the browser's "Add to Home Screen".
- **Window manager.** Float, snap zones, drag, resize, minimise, maximise, close
- **Top bar.** Global search (Ctrl+Space), clock, notifications, widget toggle
@@ -190,7 +190,7 @@ Pick from 1,467 agent templates, 12 built-in plus 196 from awesome-openclaw-agen
One-click install for agent frameworks, AI models, and services. Hardware-aware, only shows what works on your device.
### Agent Deployment
-5-step wizard: pick framework → choose model → configure → deploy into an isolated container (LXC on bare metal, Docker on VPS, auto-detected). Each agent gets its own memory system (taOSmd instance), its own file storage, and its own network identity. The framework runs inside the container but TinyAgentOS manages everything around it: memory, channels, secrets, model access, scheduled tasks, and inter-agent communication. This means the framework is a swappable component, not a lock-in decision.
+5-step wizard: pick framework → choose model → configure → deploy into an isolated container (LXC on bare metal, Docker on VPS, auto-detected). Each agent gets its own memory system (taOSmd instance), its own file storage, and its own network identity. The framework runs inside the container but taOS manages everything around it: memory, channels, secrets, model access, scheduled tasks, and inter-agent communication. This means the framework is a swappable component, not a lock-in decision.
> **Running taOS *inside* an LXC (e.g. Proxmox)?** Deploying an agent creates a *nested* container, which an **unprivileged** LXC cannot do — the kernel can't remap the nested container's filesystem, so the deploy fails with an `idmapped storage / change ownership` error. Run the taOS LXC as **privileged with nesting enabled**. On Proxmox: untick *Unprivileged container* and set Options → Features → `nesting=1` (plus `keyctl=1`, `fuse=1`), then redeploy. Bare-metal and VM installs are unaffected. (taOS detects this and surfaces the fix in the deploy error.)
@@ -201,7 +201,7 @@ One-click install for agent frameworks, AI models, and services. Hardware-aware,
The Agents app on mobile — one tap from empty to your first deployed agent.
### Channel Hub (Framework-Agnostic Messaging)
-Most agent frameworks force you to wire up Telegram, Discord, or Slack directly into their code. If you switch frameworks, you rebuild all those integrations from scratch. TinyAgentOS flips this: the platform owns the messaging connections and routes messages to whichever framework the agent currently uses. Switch an agent from SmolAgents to LangChain and it keeps every channel, every conversation, every connection. The framework never touches the bot tokens.
+Most agent frameworks force you to wire up Telegram, Discord, or Slack directly into their code. If you switch frameworks, you rebuild all those integrations from scratch. taOS flips this: the platform owns the messaging connections and routes messages to whichever framework the agent currently uses. Switch an agent from SmolAgents to LangChain and it keeps every channel, every conversation, every connection. The framework never touches the bot tokens.
- **6 connectors**. Telegram, Discord, Slack, Email (IMAP/SMTP), Web Chat (WebSocket), Webhooks
- **15 framework adapters.** Thin HTTP bridges (~25 lines each) that translate the universal message format to framework-specific APIs
@@ -209,7 +209,7 @@ Most agent frameworks force you to wire up Telegram, Discord, or Slack directly
- **Per-agent or shared bots.** Each agent gets its own bot, or share one across a group
### LLM Proxy (LiteLLM)
-Hidden internal gateway that unifies all inference providers behind a single OpenAI-compatible API. Each agent gets a virtual API key with budget and rate limits. The proxy is auto-configured from your backend list. Switch from a local Ollama backend to a cloud provider (or add both as fallbacks) and no agent config changes. The agent just calls its local API key and TinyAgentOS routes to the best available backend.
+Hidden internal gateway that unifies all inference providers behind a single OpenAI-compatible API. Each agent gets a virtual API key with budget and rate limits. The proxy is auto-configured from your backend list. Switch from a local Ollama backend to a cloud provider (or add both as fallbacks) and no agent config changes. The agent just calls its local API key and taOS routes to the best available backend.
### Dynamic Capabilities
Features unlock automatically based on your hardware and cluster. Solo Pi sees core features. Add a GPU worker and image generation, video, and training appear. No configuration, the platform just knows what's possible.
@@ -243,7 +243,7 @@ taOS wraps taOSmd with platform-specific scheduling (job queue, resource manager
- **Browse / collections**. `GET /browse`, `GET /collections`, `POST /ingest`, `POST /delete-chunk`
- **Memory browser.** Web UI to search across all agents' knowledge bases from one place
- **Framework-independent.** Memory lives on the host, not in the framework or the container. Switch frameworks and the agent's entire knowledge base stays intact.
-- **Portable.** Export an agent's config, channels, and memory. Import on another TinyAgentOS instance.
+- **Portable.** Export an agent's config, channels, and memory. Import on another taOS instance.
The embedding backend (`qmd.service`, port 7832) provides an Ollama-compatible embedding API with batch embedding and retry logic, backed by rkllama on RK3588 or node-llama-cpp elsewhere. LiteLLM also exposes a `/v1/embeddings` endpoint that routes to the same backends so frameworks using the OpenAI embeddings API work without any shim.
@@ -347,7 +347,7 @@ Search across agents, apps, messages, and files from a single endpoint. Finds an
## Architecture
```
-TinyAgentOS Controller (FastAPI + htmx + React Desktop Shell)
+taOS Controller (FastAPI + htmx + React Desktop Shell)
├── Web Desktop Shell (window manager, dock, launchpad, widgets, 36 bundled apps)
├── Mobile/Tablet Shell (widget home, dock, app title bars, swipeable pages, iOS PWA)
├── Skills & Plugins Registry (8 default skills, 15 framework adapters)
@@ -495,7 +495,7 @@ The bytecode cleanup line is belt-and-braces; Python's mtime-based invalidation
## Service Management
-TinyAgentOS ships with a systemd unit at `/etc/systemd/system/tinyagentos.service`. It auto-restarts on failure and auto-starts on boot.
+taOS ships with a systemd unit at `/etc/systemd/system/tinyagentos.service`. It auto-restarts on failure and auto-starts on boot.
```bash
sudo systemctl start tinyagentos
@@ -518,7 +518,7 @@ See [docs/mirror-policy.md](docs/mirror-policy.md) for the mirror governance pol
## TurboQuant KV cache compression
-**768K context window on a single RTX 3060 (12 GB).** TinyAgentOS integrates Google's TurboQuant (ICLR 2026) KV cache quantization via TheTom/llama-cpp-turboquant. Unlike weight quantization, which compresses model files, TurboQuant compresses the per-request KV cache -- the per-token memory that scales with context length and is the actual bottleneck on consumer hardware.
+**768K context window on a single RTX 3060 (12 GB).** taOS integrates Google's TurboQuant (ICLR 2026) KV cache quantization via TheTom/llama-cpp-turboquant. Unlike weight quantization, which compresses model files, TurboQuant compresses the per-request KV cache -- the per-token memory that scales with context length and is the actual bottleneck on consumer hardware.
Measured on Qwen3.5-9B-Q4_K_M, single RTX 3060 12 GB, decode speed stable at 52-62 t/s across the entire range:
@@ -547,7 +547,7 @@ The llama.cpp CUDA build works on Debian 12 (glibc 2.36) and older distributions
## Exo Distributed Inference
-TinyAgentOS integrates [exo](https://github.com/exo-explore/exo) for running models that are too large for any single device. While the TAOS cluster routes different tasks to different workers (task parallelism), exo splits a single large model across multiple devices (pipeline parallelism). The two are complementary.
+taOS integrates [exo](https://github.com/exo-explore/exo) for running models that are too large for any single device. While the TAOS cluster routes different tasks to different workers (task parallelism), exo splits a single large model across multiple devices (pipeline parallelism). The two are complementary.
**What exo enables:** Run 70B+ parameter models by pooling VRAM across multiple machines. A 70B model that needs ~40 GB can be split across a 12 GB desktop GPU + a 16 GB laptop + a 24 GB Mac, with exo handling the shard placement and inter-device communication automatically.
@@ -594,7 +594,7 @@ uv run exo
- [docs/design/framework-agnostic-runtime.md](docs/design/framework-agnostic-runtime.md). containers hold code, hosts hold state (load-bearing architectural rule)
- [docs/superpowers/specs/2026-04-11-taos-framework-integration-bridge-design.md](docs/superpowers/specs/2026-04-11-taos-framework-integration-bridge-design.md). TAOS Framework Integration Bridge design (OpenClaw → Hermes → OpenClaw round-trip, not yet implemented)
- [docs/mirror-policy.md](docs/mirror-policy.md). binary mirror governance: what is mirrored, SHA256 verification, self-hosting guide
-- [docs/deploy/platform.md](docs/deploy/platform.md). Runbook for the tinyagentos.com platform LXC, covering landing page, docs site, and bittorrent tracker. Uses `scripts/install-platform-lxc.sh` on the Proxmox host to provision. Infrastructure for the project's public web presence, not part of the TinyAgentOS product itself.
+- [docs/deploy/platform.md](docs/deploy/platform.md). Runbook for the tinyagentos.com platform LXC, covering landing page, docs site, and bittorrent tracker. Uses `scripts/install-platform-lxc.sh` on the Proxmox host to provision. Infrastructure for the project's public web presence, not part of the taOS product itself.
## Development
@@ -691,18 +691,19 @@ See [CONTRIBUTING.md](CONTRIBUTING.md) for setup instructions and guidelines. Jo
## Support the Project
-TinyAgentOS makes AI agents accessible on affordable hardware.
+taOS makes AI agents accessible on affordable hardware.
-- **Contact:** jaylfc25@gmail.com
+- **Website:** [taos.my](https://taos.my)
+- **Contact:** info@taos.my
- **Donate:** [Buy Me a Coffee](https://buymeacoffee.com/jaylfc)
- **Hardware donations/loans:** We test on real hardware. If you have spare SBCs, GPUs, or dev boards and want to help expand compatibility, reach out.
## Acknowledgments
-TinyAgentOS stands on a lot of excellent community work, particularly on Rockchip. Shout-outs where they are earned:
+taOS stands on a lot of excellent community work, particularly on Rockchip. Shout-outs where they are earned:
- **[c01zaut](https://huggingface.co/c01zaut)**. Qwen2.5 1.5B → 14B RKLLM model ports that let chat work on RK3588 at all.
-- **[NotPunchnox](https://github.com/NotPunchnox).** Original rkllama HTTP server that TinyAgentOS extends with a rerank patch.
+- **[NotPunchnox](https://github.com/NotPunchnox).** Original rkllama HTTP server that taOS extends with a rerank patch.
- **[tobi](https://github.com/tobi)** and contributors on [qmd](https://github.com/tobi/qmd), the embedding / reranker / query-expansion backend that taOSmd uses for vector operations, including the centralised `qmd serve` mode ([PR #511](https://github.com/tobi/qmd/pull/511)).
If you maintain one of the libraries above and want a different phrasing or a link added, open an issue and I will fix it.
@@ -720,4 +721,4 @@ taOS is better for the people testing it, filing issues, and sending fixes:
taOS Sustainable Use License v0.1 — source-available, not open source. See [LICENSE](LICENSE).
-Free to use, modify, and self-host for personal use and for your own organisation's internal business purposes — forever. A separate commercial license from JAN LABS LTD is required to sell taOS, host it as a paid service, or build it into a product or service you monetise (contact jaylfc25@gmail.com). Prior releases tagged under AGPL-3.0 remain available under AGPL-3.0.
+Free to use, modify, and self-host for personal use and for your own organisation's internal business purposes — forever. A separate commercial license from JAN LABS LTD is required to sell taOS, host it as a paid service, or build it into a product or service you monetise (contact info@taos.my). Prior releases tagged under AGPL-3.0 remain available under AGPL-3.0.
diff --git a/docs/getting-started.md b/docs/getting-started.md
index ea54902f..c41f1f0a 100644
--- a/docs/getting-started.md
+++ b/docs/getting-started.md
@@ -447,7 +447,7 @@ When filing a bug, it helps to include:
- What you expected to happen vs. what actually happened
- Relevant log output from `journalctl -u tinyagentos -n 50`
-**Email:** [jaylfc25@gmail.com](mailto:jaylfc25@gmail.com) — for anything that doesn't fit a GitHub issue.
+**Email:** [info@taos.my](mailto:info@taos.my) — for anything that doesn't fit a GitHub issue.
**A note on maturity:** TinyAgentOS is in early development. If something doesn't work, it may genuinely be a bug rather than user error — please do report it. Contributions and hardware test reports are very welcome.
From 9e1462a9300aa7dbf3cf0ea3103250a631824892 Mon Sep 17 00:00:00 2001
From: jaylfc
Date: Sat, 13 Jun 2026 16:50:11 +0100
Subject: [PATCH 03/57] feat(theme): add the Light theme - Apple-grade light
counterpart to Default (#848)
Adds a third builtin theme (Default dark / Light / Matrix Terminal). Three parts:
- builtin-themes.ts: the Light palette over the existing --color-shell-* tokens
(cool off-white body, slate accent, frosted near-white chrome, softer shadows,
light wallpaper). Contrast-checked: body ink hits ~14:1, secondary ~6.4:1.
- theme-store.ts: applyThemeConfig now derives a light/dark scheme from the
theme's --color-shell-bg luminance and tags the root data-scheme. Works for
the builtin Light theme and any agent-generated light theme alike.
- tokens.css: a light-scheme compatibility layer that inverts the ~800 hardcoded
white-overlay utilities (bg-white/N, border-white/N) and ~140 text-white uses
that apps still carry, so they read dark-on-light instead of vanishing. Gated
on data-scheme=light via attribute selectors whose specificity beats Tailwind
utilities without !important, so the dark theme is provably untouched.
The dark Default theme is unchanged. Verified live: computed styles invert under
the light scheme and stay identical under dark.
---
.../src/stores/__tests__/theme-apply.test.ts | 16 ++++++
desktop/src/stores/theme-store.ts | 22 +++++++++
desktop/src/theme/builtin-themes.ts | 41 ++++++++++++++++
desktop/src/theme/tokens.css | 49 +++++++++++++++++++
4 files changed, 128 insertions(+)
diff --git a/desktop/src/stores/__tests__/theme-apply.test.ts b/desktop/src/stores/__tests__/theme-apply.test.ts
index 1a2f0e97..04bcb3b9 100644
--- a/desktop/src/stores/__tests__/theme-apply.test.ts
+++ b/desktop/src/stores/__tests__/theme-apply.test.ts
@@ -15,4 +15,20 @@ describe("applyThemeConfig", () => {
applyThemeConfig({ tokens: { "--evil": "x" } as Record, structure: {}, effects: [], requires: [] });
expect(document.documentElement.style.getPropertyValue("--evil")).toBe("");
});
+
+ it("tags the root data-scheme from the theme's bg luminance", () => {
+ // Light bg -> light scheme (drives the compatibility layer in tokens.css).
+ applyThemeConfig({ tokens: { "--color-shell-bg": "#f4f5f7" }, structure: {}, effects: [], requires: [] });
+ expect(document.documentElement.dataset.scheme).toBe("light");
+ // rgba dark bg -> dark.
+ applyThemeConfig({ tokens: { "--color-shell-bg": "rgba(22, 25, 32, 0.92)" }, structure: {}, effects: [], requires: [] });
+ expect(document.documentElement.dataset.scheme).toBe("dark");
+ // No bg override (default theme) -> dark base.
+ applyThemeConfig({ tokens: { "--color-accent": "#abc" }, structure: {}, effects: [], requires: [] });
+ expect(document.documentElement.dataset.scheme).toBe("dark");
+ // revert resets to dark.
+ applyThemeConfig({ tokens: { "--color-shell-bg": "#ffffff" }, structure: {}, effects: [], requires: [] });
+ revertTheme();
+ expect(document.documentElement.dataset.scheme).toBe("dark");
+ });
});
diff --git a/desktop/src/stores/theme-store.ts b/desktop/src/stores/theme-store.ts
index 22027e90..d1f6cd71 100644
--- a/desktop/src/stores/theme-store.ts
+++ b/desktop/src/stores/theme-store.ts
@@ -126,6 +126,26 @@ export const useThemeStore = create((set) => ({
let _applied: string[] = []; // token keys currently set, for revert
+// Decide whether a theme reads as light or dark from its window-body colour,
+// so the light-scheme compatibility layer in tokens.css (which inverts the
+// hardcoded white overlays apps still use) keys off one attribute. Works for
+// the builtin Light theme and any agent-generated light theme alike.
+function schemeFromBg(bg: string | undefined): "light" | "dark" {
+ if (!bg) return "dark"; // no override (default theme) -> the dark base CSS
+ let r: number, g: number, b: number;
+ const hex = bg.trim().match(/^#([0-9a-fA-F]{6})$/);
+ if (hex) {
+ const n = parseInt(hex[1]!, 16);
+ [r, g, b] = [(n >> 16) & 255, (n >> 8) & 255, n & 255];
+ } else {
+ const m = bg.match(/rgba?\(\s*(\d+)[\s,]+(\d+)[\s,]+(\d+)/i);
+ if (!m) return "dark";
+ [r, g, b] = [+m[1]!, +m[2]!, +m[3]!];
+ }
+ const luminance = (0.2126 * r + 0.7152 * g + 0.0722 * b) / 255;
+ return luminance > 0.55 ? "light" : "dark";
+}
+
export function applyThemeConfig(cfg: ThemeConfig) {
revertTheme();
const root = document.documentElement;
@@ -135,6 +155,7 @@ export function applyThemeConfig(cfg: ThemeConfig) {
_applied.push(k);
}
}
+ root.dataset.scheme = schemeFromBg(cfg.tokens?.["--color-shell-bg"]);
useThemeStore.setState({ structure: cfg.structure || {}, effects: cfg.effects || [] });
}
@@ -142,6 +163,7 @@ export function revertTheme() {
const root = document.documentElement;
for (const k of _applied) root.style.removeProperty(k);
_applied = [];
+ root.dataset.scheme = "dark"; // base shell is dark
useThemeStore.setState({ structure: {}, effects: [] });
}
diff --git a/desktop/src/theme/builtin-themes.ts b/desktop/src/theme/builtin-themes.ts
index 9a61fd76..ec1a14c3 100644
--- a/desktop/src/theme/builtin-themes.ts
+++ b/desktop/src/theme/builtin-themes.ts
@@ -14,6 +14,47 @@ export const BUILTIN_THEMES: BuiltinTheme[] = [
builtin: true,
config: { tokens: {}, structure: {}, effects: [], requires: ["assistant", "launcher"], wallpaper: null },
},
+ {
+ theme_id: "light",
+ name: "Light",
+ builtin: true,
+ config: {
+ tokens: {
+ // Cool off-white window body and a slightly deeper sidebar layer.
+ "--color-shell-bg": "#f4f5f7",
+ "--color-shell-bg-deep": "#e9ebef",
+ // Surfaces invert from white-on-dark to subtle black-on-light fills.
+ "--color-shell-surface": "rgba(0, 0, 0, 0.035)",
+ "--color-shell-surface-hover": "rgba(0, 0, 0, 0.055)",
+ "--color-shell-surface-active": "rgba(0, 0, 0, 0.08)",
+ "--color-shell-border": "rgba(0, 0, 0, 0.09)",
+ "--color-shell-border-strong": "rgba(0, 0, 0, 0.15)",
+ // Near-black ink: 14:1 / 6.4:1 / 4.0:1 on the #f4f5f7 body.
+ "--color-shell-text": "rgba(0, 0, 0, 0.85)",
+ "--color-shell-text-secondary": "rgba(0, 0, 0, 0.55)",
+ "--color-shell-text-tertiary": "rgba(0, 0, 0, 0.42)",
+ // Slate accent — the dark theme's cool-neutral grey, darkened to read on light.
+ "--color-accent": "#5b6472",
+ "--color-accent-glow": "rgba(91, 100, 114, 0.25)",
+ // Frosted near-white chrome.
+ "--color-dock-bg": "rgba(245, 246, 248, 0.82)",
+ "--color-dock-border": "rgba(0, 0, 0, 0.1)",
+ "--color-topbar-bg": "rgba(245, 246, 248, 0.82)",
+ "--color-snap-preview": "rgba(91, 100, 114, 0.16)",
+ "--color-snap-border": "rgba(91, 100, 114, 0.45)",
+ // Lighter, softer shadows — heavy dark drops look wrong on a light surface.
+ "--shadow-window": "0 8px 32px rgba(0, 0, 0, 0.16)",
+ "--shadow-window-unfocused": "0 4px 16px rgba(0, 0, 0, 0.1)",
+ "--shadow-dock": "0 4px 24px rgba(0, 0, 0, 0.12)",
+ "--shadow-card": "0 1px 3px rgba(0, 0, 0, 0.1), 0 0 1px rgba(0, 0, 0, 0.06)",
+ "--shadow-card-hover": "0 8px 24px rgba(0, 0, 0, 0.14), 0 0 1px rgba(0, 0, 0, 0.08)",
+ },
+ structure: {},
+ effects: [],
+ requires: ["assistant", "launcher"],
+ wallpaper: "linear-gradient(160deg, #eef0f3 0%, #e6e9ee 45%, #dee2e8 100%)",
+ },
+ },
{
theme_id: "matrix-terminal",
name: "Matrix Terminal",
diff --git a/desktop/src/theme/tokens.css b/desktop/src/theme/tokens.css
index 854d60e5..2232bc2c 100644
--- a/desktop/src/theme/tokens.css
+++ b/desktop/src/theme/tokens.css
@@ -48,6 +48,55 @@
--shadow-card-hover: 0 8px 24px rgba(0, 0, 0, 0.3), 0 0 1px rgba(255, 255, 255, 0.08);
}
+/* ==================================================================
+ Light-scheme compatibility layer
+ ==================================================================
+ The shell tokens above (window chrome, dock, top bar, text) already
+ flip per theme. But ~800 surfaces across the apps still use hardcoded
+ white-overlay utilities (bg-white/5, border-white/10) and ~140 use
+ hardcoded white text (text-white) — additive white that vanishes on a
+ light background. Rather than migrate every call site, this layer
+ inverts those utilities to dark-on-light, but ONLY when a light theme
+ is active (data-scheme="light", set from the theme's bg luminance in
+ theme-store.ts). The dark theme is never matched, so it is untouched.
+
+ Specificity note: `:root[data-scheme="light"] [class~="x"]` is (0,3,0),
+ which beats Tailwind's single-class utilities (0,1,0) and hover
+ variants (0,2,0) without !important. Hover overlays get their own
+ :hover rules below so the affordance survives the inversion.
+ ================================================================== */
+:root[data-scheme="light"] [class~="bg-white/3"] { background-color: rgba(0, 0, 0, 0.03); }
+:root[data-scheme="light"] [class~="bg-white/5"] { background-color: rgba(0, 0, 0, 0.04); }
+:root[data-scheme="light"] [class~="bg-white/10"] { background-color: rgba(0, 0, 0, 0.06); }
+:root[data-scheme="light"] [class~="bg-white/15"] { background-color: rgba(0, 0, 0, 0.08); }
+:root[data-scheme="light"] [class~="bg-white/20"] { background-color: rgba(0, 0, 0, 0.10); }
+
+:root[data-scheme="light"] [class~="border-white/5"] { border-color: rgba(0, 0, 0, 0.08); }
+:root[data-scheme="light"] [class~="border-white/8"] { border-color: rgba(0, 0, 0, 0.10); }
+:root[data-scheme="light"] [class~="border-white/10"] { border-color: rgba(0, 0, 0, 0.12); }
+:root[data-scheme="light"] [class~="border-white/15"] { border-color: rgba(0, 0, 0, 0.14); }
+:root[data-scheme="light"] [class~="border-white/20"] { border-color: rgba(0, 0, 0, 0.16); }
+
+:root[data-scheme="light"] [class~="text-white"] { color: rgba(0, 0, 0, 0.88); }
+:root[data-scheme="light"] [class~="text-white/90"] { color: rgba(0, 0, 0, 0.84); }
+:root[data-scheme="light"] [class~="text-white/80"] { color: rgba(0, 0, 0, 0.78); }
+:root[data-scheme="light"] [class~="text-white/70"] { color: rgba(0, 0, 0, 0.68); }
+:root[data-scheme="light"] [class~="text-white/60"] { color: rgba(0, 0, 0, 0.58); }
+:root[data-scheme="light"] [class~="text-white/50"] { color: rgba(0, 0, 0, 0.50); }
+:root[data-scheme="light"] [class~="text-white/45"] { color: rgba(0, 0, 0, 0.48); }
+:root[data-scheme="light"] [class~="text-white/40"] { color: rgba(0, 0, 0, 0.45); }
+:root[data-scheme="light"] [class~="text-white/35"] { color: rgba(0, 0, 0, 0.44); }
+:root[data-scheme="light"] [class~="text-white/30"] { color: rgba(0, 0, 0, 0.42); }
+:root[data-scheme="light"] [class~="text-white/25"] { color: rgba(0, 0, 0, 0.40); }
+:root[data-scheme="light"] [class~="text-white/20"] { color: rgba(0, 0, 0, 0.40); }
+:root[data-scheme="light"] [class~="text-white/15"] { color: rgba(0, 0, 0, 0.38); }
+
+/* Hover overlays: re-assert the inverted fill on :hover so the affordance
+ does not flatten under the base-state override. */
+:root[data-scheme="light"] [class~="hover:bg-white/5"]:hover { background-color: rgba(0, 0, 0, 0.05); }
+:root[data-scheme="light"] [class~="hover:bg-white/10"]:hover { background-color: rgba(0, 0, 0, 0.07); }
+:root[data-scheme="light"] [class~="hover:bg-white/[0.06]"]:hover { background-color: rgba(0, 0, 0, 0.05); }
+
/* Wallpaper sizing
----------------
Desktop fills the viewport (cover crops edges if aspect mismatches).
From 6502b93385af0fdce5d87a217d2a98a23f76206b Mon Sep 17 00:00:00 2001
From: jaylfc
Date: Sat, 13 Jun 2026 16:50:13 +0100
Subject: [PATCH 04/57] chore(deps): bump esbuild to 0.28.1 (fixes RCE
advisory), raise build target to es2022 (#849)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Dependabot flagged esbuild < 0.28.1 (missing binary integrity verification, RCE
via NPM_CONFIG_REGISTRY). esbuild is a transitive dep of vite 6, and the bump
needs a build target of es2022+ (vite's default es2020/chrome87 target hit an
esbuild 0.28 destructuring-downlevel error). es2022 covers Chrome 94+, Safari
15.4+, Firefox 93+ — all 2021/2022, fine for a modern self-hosted desktop.
Pinned via the existing overrides block. tsc + vite build verified green.
---
desktop/package-lock.json | 221 ++++++++++++++++++--------------------
desktop/package.json | 3 +-
desktop/vite.config.ts | 1 +
3 files changed, 110 insertions(+), 115 deletions(-)
diff --git a/desktop/package-lock.json b/desktop/package-lock.json
index cf388732..5f23b178 100644
--- a/desktop/package-lock.json
+++ b/desktop/package-lock.json
@@ -36,7 +36,6 @@
"lucide-react": "^0.500.0",
"mathjs": "^15.2.0",
"motion": "^12.40.0",
- "pell": "^1.0.6",
"plyr": "^3.8.4",
"react": "^19.0.0",
"react-dom": "^19.0.0",
@@ -974,9 +973,9 @@
}
},
"node_modules/@esbuild/aix-ppc64": {
- "version": "0.25.12",
- "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz",
- "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==",
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz",
+ "integrity": "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==",
"cpu": [
"ppc64"
],
@@ -991,9 +990,9 @@
}
},
"node_modules/@esbuild/android-arm": {
- "version": "0.25.12",
- "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz",
- "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==",
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.1.tgz",
+ "integrity": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==",
"cpu": [
"arm"
],
@@ -1008,9 +1007,9 @@
}
},
"node_modules/@esbuild/android-arm64": {
- "version": "0.25.12",
- "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz",
- "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==",
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz",
+ "integrity": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==",
"cpu": [
"arm64"
],
@@ -1025,9 +1024,9 @@
}
},
"node_modules/@esbuild/android-x64": {
- "version": "0.25.12",
- "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz",
- "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==",
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.1.tgz",
+ "integrity": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==",
"cpu": [
"x64"
],
@@ -1042,9 +1041,9 @@
}
},
"node_modules/@esbuild/darwin-arm64": {
- "version": "0.25.12",
- "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz",
- "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==",
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz",
+ "integrity": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==",
"cpu": [
"arm64"
],
@@ -1059,9 +1058,9 @@
}
},
"node_modules/@esbuild/darwin-x64": {
- "version": "0.25.12",
- "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz",
- "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==",
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz",
+ "integrity": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==",
"cpu": [
"x64"
],
@@ -1076,9 +1075,9 @@
}
},
"node_modules/@esbuild/freebsd-arm64": {
- "version": "0.25.12",
- "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz",
- "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==",
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.1.tgz",
+ "integrity": "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==",
"cpu": [
"arm64"
],
@@ -1093,9 +1092,9 @@
}
},
"node_modules/@esbuild/freebsd-x64": {
- "version": "0.25.12",
- "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz",
- "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==",
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.1.tgz",
+ "integrity": "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==",
"cpu": [
"x64"
],
@@ -1110,9 +1109,9 @@
}
},
"node_modules/@esbuild/linux-arm": {
- "version": "0.25.12",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz",
- "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==",
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz",
+ "integrity": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==",
"cpu": [
"arm"
],
@@ -1127,9 +1126,9 @@
}
},
"node_modules/@esbuild/linux-arm64": {
- "version": "0.25.12",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz",
- "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==",
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz",
+ "integrity": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==",
"cpu": [
"arm64"
],
@@ -1144,9 +1143,9 @@
}
},
"node_modules/@esbuild/linux-ia32": {
- "version": "0.25.12",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz",
- "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==",
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz",
+ "integrity": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==",
"cpu": [
"ia32"
],
@@ -1161,9 +1160,9 @@
}
},
"node_modules/@esbuild/linux-loong64": {
- "version": "0.25.12",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz",
- "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==",
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.1.tgz",
+ "integrity": "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==",
"cpu": [
"loong64"
],
@@ -1178,9 +1177,9 @@
}
},
"node_modules/@esbuild/linux-mips64el": {
- "version": "0.25.12",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz",
- "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==",
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz",
+ "integrity": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==",
"cpu": [
"mips64el"
],
@@ -1195,9 +1194,9 @@
}
},
"node_modules/@esbuild/linux-ppc64": {
- "version": "0.25.12",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz",
- "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==",
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz",
+ "integrity": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==",
"cpu": [
"ppc64"
],
@@ -1212,9 +1211,9 @@
}
},
"node_modules/@esbuild/linux-riscv64": {
- "version": "0.25.12",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz",
- "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==",
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz",
+ "integrity": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==",
"cpu": [
"riscv64"
],
@@ -1229,9 +1228,9 @@
}
},
"node_modules/@esbuild/linux-s390x": {
- "version": "0.25.12",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz",
- "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==",
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.1.tgz",
+ "integrity": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==",
"cpu": [
"s390x"
],
@@ -1246,9 +1245,9 @@
}
},
"node_modules/@esbuild/linux-x64": {
- "version": "0.25.12",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz",
- "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==",
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.1.tgz",
+ "integrity": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==",
"cpu": [
"x64"
],
@@ -1263,9 +1262,9 @@
}
},
"node_modules/@esbuild/netbsd-arm64": {
- "version": "0.25.12",
- "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz",
- "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==",
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz",
+ "integrity": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==",
"cpu": [
"arm64"
],
@@ -1280,9 +1279,9 @@
}
},
"node_modules/@esbuild/netbsd-x64": {
- "version": "0.25.12",
- "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz",
- "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==",
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz",
+ "integrity": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==",
"cpu": [
"x64"
],
@@ -1297,9 +1296,9 @@
}
},
"node_modules/@esbuild/openbsd-arm64": {
- "version": "0.25.12",
- "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz",
- "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==",
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz",
+ "integrity": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==",
"cpu": [
"arm64"
],
@@ -1314,9 +1313,9 @@
}
},
"node_modules/@esbuild/openbsd-x64": {
- "version": "0.25.12",
- "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz",
- "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==",
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.1.tgz",
+ "integrity": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==",
"cpu": [
"x64"
],
@@ -1331,9 +1330,9 @@
}
},
"node_modules/@esbuild/openharmony-arm64": {
- "version": "0.25.12",
- "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz",
- "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==",
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz",
+ "integrity": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==",
"cpu": [
"arm64"
],
@@ -1348,9 +1347,9 @@
}
},
"node_modules/@esbuild/sunos-x64": {
- "version": "0.25.12",
- "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz",
- "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==",
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz",
+ "integrity": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==",
"cpu": [
"x64"
],
@@ -1365,9 +1364,9 @@
}
},
"node_modules/@esbuild/win32-arm64": {
- "version": "0.25.12",
- "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz",
- "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==",
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz",
+ "integrity": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==",
"cpu": [
"arm64"
],
@@ -1382,9 +1381,9 @@
}
},
"node_modules/@esbuild/win32-ia32": {
- "version": "0.25.12",
- "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz",
- "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==",
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz",
+ "integrity": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==",
"cpu": [
"ia32"
],
@@ -1399,9 +1398,9 @@
}
},
"node_modules/@esbuild/win32-x64": {
- "version": "0.25.12",
- "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz",
- "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==",
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.1.tgz",
+ "integrity": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==",
"cpu": [
"x64"
],
@@ -6108,9 +6107,9 @@
"license": "MIT"
},
"node_modules/esbuild": {
- "version": "0.25.12",
- "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz",
- "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==",
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz",
+ "integrity": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
@@ -6121,32 +6120,32 @@
"node": ">=18"
},
"optionalDependencies": {
- "@esbuild/aix-ppc64": "0.25.12",
- "@esbuild/android-arm": "0.25.12",
- "@esbuild/android-arm64": "0.25.12",
- "@esbuild/android-x64": "0.25.12",
- "@esbuild/darwin-arm64": "0.25.12",
- "@esbuild/darwin-x64": "0.25.12",
- "@esbuild/freebsd-arm64": "0.25.12",
- "@esbuild/freebsd-x64": "0.25.12",
- "@esbuild/linux-arm": "0.25.12",
- "@esbuild/linux-arm64": "0.25.12",
- "@esbuild/linux-ia32": "0.25.12",
- "@esbuild/linux-loong64": "0.25.12",
- "@esbuild/linux-mips64el": "0.25.12",
- "@esbuild/linux-ppc64": "0.25.12",
- "@esbuild/linux-riscv64": "0.25.12",
- "@esbuild/linux-s390x": "0.25.12",
- "@esbuild/linux-x64": "0.25.12",
- "@esbuild/netbsd-arm64": "0.25.12",
- "@esbuild/netbsd-x64": "0.25.12",
- "@esbuild/openbsd-arm64": "0.25.12",
- "@esbuild/openbsd-x64": "0.25.12",
- "@esbuild/openharmony-arm64": "0.25.12",
- "@esbuild/sunos-x64": "0.25.12",
- "@esbuild/win32-arm64": "0.25.12",
- "@esbuild/win32-ia32": "0.25.12",
- "@esbuild/win32-x64": "0.25.12"
+ "@esbuild/aix-ppc64": "0.28.1",
+ "@esbuild/android-arm": "0.28.1",
+ "@esbuild/android-arm64": "0.28.1",
+ "@esbuild/android-x64": "0.28.1",
+ "@esbuild/darwin-arm64": "0.28.1",
+ "@esbuild/darwin-x64": "0.28.1",
+ "@esbuild/freebsd-arm64": "0.28.1",
+ "@esbuild/freebsd-x64": "0.28.1",
+ "@esbuild/linux-arm": "0.28.1",
+ "@esbuild/linux-arm64": "0.28.1",
+ "@esbuild/linux-ia32": "0.28.1",
+ "@esbuild/linux-loong64": "0.28.1",
+ "@esbuild/linux-mips64el": "0.28.1",
+ "@esbuild/linux-ppc64": "0.28.1",
+ "@esbuild/linux-riscv64": "0.28.1",
+ "@esbuild/linux-s390x": "0.28.1",
+ "@esbuild/linux-x64": "0.28.1",
+ "@esbuild/netbsd-arm64": "0.28.1",
+ "@esbuild/netbsd-x64": "0.28.1",
+ "@esbuild/openbsd-arm64": "0.28.1",
+ "@esbuild/openbsd-x64": "0.28.1",
+ "@esbuild/openharmony-arm64": "0.28.1",
+ "@esbuild/sunos-x64": "0.28.1",
+ "@esbuild/win32-arm64": "0.28.1",
+ "@esbuild/win32-ia32": "0.28.1",
+ "@esbuild/win32-x64": "0.28.1"
}
},
"node_modules/escalade": {
@@ -8172,12 +8171,6 @@
"dev": true,
"license": "MIT"
},
- "node_modules/pell": {
- "version": "1.0.6",
- "resolved": "https://registry.npmjs.org/pell/-/pell-1.0.6.tgz",
- "integrity": "sha512-wuackvgjFCHmVABy7ACSmY2u53w+TlYFrVL2hN6V3rGL/iWWwVXMw2uphpZSXNnqFQTI8nMpD3UNNX8+R4BAYw==",
- "license": "MIT"
- },
"node_modules/picocolors": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
diff --git a/desktop/package.json b/desktop/package.json
index 7b4795a1..1ebbe072 100644
--- a/desktop/package.json
+++ b/desktop/package.json
@@ -68,6 +68,7 @@
"vitest": "^4.1.0"
},
"overrides": {
- "dompurify": "^3.4.0"
+ "dompurify": "^3.4.0",
+ "esbuild": "^0.28.1"
}
}
diff --git a/desktop/vite.config.ts b/desktop/vite.config.ts
index 68fd24e2..33a61bc5 100644
--- a/desktop/vite.config.ts
+++ b/desktop/vite.config.ts
@@ -21,6 +21,7 @@ export default defineConfig({
alias: { "@": path.resolve(__dirname, "src") },
},
build: {
+ target: "es2022",
outDir: "../static/desktop",
emptyOutDir: true,
// CodeMirror + mathjs + lucide each ship genuinely large libraries
From a7114181fac7773020e65c7f85c8dcb167fd3297 Mon Sep 17 00:00:00 2001
From: jaylfc
Date: Sat, 13 Jun 2026 17:35:50 +0100
Subject: [PATCH 05/57] feat(agent): unify the taOS Agent chat composer
(attach/screenshot inside the input) (#850)
The attach and screenshot buttons sat as detached siblings to the left of a
tall textarea, sinking to the bottom-left and reading as out of place. Wrap
them into one rounded composer (chat-app convention): attach + screenshot as
borderless ghost icons inside on the left, the field borderless in the middle,
a filled accent send button on the right, bottom-aligned so the icons sit on
the last line as the field grows. Shell tokens only, so it flips for the Light
theme; switched the send glyph to an up-arrow.
---
desktop/src/components/TaosAssistantPanel.tsx | 28 +++++++++++--------
1 file changed, 16 insertions(+), 12 deletions(-)
diff --git a/desktop/src/components/TaosAssistantPanel.tsx b/desktop/src/components/TaosAssistantPanel.tsx
index 5f9ba499..a8797056 100644
--- a/desktop/src/components/TaosAssistantPanel.tsx
+++ b/desktop/src/components/TaosAssistantPanel.tsx
@@ -1,6 +1,6 @@
import { useEffect, useRef, useState, useCallback } from "react";
import {
- Send,
+ ArrowUp,
Sparkles,
X,
Settings,
@@ -332,7 +332,11 @@ export function TaosAssistantPanelInner({ embedded = false }: { embedded?: boole
)}
-
+ {/* One unified composer: attach + screenshot inside on the left, the
+ field borderless in the middle, send on the right. Bottom-aligned so
+ the icons sit on the last line as the textarea grows (chat-app
+ convention). Shell tokens only, so it flips for the Light theme. */}
+
-
Cmd+Enter to send
+
Cmd+Enter to send
{/* Settings modal */}
@@ -440,9 +444,9 @@ function SendButton({ onClick, disabled }: { onClick: () => void; disabled: bool
onClick={onClick}
disabled={disabled}
aria-label="Send message"
- className="p-2.5 rounded-lg bg-accent text-white hover:bg-accent/90 transition-colors disabled:opacity-40 disabled:cursor-not-allowed shrink-0"
+ className="grid place-items-center h-8 w-8 rounded-xl bg-accent text-white hover:brightness-110 transition disabled:opacity-30 disabled:cursor-not-allowed shrink-0"
>
-
+
);
}
@@ -454,9 +458,9 @@ function AttachButton({ onClick, disabled }: { onClick: () => void; disabled: bo
disabled={disabled}
aria-label="Upload file"
title="Attach file"
- className="p-2 rounded-lg border border-white/10 hover:bg-shell-surface-hover transition-colors text-shell-text-secondary shrink-0 disabled:opacity-40"
+ className="grid place-items-center h-8 w-8 rounded-lg text-shell-text-tertiary hover:bg-shell-surface-hover hover:text-shell-text-secondary transition-colors shrink-0 disabled:opacity-40"
>
-
+
);
}
@@ -468,9 +472,9 @@ function ScreenshotButton({ onClick, disabled }: { onClick: () => void; disabled
disabled={disabled}
aria-label="Take screenshot"
title="Take screenshot"
- className="p-2 rounded-lg border border-white/10 hover:bg-shell-surface-hover transition-colors text-shell-text-secondary shrink-0 disabled:opacity-40"
+ className="grid place-items-center h-8 w-8 rounded-lg text-shell-text-tertiary hover:bg-shell-surface-hover hover:text-shell-text-secondary transition-colors shrink-0 disabled:opacity-40"
>
-
+
);
}
From 51ae91db400e25bc4b315d921f89e6bfd1b9c28d Mon Sep 17 00:00:00 2001
From: jaylfc
Date: Sat, 13 Jun 2026 17:37:23 +0100
Subject: [PATCH 06/57] fix(update): reset desktop/package-lock.json before the
ff-only pull (Install Update 500) (#852)
* fix(update): reset desktop/package-lock.json before the ff-only pull
The desktop rebuild runs npm install, which rewrites desktop/package-lock.json
and leaves it dirty. When an incoming update also touches that file (e.g. the
esbuild bump), git pull --ff-only refuses to overwrite the local change and the
Install Update button 500s. The endpoint already restored tsbuildinfo and
static/desktop before pulling; add package-lock.json to that reset so updates
keep working across rebuilds.
* docs(update-log): record the package-lock.json Install Update 500 (#852)
---
docs/UPDATE_BREAKAGE_LOG.md | 23 +++++++++++++++++++++++
tinyagentos/routes/settings.py | 18 +++++++++++-------
2 files changed, 34 insertions(+), 7 deletions(-)
diff --git a/docs/UPDATE_BREAKAGE_LOG.md b/docs/UPDATE_BREAKAGE_LOG.md
index 7e27bb8a..e11db548 100644
--- a/docs/UPDATE_BREAKAGE_LOG.md
+++ b/docs/UPDATE_BREAKAGE_LOG.md
@@ -15,6 +15,29 @@ Format per entry: date, change (PR), affected installs, symptom, check, fix.
---
+## 2026-06-13 — Install Update 500s when desktop/package-lock.json is dirty (#852)
+
+- **Affected:** any install whose desktop rebuild has run `npm install` (which
+ rewrites `desktop/package-lock.json`), AND where an incoming update also
+ changes that file. The esbuild bump (#849) changed package-lock.json, so
+ installs updating across that commit are the trigger.
+- **Symptom:** the update *check* shows updates available, but clicking Install
+ Update fails: `POST /api/settings/update` returns 500 with
+ `Update failed: ... Your local changes to desktop/package-lock.json would be
+ overwritten by merge`. No real divergence or conflict, just a rebuild-dirtied
+ lockfile blocking `git pull --ff-only`.
+- **Check:** `sudo -u taos git -C /opt/tinyagentos status --short` shows
+ `M desktop/package-lock.json`.
+- **Fix (in #852):** the apply-update endpoint now restores
+ `desktop/package-lock.json` (alongside `tsconfig.tsbuildinfo` + `static/desktop`)
+ before the pull, so it never blocks. No user action needed once on a build
+ with #852.
+- **Manual unblock on an install stuck before #852:**
+ `sudo -u taos git -C /opt/tinyagentos checkout -- desktop/package-lock.json`
+ then click Install Update again.
+
+---
+
## 2026-06-12 — LiteLLM host port default moved 4000 -> 7834 (#795, pinned in #805)
- **Affected:** existing installs that have no `litellm_port` key under `server:`
diff --git a/tinyagentos/routes/settings.py b/tinyagentos/routes/settings.py
index f8f539c7..6fa6c74d 100644
--- a/tinyagentos/routes/settings.py
+++ b/tinyagentos/routes/settings.py
@@ -699,14 +699,18 @@ async def apply_update(request: Request):
import asyncio
project_dir = Path(__file__).parent.parent.parent
- # systemd ExecStartPre rebuilds produce new content-hashed files in
- # static/desktop/assets/ and modify desktop/tsconfig.tsbuildinfo on each
- # restart, leaving the tree dirty. git pull --ff-only then refuses to
- # overwrite the locals and the Install Update button always 500s. Wipe
- # build outputs first — git pull restores them or the rebuild below
- # regenerates them.
+ # The desktop rebuild leaves the tree dirty in three ways, and a dirty
+ # tracked file makes the next git pull --ff-only refuse to overwrite the
+ # local and the Install Update button 500s:
+ # - static/desktop/assets/* : new content-hashed bundle files (npm build)
+ # - desktop/tsconfig.tsbuildinfo : touched on every tsc build
+ # - desktop/package-lock.json : npm install rewrites it (esp. when an
+ # incoming update also changes it, e.g. the esbuild bump in #849)
+ # Restore all of them before pulling; the pull restores the committed
+ # versions or the rebuild below regenerates the build outputs.
reset_proc = await asyncio.create_subprocess_exec(
- "git", "checkout", "--", "desktop/tsconfig.tsbuildinfo", "static/desktop",
+ "git", "checkout", "--",
+ "desktop/tsconfig.tsbuildinfo", "desktop/package-lock.json", "static/desktop",
stdout=asyncio.subprocess.DEVNULL, stderr=asyncio.subprocess.DEVNULL,
cwd=str(project_dir),
)
From 3679e94fe7d00ef74590386d7a0e83f624f7b2b8 Mon Sep 17 00:00:00 2001
From: jaylfc
Date: Sat, 13 Jun 2026 17:53:13 +0100
Subject: [PATCH 07/57] feat(agents): redesign the Agents app to Apple-grade
cards (#851)
* feat(agents): redesign the Agents app to Apple-grade cards (identity tile, live status, refined states)
* feat(agents): show the system agent in the standard layout with a model indicator line
The taOS system agent baked its model into the display name (taOS agent - ).
Render it like any standard agent instead (name 'taOS agent', framework 'opencode'
sub-label) and move the model to a new indicator line (extensible for future
indicators) via an optional Agent.model field + an IndicatorRow on the card.
* feat(agents): drop the System pill from the system agent card
Keep the subtle elevated ring/tint, just remove the 'System' chip per design
review. Update the AgentRow test to assert protected hides destructive actions
without the chip.
* fix(agents): don't repeat the host as the sub-label for framework-less agents
For none/generic-framework agents the sub-label fell back to the host, which is
already shown in the metadata column, so the host appeared twice on the card
(Gitar review). Omit the sub-label when there's no framework instead.
---
desktop/src/apps/AgentsApp.tsx | 52 ++-
desktop/src/apps/agents/AgentRow.test.tsx | 93 ++++++
desktop/src/apps/agents/AgentRow.tsx | 367 +++++++++++++---------
desktop/src/apps/agents/types.ts | 2 +
desktop/src/theme/tokens.css | 45 +++
5 files changed, 399 insertions(+), 160 deletions(-)
create mode 100644 desktop/src/apps/agents/AgentRow.test.tsx
diff --git a/desktop/src/apps/AgentsApp.tsx b/desktop/src/apps/AgentsApp.tsx
index dcbe50ec..e08dfeba 100644
--- a/desktop/src/apps/AgentsApp.tsx
+++ b/desktop/src/apps/AgentsApp.tsx
@@ -35,6 +35,25 @@ const TAOS_AGENT_STUB: Agent = {
paused: false,
};
+/** Placeholder card shown while the agent list is loading. Matches the
+ * AgentRow shape (identity tile + two-line stack + trailing metric) with a
+ * shimmer that respects prefers-reduced-motion (see tokens.css). */
+function AgentRowSkeleton() {
+ return (
+
+ );
+}
+
export function AgentsApp({ windowId: _windowId }: { windowId: string }) {
const [agents, setAgents] = useState([]);
const [archived, setArchived] = useState([]);
@@ -45,9 +64,9 @@ export function AgentsApp({ windowId: _windowId }: { windowId: string }) {
const [diskStates, setDiskStates] = useState>({});
const [quotaErrors, setQuotaErrors] = useState>({});
const [latestByFramework, setLatestByFramework] = useState>({});
- // Hydrate the taOS agent stub with live model info (display only — the detail
- // panel fetches its own config on open). We only use this to show the current
- // model in the row's framework pill area; failures are silently ignored.
+ // Hydrate the taOS agent stub with live model info (display only; the detail
+ // panel fetches its own config on open). Shown as the model indicator line on
+ // the system agent's card; failures are silently ignored.
const [taosModel, setTaosModel] = useState(undefined);
const isMobile = useIsMobile();
const openWindow = useProcessStore((s) => s.openWindow);
@@ -469,15 +488,17 @@ export function AgentsApp({ windowId: _windowId }: { windowId: string }) {
{/* Content */}
{loading ? (
-
- Loading agents...
+
+ {[0, 1, 2].map((i) => (
+
+ ))}
) : agents.length === 0 && archived.length === 0 ? (
System agent
setTaosDetailOpen(true)}
@@ -488,23 +509,22 @@ export function AgentsApp({ windowId: _windowId }: { windowId: string }) {
protected
/>
-
-
-
+
+
+
-
No agents deployed yet
-
Deploy your first AI agent to start automating tasks on your device.
+
No agents deployed yet
+
Deploy your first AI agent to start automating tasks on your device.
setWizardOpen(true)}
className="text-white shadow-lg hover:shadow-xl hover:-translate-y-0.5 hover:brightness-110 border-0 mt-1"
style={{ background: "linear-gradient(135deg, #8b92a3, #5b6170)" }}
+ aria-label="Deploy agent"
>
- Deploy your first agent
+ Deploy Agent
@@ -512,7 +532,7 @@ export function AgentsApp({ windowId: _windowId }: { windowId: string }) {
System agent
setTaosDetailOpen(true)}
@@ -534,7 +554,7 @@ export function AgentsApp({ windowId: _windowId }: { windowId: string }) {
{/* System agent — always shown above the deployed agents list */}
System agent
setTaosDetailOpen(true)}
diff --git a/desktop/src/apps/agents/AgentRow.test.tsx b/desktop/src/apps/agents/AgentRow.test.tsx
new file mode 100644
index 00000000..19aafe6c
--- /dev/null
+++ b/desktop/src/apps/agents/AgentRow.test.tsx
@@ -0,0 +1,93 @@
+import { render, screen } from "@testing-library/react";
+import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
+import { AgentRow } from "./AgentRow";
+import * as useIsMobileModule from "@/hooks/use-is-mobile";
+import { type Agent } from "./types";
+
+vi.mock("@/hooks/use-is-mobile");
+
+const baseAgent: Agent = {
+ name: "scout",
+ display_name: "Scout",
+ host: "localhost",
+ color: "#3b82f6",
+ emoji: "🤖",
+ status: "running",
+ vectors: 1234,
+ framework: "openclaw",
+ paused: false,
+};
+
+function renderRow(overrides: Partial = {}, props: Partial[0]> = {}) {
+ return render(
+ ,
+ );
+}
+
+describe("AgentRow", () => {
+ beforeEach(() => {
+ vi.mocked(useIsMobileModule.useIsMobile).mockReturnValue(false);
+ });
+ afterEach(() => {
+ vi.clearAllMocks();
+ });
+
+ it("shows the agent name and a running status", () => {
+ renderRow();
+ expect(screen.getByText("Scout")).toBeInTheDocument();
+ expect(screen.getByLabelText("Status: Running")).toBeInTheDocument();
+ });
+
+ it("exposes management actions with aria-labels while running", () => {
+ renderRow();
+ expect(screen.getByRole("button", { name: "View logs for scout" })).toBeEnabled();
+ expect(screen.getByRole("button", { name: "Manage skills for scout" })).toBeEnabled();
+ expect(screen.getByRole("button", { name: "View messages for scout" })).toBeEnabled();
+ expect(screen.getByRole("button", { name: "Delete scout" })).toBeInTheDocument();
+ });
+
+ it("disables management actions when the agent is not running", () => {
+ renderRow({ status: "stopped" });
+ expect(
+ screen.getByRole("button", { name: "View logs for scout (Agent is not running)" }),
+ ).toBeDisabled();
+ expect(screen.getByLabelText("Status: Stopped")).toBeInTheDocument();
+ });
+
+ it("hides destructive actions when protected", () => {
+ renderRow({}, { protected: true });
+ expect(screen.queryByRole("button", { name: "Delete scout" })).toBeNull();
+ });
+
+ it("shows a resume action and Paused status when paused", () => {
+ renderRow({ paused: true });
+ expect(screen.getByRole("button", { name: "Resume scout" })).toBeInTheDocument();
+ expect(screen.getByLabelText("Status: Paused")).toBeInTheDocument();
+ });
+
+ it("surfaces the framework-update indicator", () => {
+ render(
+ ,
+ );
+ expect(screen.getByLabelText("framework update available")).toBeInTheDocument();
+ });
+});
diff --git a/desktop/src/apps/agents/AgentRow.tsx b/desktop/src/apps/agents/AgentRow.tsx
index db807255..ba79496c 100644
--- a/desktop/src/apps/agents/AgentRow.tsx
+++ b/desktop/src/apps/agents/AgentRow.tsx
@@ -1,16 +1,128 @@
import { type ReactNode } from "react";
import { useIsMobile } from "@/hooks/use-is-mobile";
-import { ScrollText, Trash2, Server, Wrench, MessageSquare, PauseCircle, RotateCcw, HardDrive } from "lucide-react";
+import { ScrollText, Trash2, Server, Wrench, MessageSquare, PauseCircle, RotateCcw, HardDrive, Database, Cpu } from "lucide-react";
import { LatestVersion } from "@/lib/framework-api";
import { resolveAgentEmoji } from "@/lib/agent-emoji";
import { Button, Card } from "@/components/ui";
import { type Agent, type DiskState } from "./types";
-import { STATUS_STYLES } from "./constants";
/* ------------------------------------------------------------------ */
/* AgentRow */
/* ------------------------------------------------------------------ */
+type AgentStatus = Agent["status"];
+
+/** Semantic colour + copy for the status indicator dot. Colour here carries
+ * meaning (running/paused/error), so Tailwind colour utilities are allowed,
+ * kept at tasteful alpha so they read in both the dark and light themes. */
+type StatusMeta = { label: string; dot: string; text: string; pulse: boolean };
+
+const STATUS_STOPPED: StatusMeta = { label: "Stopped", dot: "bg-shell-text-tertiary", text: "text-shell-text-tertiary", pulse: false };
+const STATUS_META: Record = {
+ running: { label: "Running", dot: "bg-emerald-400", text: "text-emerald-400", pulse: true },
+ stopped: STATUS_STOPPED,
+ error: { label: "Error", dot: "bg-red-400", text: "text-red-400", pulse: false },
+ deploying: { label: "Deploying", dot: "bg-amber-400", text: "text-amber-400", pulse: false },
+};
+
+/** A small dot + label conveying live agent state. The running dot breathes
+ * via a halo that is disabled under prefers-reduced-motion (see tokens.css). */
+function StatusIndicator({ status, paused }: { status: AgentStatus; paused?: boolean }) {
+ // A paused agent reads as paused regardless of its container status.
+ const meta: StatusMeta = paused
+ ? { label: "Paused", dot: "bg-amber-400", text: "text-amber-400", pulse: false }
+ : STATUS_META[status] ?? STATUS_STOPPED;
+ return (
+
+
+ {meta.pulse && (
+
+ )}
+
+
+ {meta.label}
+
+ );
+}
+
+/** The agent-identity tile: a colour-tinted rounded square holding the emoji. */
+function IdentityTile({ color, emoji, size = 36 }: { color: string; emoji: string; size?: number }) {
+ return (
+
+ {emoji}
+
+ );
+}
+
+function PausedChip() {
+ return (
+
+
+ paused
+
+ );
+}
+
+/** A single indicator chip (icon + value) for the indicators line. */
+function IndicatorChip({ icon, value, title }: { icon: ReactNode; value: string; title: string }) {
+ return (
+
+ {icon}
+ {value}
+
+ );
+}
+
+/** The indicators line under an agent's identity: model now, room for more
+ * (memory, region, ...) later. Renders nothing when there is nothing to show. */
+function IndicatorRow({ agent }: { agent: Agent }) {
+ if (!agent.model) return null;
+ return (
+
+ }
+ value={agent.model}
+ title={`Model: ${agent.model}`}
+ />
+
+ );
+}
+
+function DiskChip({ diskState, verbose }: { diskState: DiskState; verbose?: boolean }) {
+ const isHard = diskState.state === "hard";
+ return (
+
+
+ {diskState.used_gib}/{diskState.quota_gib} GiB{verbose ? ` (${diskState.percent}%)` : ""}
+
+ );
+}
+
export function AgentRow({
agent,
diskState,
@@ -42,38 +154,42 @@ export function AgentRow({
agent.framework_version_sha &&
latestForAgent &&
latestForAgent.sha !== agent.framework_version_sha;
- // The framework an agent runs on (openclaw, hermes, …). The emoji alone is
- // ambiguous, so surface the name as a small pill. "none"/"generic" agents
- // have no meaningful framework to show.
+ // The framework an agent runs on (openclaw, hermes, ...). The emoji alone is
+ // ambiguous, so surface the name as the identity sub-label. "none"/"generic"
+ // agents have no meaningful framework, so the sub-label is simply omitted
+ // (the host already has its own metadata slot, so don't repeat it here).
const frameworkLabel =
agent.framework && !["none", "generic"].includes(agent.framework)
? agent.framework
: null;
- const FrameworkPill = frameworkLabel ? (
-
- {frameworkLabel}
-
- ) : null;
+ const subLabel = frameworkLabel;
- const btnCls = isMobile ? "h-11 w-11" : "h-8 w-8";
- // Only allow management actions while the agent is running
+ // Only allow management actions while the agent is running.
const running = agent.status === "running";
const disabledCls = running
- ? "hover:bg-shell-surface-hover"
+ ? "hover:bg-shell-surface-hover hover:text-shell-text"
: "opacity-40 cursor-not-allowed";
const disabledAria = running ? undefined : "Agent is not running";
+ // Icon-button base: quiet by default, brighten on hover. >=44px tap target
+ // on mobile, compact on desktop.
+ const btnCls = `${isMobile ? "h-11 w-11" : "h-8 w-8"} text-shell-text-tertiary`;
+
+ const updateDot = updateAvailable ? (
+
+ ) : null;
+
const actionButtons = (
<>
{!isProtected && agent.paused && (
onResume(agent.name)}
aria-label={`Resume ${agent.name}`}
title="Resume agent"
@@ -118,7 +234,7 @@ export function AgentRow({
onDelete(agent.name)}
aria-label={`Delete ${agent.name}`}
title="Delete"
@@ -129,149 +245,112 @@ export function AgentRow({
>
);
+ // Shared card surface: one radius, token colours, tactile press + focus ring.
+ // The "System" agent gets a faintly elevated ring + tint.
+ const cardCls = [
+ "taos-card-enter group rounded-xl bg-shell-surface shadow-[var(--shadow-card)]",
+ "transition-[background-color,box-shadow,transform] duration-200",
+ "hover:bg-shell-surface-hover hover:shadow-[var(--shadow-card-hover)]",
+ "active:translate-y-px",
+ "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent/50",
+ isProtected
+ ? "border border-shell-border-strong bg-[color-mix(in_srgb,var(--color-accent)_6%,transparent)]"
+ : "border border-shell-border",
+ ].join(" ");
+
+
if (isMobile) {
return (
-
- {/* Row 1: identity + status chip */}
-
-
-
- {emoji}
-
-
- {agent.display_name || agent.name}
-
- {updateAvailable && (
-
- )}
-
- {agent.status}
-
+
+ {/* Row 1: identity tile + name/sub-label + status */}
+
+
+
+
+
+ {agent.display_name || agent.name}
+
+ {updateDot}
+
+ {subLabel && (
+
+ {subLabel}
+
+ )}
+
+
+
- {/* Row 2: host + vectors + optional chips */}
-
-
-
{agent.host}
- {FrameworkPill}
-
- {agent.vectors.toLocaleString()} vectors
+ {/* Row 2: host + vectors */}
+
+
+
+ {agent.host}
+
+
+
+ {agent.vectors.toLocaleString()}
+ vectors
{(agent.paused || (diskState && diskState.state !== "ok")) && (
-
- {agent.paused && (
-
-
- paused
-
- )}
- {diskState && diskState.state !== "ok" && (
-
-
- {diskState.used_gib}/{diskState.quota_gib} GiB
-
- )}
+
+ {agent.paused &&
}
+ {diskState && diskState.state !== "ok" &&
}
)}
{/* Row 3: action buttons */}
-
-
- {leftActions}
-
-
- {actionButtons}
-
+
+
{leftActions}
+
{actionButtons}
);
}
return (
-
-
-
-
- {emoji}
-
-
{agent.display_name || agent.name}
- {FrameworkPill}
- {updateAvailable && (
-
- )}
- {agent.paused && (
-
-
- paused
-
- )}
- {diskState && diskState.state !== "ok" && (
-
-
- {diskState.used_gib}/{diskState.quota_gib} GiB ({diskState.percent}%)
-
- )}
+
+ {/* Identity: tile + two-line name / sub-label */}
+
+
+
+
+
+ {agent.display_name || agent.name}
+
+ {updateDot}
+
+ {subLabel && (
+
+ {subLabel}
+
+ )}
+
+
+ {agent.paused &&
}
+ {diskState && diskState.state !== "ok" &&
}
-
-
-
{agent.host}
+
+ {/* Metadata: host */}
+
+
+ {agent.host}
-
- {agent.status}
-
-
- {agent.vectors.toLocaleString()}
+
+ {/* Status */}
+
+
+ {/* Vectors metric (de-emphasized) */}
+
+
+
+ {agent.vectors.toLocaleString()}
+
+ vectors
-
+
+ {/* Actions */}
+
{leftActions}
{actionButtons}
diff --git a/desktop/src/apps/agents/types.ts b/desktop/src/apps/agents/types.ts
index 85eb8d30..60326975 100644
--- a/desktop/src/apps/agents/types.ts
+++ b/desktop/src/apps/agents/types.ts
@@ -13,6 +13,8 @@ export interface Agent {
status: "running" | "stopped" | "error" | "deploying";
vectors: number;
framework?: string;
+ /** Current chat model, shown as an indicator line on the card when known. */
+ model?: string;
paused?: boolean;
on_worker_failure?: "pause" | "fallback" | "escalate-immediately";
fallback_models?: string[];
diff --git a/desktop/src/theme/tokens.css b/desktop/src/theme/tokens.css
index 2232bc2c..ef21b35c 100644
--- a/desktop/src/theme/tokens.css
+++ b/desktop/src/theme/tokens.css
@@ -141,6 +141,51 @@
transition: outline 0.2s ease-out;
}
+/* Agents app — gentle breathing pulse for the running status dot.
+ Scales/fades a halo behind the dot so it reads as "live" without the
+ jarring full-opacity blink of animate-pulse. Disabled under
+ prefers-reduced-motion. */
+@keyframes taos-status-pulse {
+ 0%, 100% { transform: scale(1); opacity: 0.55; }
+ 50% { transform: scale(1.9); opacity: 0; }
+}
+.taos-status-pulse {
+ animation: taos-status-pulse 2.4s ease-in-out infinite;
+}
+
+/* Agents app — skeleton shimmer for loading cards. */
+@keyframes taos-shimmer {
+ 0% { background-position: -200% 0; }
+ 100% { background-position: 200% 0; }
+}
+.taos-shimmer {
+ background-image: linear-gradient(
+ 90deg,
+ transparent 0%,
+ rgba(255, 255, 255, 0.06) 50%,
+ transparent 100%
+ );
+ background-size: 200% 100%;
+ animation: taos-shimmer 1.6s ease-in-out infinite;
+}
+
+/* Agents app — subtle card entrance (mount fade + lift). */
+@keyframes taos-card-enter {
+ from { opacity: 0; transform: translateY(4px); }
+ to { opacity: 1; transform: translateY(0); }
+}
+.taos-card-enter {
+ animation: taos-card-enter 0.24s ease-out both;
+}
+
+@media (prefers-reduced-motion: reduce) {
+ .taos-status-pulse,
+ .taos-shimmer,
+ .taos-card-enter {
+ animation: none;
+ }
+}
+
/* Projects board — Apple-grade chrome (Pass B Kanban) */
:root {
--board-accent-violet: #a78bfa;
From cb0ee722cbef472091d74dad8972f7bdc737ebb1 Mon Sep 17 00:00:00 2001
From: jaylfc
Date: Sat, 13 Jun 2026 17:53:15 +0100
Subject: [PATCH 08/57] feat(chat): Slack-polish the message rows (avatar
gutter, grouped follow-ups, refined reactions/threads) (#853)
* feat(chat): Slack-polish the message rows (avatar gutter, grouped follow-ups, refined reactions/threads)
* fix(chat): use a single sky shade for the agent accent (drop OS-keyed dark: variants)
taOS themes via data-scheme + tokens, not Tailwind's dark: class, so dark:
keyed off the OS preference rather than the active taOS theme. Use one sky
shade legible on both the dark and light theme backgrounds instead.
---
desktop/src/apps/MessagesApp.tsx | 108 ++++++++++++------
desktop/src/apps/chat/MessageAvatar.tsx | 99 ++++++++++++++++
desktop/src/apps/chat/MessageHoverActions.tsx | 8 +-
3 files changed, 173 insertions(+), 42 deletions(-)
create mode 100644 desktop/src/apps/chat/MessageAvatar.tsx
diff --git a/desktop/src/apps/MessagesApp.tsx b/desktop/src/apps/MessagesApp.tsx
index d31653c5..271ef501 100644
--- a/desktop/src/apps/MessagesApp.tsx
+++ b/desktop/src/apps/MessagesApp.tsx
@@ -37,6 +37,7 @@ import { useVisualViewport } from "@/hooks/use-visual-viewport";
import { useDropTarget } from "@/shell/dnd/use-drop-target";
import { startDrag, endDrag } from "@/shell/dnd/dnd-bus";
import { resolveAgentEmoji } from "@/lib/agent-emoji";
+import { MessageAvatar } from "./chat/MessageAvatar";
import { ChannelSettingsPanel } from "./chat/ChannelSettingsPanel";
import { AgentContextMenu } from "./chat/AgentContextMenu";
import { SlashMenu, type SlashCommandsBySlug } from "./chat/SlashMenu";
@@ -2122,12 +2123,46 @@ export function MessagesApp({
)}
setHoveredMessageId(msg.id)}
onMouseLeave={() => setHoveredMessageId((id) => id === msg.id ? null : id)}
>
+ {/* avatar gutter */}
+
{
+ if (msg.author_type !== "agent") return;
+ e.preventDefault();
+ setContextMenu({ slug: msg.author_id, x: e.clientX, y: e.clientY });
+ }}
+ >
+ {showAuthor ? (
+ (() => {
+ const agent = isAgent ? liveAgents.find((a) => a.name === msg.author_id) : undefined;
+ return (
+
+ );
+ })()
+ ) : (
+
+ {new Date(toMs(msg.created_at)).toLocaleTimeString(undefined, { hour: "2-digit", minute: "2-digit" })}
+
+ )}
+
+ {/* content column */}
+
{showAuthor && (
- {isAgent && !isDeadAgent && (() => {
- const agent = liveAgents.find((a) => a.name === msg.author_id);
- if (!agent) return null;
- return (
-
- {resolveAgentEmoji(agent.emoji, agent.framework)}
-
- );
- })()}
{isAgent && !isDeadAgent && (
-
+
Agent
)}
{isDeadAgent && (
-
+
{authorState === "archived" ? "inactive" : "removed"}
)}
{relativeTime(msg.created_at, nowMs)}
- {msg.edited_at && (edited) }
+ {msg.edited_at && (edited) }
)}
{msg.deleted_at ? (
@@ -2189,10 +2212,10 @@ export function MessagesApp({
onCancel={() => setEditingMessageId(null)}
/>
) : (
-
+
{renderContent(msg.content)}
{msg.state === "pending" && (
-
...
+
...
)}
{msg.state === "streaming" && (
@@ -2223,7 +2246,7 @@ export function MessagesApp({
const url = msg.metadata?.canvas_url ?? `/canvas/${msg.metadata?.canvas_id}`;
setViewingCanvas({ url, title: msg.metadata?.canvas_title as string | undefined });
}}
- className="h-7 px-2.5 text-[12px] gap-1.5 bg-white/[0.04] border-white/10 hover:bg-white/[0.08]"
+ className="h-7 px-2.5 text-[12px] gap-1.5 bg-shell-surface border-shell-border-strong hover:bg-shell-surface-hover"
aria-label="View canvas"
>
@@ -2234,17 +2257,25 @@ export function MessagesApp({
{/* reactions */}
{msg.reactions && Object.keys(msg.reactions).length > 0 && (
-
- {Object.entries(msg.reactions).map(([emoji, users]) => (
-
toggleReaction(msg.id, emoji)}
- className="text-[12px] bg-white/[0.06] hover:bg-white/10 border border-white/[0.06] rounded-full px-2 py-0.5 flex items-center gap-1 transition-colors"
- >
- {emoji}
- {users.length}
-
- ))}
+
+ {Object.entries(msg.reactions).map(([emoji, users]) => {
+ const mine = currentUserId != null && users.includes(currentUserId);
+ return (
+ toggleReaction(msg.id, emoji)}
+ aria-pressed={mine}
+ className={`text-[12px] rounded-full px-2 py-0.5 flex items-center gap-1 border transition-colors ${
+ mine
+ ? "bg-sky-500/15 border-sky-500/40 text-sky-600"
+ : "bg-shell-surface border-shell-border hover:bg-shell-surface-hover text-shell-text-secondary"
+ }`}
+ >
+ {emoji}
+ {users.length}
+
+ );
+ })}
)}
@@ -2253,7 +2284,7 @@ export function MessagesApp({
const excerpt = (msg.content || "").slice(0, 80);
const msgChannelId = msg.channel_id ?? selectedChannel ?? "";
return (
-
+
{
if (showEmoji && showEmoji.messageId === msg.id) {
@@ -2354,6 +2385,7 @@ export function MessagesApp({
})(),
document.body,
)}
+
);
diff --git a/desktop/src/apps/chat/MessageAvatar.tsx b/desktop/src/apps/chat/MessageAvatar.tsx
new file mode 100644
index 00000000..9c36cf73
--- /dev/null
+++ b/desktop/src/apps/chat/MessageAvatar.tsx
@@ -0,0 +1,99 @@
+import { resolveAgentEmoji } from "@/lib/agent-emoji";
+
+/**
+ * Slack-style avatar tile shown in the gutter on the first message of a group.
+ *
+ * - Agent (active): the agent emoji on a tile tinted with a stable per-agent
+ * colour derived from its slug (LiveAgent has no explicit colour field).
+ * - Agent (dead / removed): a muted neutral tile.
+ * - User: 1-2 character initials on a neutral surface tile.
+ *
+ * Structural colour uses shell tokens so it adapts to the active theme.
+ */
+
+/** Stable hue (0-359) hashed from an arbitrary string. */
+function hueFromString(input: string): number {
+ let hash = 0;
+ for (let i = 0; i < input.length; i++) {
+ hash = (hash * 31 + input.charCodeAt(i)) | 0;
+ }
+ return Math.abs(hash) % 360;
+}
+
+/** A low-alpha tint for an agent tile background, stable per slug. */
+function agentTileBg(slug: string): string {
+ return `hsl(${hueFromString(slug)} 58% 58% / 0.15)`;
+}
+
+/** Up to two initials from a display name or id. */
+function initialsFor(name: string): string {
+ const parts = name.trim().split(/[\s._-]+/).filter(Boolean);
+ if (parts.length === 0) return "?";
+ if (parts.length === 1) return (parts[0] ?? "").slice(0, 2).toUpperCase() || "?";
+ return ((parts[0]?.[0] ?? "") + (parts[1]?.[0] ?? "")).toUpperCase() || "?";
+}
+
+export interface MessageAvatarProps {
+ /** Avatar size in px (desktop ~38, mobile ~34). */
+ size: number;
+ /** Agent slug or user display name driving the colour / initials. */
+ authorId: string;
+ /** Display name used for user initials. */
+ displayName: string;
+ kind: "agent" | "user";
+ /** Dead / archived / removed agents render a muted neutral tile. */
+ dead?: boolean;
+ /** Resolved agent emoji (already passed through resolveAgentEmoji upstream). */
+ emoji?: string;
+}
+
+export function MessageAvatar({
+ size,
+ authorId,
+ displayName,
+ kind,
+ dead = false,
+ emoji,
+}: MessageAvatarProps) {
+ const dim = { width: size, height: size };
+ const fontPx = Math.round(size * 0.46);
+
+ if (kind === "agent") {
+ if (dead) {
+ return (
+
+
+ {emoji ?? resolveAgentEmoji(undefined, undefined)}
+
+
+ );
+ }
+ return (
+
+
+ {emoji ?? resolveAgentEmoji(undefined, undefined)}
+
+
+ );
+ }
+
+ return (
+
+
+ {initialsFor(displayName || authorId)}
+
+
+ );
+}
diff --git a/desktop/src/apps/chat/MessageHoverActions.tsx b/desktop/src/apps/chat/MessageHoverActions.tsx
index e6439643..1dbeeb7a 100644
--- a/desktop/src/apps/chat/MessageHoverActions.tsx
+++ b/desktop/src/apps/chat/MessageHoverActions.tsx
@@ -13,12 +13,12 @@ export function MessageHoverActions({
{dragHandle}
- 😀
- 💬
- ⋯
+ 😀
+ 💬
+ ⋯
);
}
From 9624892ede535ec4806efc5cf54ea7a8ec941c5e Mon Sep 17 00:00:00 2001
From: jaylfc
Date: Sat, 13 Jun 2026 18:08:55 +0100
Subject: [PATCH 09/57] feat(agents): top-bar agent kill switch (#132) (#857)
A quick-access dropdown in the top bar to stop a runaway agent without opening
the Agents app: Kill all agents (with a live running count) plus each running
agent by name. Every action is gated behind a confirmation dialog (kill is
destructive). Wired to POST /api/agents/bulk/stop and /api/agents/{name}/stop;
follows the existing Power-menu dropdown pattern (radix), confirm via radix
Dialog, all on shell tokens.
---
desktop/src/components/AgentKillSwitch.tsx | 188 +++++++++++++++++++++
desktop/src/components/TopBar.tsx | 2 +
2 files changed, 190 insertions(+)
create mode 100644 desktop/src/components/AgentKillSwitch.tsx
diff --git a/desktop/src/components/AgentKillSwitch.tsx b/desktop/src/components/AgentKillSwitch.tsx
new file mode 100644
index 00000000..8da39f39
--- /dev/null
+++ b/desktop/src/components/AgentKillSwitch.tsx
@@ -0,0 +1,188 @@
+import { useCallback, useState } from "react";
+import * as DropdownMenu from "@radix-ui/react-dropdown-menu";
+import * as Dialog from "@radix-ui/react-dialog";
+import { CircleStop, OctagonX } from "lucide-react";
+import { withCsrf } from "@/lib/csrf";
+
+/* ------------------------------------------------------------------ */
+/* AgentKillSwitch */
+/* Top-bar quick access to stop a runaway agent without opening the */
+/* Agents app: a dropdown with "Kill all" plus each running agent, */
+/* every action gated behind a confirmation dialog (kill is */
+/* destructive). Backend: POST /api/agents/bulk/stop and */
+/* POST /api/agents/{name}/stop. */
+/* ------------------------------------------------------------------ */
+
+interface RunningAgent {
+ name: string;
+ display_name?: string;
+}
+
+type Pending = { mode: "all" } | { mode: "one"; name: string; label: string } | null;
+
+async function postStop(path: string): Promise {
+ try {
+ const res = await fetch(path, {
+ method: "POST",
+ credentials: "include",
+ headers: withCsrf({ method: "POST" })?.headers,
+ });
+ return res.ok;
+ } catch {
+ return false;
+ }
+}
+
+export function AgentKillSwitch() {
+ const [agents, setAgents] = useState([]);
+ const [pending, setPending] = useState(null);
+ const [busy, setBusy] = useState(false);
+
+ // Refresh the running-agent list each time the menu opens (cheap, and keeps
+ // the list current without a global store).
+ const loadAgents = useCallback(async (open: boolean) => {
+ if (!open) return;
+ try {
+ const res = await fetch("/api/agents", { credentials: "include" });
+ if (!res.ok) return;
+ const data = (await res.json()) as Array>;
+ setAgents(
+ (Array.isArray(data) ? data : [])
+ .filter((a) => String(a.status ?? "") === "running")
+ .map((a) => ({
+ name: String(a.name ?? ""),
+ display_name: a.display_name ? String(a.display_name) : undefined,
+ }))
+ .filter((a) => a.name),
+ );
+ } catch {
+ // best-effort; leave the prior list
+ }
+ }, []);
+
+ const confirmKill = useCallback(async () => {
+ if (!pending) return;
+ setBusy(true);
+ const ok =
+ pending.mode === "all"
+ ? await postStop("/api/agents/bulk/stop")
+ : await postStop(`/api/agents/${encodeURIComponent(pending.name)}/stop`);
+ setBusy(false);
+ if (ok) {
+ window.dispatchEvent(new CustomEvent("taos:agents-changed"));
+ setAgents((prev) =>
+ pending.mode === "all" ? [] : prev.filter((a) => a.name !== pending.name),
+ );
+ }
+ setPending(null);
+ }, [pending]);
+
+ const menuItem =
+ "flex items-center gap-2.5 w-full px-3 py-2 text-sm rounded-md outline-none cursor-pointer select-none transition-colors";
+ const dangerItem = `${menuItem} text-red-400 hover:bg-red-500/15 focus:bg-red-500/15`;
+ const plainItem = `${menuItem} text-shell-text-secondary hover:bg-shell-surface-hover hover:text-shell-text focus:bg-shell-surface-hover focus:text-shell-text`;
+
+ const dialogTitle = pending?.mode === "all" ? "Kill all agents?" : `Kill ${pending?.mode === "one" ? pending.label : ""}?`;
+ const dialogBody =
+ pending?.mode === "all"
+ ? "Every running agent will be stopped immediately. In-flight work is lost. You can start them again from the Agents app."
+ : "This agent will be stopped immediately. In-flight work is lost. You can start it again from the Agents app.";
+
+ return (
+ <>
+
+
+
+
+
+
+
+
+
+
+ Stop agents
+
+
+ agents.length > 0 && setPending({ mode: "all" })}
+ >
+
+ Kill all agents
+ {agents.length}
+
+
+ {agents.length > 0 && (
+
+ )}
+
+ {agents.length === 0 ? (
+ No running agents
+ ) : (
+ agents.map((a) => {
+ const label = a.display_name || a.name;
+ return (
+ setPending({ mode: "one", name: a.name, label })}
+ >
+
+ {label}
+
+ );
+ })
+ )}
+
+
+
+
+ !o && setPending(null)}>
+
+
+
+
+
+
+
+
+ {dialogTitle}
+
+ {dialogBody}
+
+
+
+
+ setPending(null)}
+ disabled={busy}
+ className="px-3.5 py-2 rounded-lg text-sm font-medium text-shell-text-secondary hover:bg-shell-surface-hover transition-colors disabled:opacity-50"
+ >
+ Cancel
+
+
+ {busy ? "Stopping..." : pending?.mode === "all" ? "Kill all" : "Kill agent"}
+
+
+
+
+
+ >
+ );
+}
diff --git a/desktop/src/components/TopBar.tsx b/desktop/src/components/TopBar.tsx
index 7772a863..7d2e39a5 100644
--- a/desktop/src/components/TopBar.tsx
+++ b/desktop/src/components/TopBar.tsx
@@ -5,6 +5,7 @@ import { useWidgetStore } from "@/stores/widget-store";
import { useNotificationStore } from "@/stores/notification-store";
import { useProcessStore } from "@/stores/process-store";
import { StatusIndicators } from "./StatusIndicators";
+import { AgentKillSwitch } from "./AgentKillSwitch";
import { withCsrf } from "@/lib/csrf";
interface Props {
@@ -137,6 +138,7 @@ export function TopBar({ onSearchOpen, onAssistantOpen }: Props) {
{clock}
+
Date: Sat, 13 Jun 2026 18:13:53 +0100
Subject: [PATCH 10/57] docs(status): freshness sweep 2026-06-13 -- sync branch
tips, open PRs, done items
---
docs/STATUS.md | 74 ++++++++++++++++++++++++++------------------------
1 file changed, 38 insertions(+), 36 deletions(-)
diff --git a/docs/STATUS.md b/docs/STATUS.md
index b702f0a6..5143f9ac 100644
--- a/docs/STATUS.md
+++ b/docs/STATUS.md
@@ -1,42 +1,44 @@
SINGLE SOURCE OF TRUTH for cross-agent handoff.
Last updated: 2026-06-13, @taOS (freshness sweep).
-Branch tips: master=99cf786e. dev=355bb5ef (30 ahead of master; do NOT promote dev->master, Jay 2026-06-13: everything to dev only). On dev: Messages train, activity fix, deep-nav API, agent jobs 8/12/16/17, xdist CI fix (#839).
-
-Session state: ACTIVE. A2A poll monitor ARMED. Freshness cron :08/:38 ARMED (now incl. 0f CodeRabbit retrigger). 5h resume-pair ARMED (primary 15:53, retry 16:12 local; session-scoped). 5h usage 78%, weekly 8%. Usage policy: push until ~90% (98% max), don't stop at 70; crons monitor-only past 90.
-NOTE: a parallel session's freshness cron keeps reverting this file to a stale dev tip; if you see an old tip + "usage 47%", it is that churn, re-sync to the real dev tip.
-WEBSITE: all 4 PRs merged to taos-website main (stats/changelog/nav/accessibility); Coolify redeploys taos.my.
-CI: test suite parallelized via #839 (xdist -n auto), ~22 min -> ~13 min. CodeRabbit out of org credits -> rate-limits; freshness 0f retriggers oldest unreviewed PR with "@coderabbitai full review", never merge on a fake pass.
-OPEN: ALL 26 agent jobs DONE. #838 = complete Messages-polish batch (jobs 24/21/22/23/26/10/19/18/13/9); #842 = agent manual templates (job 14). Both: CI+Kilo will pass; BLOCKED on a real CodeRabbit review (org credits exhausted) before merge to dev. jobs 8/12/16/17 + website (11/15/20/25 on taos-website main) already landed.
-
-Done (since last STATUS.md update, 2026-06-13):
-- Messages-polish train (jobs 1-7) ALL on dev via #826/#829/#830 direct + #833 integration (#827/#828/#831/#832); sub-PRs closed superseded, branches deleted.
-- #783 VERIFIED FIXED on Pi: qwen 2.5 3b + 7b instruct rkllm pull to 100%, load, infer on NPU. CAVEAT: tested rkllama directly, NOT the store-UI /api/store install route.
-- fix(activity): dedupe local node in scheduler + detect ARM SoC (RK3588) for CPU label (c55f9292, 4 tests). Pi shows it after next deploy (hardware profile cached).
-- feat(desktop): deep-navigation API (?app= url + taos:open-app event), extracted to tested useDeepNavigation hook (14 tests). Tracked #836.
-- Agent jobs done direct-to-dev: 8 Cmd+K switcher, 12 theme inventory (#837 merged), 16 CodeBlock tests, 17 update-ping toggle.
-- Ideas filed: #796 benchmark pause/resume, #797 native phone, #798 native desktop shared-API, #799 TUI, #834 edit-before-send, #835 copy agent text, #836 deep-nav agent tool.
-- Untracked docs/AGENT_HANDOFF.md (was committed before .gitignore; exposed Pi LAN IP). Restored from memory backup after a branch-switch deleted the working copy.
-
-OPEN PRs (all need: merge on green CI + Kilo + my review; CodeRabbit is out of org credits so reviews are rate-limited fake-passes):
-- #838 feat(messages): empty states (job 24) on feat/msg-polish-2. Per Jay, BATCH more jobs onto this branch before merging (conserve CodeRabbit reviews). CodeRabbit rate-limited.
-- #839 ci: pytest-xdist -n auto. Investigation found the test job was NOT hanging, just slow (~22 min serial, 4845 tests). This parallelizes it. Validate via its own CI run timing, then merge first so the rest merge fast.
-- taos-website #1 stats / #2 changelog / #3 nav / #4 accessibility. Combined preview served from Mac tailscale :8899 for Jay; merge after Jay approves.
-
-Decisions (Jay, 2026-06-13): ALWAYS PR for code review (no direct-to-dev). Batch jobs into fewer big PRs (CodeRabbit credits exhausted). Investigate CI slowness (done: #839). Use impeccable + style skills for design work. Widget epic AFTER the job queue.
-
-Next queue (ordered):
-1. Land #839 (fast CI), then finish remaining agent jobs BATCHED onto feat/msg-polish-2/#838: 9, 10, 13, 14, 18, 19, 21, 22, 23, 26
-2. Bring website PRs #1-4 in after Jay views the preview
-3. Light theme (new separate theme; use impeccable skill; theme engine partial in desktop/src/theme)
-4. Agent-friendly API: #836 agent tool to dispatch taos:open-app (deep-nav already shipped)
-5. Build-widget epic: slim userspace runtime from #476 + My Apps home + agent build tool + share gate
-6. #825 key-scope fix; #737 Phase 3 UI (design with Jay)
-
-Pending Jay calls: promote dev->master? enable CodeRabbit add-on (billing) to restore real reviews? store-UI install-path check if model store still errors?
-
-Blockers: theme/userspace need a working session. taos.my Coolify deploy pending Jay.
+Branch tips: master=6394a3ed (PR #845 batch). dev=cb0ee722 (8 ahead of master). On dev since #845: rkllama install fix (#843), brand rename to taOS + contact info@taos.my (#847), Light theme (#848), esbuild 0.28.1 RCE fix (#849), unified chat composer (#850), Agents app Apple-card redesign (#851), update reset-before-pull fix (#852), Chat Slack-polish (#853).
+
+Session state: freshness sweep only. No active session crons armed this session.
+
+WEBSITE: taos.my live. All 4 taos-website PRs merged (stats/changelog/nav/accessibility).
+
+CI: test suite parallelized via #839 (xdist -n auto). CodeRabbit may be out of credits -- do not merge on a fake rate-limit pass. Use @coderabbitai full review to retrigger; manual review OK for tiny already-reviewed PRs.
+
+OPEN PRs:
+- #857 feat(agents): top-bar agent kill switch (feat/agent-kill-switch) -- in review
+- #846 dependabot esbuild bump -- LIKELY SUPERSEDED by #849 (already on dev); verify and close if so
+- #476 DRAFT feat(userspace): App Runtime v1 -- stays DRAFT, not ready to merge
+
+Notable open issues (bugs first):
+- #844 rkllama store-UI install chain broken (wrong script + non-interactive false-success) -- unresolved
+- #841 update check shows no updates when local branch diverged from origin -- unresolved
+- #825 taOS agent model swap breaks routing (stale per-agent key preferred over master key)
+- #840 chat: per-agent framework slash commands (Telegram-style) in DMs and via @agent /
+- #836 deep-navigation API for taOS agent to drive desktop (hook shipped; agent tool side pending)
+
+Done (since last STATUS.md update):
+- ALL 26 agent jobs COMPLETE and on master (via #845 batch).
+- Messages-polish (#838), agent manual templates (#842), CI parallelization (#839) all merged to dev then master.
+- Light theme (#848), esbuild RCE patch (#849), brand rename (#847), chat composer unified (#850), Agents redesign (#851), update flow fix (#852), Chat Slack-polish (#853) all on dev.
+
+Next queue:
+1. Verify and close #846 (superseded by #849).
+2. Land #857 (kill switch) after CI + review.
+3. Fix #844 and #841 (bugs, high user impact).
+4. #825 key-scope fix.
+5. Desktop overhaul (#824) and widget epic: needs Jay design session first.
+
+Decisions (carried from prior sessions):
+- PR for all code changes (no direct-to-dev commits for features).
+- Never --delete-branch on a dev->master PR (deletes dev, closes all dev-targeting PRs).
+- Jay updates Pi manually -- do not SSH-deploy after merges.
+- gh pr merge 401s -- use GitHub UI or gh api PUT for merges.
Security queue: #747 #737 #672 #658 #655 #654 #653 #651 #650 #647
-GOTCHA: gh pr merge 401s -- use gh api PUT (squash for sub-PRs, rebase/merge for integration). Admin-merge OK for frontend-only PRs when Python test jobs hang on infra AND spa-build is green. Never --delete-branch on dev->master PR. Jay updates Pi manually.
+GOTCHA: docs/AGENT_HANDOFF.md is intentionally untracked (exposed Pi LAN IP in a prior commit; restored from memory but kept out of git). The RESTART CHECK at its top is stale (referenced #752, long-closed); ignore it.
From 5225fdf80ee28661c964c990f7f2ed77ffe150ae Mon Sep 17 00:00:00 2001
From: jaylfc
Date: Sat, 13 Jun 2026 18:15:14 +0100
Subject: [PATCH 11/57] docs(getting-started): rename TinyAgentOS to taOS
throughout
---
docs/getting-started.md | 46 ++++++++++++++++++++---------------------
1 file changed, 23 insertions(+), 23 deletions(-)
diff --git a/docs/getting-started.md b/docs/getting-started.md
index c41f1f0a..de9da39a 100644
--- a/docs/getting-started.md
+++ b/docs/getting-started.md
@@ -1,12 +1,12 @@
-# Getting Started with TinyAgentOS
+# Getting Started with taOS
-Welcome. This guide walks you through installing TinyAgentOS, getting your first AI agent running, and finding your way around the platform. It assumes you're comfortable with a Linux terminal but have never run an AI agent before — every concept is explained along the way.
+Welcome. This guide walks you through installing taOS, getting your first AI agent running, and finding your way around the platform. It assumes you're comfortable with a Linux terminal but have never run an AI agent before — every concept is explained along the way.
---
-## What is TinyAgentOS?
+## What is taOS?
-TinyAgentOS is a self-hosted platform for running AI agents on affordable hardware — a single-board computer, a budget PC, or anything in between. You get a web dashboard (no coding required) where you can browse an app store of agent frameworks and models, deploy agents in isolated containers, configure how they communicate (Telegram, Discord, web chat), and monitor everything in one place.
+taOS is a self-hosted platform for running AI agents on affordable hardware — a single-board computer, a budget PC, or anything in between. You get a web dashboard (no coding required) where you can browse an app store of agent frameworks and models, deploy agents in isolated containers, configure how they communicate (Telegram, Discord, web chat), and monitor everything in one place.
Think of it like a home server for AI agents: you own the hardware, you own the data, and nothing phones home.
@@ -35,7 +35,7 @@ The platform itself uses roughly 345 MB of RAM when idle, so it runs comfortably
- **OS:** Armbian or Debian-based Linux (Ubuntu works too). The installer handles everything else.
- **Network:** The device needs internet access to download models and framework packages.
-- **Browser:** On any other device on the same network (laptop, phone, tablet). The TinyAgentOS web GUI runs on your device; you access it from your browser.
+- **Browser:** On any other device on the same network (laptop, phone, tablet). The taOS web GUI runs on your device; you access it from your browser.
**SQLCipher (for the browser app's encrypted cookie jar)** — the browser app needs the SQLCipher C library installed at the system level before `pip install` can build its `sqlcipher3` Python binding:
@@ -58,14 +58,14 @@ curl -fsSL https://raw.githubusercontent.com/jaylfc/tinyagentos/master/scripts/i
This script will:
1. Install system dependencies (`python3`, `git`, `nodejs`, `avahi-daemon` for mDNS, and others)
-2. Clone TinyAgentOS to `~/tinyagentos` (override with `TAOS_INSTALL_DIR`)
+2. Clone taOS to `~/tinyagentos` (override with `TAOS_INSTALL_DIR`)
3. Create a Python virtual environment and install all Python packages
-4. Register and start a `systemd` service so TinyAgentOS runs automatically on boot
+4. Register and start a `systemd` service so taOS runs automatically on boot
At the end, it prints your device's IP address:
```
- TinyAgentOS installed successfully!
+ taOS installed successfully!
Open: http://your-device-ip:6969
@@ -81,7 +81,7 @@ Open that URL in your browser. You're done with installation.
### Manual Install
-If you prefer to install manually or want to run TinyAgentOS in a specific location:
+If you prefer to install manually or want to run taOS in a specific location:
```bash
# Clone the repository
@@ -92,7 +92,7 @@ cd tinyagentos
python3 -m venv venv
source venv/bin/activate
-# Install TinyAgentOS and its dependencies
+# Install taOS and its dependencies
pip install -e .
# Start the server
@@ -112,7 +112,7 @@ sudo systemctl enable --now tinyagentos
## 3. First Boot
-When you open `http://your-host:6969` for the first time, TinyAgentOS loads the **web desktop shell** directly — a full browser-based desktop environment with a dock, launchpad, window manager, and 26 bundled apps. On phones and tablets it automatically swaps to a touch-first mobile view with a home grid and card switcher. A setup wizard runs on first launch to walk you through hardware detection and your first agent.
+When you open `http://your-host:6969` for the first time, taOS loads the **web desktop shell** directly — a full browser-based desktop environment with a dock, launchpad, window manager, and 26 bundled apps. On phones and tablets it automatically swaps to a touch-first mobile view with a home grid and card switcher. A setup wizard runs on first launch to walk you through hardware detection and your first agent.
### Hardware Auto-Detection
@@ -138,7 +138,7 @@ You don't have to follow the wizard step-by-step — you can dismiss it and navi
## 3a. Using the Desktop Shell
-The desktop is the main way you'll interact with TinyAgentOS. It works like a regular operating system's desktop — but it runs entirely in your browser.
+The desktop is the main way you'll interact with taOS. It works like a regular operating system's desktop — but it runs entirely in your browser.
### The window manager
@@ -262,7 +262,7 @@ Here's a quick map of the apps available from the desktop dock and launchpad.
| **Tasks** | Schedule recurring jobs for your agents — daily summaries, memory cleanup, data imports. Built-in presets for common patterns. |
| **Import** | Drag and drop files to embed into an agent's memory. Supported formats: `.txt`, `.md`, `.pdf`, `.html`, `.json`, `.csv`. |
| **Files** | Real virtual filesystem with your personal workspace and shared folders that agents can read and write to. |
-| **Settings** | System info, storage usage, backup/restore, update TinyAgentOS, test backend connections, toggle dark/light theme, and per-category toggles for User Memory auto-capture. taOS periodically checks for updates and reports an anonymous install count (a daily aggregate estimate, no identifiers); disable with `TAOS_NO_UPDATE_PING=1` or in Settings. |
+| **Settings** | System info, storage usage, backup/restore, update taOS, test backend connections, toggle dark/light theme, and per-category toggles for User Memory auto-capture. taOS periodically checks for updates and reports an anonymous install count (a daily aggregate estimate, no identifiers); disable with `TAOS_NO_UPDATE_PING=1` or in Settings. |
### OS apps
@@ -289,13 +289,13 @@ Here's a quick map of the apps available from the desktop dock and launchpad.
## 5a. Mobile Install (iOS / Android)
-TinyAgentOS works as a fullscreen Progressive Web App (PWA) on phones and tablets. Once installed, it hides the browser chrome, respects the device's safe area, and behaves like a native app.
+taOS works as a fullscreen Progressive Web App (PWA) on phones and tablets. Once installed, it hides the browser chrome, respects the device's safe area, and behaves like a native app.
### iOS / iPadOS
1. Open `http://your-host:6969` in **Safari** (PWA install must go through Safari on iOS).
2. Tap the **Share** button, then **Add to Home Screen**.
-3. Launch TinyAgentOS from the home screen icon — it opens fullscreen with no browser bars.
+3. Launch taOS from the home screen icon — it opens fullscreen with no browser bars.
The Messages app has its own dedicated PWA at `http://your-host:6969/chat-pwa` — install it the same way to get a private, agent-only messenger on your home screen (works like an internal Discord).
@@ -313,11 +313,11 @@ The mobile shell uses a bottom **pill bar** for navigation: tap the pill to go h
## 5b. User Memory
-TinyAgentOS includes a personal memory system just for you, think of it as your own private notebook that the platform helps fill in automatically. It's separate from agent memories. Under the hood it is powered by taosmd (`pip install taosmd`, the same library that backs agent memories): writes go to taosmd's `POST /ingest/batch` endpoint and keyword reads go to `GET /search?mode=bm25`. A local SQLite FTS5 store acts as an automatic fallback if taosmd is unreachable. You configure the taosmd address via the `TAOS_USER_MEMORY_URL` environment variable.
+taOS includes a personal memory system just for you, think of it as your own private notebook that the platform helps fill in automatically. It's separate from agent memories. Under the hood it is powered by taosmd (`pip install taosmd`, the same library that backs agent memories): writes go to taosmd's `POST /ingest/batch` endpoint and keyword reads go to `GET /search?mode=bm25`. A local SQLite FTS5 store acts as an automatic fallback if taosmd is unreachable. You configure the taosmd address via the `TAOS_USER_MEMORY_URL` environment variable.
### What gets captured
-By default, TinyAgentOS can auto-capture:
+By default, taOS can auto-capture:
- **Conversations** from the Messages app
- **Notes** you write in the Text Editor
- **File activity** in the Files app
@@ -355,7 +355,7 @@ BotFather will ask for a name (displayed to users) and a username (must end in `
**Step 2 — Store the token**
-In TinyAgentOS, go to **Secrets** and click **Add Secret**. Name it something like `telegram_bot_token`, paste your token as the value, and save. The token is now encrypted on your device.
+In taOS, go to **Secrets** and click **Add Secret**. Name it something like `telegram_bot_token`, paste your token as the value, and save. The token is now encrypted on your device.
**Step 3 — Configure the channel**
@@ -400,7 +400,7 @@ Click the bell to see notification history. Click a notification to mark it read
Each agent has its own log stream. Go to **Agents**, click on an agent, then open the **Logs** tab. Logs stream live — useful for debugging why an agent isn't responding or is producing unexpected output.
-For TinyAgentOS itself (not individual agents), you can view system logs from the terminal:
+For taOS itself (not individual agents), you can view system logs from the terminal:
```bash
journalctl -u tinyagentos -f
@@ -412,7 +412,7 @@ The `-f` flag follows the log in real time.
## 8. Backup
-TinyAgentOS stores your configuration, agent data, secrets, and memories in `~/tinyagentos/data/` (or `$TAOS_INSTALL_DIR/data/` if you set a custom install path).
+taOS stores your configuration, agent data, secrets, and memories in `~/tinyagentos/data/` (or `$TAOS_INSTALL_DIR/data/` if you set a custom install path).
### Backup via Settings
@@ -424,7 +424,7 @@ Go to **Settings** and scroll to the **Backup** section. Click **Create Backup**
### Restore
-On a fresh TinyAgentOS install, go to **Settings > Backup > Restore**, upload your backup file, and click **Restore**. Your agents, channels, and secrets will be recreated.
+On a fresh taOS install, go to **Settings > Backup > Restore**, upload your backup file, and click **Restore**. Your agents, channels, and secrets will be recreated.
### Manual Backup
@@ -443,13 +443,13 @@ cp -r ~/tinyagentos/data /your/backup/location/tinyagentos-data-$(date +%Y%m%d)
When filing a bug, it helps to include:
- Your hardware (e.g. "Orange Pi 5 Plus, 16 GB, Armbian 24.x")
-- The TinyAgentOS version (visible in **Settings > System Info**)
+- The taOS version (visible in **Settings > System Info**)
- What you expected to happen vs. what actually happened
- Relevant log output from `journalctl -u tinyagentos -n 50`
**Email:** [info@taos.my](mailto:info@taos.my) — for anything that doesn't fit a GitHub issue.
-**A note on maturity:** TinyAgentOS is in early development. If something doesn't work, it may genuinely be a bug rather than user error — please do report it. Contributions and hardware test reports are very welcome.
+**A note on maturity:** taOS is in early development. If something doesn't work, it may genuinely be a bug rather than user error — please do report it. Contributions and hardware test reports are very welcome.
---
From 244f183d4812084d900f073ec95a40f0368d086f Mon Sep 17 00:00:00 2001
From: jaylfc
Date: Sat, 13 Jun 2026 18:15:46 +0100
Subject: [PATCH 12/57] docs(contributing): rename TinyAgentOS to taOS in title
and intro
---
CONTRIBUTING.md | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 66e5b0f0..56d17387 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -1,6 +1,6 @@
-# Contributing to TinyAgentOS
+# Contributing to taOS
-Welcome — and thanks for your interest in contributing. TinyAgentOS is a self-hosted AI agent platform for low-power hardware. Before diving in, please read the [README](README.md) for a project overview.
+Welcome — and thanks for your interest in contributing. taOS is a self-hosted AI agent platform for low-power hardware. Before diving in, please read the [README](README.md) for a project overview.
> **Note:** The project is in early development. APIs and interfaces may change. That is fine — contributions of all sizes are welcome.
From 09a497a270ccd721b273e65dfa5cbe34f97029d3 Mon Sep 17 00:00:00 2001
From: jaylfc
Date: Sat, 13 Jun 2026 18:15:51 +0100
Subject: [PATCH 13/57] docs(readme): remove parenthetical TinyAgentOS from
product description
---
README.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/README.md b/README.md
index 9df79226..1c7528f0 100644
--- a/README.md
+++ b/README.md
@@ -18,7 +18,7 @@
-Self-hosted AI agent platform that runs on whatever hardware you have. An old laptop, a Raspberry Pi, a gaming PC, an SBC gathering dust, or all of them at once. taOS (short for TinyAgentOS) turns your spare hardware into a distributed AI compute cluster.
+Self-hosted AI agent platform that runs on whatever hardware you have. An old laptop, a Raspberry Pi, a gaming PC, an SBC gathering dust, or all of them at once. taOS turns your spare hardware into a distributed AI compute cluster.
A full web desktop environment with 36 bundled apps, 108 catalog apps, 47 MCP plugins, 16 agent frameworks, a curated local model catalog of 112 manifests covering LLMs, vision, embeddings, audio, and image generation (including RK3588 NPU variants via c01zaut/happyme531), plus 167k+ searchable models from HuggingFace, agent deployment, training, image/video/audio generation, and full system monitoring, all from a single web dashboard. Supports Apple Silicon (MLX), NVIDIA, AMD, Rockchip NPU, Raspberry Pi, Android phones, and more.
From d9aab978b9e06029a64e63fc354bfb57358aa04f Mon Sep 17 00:00:00 2001
From: jaylfc
Date: Sat, 13 Jun 2026 18:17:18 +0100
Subject: [PATCH 14/57] docs(status): correct -- #857 already merged, #859 new,
dev=09a497a2 (13 ahead)
---
docs/STATUS.md | 13 +++++++------
1 file changed, 7 insertions(+), 6 deletions(-)
diff --git a/docs/STATUS.md b/docs/STATUS.md
index 5143f9ac..e4447e7a 100644
--- a/docs/STATUS.md
+++ b/docs/STATUS.md
@@ -1,7 +1,7 @@
SINGLE SOURCE OF TRUTH for cross-agent handoff.
Last updated: 2026-06-13, @taOS (freshness sweep).
-Branch tips: master=6394a3ed (PR #845 batch). dev=cb0ee722 (8 ahead of master). On dev since #845: rkllama install fix (#843), brand rename to taOS + contact info@taos.my (#847), Light theme (#848), esbuild 0.28.1 RCE fix (#849), unified chat composer (#850), Agents app Apple-card redesign (#851), update reset-before-pull fix (#852), Chat Slack-polish (#853).
+Branch tips: master=6394a3ed (PR #845 batch). dev=09a497a2 (13 ahead of master). On dev since #845: rkllama install fix (#843), brand rename to taOS (#847), Light theme (#848), esbuild 0.28.1 RCE fix (#849), unified chat composer (#850), Agents redesign (#851), update reset-before-pull fix (#852), Chat Slack-polish (#853), agent kill switch (#857), doc fixes -- getting-started/CONTRIBUTING/README brand rename (#858-9-60 via GitHub API, parent 09a497a2).
Session state: freshness sweep only. No active session crons armed this session.
@@ -10,8 +10,8 @@ WEBSITE: taos.my live. All 4 taos-website PRs merged (stats/changelog/nav/access
CI: test suite parallelized via #839 (xdist -n auto). CodeRabbit may be out of credits -- do not merge on a fake rate-limit pass. Use @coderabbitai full review to retrigger; manual review OK for tiny already-reviewed PRs.
OPEN PRs:
-- #857 feat(agents): top-bar agent kill switch (feat/agent-kill-switch) -- in review
-- #846 dependabot esbuild bump -- LIKELY SUPERSEDED by #849 (already on dev); verify and close if so
+- #859 fix(agents): kill switch surfaces failures -- Gitar review follow-up on #857, fix/kill-switch-error-handling
+- #846 dependabot esbuild bump -- SUPERSEDED by #849 (already on dev); close it
- #476 DRAFT feat(userspace): App Runtime v1 -- stays DRAFT, not ready to merge
Notable open issues (bugs first):
@@ -24,11 +24,12 @@ Notable open issues (bugs first):
Done (since last STATUS.md update):
- ALL 26 agent jobs COMPLETE and on master (via #845 batch).
- Messages-polish (#838), agent manual templates (#842), CI parallelization (#839) all merged to dev then master.
-- Light theme (#848), esbuild RCE patch (#849), brand rename (#847), chat composer unified (#850), Agents redesign (#851), update flow fix (#852), Chat Slack-polish (#853) all on dev.
+- Light theme (#848), esbuild RCE patch (#849), brand rename (#847), chat composer unified (#850), Agents redesign (#851), update flow fix (#852), Chat Slack-polish (#853), agent kill switch (#857) all on dev.
+- This sweep: docs/STATUS.md, docs/getting-started.md, CONTRIBUTING.md, README.md brand rename residue fixed.
Next queue:
-1. Verify and close #846 (superseded by #849).
-2. Land #857 (kill switch) after CI + review.
+1. Land #859 (kill switch error handling) after CI + review.
+2. Close #846 (superseded by #849).
3. Fix #844 and #841 (bugs, high user impact).
4. #825 key-scope fix.
5. Desktop overhaul (#824) and widget epic: needs Jay design session first.
From d4147c44118480b49cadabea53e3a245a09b8f86 Mon Sep 17 00:00:00 2001
From: jaylfc
Date: Sat, 13 Jun 2026 18:38:22 +0100
Subject: [PATCH 15/57] docs(status): freshness sweep -- add PR #860, correct
dev tip to d9aab978
---
docs/STATUS.md | 75 ++++++++++++++++++++++++--------------------------
1 file changed, 36 insertions(+), 39 deletions(-)
diff --git a/docs/STATUS.md b/docs/STATUS.md
index e4447e7a..b702f0a6 100644
--- a/docs/STATUS.md
+++ b/docs/STATUS.md
@@ -1,45 +1,42 @@
SINGLE SOURCE OF TRUTH for cross-agent handoff.
Last updated: 2026-06-13, @taOS (freshness sweep).
-Branch tips: master=6394a3ed (PR #845 batch). dev=09a497a2 (13 ahead of master). On dev since #845: rkllama install fix (#843), brand rename to taOS (#847), Light theme (#848), esbuild 0.28.1 RCE fix (#849), unified chat composer (#850), Agents redesign (#851), update reset-before-pull fix (#852), Chat Slack-polish (#853), agent kill switch (#857), doc fixes -- getting-started/CONTRIBUTING/README brand rename (#858-9-60 via GitHub API, parent 09a497a2).
-
-Session state: freshness sweep only. No active session crons armed this session.
-
-WEBSITE: taos.my live. All 4 taos-website PRs merged (stats/changelog/nav/accessibility).
-
-CI: test suite parallelized via #839 (xdist -n auto). CodeRabbit may be out of credits -- do not merge on a fake rate-limit pass. Use @coderabbitai full review to retrigger; manual review OK for tiny already-reviewed PRs.
-
-OPEN PRs:
-- #859 fix(agents): kill switch surfaces failures -- Gitar review follow-up on #857, fix/kill-switch-error-handling
-- #846 dependabot esbuild bump -- SUPERSEDED by #849 (already on dev); close it
-- #476 DRAFT feat(userspace): App Runtime v1 -- stays DRAFT, not ready to merge
-
-Notable open issues (bugs first):
-- #844 rkllama store-UI install chain broken (wrong script + non-interactive false-success) -- unresolved
-- #841 update check shows no updates when local branch diverged from origin -- unresolved
-- #825 taOS agent model swap breaks routing (stale per-agent key preferred over master key)
-- #840 chat: per-agent framework slash commands (Telegram-style) in DMs and via @agent /
-- #836 deep-navigation API for taOS agent to drive desktop (hook shipped; agent tool side pending)
-
-Done (since last STATUS.md update):
-- ALL 26 agent jobs COMPLETE and on master (via #845 batch).
-- Messages-polish (#838), agent manual templates (#842), CI parallelization (#839) all merged to dev then master.
-- Light theme (#848), esbuild RCE patch (#849), brand rename (#847), chat composer unified (#850), Agents redesign (#851), update flow fix (#852), Chat Slack-polish (#853), agent kill switch (#857) all on dev.
-- This sweep: docs/STATUS.md, docs/getting-started.md, CONTRIBUTING.md, README.md brand rename residue fixed.
-
-Next queue:
-1. Land #859 (kill switch error handling) after CI + review.
-2. Close #846 (superseded by #849).
-3. Fix #844 and #841 (bugs, high user impact).
-4. #825 key-scope fix.
-5. Desktop overhaul (#824) and widget epic: needs Jay design session first.
-
-Decisions (carried from prior sessions):
-- PR for all code changes (no direct-to-dev commits for features).
-- Never --delete-branch on a dev->master PR (deletes dev, closes all dev-targeting PRs).
-- Jay updates Pi manually -- do not SSH-deploy after merges.
-- gh pr merge 401s -- use GitHub UI or gh api PUT for merges.
+Branch tips: master=99cf786e. dev=355bb5ef (30 ahead of master; do NOT promote dev->master, Jay 2026-06-13: everything to dev only). On dev: Messages train, activity fix, deep-nav API, agent jobs 8/12/16/17, xdist CI fix (#839).
+
+Session state: ACTIVE. A2A poll monitor ARMED. Freshness cron :08/:38 ARMED (now incl. 0f CodeRabbit retrigger). 5h resume-pair ARMED (primary 15:53, retry 16:12 local; session-scoped). 5h usage 78%, weekly 8%. Usage policy: push until ~90% (98% max), don't stop at 70; crons monitor-only past 90.
+NOTE: a parallel session's freshness cron keeps reverting this file to a stale dev tip; if you see an old tip + "usage 47%", it is that churn, re-sync to the real dev tip.
+WEBSITE: all 4 PRs merged to taos-website main (stats/changelog/nav/accessibility); Coolify redeploys taos.my.
+CI: test suite parallelized via #839 (xdist -n auto), ~22 min -> ~13 min. CodeRabbit out of org credits -> rate-limits; freshness 0f retriggers oldest unreviewed PR with "@coderabbitai full review", never merge on a fake pass.
+OPEN: ALL 26 agent jobs DONE. #838 = complete Messages-polish batch (jobs 24/21/22/23/26/10/19/18/13/9); #842 = agent manual templates (job 14). Both: CI+Kilo will pass; BLOCKED on a real CodeRabbit review (org credits exhausted) before merge to dev. jobs 8/12/16/17 + website (11/15/20/25 on taos-website main) already landed.
+
+Done (since last STATUS.md update, 2026-06-13):
+- Messages-polish train (jobs 1-7) ALL on dev via #826/#829/#830 direct + #833 integration (#827/#828/#831/#832); sub-PRs closed superseded, branches deleted.
+- #783 VERIFIED FIXED on Pi: qwen 2.5 3b + 7b instruct rkllm pull to 100%, load, infer on NPU. CAVEAT: tested rkllama directly, NOT the store-UI /api/store install route.
+- fix(activity): dedupe local node in scheduler + detect ARM SoC (RK3588) for CPU label (c55f9292, 4 tests). Pi shows it after next deploy (hardware profile cached).
+- feat(desktop): deep-navigation API (?app= url + taos:open-app event), extracted to tested useDeepNavigation hook (14 tests). Tracked #836.
+- Agent jobs done direct-to-dev: 8 Cmd+K switcher, 12 theme inventory (#837 merged), 16 CodeBlock tests, 17 update-ping toggle.
+- Ideas filed: #796 benchmark pause/resume, #797 native phone, #798 native desktop shared-API, #799 TUI, #834 edit-before-send, #835 copy agent text, #836 deep-nav agent tool.
+- Untracked docs/AGENT_HANDOFF.md (was committed before .gitignore; exposed Pi LAN IP). Restored from memory backup after a branch-switch deleted the working copy.
+
+OPEN PRs (all need: merge on green CI + Kilo + my review; CodeRabbit is out of org credits so reviews are rate-limited fake-passes):
+- #838 feat(messages): empty states (job 24) on feat/msg-polish-2. Per Jay, BATCH more jobs onto this branch before merging (conserve CodeRabbit reviews). CodeRabbit rate-limited.
+- #839 ci: pytest-xdist -n auto. Investigation found the test job was NOT hanging, just slow (~22 min serial, 4845 tests). This parallelizes it. Validate via its own CI run timing, then merge first so the rest merge fast.
+- taos-website #1 stats / #2 changelog / #3 nav / #4 accessibility. Combined preview served from Mac tailscale :8899 for Jay; merge after Jay approves.
+
+Decisions (Jay, 2026-06-13): ALWAYS PR for code review (no direct-to-dev). Batch jobs into fewer big PRs (CodeRabbit credits exhausted). Investigate CI slowness (done: #839). Use impeccable + style skills for design work. Widget epic AFTER the job queue.
+
+Next queue (ordered):
+1. Land #839 (fast CI), then finish remaining agent jobs BATCHED onto feat/msg-polish-2/#838: 9, 10, 13, 14, 18, 19, 21, 22, 23, 26
+2. Bring website PRs #1-4 in after Jay views the preview
+3. Light theme (new separate theme; use impeccable skill; theme engine partial in desktop/src/theme)
+4. Agent-friendly API: #836 agent tool to dispatch taos:open-app (deep-nav already shipped)
+5. Build-widget epic: slim userspace runtime from #476 + My Apps home + agent build tool + share gate
+6. #825 key-scope fix; #737 Phase 3 UI (design with Jay)
+
+Pending Jay calls: promote dev->master? enable CodeRabbit add-on (billing) to restore real reviews? store-UI install-path check if model store still errors?
+
+Blockers: theme/userspace need a working session. taos.my Coolify deploy pending Jay.
Security queue: #747 #737 #672 #658 #655 #654 #653 #651 #650 #647
-GOTCHA: docs/AGENT_HANDOFF.md is intentionally untracked (exposed Pi LAN IP in a prior commit; restored from memory but kept out of git). The RESTART CHECK at its top is stale (referenced #752, long-closed); ignore it.
+GOTCHA: gh pr merge 401s -- use gh api PUT (squash for sub-PRs, rebase/merge for integration). Admin-merge OK for frontend-only PRs when Python test jobs hang on infra AND spa-build is green. Never --delete-branch on dev->master PR. Jay updates Pi manually.
From f56efcb6b18366a7105bb40a2083c2d515159d2e Mon Sep 17 00:00:00 2001
From: jaylfc
Date: Sat, 13 Jun 2026 18:40:57 +0100
Subject: [PATCH 16/57] docs(status): restore correct dev content, add PR #860,
fix dev tip to d9aab978
---
docs/STATUS.md | 76 ++++++++++++++++++++++++++------------------------
1 file changed, 40 insertions(+), 36 deletions(-)
diff --git a/docs/STATUS.md b/docs/STATUS.md
index b702f0a6..4923409a 100644
--- a/docs/STATUS.md
+++ b/docs/STATUS.md
@@ -1,42 +1,46 @@
SINGLE SOURCE OF TRUTH for cross-agent handoff.
Last updated: 2026-06-13, @taOS (freshness sweep).
-Branch tips: master=99cf786e. dev=355bb5ef (30 ahead of master; do NOT promote dev->master, Jay 2026-06-13: everything to dev only). On dev: Messages train, activity fix, deep-nav API, agent jobs 8/12/16/17, xdist CI fix (#839).
-
-Session state: ACTIVE. A2A poll monitor ARMED. Freshness cron :08/:38 ARMED (now incl. 0f CodeRabbit retrigger). 5h resume-pair ARMED (primary 15:53, retry 16:12 local; session-scoped). 5h usage 78%, weekly 8%. Usage policy: push until ~90% (98% max), don't stop at 70; crons monitor-only past 90.
-NOTE: a parallel session's freshness cron keeps reverting this file to a stale dev tip; if you see an old tip + "usage 47%", it is that churn, re-sync to the real dev tip.
-WEBSITE: all 4 PRs merged to taos-website main (stats/changelog/nav/accessibility); Coolify redeploys taos.my.
-CI: test suite parallelized via #839 (xdist -n auto), ~22 min -> ~13 min. CodeRabbit out of org credits -> rate-limits; freshness 0f retriggers oldest unreviewed PR with "@coderabbitai full review", never merge on a fake pass.
-OPEN: ALL 26 agent jobs DONE. #838 = complete Messages-polish batch (jobs 24/21/22/23/26/10/19/18/13/9); #842 = agent manual templates (job 14). Both: CI+Kilo will pass; BLOCKED on a real CodeRabbit review (org credits exhausted) before merge to dev. jobs 8/12/16/17 + website (11/15/20/25 on taos-website main) already landed.
-
-Done (since last STATUS.md update, 2026-06-13):
-- Messages-polish train (jobs 1-7) ALL on dev via #826/#829/#830 direct + #833 integration (#827/#828/#831/#832); sub-PRs closed superseded, branches deleted.
-- #783 VERIFIED FIXED on Pi: qwen 2.5 3b + 7b instruct rkllm pull to 100%, load, infer on NPU. CAVEAT: tested rkllama directly, NOT the store-UI /api/store install route.
-- fix(activity): dedupe local node in scheduler + detect ARM SoC (RK3588) for CPU label (c55f9292, 4 tests). Pi shows it after next deploy (hardware profile cached).
-- feat(desktop): deep-navigation API (?app= url + taos:open-app event), extracted to tested useDeepNavigation hook (14 tests). Tracked #836.
-- Agent jobs done direct-to-dev: 8 Cmd+K switcher, 12 theme inventory (#837 merged), 16 CodeBlock tests, 17 update-ping toggle.
-- Ideas filed: #796 benchmark pause/resume, #797 native phone, #798 native desktop shared-API, #799 TUI, #834 edit-before-send, #835 copy agent text, #836 deep-nav agent tool.
-- Untracked docs/AGENT_HANDOFF.md (was committed before .gitignore; exposed Pi LAN IP). Restored from memory backup after a branch-switch deleted the working copy.
-
-OPEN PRs (all need: merge on green CI + Kilo + my review; CodeRabbit is out of org credits so reviews are rate-limited fake-passes):
-- #838 feat(messages): empty states (job 24) on feat/msg-polish-2. Per Jay, BATCH more jobs onto this branch before merging (conserve CodeRabbit reviews). CodeRabbit rate-limited.
-- #839 ci: pytest-xdist -n auto. Investigation found the test job was NOT hanging, just slow (~22 min serial, 4845 tests). This parallelizes it. Validate via its own CI run timing, then merge first so the rest merge fast.
-- taos-website #1 stats / #2 changelog / #3 nav / #4 accessibility. Combined preview served from Mac tailscale :8899 for Jay; merge after Jay approves.
-
-Decisions (Jay, 2026-06-13): ALWAYS PR for code review (no direct-to-dev). Batch jobs into fewer big PRs (CodeRabbit credits exhausted). Investigate CI slowness (done: #839). Use impeccable + style skills for design work. Widget epic AFTER the job queue.
-
-Next queue (ordered):
-1. Land #839 (fast CI), then finish remaining agent jobs BATCHED onto feat/msg-polish-2/#838: 9, 10, 13, 14, 18, 19, 21, 22, 23, 26
-2. Bring website PRs #1-4 in after Jay views the preview
-3. Light theme (new separate theme; use impeccable skill; theme engine partial in desktop/src/theme)
-4. Agent-friendly API: #836 agent tool to dispatch taos:open-app (deep-nav already shipped)
-5. Build-widget epic: slim userspace runtime from #476 + My Apps home + agent build tool + share gate
-6. #825 key-scope fix; #737 Phase 3 UI (design with Jay)
-
-Pending Jay calls: promote dev->master? enable CodeRabbit add-on (billing) to restore real reviews? store-UI install-path check if model store still errors?
-
-Blockers: theme/userspace need a working session. taos.my Coolify deploy pending Jay.
+Branch tips: master=6394a3ed (PR #845 batch). dev=d9aab978 (14 ahead of master). On dev since #845: rkllama install fix (#843), brand rename to taOS (#847), Light theme (#848), esbuild 0.28.1 RCE fix (#849), unified chat composer (#850), Agents redesign (#851), update reset-before-pull fix (#852), Chat Slack-polish (#853), agent kill switch (#857), doc fixes -- getting-started/CONTRIBUTING/README brand rename (#858-9-60 via GitHub API, parent d9aab978).
+
+Session state: freshness sweep only. No active session crons armed this session.
+
+WEBSITE: taos.my live. All 4 taos-website PRs merged (stats/changelog/nav/accessibility).
+
+CI: test suite parallelized via #839 (xdist -n auto). CodeRabbit may be out of credits -- do not merge on a fake rate-limit pass. Use @coderabbitai full review to retrigger; manual review OK for tiny already-reviewed PRs.
+
+OPEN PRs:
+- #860 feat(theme): theme chooser shows only taOS Dark + taOS Light (remove Matrix Terminal) -- feat/themes-taos-light-dark
+- #859 fix(agents): kill switch surfaces failures -- Gitar review follow-up on #857, fix/kill-switch-error-handling
+- #846 dependabot esbuild bump -- SUPERSEDED by #849 (already on dev); close it
+- #476 DRAFT feat(userspace): App Runtime v1 -- stays DRAFT, not ready to merge
+
+Notable open issues (bugs first):
+- #844 rkllama store-UI install chain broken (wrong script + non-interactive false-success) -- unresolved
+- #841 update check shows no updates when local branch diverged from origin -- unresolved
+- #825 taOS agent model swap breaks routing (stale per-agent key preferred over master key)
+- #840 chat: per-agent framework slash commands (Telegram-style) in DMs and via @agent /
+- #836 deep-navigation API for taOS agent to drive desktop (hook shipped; agent tool side pending)
+
+Done (since last STATUS.md update):
+- ALL 26 agent jobs COMPLETE and on master (via #845 batch).
+- Messages-polish (#838), agent manual templates (#842), CI parallelization (#839) all merged to dev then master.
+- Light theme (#848), esbuild RCE patch (#849), brand rename (#847), chat composer unified (#850), Agents redesign (#851), update flow fix (#852), Chat Slack-polish (#853), agent kill switch (#857) all on dev.
+- This sweep: docs/STATUS.md, docs/getting-started.md, CONTRIBUTING.md, README.md brand rename residue fixed.
+
+Next queue:
+1. Land #859 and #860 after CI + review.
+2. Close #846 (superseded by #849).
+3. Fix #844 and #841 (bugs, high user impact).
+4. #825 key-scope fix.
+5. Desktop overhaul (#824) and widget epic: needs Jay design session first.
+
+Decisions (carried from prior sessions):
+- PR for all code changes (no direct-to-dev commits for features).
+- Never --delete-branch on a dev->master PR (deletes dev, closes all dev-targeting PRs).
+- Jay updates Pi manually -- do not SSH-deploy after merges.
+- gh pr merge 401s -- use GitHub UI or gh api PUT for merges.
Security queue: #747 #737 #672 #658 #655 #654 #653 #651 #650 #647
-GOTCHA: gh pr merge 401s -- use gh api PUT (squash for sub-PRs, rebase/merge for integration). Admin-merge OK for frontend-only PRs when Python test jobs hang on infra AND spa-build is green. Never --delete-branch on dev->master PR. Jay updates Pi manually.
+GOTCHA: docs/AGENT_HANDOFF.md is intentionally untracked (exposed Pi LAN IP in a prior commit; restored from memory but kept out of git). The RESTART CHECK at its top is stale (referenced #752, long-closed); ignore it.
From 40388300c67921d3a79cf61cf0ade4323abdff8a Mon Sep 17 00:00:00 2001
From: jaylfc
Date: Sat, 13 Jun 2026 18:42:29 +0100
Subject: [PATCH 17/57] docs(agent-qmd-serve-setup): rename TinyAgentOS to taOS
---
docs/agent-qmd-serve-setup.md | 10 +++++-----
1 file changed, 5 insertions(+), 5 deletions(-)
diff --git a/docs/agent-qmd-serve-setup.md b/docs/agent-qmd-serve-setup.md
index 67c2ebf6..e614f2af 100644
--- a/docs/agent-qmd-serve-setup.md
+++ b/docs/agent-qmd-serve-setup.md
@@ -7,7 +7,7 @@ Each agent runs its own `qmd serve` instance inside its LXC container. This keep
```
Host (Orange Pi / x86)
├── rkllama (port 7833) — shared NPU/GPU inference
-├── TinyAgentOS (port 6969) — web GUI, talks to each agent's qmd serve
+├── taOS (port 6969) — web GUI, talks to each agent's qmd serve
│
├── LXC: agent-alpha
│ ├── agent framework gateway
@@ -25,7 +25,7 @@ Host (Orange Pi / x86)
└── ~/.cache/qmd/index.sqlite
```
-**Key point:** Each agent's `qmd serve` uses the shared rkllama/ollama backend for inference but stores its own index database locally. TinyAgentOS accesses each agent's memory via the agent's `qmd_url`.
+**Key point:** Each agent's `qmd serve` uses the shared rkllama/ollama backend for inference but stores its own index database locally. taOS accesses each agent's memory via the agent's `qmd_url`.
## Install QMD in Agent LXC
@@ -84,9 +84,9 @@ sudo systemctl daemon-reload
sudo systemctl enable --now qmd-serve
```
-## TinyAgentOS Config
+## taOS Config
-In TinyAgentOS's `data/config.yaml`, point each agent to its QMD serve instance:
+In taOS's `data/config.yaml`, point each agent to its QMD serve instance:
```yaml
agents:
@@ -104,7 +104,7 @@ agents:
color: "#ff7eb3"
```
-TinyAgentOS then queries each agent's endpoints:
+taOS then queries each agent's endpoints:
- `GET /status` — index health
- `GET /collections` — list memory collections
- `GET /search?q=X` — keyword search
From 9c7c91fe21f886d33db47d9d8637f8fbd15c134f Mon Sep 17 00:00:00 2001
From: jaylfc
Date: Sat, 13 Jun 2026 18:42:34 +0100
Subject: [PATCH 18/57] docs(mirror-policy): rename TinyAgentOS to taOS
---
docs/mirror-policy.md | 12 ++++++------
1 file changed, 6 insertions(+), 6 deletions(-)
diff --git a/docs/mirror-policy.md b/docs/mirror-policy.md
index fa36980a..f951bf8f 100644
--- a/docs/mirror-policy.md
+++ b/docs/mirror-policy.md
@@ -1,8 +1,8 @@
-# TinyAgentOS Mirror Policy
+# taOS Mirror Policy
-TinyAgentOS's verified install paths depend on binaries — runtime libraries, NPU-converted model weights, firmware — that are not part of any official OS package index. Most of these live in ad-hoc community HuggingFace repos maintained by a single contributor, or on vendor FTPs, or on forums. Any of those sources can disappear, be renamed, or be re-uploaded with different contents at any time, without warning.
+taOS's verified install paths depend on binaries — runtime libraries, NPU-converted model weights, firmware — that are not part of any official OS package index. Most of these live in ad-hoc community HuggingFace repos maintained by a single contributor, or on vendor FTPs, or on forums. Any of those sources can disappear, be renamed, or be re-uploaded with different contents at any time, without warning.
-To protect the install path from that class of failure, TinyAgentOS maintains its own binary mirrors. This document describes what we mirror, when we update, how a user can verify integrity, and how the policy applies across every accelerator class we support.
+To protect the install path from that class of failure, taOS maintains its own binary mirrors. This document describes what we mirror, when we update, how a user can verify integrity, and how the policy applies across every accelerator class we support.
## What we mirror
@@ -24,7 +24,7 @@ The same policy applies to every class. There is no "trust the upstream" tier.
## When we update the mirror
-The mirror is updated **only after re-verifying the new version end-to-end against a clean install** of TinyAgentOS on the target hardware. Specifically:
+The mirror is updated **only after re-verifying the new version end-to-end against a clean install** of taOS on the target hardware. Specifically:
1. A new upstream version is identified (new `librknnrt` build, new `dulimov/*` conversion, new in-house rkllm export, etc.).
2. The candidate file is installed on a clean TAOS image on the target SBC.
@@ -48,7 +48,7 @@ Two layers of verification, both covered by the same SHA256 hashes:
## How to self-host the same mirror
-TinyAgentOS does not want to be a single point of failure. If the upstream HuggingFace repo at `jaysom/tinyagentos-rockchip-mirror` ever becomes unreachable, or an air-gapped deployment needs a local mirror, the process is:
+taOS does not want to be a single point of failure. If the upstream HuggingFace repo at `jaysom/tinyagentos-rockchip-mirror` ever becomes unreachable, or an air-gapped deployment needs a local mirror, the process is:
1. Clone the HF mirror repo (`git clone https://huggingface.co/jaysom/tinyagentos-rockchip-mirror`) or use `huggingface-cli download` to pull every file.
2. Host the files on your own HTTP server / S3 bucket / LAN NAS / internal HF instance. Preserve the same relative paths (`librknnrt-...so`, `models/*.rkllm`).
@@ -99,6 +99,6 @@ The mirror as it stands is static: files are uploaded, SHA256s are recorded, and
- Fetches every file listed in its README.
- Verifies each file's SHA256 against the expected value.
- Checks that the URL returns a 200 and not a 404 / 403 / gateway error.
-- Surfaces pass/fail status on the public TinyAgentOS dashboard so anyone can see at a glance whether every verified install path's binary supply chain is currently healthy.
+- Surfaces pass/fail status on the public taOS dashboard so anyone can see at a glance whether every verified install path's binary supply chain is currently healthy.
This closes the loop: today the installer hard-fails if a mirror file has drifted, but only at install time. With nightly health checks, we find out before a user ever tries to install.
From 2464b9c30c9358d81cbc16ba11c673f67707adde Mon Sep 17 00:00:00 2001
From: jaylfc
Date: Sat, 13 Jun 2026 18:43:22 +0100
Subject: [PATCH 19/57] docs(status): freshness sweep complete -- dev=9c7c91fe
(18 ahead), doc renames done
---
docs/STATUS.md | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/docs/STATUS.md b/docs/STATUS.md
index 4923409a..65d20c73 100644
--- a/docs/STATUS.md
+++ b/docs/STATUS.md
@@ -1,7 +1,7 @@
SINGLE SOURCE OF TRUTH for cross-agent handoff.
Last updated: 2026-06-13, @taOS (freshness sweep).
-Branch tips: master=6394a3ed (PR #845 batch). dev=d9aab978 (14 ahead of master). On dev since #845: rkllama install fix (#843), brand rename to taOS (#847), Light theme (#848), esbuild 0.28.1 RCE fix (#849), unified chat composer (#850), Agents redesign (#851), update reset-before-pull fix (#852), Chat Slack-polish (#853), agent kill switch (#857), doc fixes -- getting-started/CONTRIBUTING/README brand rename (#858-9-60 via GitHub API, parent d9aab978).
+Branch tips: master=6394a3ed (PR #845 batch). dev=9c7c91fe (18 ahead of master). On dev since #845: rkllama install fix (#843), brand rename to taOS (#847), Light theme (#848), esbuild 0.28.1 RCE fix (#849), unified chat composer (#850), Agents redesign (#851), update reset-before-pull fix (#852), Chat Slack-polish (#853), agent kill switch (#857), doc fixes -- getting-started/CONTRIBUTING/README brand rename (#858-9-60 via GitHub API, parent 9c7c91fe).
Session state: freshness sweep only. No active session crons armed this session.
@@ -26,7 +26,7 @@ Done (since last STATUS.md update):
- ALL 26 agent jobs COMPLETE and on master (via #845 batch).
- Messages-polish (#838), agent manual templates (#842), CI parallelization (#839) all merged to dev then master.
- Light theme (#848), esbuild RCE patch (#849), brand rename (#847), chat composer unified (#850), Agents redesign (#851), update flow fix (#852), Chat Slack-polish (#853), agent kill switch (#857) all on dev.
-- This sweep: docs/STATUS.md, docs/getting-started.md, CONTRIBUTING.md, README.md brand rename residue fixed.
+- This sweep: docs/STATUS.md (dev tip + #860), docs/agent-qmd-serve-setup.md + docs/mirror-policy.md (brand rename TinyAgentOS->taOS).
Next queue:
1. Land #859 and #860 after CI + review.
From 7b6eae885e614f41e2ad514ae9ab25f0f2a7c4f0 Mon Sep 17 00:00:00 2001
From: jaylfc
Date: Sat, 13 Jun 2026 18:52:46 +0100
Subject: [PATCH 20/57] fix(agents): kill switch surfaces failures (Gitar
review follow-up on #857) (#859)
* fix(agents): surface kill failures instead of closing silently (Gitar review on #857)
- If a stop request fails, keep the dialog open and show an error + 'Try again'
rather than closing as if it succeeded (silent-failure fix).
- Retain the last target in a 'shown' state so the dialog title no longer
flashes 'Kill ?' during the close animation.
- Add a render/contract test for AgentKillSwitch.
* fix(agents): reset kill-switch error state on dialog close + reset test stubs
Address bot review: clear the failed state when the confirm dialog closes (via
Cancel or dismiss) so a stale error cannot linger into the next open; add
afterEach(unstubAllGlobals) to the test.
---
.../src/components/AgentKillSwitch.test.tsx | 26 ++++++++++
desktop/src/components/AgentKillSwitch.tsx | 47 +++++++++++++++----
2 files changed, 65 insertions(+), 8 deletions(-)
create mode 100644 desktop/src/components/AgentKillSwitch.test.tsx
diff --git a/desktop/src/components/AgentKillSwitch.test.tsx b/desktop/src/components/AgentKillSwitch.test.tsx
new file mode 100644
index 00000000..d9c15cd9
--- /dev/null
+++ b/desktop/src/components/AgentKillSwitch.test.tsx
@@ -0,0 +1,26 @@
+import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
+import { render, screen } from "@testing-library/react";
+import { AgentKillSwitch } from "./AgentKillSwitch";
+
+// Radix portals + pointer-event opening are flaky under jsdom, so this covers
+// the render contract; the open/confirm/kill paths are exercised manually.
+describe("AgentKillSwitch", () => {
+ beforeEach(() => {
+ vi.stubGlobal("fetch", vi.fn(() => Promise.resolve({ ok: true, json: () => Promise.resolve([]) })) as unknown as typeof fetch);
+ });
+ afterEach(() => {
+ vi.unstubAllGlobals();
+ });
+
+ it("renders the top-bar trigger with an accessible label", () => {
+ render( );
+ const trigger = screen.getByRole("button", { name: "Stop agents" });
+ expect(trigger).toBeInTheDocument();
+ });
+
+ it("does not open a confirmation dialog before any action", () => {
+ render( );
+ // The destructive confirm dialog is only mounted once a target is chosen.
+ expect(screen.queryByText(/Kill all agents\?/)).toBeNull();
+ });
+});
diff --git a/desktop/src/components/AgentKillSwitch.tsx b/desktop/src/components/AgentKillSwitch.tsx
index 8da39f39..4769001c 100644
--- a/desktop/src/components/AgentKillSwitch.tsx
+++ b/desktop/src/components/AgentKillSwitch.tsx
@@ -36,7 +36,17 @@ async function postStop(path: string): Promise {
export function AgentKillSwitch() {
const [agents, setAgents] = useState([]);
const [pending, setPending] = useState(null);
+ // Retains the last target so the dialog title does not flash "Kill ?" during
+ // the close animation (pending goes null before the content unmounts).
+ const [shown, setShown] = useState | null>(null);
const [busy, setBusy] = useState(false);
+ const [failed, setFailed] = useState(false);
+
+ const openConfirm = useCallback((p: Exclude) => {
+ setShown(p);
+ setFailed(false);
+ setPending(p);
+ }, []);
// Refresh the running-agent list each time the menu opens (cheap, and keeps
// the list current without a global store).
@@ -63,6 +73,7 @@ export function AgentKillSwitch() {
const confirmKill = useCallback(async () => {
if (!pending) return;
setBusy(true);
+ setFailed(false);
const ok =
pending.mode === "all"
? await postStop("/api/agents/bulk/stop")
@@ -73,8 +84,12 @@ export function AgentKillSwitch() {
setAgents((prev) =>
pending.mode === "all" ? [] : prev.filter((a) => a.name !== pending.name),
);
+ setPending(null);
+ } else {
+ // Surface the failure and keep the dialog open instead of closing as if
+ // the kill succeeded.
+ setFailed(true);
}
- setPending(null);
}, [pending]);
const menuItem =
@@ -82,9 +97,9 @@ export function AgentKillSwitch() {
const dangerItem = `${menuItem} text-red-400 hover:bg-red-500/15 focus:bg-red-500/15`;
const plainItem = `${menuItem} text-shell-text-secondary hover:bg-shell-surface-hover hover:text-shell-text focus:bg-shell-surface-hover focus:text-shell-text`;
- const dialogTitle = pending?.mode === "all" ? "Kill all agents?" : `Kill ${pending?.mode === "one" ? pending.label : ""}?`;
+ const dialogTitle = shown?.mode === "all" ? "Kill all agents?" : `Kill ${shown?.mode === "one" ? shown.label : ""}?`;
const dialogBody =
- pending?.mode === "all"
+ shown?.mode === "all"
? "Every running agent will be stopped immediately. In-flight work is lost. You can start them again from the Agents app."
: "This agent will be stopped immediately. In-flight work is lost. You can start it again from the Agents app.";
@@ -114,7 +129,7 @@ export function AgentKillSwitch() {
agents.length > 0 && setPending({ mode: "all" })}
+ onSelect={() => agents.length > 0 && openConfirm({ mode: "all" })}
>
Kill all agents
@@ -134,7 +149,7 @@ export function AgentKillSwitch() {
setPending({ mode: "one", name: a.name, label })}
+ onSelect={() => openConfirm({ mode: "one", name: a.name, label })}
>
{label}
@@ -146,7 +161,15 @@ export function AgentKillSwitch() {
- !o && setPending(null)}>
+ {
+ if (!o) {
+ setPending(null);
+ setFailed(false);
+ }
+ }}
+ >
+ {failed && (
+
+ Could not stop {shown?.mode === "all" ? "the agents" : "the agent"}. Please try again.
+
+ )}
setPending(null)}
+ onClick={() => {
+ setPending(null);
+ setFailed(false);
+ }}
disabled={busy}
className="px-3.5 py-2 rounded-lg text-sm font-medium text-shell-text-secondary hover:bg-shell-surface-hover transition-colors disabled:opacity-50"
>
@@ -177,7 +208,7 @@ export function AgentKillSwitch() {
disabled={busy}
className="px-3.5 py-2 rounded-lg text-sm font-semibold bg-red-500 text-white hover:bg-red-600 transition-colors disabled:opacity-50"
>
- {busy ? "Stopping..." : pending?.mode === "all" ? "Kill all" : "Kill agent"}
+ {busy ? "Stopping..." : failed ? "Try again" : shown?.mode === "all" ? "Kill all" : "Kill agent"}
From d09b100bb5c27d44a444110d8ba2dd84e7ba34e8 Mon Sep 17 00:00:00 2001
From: jaylfc
Date: Sat, 13 Jun 2026 18:52:48 +0100
Subject: [PATCH 21/57] feat(theme): two themes only - taOS Dark and taOS Light
(remove Matrix Terminal) (#860)
The theme chooser now shows just taOS Dark (the default) and taOS Light. Remove
the Matrix Terminal builtin and rename Default -> taOS Dark, Light -> taOS Light.
Update the theme tests that referenced matrix-terminal.
---
.../__tests__/ThemesPanel.test.tsx | 6 ++--
.../stores/__tests__/restore-theme.test.ts | 6 ++--
.../theme/__tests__/builtin-themes.test.ts | 4 +--
desktop/src/theme/builtin-themes.ts | 33 ++-----------------
4 files changed, 10 insertions(+), 39 deletions(-)
diff --git a/desktop/src/apps/SettingsApp/__tests__/ThemesPanel.test.tsx b/desktop/src/apps/SettingsApp/__tests__/ThemesPanel.test.tsx
index 8a4c7e15..344de32c 100644
--- a/desktop/src/apps/SettingsApp/__tests__/ThemesPanel.test.tsx
+++ b/desktop/src/apps/SettingsApp/__tests__/ThemesPanel.test.tsx
@@ -20,10 +20,10 @@ describe("ThemesPanel", () => {
expect(document.documentElement.style.getPropertyValue("--color-accent")).toBe("");
});
- it("shows built-in Default and Matrix Terminal even when server returns empty list", async () => {
+ it("shows built-in taOS Dark and taOS Light even when server returns empty list", async () => {
vi.stubGlobal("fetch", vi.fn().mockResolvedValue({ ok: true, json: async () => [] }));
render( );
- expect(await screen.findByText("Default")).toBeInTheDocument();
- expect(screen.getByText("Matrix Terminal")).toBeInTheDocument();
+ expect(await screen.findByText("taOS Dark")).toBeInTheDocument();
+ expect(screen.getByText("taOS Light")).toBeInTheDocument();
});
});
diff --git a/desktop/src/stores/__tests__/restore-theme.test.ts b/desktop/src/stores/__tests__/restore-theme.test.ts
index ce6a9b42..b195cbd2 100644
--- a/desktop/src/stores/__tests__/restore-theme.test.ts
+++ b/desktop/src/stores/__tests__/restore-theme.test.ts
@@ -15,15 +15,15 @@ describe("restoreActiveTheme", () => {
vi.spyOn(globalThis, "fetch").mockImplementation((input: RequestInfo | URL) => {
const url = typeof input === "string" ? input : input.toString();
if (url.includes("/api/preferences/themes")) {
- return Promise.resolve(new Response(JSON.stringify({ active_theme_id: "matrix-terminal" })));
+ return Promise.resolve(new Response(JSON.stringify({ active_theme_id: "light" })));
}
return Promise.resolve(new Response(JSON.stringify([])));
});
await restoreActiveTheme();
- expect(document.documentElement.style.getPropertyValue("--color-accent")).toBe("#00ff46");
- expect(useThemeStore.getState().activeThemeId).toBe("matrix-terminal");
+ expect(document.documentElement.style.getPropertyValue("--color-accent")).toBe("#5b6472");
+ expect(useThemeStore.getState().activeThemeId).toBe("light");
});
it("is a no-op when no active theme is saved", async () => {
diff --git a/desktop/src/theme/__tests__/builtin-themes.test.ts b/desktop/src/theme/__tests__/builtin-themes.test.ts
index 73a68a1b..e96ab1c1 100644
--- a/desktop/src/theme/__tests__/builtin-themes.test.ts
+++ b/desktop/src/theme/__tests__/builtin-themes.test.ts
@@ -4,10 +4,10 @@ import { BUILTIN_THEMES } from "../builtin-themes";
import { ALLOWED_TOKENS } from "../theme-config";
describe("builtin themes", () => {
- it("includes an undeletable Default and a Matrix Terminal", () => {
+ it("includes an undeletable taOS Dark (default) and taOS Light", () => {
const ids = BUILTIN_THEMES.map((t) => t.theme_id);
expect(ids).toContain("default");
- expect(ids).toContain("matrix-terminal");
+ expect(ids).toContain("light");
expect(BUILTIN_THEMES.find((t) => t.theme_id === "default")!.builtin).toBe(true);
});
it("only uses allowlisted tokens", () => {
diff --git a/desktop/src/theme/builtin-themes.ts b/desktop/src/theme/builtin-themes.ts
index ec1a14c3..8354c94f 100644
--- a/desktop/src/theme/builtin-themes.ts
+++ b/desktop/src/theme/builtin-themes.ts
@@ -10,13 +10,13 @@ export interface BuiltinTheme {
export const BUILTIN_THEMES: BuiltinTheme[] = [
{
theme_id: "default",
- name: "Default",
+ name: "taOS Dark",
builtin: true,
config: { tokens: {}, structure: {}, effects: [], requires: ["assistant", "launcher"], wallpaper: null },
},
{
theme_id: "light",
- name: "Light",
+ name: "taOS Light",
builtin: true,
config: {
tokens: {
@@ -55,33 +55,4 @@ export const BUILTIN_THEMES: BuiltinTheme[] = [
wallpaper: "linear-gradient(160deg, #eef0f3 0%, #e6e9ee 45%, #dee2e8 100%)",
},
},
- {
- theme_id: "matrix-terminal",
- name: "Matrix Terminal",
- builtin: true,
- config: {
- tokens: {
- "--color-shell-bg": "#000800",
- "--color-shell-bg-deep": "#000400",
- "--color-shell-surface": "rgba(0, 255, 70, 0.06)",
- "--color-shell-surface-hover": "rgba(0, 255, 70, 0.10)",
- "--color-shell-surface-active": "rgba(0, 255, 70, 0.14)",
- "--color-shell-border": "rgba(0, 255, 70, 0.18)",
- "--color-shell-border-strong": "rgba(0, 255, 70, 0.35)",
- "--color-shell-text": "#33ff66",
- "--color-shell-text-secondary": "rgba(51, 255, 102, 0.7)",
- "--color-shell-text-tertiary": "rgba(51, 255, 102, 0.45)",
- "--color-accent": "#00ff46",
- "--color-accent-glow": "rgba(0, 255, 70, 0.45)",
- "--color-dock-bg": "rgba(0, 20, 0, 0.92)",
- "--color-topbar-bg": "rgba(0, 20, 0, 0.92)",
- "--font-ui": "'JetBrains Mono', 'SF Mono', ui-monospace, monospace",
- "--font-mono": "'JetBrains Mono', 'SF Mono', ui-monospace, monospace",
- },
- structure: {},
- effects: [{ module: "crt" }, { module: "scanlines" }, { module: "glow" }],
- requires: ["assistant", "launcher"],
- wallpaper: "radial-gradient(ellipse at center, #001a00 0%, #000400 100%)",
- },
- },
];
From 794d35f149436383ed79ef0905283075fda873e9 Mon Sep 17 00:00:00 2001
From: jaylfc
Date: Sat, 13 Jun 2026 19:18:21 +0100
Subject: [PATCH 22/57] fix(agents): model chip on every agent + aligned row
columns (#861)
* fix(agents): show the model on every agent + align the row columns
- Map the agent's model from /api/agents so every agent (not just the system
agent) shows the model indicator chip.
- Give the status and actions columns fixed widths so the metadata columns line
up row to row; the protected system agent's 3 action icons now reserve the
same column as a deployed agent's 4 (right-aligned), so nothing drifts.
* fix(agents): show the model as plain text, no pill or icon
Drop the chip styling and the Cpu icon from the model label; it now reads as a
plain muted mono line under the framework sub-label.
---
desktop/src/apps/AgentsApp.tsx | 1 +
desktop/src/apps/agents/AgentRow.tsx | 41 ++++++++++------------------
2 files changed, 16 insertions(+), 26 deletions(-)
diff --git a/desktop/src/apps/AgentsApp.tsx b/desktop/src/apps/AgentsApp.tsx
index e08dfeba..f9842697 100644
--- a/desktop/src/apps/AgentsApp.tsx
+++ b/desktop/src/apps/AgentsApp.tsx
@@ -89,6 +89,7 @@ export function AgentsApp({ windowId: _windowId }: { windowId: string }) {
status: String(a.status ?? "stopped") as Agent["status"],
vectors: Number(a.vectors ?? 0),
framework: a.framework ? String(a.framework) : undefined,
+ model: a.model ? String(a.model) : undefined,
paused: Boolean(a.paused),
on_worker_failure: (a.on_worker_failure as Agent["on_worker_failure"]) ?? "pause",
fallback_models: Array.isArray(a.fallback_models) ? (a.fallback_models as string[]) : [],
diff --git a/desktop/src/apps/agents/AgentRow.tsx b/desktop/src/apps/agents/AgentRow.tsx
index ba79496c..be034ec6 100644
--- a/desktop/src/apps/agents/AgentRow.tsx
+++ b/desktop/src/apps/agents/AgentRow.tsx
@@ -1,6 +1,6 @@
import { type ReactNode } from "react";
import { useIsMobile } from "@/hooks/use-is-mobile";
-import { ScrollText, Trash2, Server, Wrench, MessageSquare, PauseCircle, RotateCcw, HardDrive, Database, Cpu } from "lucide-react";
+import { ScrollText, Trash2, Server, Wrench, MessageSquare, PauseCircle, RotateCcw, HardDrive, Database } from "lucide-react";
import { LatestVersion } from "@/lib/framework-api";
import { resolveAgentEmoji } from "@/lib/agent-emoji";
import { Button, Card } from "@/components/ui";
@@ -73,34 +73,20 @@ function PausedChip() {
);
}
-/** A single indicator chip (icon + value) for the indicators line. */
-function IndicatorChip({ icon, value, title }: { icon: ReactNode; value: string; title: string }) {
+/** The model label under an agent's identity: plain muted text, no pill or
+ * icon. Renders nothing when there is no model. */
+function IndicatorRow({ agent }: { agent: Agent }) {
+ if (!agent.model) return null;
return (
- {icon}
- {value}
+ {agent.model}
);
}
-/** The indicators line under an agent's identity: model now, room for more
- * (memory, region, ...) later. Renders nothing when there is nothing to show. */
-function IndicatorRow({ agent }: { agent: Agent }) {
- if (!agent.model) return null;
- return (
-
- }
- value={agent.model}
- title={`Model: ${agent.model}`}
- />
-
- );
-}
-
function DiskChip({ diskState, verbose }: { diskState: DiskState; verbose?: boolean }) {
const isHard = diskState.state === "hard";
return (
@@ -337,8 +323,10 @@ export function AgentRow({
{agent.host}
- {/* Status */}
-
+ {/* Status (fixed-width column so the metadata columns line up row to row) */}
+
+
+
{/* Vectors metric (de-emphasized) */}
@@ -349,8 +337,9 @@ export function AgentRow({
vectors
- {/* Actions */}
-
+ {/* Actions: fixed-width + right-aligned so a protected agent's 3 icons
+ reserve the same column as a deployed agent's 4 (no column drift). */}
+
{leftActions}
{actionButtons}
From 44fcd16325580b040318163a7ccfc3a5d7e668a8 Mon Sep 17 00:00:00 2001
From: jaylfc
Date: Sat, 13 Jun 2026 19:47:12 +0100
Subject: [PATCH 23/57] feat(github): Device-Flow connect for GitHub identities
in Secrets (#858 phase 1) (#862)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* feat(github): device-flow connect for GitHub identities in Secrets (#858 phase 1)
* fix(github): dedupe identities on reconnect + RFC slow_down backoff (Gitar review)
- Reconnecting an already-connected GitHub account (same login) refreshes the
token in place instead of creating a duplicate identity row.
- Device poll signals slow_down so the frontend increases its poll interval by
5s per RFC 8628 §3.5. Tests for both.
---
desktop/src/apps/SecretsApp.tsx | 4 +
desktop/src/apps/secrets/GitHubConnect.tsx | 247 +++++++++++++++++++++
desktop/src/lib/github.ts | 60 +++++
tests/test_github_oauth.py | 221 ++++++++++++++++++
tinyagentos/app.py | 5 +
tinyagentos/github_identities.py | 111 +++++++++
tinyagentos/github_oauth.py | 31 +++
tinyagentos/routes/__init__.py | 3 +
tinyagentos/routes/github_oauth.py | 203 +++++++++++++++++
9 files changed, 885 insertions(+)
create mode 100644 desktop/src/apps/secrets/GitHubConnect.tsx
create mode 100644 tests/test_github_oauth.py
create mode 100644 tinyagentos/github_identities.py
create mode 100644 tinyagentos/github_oauth.py
create mode 100644 tinyagentos/routes/github_oauth.py
diff --git a/desktop/src/apps/SecretsApp.tsx b/desktop/src/apps/SecretsApp.tsx
index 658b85b1..8fa52355 100644
--- a/desktop/src/apps/SecretsApp.tsx
+++ b/desktop/src/apps/SecretsApp.tsx
@@ -9,6 +9,7 @@ import {
Input,
Label,
} from "@/components/ui";
+import { GitHubConnect } from "./secrets/GitHubConnect";
/* ------------------------------------------------------------------ */
/* Types */
@@ -345,6 +346,9 @@ export function SecretsApp({ windowId: _windowId }: { windowId: string }) {
{/* Content */}
+
+
+
{loading ? (
Loading secrets...
diff --git a/desktop/src/apps/secrets/GitHubConnect.tsx b/desktop/src/apps/secrets/GitHubConnect.tsx
new file mode 100644
index 00000000..20284f52
--- /dev/null
+++ b/desktop/src/apps/secrets/GitHubConnect.tsx
@@ -0,0 +1,247 @@
+import { useState, useEffect, useCallback, useRef } from "react";
+import { Github, Copy, Check, ExternalLink, Trash2, Plus, Loader2 } from "lucide-react";
+import { Button, Card, CardContent } from "@/components/ui";
+import {
+ startDeviceFlow,
+ pollDeviceFlow,
+ listIdentities,
+ deleteIdentity,
+ type GitHubIdentity,
+} from "@/lib/github";
+
+type FlowState =
+ | { phase: "idle" }
+ | { phase: "starting" }
+ | {
+ phase: "awaiting";
+ userCode: string;
+ verificationUri: string;
+ deviceCode: string;
+ }
+ | { phase: "error"; message: string };
+
+/* ------------------------------------------------------------------ */
+/* GitHubConnect */
+/* ------------------------------------------------------------------ */
+
+export function GitHubConnect() {
+ const [identities, setIdentities] = useState
([]);
+ const [flow, setFlow] = useState({ phase: "idle" });
+ const [copied, setCopied] = useState(false);
+
+ // Refs so the polling loop can be cancelled cleanly on unmount / restart.
+ const pollTimer = useRef | null>(null);
+ const expiryTimer = useRef | null>(null);
+
+ const refreshIdentities = useCallback(async () => {
+ setIdentities(await listIdentities());
+ }, []);
+
+ useEffect(() => {
+ refreshIdentities();
+ }, [refreshIdentities]);
+
+ const stopPolling = useCallback(() => {
+ if (pollTimer.current) clearTimeout(pollTimer.current);
+ if (expiryTimer.current) clearTimeout(expiryTimer.current);
+ pollTimer.current = null;
+ expiryTimer.current = null;
+ }, []);
+
+ useEffect(() => stopPolling, [stopPolling]);
+
+ const beginPolling = useCallback(
+ (deviceCode: string, intervalSec: number, expiresInSec: number) => {
+ let intervalMs = Math.max(intervalSec, 1) * 1000;
+
+ const tick = async () => {
+ const result = await pollDeviceFlow(deviceCode);
+ if (result.status === "connected") {
+ stopPolling();
+ setFlow({ phase: "idle" });
+ await refreshIdentities();
+ return;
+ }
+ if (result.status === "error") {
+ stopPolling();
+ setFlow({
+ phase: "error",
+ message:
+ result.error === "expired_token"
+ ? "The code expired. Please try again."
+ : result.error === "access_denied"
+ ? "Authorization was denied."
+ : "Could not connect. Please try again.",
+ });
+ return;
+ }
+ // pending -> back off by 5s on slow_down (RFC 8628 §3.5), then poll again
+ if ("slow_down" in result && result.slow_down) {
+ intervalMs += 5000;
+ }
+ pollTimer.current = setTimeout(tick, intervalMs);
+ };
+
+ pollTimer.current = setTimeout(tick, intervalMs);
+ expiryTimer.current = setTimeout(() => {
+ stopPolling();
+ setFlow({ phase: "error", message: "The code expired. Please try again." });
+ }, expiresInSec * 1000);
+ },
+ [refreshIdentities, stopPolling],
+ );
+
+ const handleConnect = useCallback(async () => {
+ stopPolling();
+ setCopied(false);
+ setFlow({ phase: "starting" });
+ try {
+ const start = await startDeviceFlow();
+ setFlow({
+ phase: "awaiting",
+ userCode: start.user_code,
+ verificationUri: start.verification_uri,
+ deviceCode: start.device_code,
+ });
+ beginPolling(start.device_code, start.interval, start.expires_in);
+ } catch {
+ setFlow({ phase: "error", message: "Could not start the connect flow. Please try again." });
+ }
+ }, [beginPolling, stopPolling]);
+
+ const handleCancel = useCallback(() => {
+ stopPolling();
+ setFlow({ phase: "idle" });
+ }, [stopPolling]);
+
+ const handleCopy = useCallback(async (code: string) => {
+ try {
+ await navigator.clipboard.writeText(code);
+ setCopied(true);
+ setTimeout(() => setCopied(false), 2000);
+ } catch {
+ /* clipboard may be unavailable; ignore */
+ }
+ }, []);
+
+ const handleRemove = useCallback(
+ async (id: string) => {
+ if (await deleteIdentity(id)) await refreshIdentities();
+ },
+ [refreshIdentities],
+ );
+
+ return (
+
+
+ {/* Header */}
+
+
+
+
GitHub
+
+ {identities.length} connected
+
+
+ {flow.phase !== "awaiting" && flow.phase !== "starting" && (
+
+
+ Connect GitHub account
+
+ )}
+
+
+ {/* Flow card */}
+ {flow.phase === "starting" && (
+
+
+ Starting...
+
+ )}
+
+ {flow.phase === "awaiting" && (
+
+
+ Enter this code on GitHub to authorize taOS:
+
+
+
+ {flow.userCode}
+
+ handleCopy(flow.userCode)}
+ aria-label="Copy code"
+ title="Copy code"
+ className="h-8 w-8"
+ >
+ {copied ? : }
+
+
+
+
+
+ Waiting for you to authorize on GitHub...
+
+
+ )}
+
+ {flow.phase === "error" && (
+
+ {flow.message}
+
+ )}
+
+ {/* Connected identities */}
+ {identities.length > 0 && (
+
+ {identities.map((id) => (
+
+
+ {id.avatar_url ? (
+
+ ) : (
+
+
+
+ )}
+
+ {id.login}
+
+
+ handleRemove(id.id)}
+ className="h-7 w-7 hover:text-red-400 hover:bg-red-500/15"
+ aria-label={`Remove ${id.login}`}
+ title="Remove"
+ >
+
+
+
+ ))}
+
+ )}
+
+
+ );
+}
diff --git a/desktop/src/lib/github.ts b/desktop/src/lib/github.ts
index ef57f442..fb56b44f 100644
--- a/desktop/src/lib/github.ts
+++ b/desktop/src/lib/github.ts
@@ -1,3 +1,5 @@
+import { withCsrf } from "./csrf";
+
/* ------------------------------------------------------------------ */
/* Types */
/* ------------------------------------------------------------------ */
@@ -51,6 +53,26 @@ export interface GitHubAuthStatus {
method?: string;
}
+export interface GitHubIdentity {
+ id: string;
+ login: string;
+ avatar_url: string;
+ created_at: number;
+}
+
+export interface DeviceStart {
+ user_code: string;
+ verification_uri: string;
+ device_code: string;
+ interval: number;
+ expires_in: number;
+}
+
+export type DevicePoll =
+ | { status: "pending"; slow_down?: boolean }
+ | { status: "connected"; identity: GitHubIdentity }
+ | { status: "error"; error: string };
+
/* ------------------------------------------------------------------ */
/* Helpers */
/* ------------------------------------------------------------------ */
@@ -197,3 +219,41 @@ export async function saveToLibrary(url: string): Promise<{ id: string; status:
source: "github-browser",
}, null);
}
+
+/* ------------------------------------------------------------------ */
+/* OAuth Device Flow (Connect GitHub) */
+/* ------------------------------------------------------------------ */
+
+export async function startDeviceFlow(): Promise {
+ const res = await fetch(
+ "/api/github/oauth/device/start",
+ withCsrf({ method: "POST", headers: { Accept: "application/json" } }),
+ );
+ if (!res.ok) throw new Error("Failed to start GitHub connect");
+ return res.json();
+}
+
+export async function pollDeviceFlow(deviceCode: string): Promise {
+ const res = await fetch(
+ "/api/github/oauth/device/poll",
+ withCsrf({
+ method: "POST",
+ headers: { "Content-Type": "application/json", Accept: "application/json" },
+ body: JSON.stringify({ device_code: deviceCode }),
+ }),
+ );
+ if (!res.ok) return { status: "error", error: "poll_failed" };
+ return res.json();
+}
+
+export async function listIdentities(): Promise {
+ return fetchJson("/api/github/identities", []);
+}
+
+export async function deleteIdentity(id: string): Promise {
+ const res = await fetch(
+ `/api/github/identities/${encodeURIComponent(id)}`,
+ withCsrf({ method: "DELETE", headers: { Accept: "application/json" } }),
+ );
+ return res.ok;
+}
diff --git a/tests/test_github_oauth.py b/tests/test_github_oauth.py
new file mode 100644
index 00000000..3670cece
--- /dev/null
+++ b/tests/test_github_oauth.py
@@ -0,0 +1,221 @@
+"""Tests for the GitHub OAuth device-flow routes + identities store.
+
+A minimal FastAPI app with only the github_oauth router mounted, plus a real
+GitHubIdentitiesStore on a tmp_path DB, so the tests run fast without the full
+create_app initialisation.
+"""
+from __future__ import annotations
+
+import json
+from unittest.mock import AsyncMock, MagicMock
+
+import pytest
+import pytest_asyncio
+from fastapi import FastAPI
+from httpx import ASGITransport, AsyncClient
+
+from tinyagentos.github_identities import GitHubIdentitiesStore
+from tinyagentos.routes.github_oauth import router as github_oauth_router
+
+
+def _make_response(data, status_code: int = 200):
+ resp = MagicMock()
+ resp.status_code = status_code
+ resp.json = MagicMock(return_value=data)
+ resp.text = json.dumps(data) if isinstance(data, (dict, list)) else str(data)
+ resp.raise_for_status = MagicMock()
+ return resp
+
+
+@pytest_asyncio.fixture
+async def store(tmp_path):
+ s = GitHubIdentitiesStore(tmp_path / "github_identities.db")
+ await s.init()
+ yield s
+ await s.close()
+
+
+def _build_app(store, *, post_effects=(), get_effects=()):
+ app = FastAPI()
+ app.include_router(github_oauth_router)
+ http = MagicMock()
+ http.post = AsyncMock(side_effect=list(post_effects)) if post_effects else AsyncMock()
+ http.get = AsyncMock(side_effect=list(get_effects)) if get_effects else AsyncMock()
+ app.state.http_client = http
+ app.state.github_identities = store
+ return app
+
+
+@pytest_asyncio.fixture
+async def client_factory(store):
+ clients = []
+
+ async def _make(**kwargs):
+ app = _build_app(store, **kwargs)
+ transport = ASGITransport(app=app)
+ c = AsyncClient(transport=transport, base_url="http://test")
+ clients.append(c)
+ return c
+
+ yield _make
+ for c in clients:
+ await c.aclose()
+
+
+# ---------------------------------------------------------------------------
+# device/start
+# ---------------------------------------------------------------------------
+
+@pytest.mark.asyncio
+async def test_device_start_returns_user_code(client_factory):
+ gh = _make_response({
+ "device_code": "DEV123",
+ "user_code": "WXYZ-1234",
+ "verification_uri": "https://github.com/login/device",
+ "interval": 5,
+ "expires_in": 900,
+ })
+ c = await client_factory(post_effects=[gh])
+ resp = await c.post("/api/github/oauth/device/start")
+ assert resp.status_code == 200
+ data = resp.json()
+ assert data["user_code"] == "WXYZ-1234"
+ assert data["device_code"] == "DEV123"
+ assert data["verification_uri"] == "https://github.com/login/device"
+ assert data["interval"] == 5
+
+
+@pytest.mark.asyncio
+async def test_device_start_bad_response_returns_502(client_factory):
+ gh = _make_response({"error": "invalid_client", "error_description": "Bad client"})
+ c = await client_factory(post_effects=[gh])
+ resp = await c.post("/api/github/oauth/device/start")
+ assert resp.status_code == 502
+
+
+# ---------------------------------------------------------------------------
+# device/poll
+# ---------------------------------------------------------------------------
+
+@pytest.mark.asyncio
+async def test_device_poll_pending(client_factory):
+ gh = _make_response({"error": "authorization_pending"})
+ c = await client_factory(post_effects=[gh])
+ resp = await c.post("/api/github/oauth/device/poll", json={"device_code": "DEV123"})
+ assert resp.status_code == 200
+ assert resp.json()["status"] == "pending"
+
+
+@pytest.mark.asyncio
+async def test_device_poll_slow_down_is_pending(client_factory):
+ gh = _make_response({"error": "slow_down"})
+ c = await client_factory(post_effects=[gh])
+ resp = await c.post("/api/github/oauth/device/poll", json={"device_code": "DEV123"})
+ body = resp.json()
+ assert body["status"] == "pending"
+ # The frontend backs off its poll interval when slow_down is signalled.
+ assert body.get("slow_down") is True
+
+
+@pytest.mark.asyncio
+async def test_reconnect_same_login_updates_not_duplicates(store):
+ first = await store.add("octocat", "a1", "gho_token1", "repo")
+ second = await store.add("octocat", "a2", "gho_token2", "repo")
+ # Same login -> same row refreshed in place, no duplicate.
+ assert first["id"] == second["id"]
+ assert second["avatar_url"] == "a2"
+ identities = await store.list()
+ assert len(identities) == 1
+ assert await store.get_token(first["id"]) == "gho_token2"
+
+
+@pytest.mark.asyncio
+async def test_device_poll_connected_stores_identity(client_factory, store):
+ token_resp = _make_response({"access_token": "gho_secrettoken", "scope": "repo,read:user"})
+ user_resp = _make_response({"login": "octocat", "avatar_url": "https://avatars/octocat.png"})
+ c = await client_factory(post_effects=[token_resp], get_effects=[user_resp])
+
+ resp = await c.post("/api/github/oauth/device/poll", json={"device_code": "DEV123"})
+ assert resp.status_code == 200
+ data = resp.json()
+ assert data["status"] == "connected"
+ assert data["identity"]["login"] == "octocat"
+ assert data["identity"]["avatar_url"] == "https://avatars/octocat.png"
+ # No token must ever appear in the response payload.
+ assert "token" not in json.dumps(data)
+
+ # The token was stored encrypted and is retrievable internally.
+ identities = await store.list()
+ assert len(identities) == 1
+ identity_id = identities[0]["id"]
+ assert await store.get_token(identity_id) == "gho_secrettoken"
+
+
+@pytest.mark.asyncio
+async def test_device_poll_expired_is_error(client_factory):
+ gh = _make_response({"error": "expired_token"})
+ c = await client_factory(post_effects=[gh])
+ resp = await c.post("/api/github/oauth/device/poll", json={"device_code": "DEV123"})
+ data = resp.json()
+ assert data["status"] == "error"
+ assert data["error"] == "expired_token"
+
+
+# ---------------------------------------------------------------------------
+# identities list / delete
+# ---------------------------------------------------------------------------
+
+@pytest.mark.asyncio
+async def test_identities_list_excludes_token(client_factory, store):
+ await store.add("octocat", "https://avatars/octocat.png", "gho_secrettoken", "repo")
+ c = await client_factory()
+ resp = await c.get("/api/github/identities")
+ assert resp.status_code == 200
+ items = resp.json()
+ assert len(items) == 1
+ assert set(items[0].keys()) == {"id", "login", "avatar_url", "created_at"}
+ assert "token" not in json.dumps(items)
+ assert "gho_secrettoken" not in json.dumps(items)
+
+
+@pytest.mark.asyncio
+async def test_delete_identity(client_factory, store):
+ identity = await store.add("octocat", "", "gho_secrettoken", "repo")
+ c = await client_factory()
+ resp = await c.delete(f"/api/github/identities/{identity['id']}")
+ assert resp.status_code == 200
+ assert resp.json()["status"] == "deleted"
+ assert await store.list() == []
+
+
+@pytest.mark.asyncio
+async def test_delete_identity_invalid_uuid_returns_400(client_factory):
+ c = await client_factory()
+ resp = await c.delete("/api/github/identities/not-a-uuid")
+ assert resp.status_code == 400
+
+
+@pytest.mark.asyncio
+async def test_delete_identity_not_found_returns_404(client_factory):
+ import uuid as _uuid
+ c = await client_factory()
+ resp = await c.delete(f"/api/github/identities/{_uuid.uuid4()}")
+ assert resp.status_code == 404
+
+
+# ---------------------------------------------------------------------------
+# Store: token encryption at rest
+# ---------------------------------------------------------------------------
+
+@pytest.mark.asyncio
+async def test_token_encrypted_at_rest(store):
+ identity = await store.add("octocat", "", "gho_plaintexttoken", "repo")
+ async with store._db.execute(
+ "SELECT token FROM github_identities WHERE id = ?", (identity["id"],)
+ ) as cur:
+ row = await cur.fetchone()
+ # Raw DB value must NOT be the plaintext token.
+ assert row[0] != "gho_plaintexttoken"
+ assert "gho_plaintexttoken" not in row[0]
+ # And get_token decrypts it back.
+ assert await store.get_token(identity["id"]) == "gho_plaintexttoken"
diff --git a/tinyagentos/app.py b/tinyagentos/app.py
index 0768ba32..7c52fad6 100644
--- a/tinyagentos/app.py
+++ b/tinyagentos/app.py
@@ -58,6 +58,7 @@ async def get_response(self, path, scope):
from tinyagentos.scheduler.discovery import build_scheduler as build_resource_scheduler
from tinyagentos.torrent_settings import TorrentSettingsStore
from tinyagentos.relationships import RelationshipManager
+from tinyagentos.github_identities import GitHubIdentitiesStore
from tinyagentos.secrets import SecretsStore
from tinyagentos.training import TrainingManager
from tinyagentos.conversion import ConversionManager
@@ -260,6 +261,7 @@ def create_app(data_dir: Path | None = None, catalog_dir: Path | None = None) ->
torrent_settings_store = TorrentSettingsStore(data_dir / "torrent_settings.json")
download_manager = DownloadManager(torrent_settings_store=torrent_settings_store)
secrets_store = SecretsStore(data_dir / "secrets.db")
+ github_identities_store = GitHubIdentitiesStore(data_dir / "github_identities.db")
relationship_mgr = RelationshipManager(data_dir / "relationships.db")
channel_store = ChannelStore(data_dir / "channels.db")
scheduler = TaskScheduler(data_dir / "scheduler.db")
@@ -388,6 +390,7 @@ async def lifespan(app: FastAPI):
await notif_store.init()
await qmd_client.init()
await secrets_store.init()
+ await github_identities_store.init()
await relationship_mgr.init()
await channel_store.init()
await scheduler.init()
@@ -595,6 +598,7 @@ async def _browser_reap_loop(app: FastAPI) -> None:
app.state.download_manager = download_manager
app.state.torrent_settings_store = torrent_settings_store
app.state.secrets = secrets_store
+ app.state.github_identities = github_identities_store
app.state.relationships = relationship_mgr
app.state.channels = channel_store
app.state.fallback = fallback
@@ -1185,6 +1189,7 @@ async def dispatch(self, request, call_next):
app.state.http_client = http_client
app.state.download_manager = download_manager
app.state.secrets = secrets_store
+ app.state.github_identities = github_identities_store
app.state.relationships = relationship_mgr
app.state.channels = channel_store
app.state.fallback = fallback
diff --git a/tinyagentos/github_identities.py b/tinyagentos/github_identities.py
new file mode 100644
index 00000000..e3dc42c0
--- /dev/null
+++ b/tinyagentos/github_identities.py
@@ -0,0 +1,111 @@
+"""Store for connected GitHub identities (OAuth device-flow tokens).
+
+Tokens are encrypted at rest with the exact same Fernet helper the
+``SecretsStore`` uses (``.secrets_key`` in the data dir) — no new crypto is
+introduced. The token is NEVER returned by :meth:`list`; only
+:meth:`get_token` (an internal helper) exposes the plaintext.
+"""
+from __future__ import annotations
+
+import time
+import uuid
+from pathlib import Path
+
+from tinyagentos.base_store import BaseStore
+from tinyagentos.secrets import _decrypt, _encrypt
+
+GITHUB_IDENTITIES_SCHEMA = """
+CREATE TABLE IF NOT EXISTS github_identities (
+ id TEXT PRIMARY KEY,
+ login TEXT NOT NULL,
+ avatar_url TEXT NOT NULL DEFAULT '',
+ token TEXT NOT NULL,
+ scopes TEXT NOT NULL DEFAULT '',
+ created_at INTEGER NOT NULL
+);
+"""
+
+
+class GitHubIdentitiesStore(BaseStore):
+ SCHEMA = GITHUB_IDENTITIES_SCHEMA
+
+ def __init__(self, db_path: Path):
+ super().__init__(db_path)
+ # Reuse the secrets key dir (same .secrets_key) so tokens use the same
+ # encryption key as the rest of taOS.
+ self._key_dir: Path = Path(db_path).parent
+
+ async def add(
+ self,
+ login: str,
+ avatar_url: str,
+ token: str,
+ scopes: str = "",
+ ) -> dict:
+ """Store an identity. Returns the public fields (never the token).
+
+ Reconnecting an already-connected account (same login) refreshes the
+ token in place rather than creating a duplicate row.
+ """
+ encrypted = _encrypt(token, self._key_dir)
+ async with self._db.execute(
+ "SELECT id, created_at FROM github_identities WHERE login = ?", (login,)
+ ) as cursor:
+ existing = await cursor.fetchone()
+ if existing:
+ identity_id, created_at = existing[0], existing[1]
+ await self._db.execute(
+ "UPDATE github_identities SET avatar_url = ?, token = ?, scopes = ? "
+ "WHERE id = ?",
+ (avatar_url, encrypted, scopes, identity_id),
+ )
+ await self._db.commit()
+ return {
+ "id": identity_id,
+ "login": login,
+ "avatar_url": avatar_url,
+ "created_at": created_at,
+ }
+ identity_id = str(uuid.uuid4())
+ now = int(time.time())
+ await self._db.execute(
+ "INSERT INTO github_identities (id, login, avatar_url, token, scopes, created_at) "
+ "VALUES (?, ?, ?, ?, ?, ?)",
+ (identity_id, login, avatar_url, encrypted, scopes, now),
+ )
+ await self._db.commit()
+ return {
+ "id": identity_id,
+ "login": login,
+ "avatar_url": avatar_url,
+ "created_at": now,
+ }
+
+ async def list(self) -> list[dict]:
+ """Return all identities WITHOUT tokens (id/login/avatar_url/created_at)."""
+ async with self._db.execute(
+ "SELECT id, login, avatar_url, created_at FROM github_identities "
+ "ORDER BY created_at DESC"
+ ) as cursor:
+ rows = await cursor.fetchall()
+ return [
+ {"id": r[0], "login": r[1], "avatar_url": r[2], "created_at": r[3]}
+ for r in rows
+ ]
+
+ async def get_token(self, identity_id: str) -> str | None:
+ """Internal: return the decrypted token for *identity_id* or None."""
+ async with self._db.execute(
+ "SELECT token FROM github_identities WHERE id = ?", (identity_id,)
+ ) as cursor:
+ row = await cursor.fetchone()
+ if not row:
+ return None
+ return _decrypt(row[0], self._key_dir)
+
+ async def delete(self, identity_id: str) -> bool:
+ cursor = await self._db.execute(
+ "DELETE FROM github_identities WHERE id = ?", (identity_id,)
+ )
+ await self._db.commit()
+ return cursor.rowcount > 0
diff --git a/tinyagentos/github_oauth.py b/tinyagentos/github_oauth.py
new file mode 100644
index 00000000..1b5edbfa
--- /dev/null
+++ b/tinyagentos/github_oauth.py
@@ -0,0 +1,31 @@
+"""GitHub OAuth Device Flow configuration.
+
+taOS is self-hosted; instances have no fixed OAuth callback URL, so we use
+GitHub's OAuth *Device Flow*, which needs only the public Client ID (no client
+secret). Public OAuth Client IDs are safe to ship in source — they are
+exposed to every browser that starts the flow and carry no privilege on their
+own.
+"""
+from __future__ import annotations
+
+import os
+
+# The single taOS OAuth App Client ID. Public and safe in source.
+# Override per-instance with the GITHUB_OAUTH_CLIENT_ID env var.
+DEFAULT_CLIENT_ID = "Ov23licVGSIqagQLXAqb"
+
+# Device-flow scopes: repo access + read the authenticated user's profile.
+DEVICE_FLOW_SCOPE = "repo read:user"
+
+# GitHub endpoints.
+DEVICE_CODE_URL = "https://github.com/login/device/code"
+ACCESS_TOKEN_URL = "https://github.com/login/oauth/access_token"
+USER_URL = "https://api.github.com/user"
+
+# device_code grant type per RFC 8628.
+DEVICE_GRANT_TYPE = "urn:ietf:params:oauth:grant-type:device_code"
+
+
+def client_id() -> str:
+ """Return the configured GitHub OAuth Client ID."""
+ return os.environ.get("GITHUB_OAUTH_CLIENT_ID", DEFAULT_CLIENT_ID)
diff --git a/tinyagentos/routes/__init__.py b/tinyagentos/routes/__init__.py
index 5fdfd4f9..0bc0c7fa 100644
--- a/tinyagentos/routes/__init__.py
+++ b/tinyagentos/routes/__init__.py
@@ -184,6 +184,9 @@ def register_all_routers(app):
from tinyagentos.routes.github import router as github_router
app.include_router(github_router)
+ from tinyagentos.routes.github_oauth import router as github_oauth_router
+ app.include_router(github_oauth_router)
+
from tinyagentos.routes.youtube import router as youtube_router
app.include_router(youtube_router)
diff --git a/tinyagentos/routes/github_oauth.py b/tinyagentos/routes/github_oauth.py
new file mode 100644
index 00000000..ec774a00
--- /dev/null
+++ b/tinyagentos/routes/github_oauth.py
@@ -0,0 +1,203 @@
+"""API routes for the GitHub OAuth Device Flow ("Connect GitHub").
+
+taOS instances have no fixed callback URL, so we use GitHub's OAuth Device
+Flow (RFC 8628), which needs only the public Client ID — no client secret.
+
+Routes (all under /api/github/):
+- POST /oauth/device/start -> begin the flow, return the user_code + URLs
+- POST /oauth/device/poll -> poll once for the token; store identity on success
+- GET /identities -> list connected identities (NO tokens)
+- DELETE /identities/{id} -> remove an identity
+
+SECURITY: tokens are encrypted at rest (Fernet, shared secrets key) and are
+NEVER logged or returned by any endpoint.
+"""
+from __future__ import annotations
+
+import logging
+import uuid
+
+from fastapi import APIRouter, Request
+from fastapi.responses import JSONResponse
+from pydantic import BaseModel
+
+from tinyagentos.github_oauth import (
+ ACCESS_TOKEN_URL,
+ DEVICE_CODE_URL,
+ DEVICE_FLOW_SCOPE,
+ DEVICE_GRANT_TYPE,
+ USER_URL,
+ client_id,
+)
+
+logger = logging.getLogger(__name__)
+
+router = APIRouter()
+
+_TIMEOUT = 10.0
+_JSON = {"Accept": "application/json"}
+
+# Token-endpoint errors that mean "keep waiting" vs. "give up".
+_PENDING_ERRORS = {"authorization_pending", "slow_down"}
+_TERMINAL_ERRORS = {"expired_token", "access_denied", "unsupported_grant_type"}
+
+
+class DevicePollBody(BaseModel):
+ device_code: str
+
+
+def _http(request: Request):
+ return request.app.state.http_client
+
+
+def _identities_store(request: Request):
+ return getattr(request.app.state, "github_identities", None)
+
+
+# ---------------------------------------------------------------------------
+# Device flow: start
+# ---------------------------------------------------------------------------
+
+@router.post("/api/github/oauth/device/start")
+async def device_start(request: Request):
+ """Begin the device flow. Returns user_code, verification_uri, device_code."""
+ http = _http(request)
+ try:
+ resp = await http.post(
+ DEVICE_CODE_URL,
+ data={"client_id": client_id(), "scope": DEVICE_FLOW_SCOPE},
+ headers=_JSON,
+ timeout=_TIMEOUT,
+ )
+ resp.raise_for_status()
+ data = resp.json()
+ except Exception as exc:
+ logger.exception("github device/start failed: %s", exc)
+ return JSONResponse(
+ {"error": "Failed to start GitHub device flow"}, status_code=502
+ )
+
+ if "device_code" not in data or "user_code" not in data:
+ # GitHub returns {error: ...} on bad client_id etc. Never echo secrets.
+ logger.warning("github device/start unexpected response: %s", data.get("error"))
+ return JSONResponse(
+ {"error": data.get("error_description") or "GitHub did not return a device code"},
+ status_code=502,
+ )
+
+ # device_code is returned to the client so it can poll; this is standard
+ # per the protocol and is not a long-lived credential.
+ return {
+ "user_code": data["user_code"],
+ "verification_uri": data.get("verification_uri", "https://github.com/login/device"),
+ "device_code": data["device_code"],
+ "interval": data.get("interval", 5),
+ "expires_in": data.get("expires_in", 900),
+ }
+
+
+# ---------------------------------------------------------------------------
+# Device flow: poll (single poll per call; frontend drives the loop)
+# ---------------------------------------------------------------------------
+
+@router.post("/api/github/oauth/device/poll")
+async def device_poll(request: Request, body: DevicePollBody):
+ """Poll the token endpoint once for *device_code*.
+
+ - access_token -> fetch the user, store the identity, status="connected"
+ - authorization_pending / slow_down -> status="pending"
+ - expired_token / access_denied -> status="error"
+ """
+ http = _http(request)
+ try:
+ resp = await http.post(
+ ACCESS_TOKEN_URL,
+ data={
+ "client_id": client_id(),
+ "device_code": body.device_code,
+ "grant_type": DEVICE_GRANT_TYPE,
+ },
+ headers=_JSON,
+ timeout=_TIMEOUT,
+ )
+ resp.raise_for_status()
+ data = resp.json()
+ except Exception as exc:
+ logger.exception("github device/poll failed: %s", exc)
+ return JSONResponse(
+ {"status": "error", "error": "poll_failed"}, status_code=502
+ )
+
+ access_token = data.get("access_token")
+ if access_token:
+ scopes = data.get("scope", "")
+ try:
+ user_resp = await http.get(
+ USER_URL,
+ headers={"Accept": "application/json", "Authorization": f"Bearer {access_token}"},
+ timeout=_TIMEOUT,
+ )
+ user_resp.raise_for_status()
+ user = user_resp.json()
+ except Exception as exc:
+ # Do NOT log the token. Only log that the user lookup failed.
+ logger.exception("github user lookup after device flow failed: %s", exc)
+ return JSONResponse(
+ {"status": "error", "error": "user_lookup_failed"}, status_code=502
+ )
+
+ store = _identities_store(request)
+ if store is None:
+ logger.error("github_identities store not configured")
+ return JSONResponse(
+ {"status": "error", "error": "store_unavailable"}, status_code=500
+ )
+
+ identity = await store.add(
+ login=user.get("login", ""),
+ avatar_url=user.get("avatar_url", ""),
+ token=access_token,
+ scopes=scopes,
+ )
+ return {"status": "connected", "identity": identity}
+
+ error = data.get("error", "")
+ if error == "slow_down":
+ # RFC 8628 §3.5: the client must back off. Signal the frontend to add
+ # to its poll interval.
+ return {"status": "pending", "slow_down": True}
+ if error in _PENDING_ERRORS:
+ return {"status": "pending"}
+ if error in _TERMINAL_ERRORS:
+ return {"status": "error", "error": error}
+ # Unknown error shape — surface generically without leaking detail.
+ return {"status": "error", "error": error or "unknown"}
+
+
+# ---------------------------------------------------------------------------
+# Identities: list / delete (NO tokens ever returned)
+# ---------------------------------------------------------------------------
+
+@router.get("/api/github/identities")
+async def list_identities(request: Request):
+ store = _identities_store(request)
+ if store is None:
+ return []
+ return await store.list()
+
+
+@router.delete("/api/github/identities/{identity_id}")
+async def delete_identity(request: Request, identity_id: str):
+ # Validate the path param is a UUID before touching the store.
+ try:
+ uuid.UUID(identity_id)
+ except ValueError:
+ return JSONResponse({"error": "Invalid identity id"}, status_code=400)
+
+ store = _identities_store(request)
+ if store is None:
+ return JSONResponse({"error": "Store unavailable"}, status_code=500)
+ deleted = await store.delete(identity_id)
+ if not deleted:
+ return JSONResponse({"error": "Identity not found"}, status_code=404)
+ return {"status": "deleted"}
From 8c272cb90c7ee694d01ffcf38f410f35a359ee43 Mon Sep 17 00:00:00 2001
From: jaylfc
Date: Sat, 13 Jun 2026 19:48:02 +0100
Subject: [PATCH 24/57] docs(status): wind-down at 92% 5h usage; wake queue =
macOS-dark palette, mobile audit, wallpaper Phase 1, island v2, GitHub phases
---
docs/STATUS.md | 13 ++++++++++---
1 file changed, 10 insertions(+), 3 deletions(-)
diff --git a/docs/STATUS.md b/docs/STATUS.md
index 65d20c73..7252c68e 100644
--- a/docs/STATUS.md
+++ b/docs/STATUS.md
@@ -1,9 +1,16 @@
SINGLE SOURCE OF TRUTH for cross-agent handoff.
-Last updated: 2026-06-13, @taOS (freshness sweep).
+Last updated: 2026-06-13 ~18:46, @taOS (wind-down at 5h usage 92%).
-Branch tips: master=6394a3ed (PR #845 batch). dev=9c7c91fe (18 ahead of master). On dev since #845: rkllama install fix (#843), brand rename to taOS (#847), Light theme (#848), esbuild 0.28.1 RCE fix (#849), unified chat composer (#850), Agents redesign (#851), update reset-before-pull fix (#852), Chat Slack-polish (#853), agent kill switch (#857), doc fixes -- getting-started/CONTRIBUTING/README brand rename (#858-9-60 via GitHub API, parent 9c7c91fe).
+▶ WAKE QUEUE (resume here after the 19:40 UTC 5h reset, Jay's active queue):
+1. THEME: move taOS Dark off the blue-purple base (#1a1b2e) to macOS dark-mode NEUTRALS (#865). Edit dark @theme tokens in desktop/src/theme/tokens.css + retune WALLPAPERS off indigo; accent stays neutral grey; keep Light theme; verify contrast.
+2. MOBILE AUDIT: check the Agents app + chat/Messages + composer look right on mobile (Jay flagged).
+3. WALLPAPER picker Phase 1 (#864): reorg into Theme-default / Built-in / Your-wallpapers(+upload) sections + a Settings entry point; Phase 2 = Wallhaven KEYLESS browse + optional API-key entry. Mock approved.
+4. DYNAMIC ISLAND v2 (#854): design+mocks approved (island holds agent+search, agent chat bubble replaces side panel + poppable window, search bubble, Mac animations). Build plan then build.
+5. GITHUB (#858): Phase 1 connect flow MERGED (#862). Next: Phase 2 time-scoped sharing + consent picker; Phase 3 agent access-request + runtime token injection; Phase 4 fork->PR ops. OAuth app registered (Client ID Ov23licVGSIqagQLXAqb public/in-source; secret stays host-side, NOT in repo; device flow needs no secret).
-Session state: freshness sweep only. No active session crons armed this session.
+Branch tips: master=6394a3ed. dev advanced well past #845: merged this session = #843 #847 #848 #849 #850 #851 #852 #853 #857 #859 #860 #861 #862. OPEN PR baking: #863 (agents spacing + System-agent-label removal; merge on green).
+
+Session state: wind-down. Merge #863 when green. Resume the WAKE QUEUE after reset.
WEBSITE: taos.my live. All 4 taos-website PRs merged (stats/changelog/nav/accessibility).
From 7f14fc885a65cdb36ecfd6d4de4fd62edd521b66 Mon Sep 17 00:00:00 2001
From: jaylfc
Date: Sat, 13 Jun 2026 20:42:17 +0100
Subject: [PATCH 25/57] fix(agents): remove the 'System agent' label + even out
card spacing (#863)
Drop the SYSTEM AGENT group title (all three render branches) and give every
card a uniform gap: the system agent gets mb-3 above the deployed list, and the
deployed list is space-y-3 to match.
---
desktop/src/apps/AgentsApp.tsx | 30 ++++++++++++++----------------
1 file changed, 14 insertions(+), 16 deletions(-)
diff --git a/desktop/src/apps/AgentsApp.tsx b/desktop/src/apps/AgentsApp.tsx
index f9842697..84966846 100644
--- a/desktop/src/apps/AgentsApp.tsx
+++ b/desktop/src/apps/AgentsApp.tsx
@@ -497,7 +497,6 @@ export function AgentsApp({ windowId: _windowId }: { windowId: string }) {
) : agents.length === 0 && archived.length === 0 ? (
-
System agent
) : agents.length === 0 ? (
-
System agent
) : (
- {/* System agent — always shown above the deployed agents list */}
-
System agent
-
setTaosDetailOpen(true)}
- onViewSkills={() => setTaosDetailOpen(true)}
- onViewMessages={() => setTaosDetailOpen(true)}
- onDelete={() => {}}
- onResume={() => {}}
- protected
- />
+
+
setTaosDetailOpen(true)}
+ onViewSkills={() => setTaosDetailOpen(true)}
+ onViewMessages={() => setTaosDetailOpen(true)}
+ onDelete={() => {}}
+ onResume={() => {}}
+ protected
+ />
+
{/* Disk quota notification cards */}
{agents
@@ -615,7 +613,7 @@ export function AgentsApp({ windowId: _windowId }: { windowId: string }) {
);
})}
-
+
{agents.map((agent) => (
Date: Sat, 13 Jun 2026 21:27:33 +0100
Subject: [PATCH 26/57] docs(status): resumed post-reset; #863 merged,
#867+#868 open, queue advanced
---
docs/STATUS.md | 24 +++++++++++++-----------
1 file changed, 13 insertions(+), 11 deletions(-)
diff --git a/docs/STATUS.md b/docs/STATUS.md
index 7252c68e..955df2f0 100644
--- a/docs/STATUS.md
+++ b/docs/STATUS.md
@@ -1,26 +1,28 @@
SINGLE SOURCE OF TRUTH for cross-agent handoff.
-Last updated: 2026-06-13 ~18:46, @taOS (wind-down at 5h usage 92%).
+Last updated: 2026-06-13 ~21:25 BST, @taOS (active, 5h usage 21% post-reset).
-▶ WAKE QUEUE (resume here after the 19:40 UTC 5h reset, Jay's active queue):
-1. THEME: move taOS Dark off the blue-purple base (#1a1b2e) to macOS dark-mode NEUTRALS (#865). Edit dark @theme tokens in desktop/src/theme/tokens.css + retune WALLPAPERS off indigo; accent stays neutral grey; keep Light theme; verify contrast.
-2. MOBILE AUDIT: check the Agents app + chat/Messages + composer look right on mobile (Jay flagged).
-3. WALLPAPER picker Phase 1 (#864): reorg into Theme-default / Built-in / Your-wallpapers(+upload) sections + a Settings entry point; Phase 2 = Wallhaven KEYLESS browse + optional API-key entry. Mock approved.
-4. DYNAMIC ISLAND v2 (#854): design+mocks approved (island holds agent+search, agent chat bubble replaces side panel + poppable window, search bubble, Mac animations). Build plan then build.
-5. GITHUB (#858): Phase 1 connect flow MERGED (#862). Next: Phase 2 time-scoped sharing + consent picker; Phase 3 agent access-request + runtime token injection; Phase 4 fork->PR ops. OAuth app registered (Client ID Ov23licVGSIqagQLXAqb public/in-source; secret stays host-side, NOT in repo; device flow needs no secret).
+▶ WAKE QUEUE (Jay's active queue, resumed post-reset):
+1. THEME #865 DONE-IN-PR: taOS Dark moved off blue-indigo (#1a1b2e) to macOS graphite NEUTRALS (#1d1d1f / #171717), neutral frosted chrome, accent grey kept, Light theme untouched. Shipped with a LIVE adaptive neural wallpaper (canvas, any aspect ratio incl 3840x1200 ultrawide; optional taOS wordmark toggle). PR #868. Also Safari backdrop-filter repaint fix = PR #867. Both need Jay's live Pi visual check (animation + Safari dark<->light are invisible to screenshots). Merge each when bot-review green.
+2. BRAINSTORM (next-up, Jay's call): live-wallpaper PACKAGE format + agent authoring guidelines + store sharing. Prototype proven (neural graphite). See memory [[live-wallpapers]]. Brainstorm after Jay confirms #868 live.
+3. MOBILE AUDIT: check the Agents app + chat/Messages + composer look right on mobile (Jay flagged); verify the new neural wallpaper + graphite chrome on mobile too.
+4. WALLPAPER picker Phase 1 (#864): reorg into Theme-default / Built-in / Your-wallpapers(+upload) sections + a Settings entry point; Phase 2 = Wallhaven KEYLESS browse + optional API-key entry. Mock approved. NOTE: #868 already added a wallpaper "kind" + wordmark toggle to the picker; build on that.
+5. DYNAMIC ISLAND v2 (#854): design+mocks approved (island holds agent+search, agent chat bubble replaces side panel + poppable window, search bubble, Mac animations). Build plan then build.
+6. GITHUB (#858): Phase 1 connect flow MERGED (#862). Next: Phase 2 time-scoped sharing + consent picker; Phase 3 agent access-request + runtime token injection; Phase 4 fork->PR ops. OAuth app registered (Client ID Ov23licVGSIqagQLXAqb public/in-source; secret stays host-side, NOT in repo; device flow needs no secret).
-Branch tips: master=6394a3ed. dev advanced well past #845: merged this session = #843 #847 #848 #849 #850 #851 #852 #853 #857 #859 #860 #861 #862. OPEN PR baking: #863 (agents spacing + System-agent-label removal; merge on green).
+Branch tips: master=6394a3ed. dev=7f14fc88 (#863 merged). OPEN PRs baking (merge on green): #867 (Safari backdrop-filter repaint), #868 (macOS-dark graphite + live neural wallpaper, #865).
-Session state: wind-down. Merge #863 when green. Resume the WAKE QUEUE after reset.
+Session state: ACTIVE (resumed post-reset, 5h 21%). PRs #867 + #868 open for review; resume the WAKE QUEUE.
WEBSITE: taos.my live. All 4 taos-website PRs merged (stats/changelog/nav/accessibility).
CI: test suite parallelized via #839 (xdist -n auto). CodeRabbit may be out of credits -- do not merge on a fake rate-limit pass. Use @coderabbitai full review to retrigger; manual review OK for tiny already-reviewed PRs.
OPEN PRs:
-- #860 feat(theme): theme chooser shows only taOS Dark + taOS Light (remove Matrix Terminal) -- feat/themes-taos-light-dark
-- #859 fix(agents): kill switch surfaces failures -- Gitar review follow-up on #857, fix/kill-switch-error-handling
+- #868 feat(theme): macOS-dark graphite palette + live neural wallpaper (#865) -- feat/macos-dark-theme; merge on green; needs Jay live Pi check
+- #867 fix(theme): repaint backdrop-filter layers on theme switch (Safari blacked-out) -- fix/safari-theme-repaint; merge on green; Safari-only, verify live
- #846 dependabot esbuild bump -- SUPERSEDED by #849 (already on dev); close it
- #476 DRAFT feat(userspace): App Runtime v1 -- stays DRAFT, not ready to merge
+(merged since last update: #859 kill-switch errors, #860 theme chooser taOS Dark+Light, #863 agents spacing)
Notable open issues (bugs first):
- #844 rkllama store-UI install chain broken (wrong script + non-interactive false-success) -- unresolved
From 9ccfd7d032c147e93fdf254e8bae1879efb8d604 Mon Sep 17 00:00:00 2001
From: jaylfc
Date: Sat, 13 Jun 2026 22:21:57 +0100
Subject: [PATCH 27/57] fix(theme): repaint backdrop-filter layers on theme
switch (Safari blacked-out elements) (#867)
* fix(theme): force WebKit to repaint backdrop-filter layers on theme switch
Safari keeps backdrop-filter elements (dock, top bar, modals, widgets) on
cached GPU compositing layers and does not re-rasterize them when the theme
custom properties on :root change at runtime. Switching dark<->light could
leave those surfaces rendering black on the live screen, while screenshots
looked correct because the screenshot path forces a full raster (which is
also why the bug was invisible to screenshot-based testing).
Drop backdrop-filter for a single frame via a [data-theme-switching]
attribute set in applyThemeConfig/revertTheme, forcing WebKit to rebuild
each backdrop layer against the new tokens. Imperceptible (~1 frame) and a
no-op in Chrome/Firefox, which re-raster eagerly.
* fix(theme): single repaint per theme apply + timer fallback for hidden tabs
Addresses Gitar review on the Safari backdrop-filter repaint:
- revertTheme now takes a silent flag; applyThemeConfig calls it silently and
owns the single repaint after the new tokens are written, avoiding a
redundant forced reflow (and an early repaint before tokens existed).
- forceCompositingRepaint adds a setTimeout(250ms) fallback so the
data-theme-switching attribute is always cleared even when rAF is paused on
a hidden/background tab.
---
desktop/src/stores/theme-store.ts | 32 +++++++++++++++++++++++++++++--
desktop/src/theme/tokens.css | 15 +++++++++++++++
2 files changed, 45 insertions(+), 2 deletions(-)
diff --git a/desktop/src/stores/theme-store.ts b/desktop/src/stores/theme-store.ts
index d1f6cd71..297d1871 100644
--- a/desktop/src/stores/theme-store.ts
+++ b/desktop/src/stores/theme-store.ts
@@ -146,8 +146,32 @@ function schemeFromBg(bg: string | undefined): "light" | "dark" {
return luminance > 0.55 ? "light" : "dark";
}
+// Safari/WebKit leaves backdrop-filter elements (dock, top bar, modals,
+// widgets) on stale GPU compositing layers when the theme custom properties
+// on :root change at runtime: the layer keeps its old raster until something
+// forces a re-composite (scroll, resize, screenshot), so on a theme switch it
+// can flash black on the live screen even though screenshots look correct
+// (the screenshot path forces a full raster). Dropping backdrop-filter for a
+// frame via [data-theme-switching] (see tokens.css) forces WebKit to rebuild
+// every backdrop layer against the new tokens. No-op outside the browser.
+function forceCompositingRepaint() {
+ if (typeof document === "undefined") return;
+ const root = document.documentElement;
+ root.setAttribute("data-theme-switching", "");
+ void root.offsetHeight; // flush the filter:none state before restoring it
+ const clear = () => root.removeAttribute("data-theme-switching");
+ // Two nested rAFs let one frame paint with the filter off before restoring it.
+ if (typeof requestAnimationFrame === "function") {
+ requestAnimationFrame(() => requestAnimationFrame(clear));
+ }
+ // rAF is paused on a hidden/background tab, which would otherwise leave the
+ // attribute (and the backdrop-filter:none rule) stuck until the tab is shown.
+ // A timer guarantees cleanup regardless; removeAttribute is idempotent.
+ setTimeout(clear, 250);
+}
+
export function applyThemeConfig(cfg: ThemeConfig) {
- revertTheme();
+ revertTheme({ silent: true }); // applyThemeConfig owns the single repaint below
const root = document.documentElement;
for (const [k, v] of Object.entries(cfg.tokens || {})) {
if (ALLOWED_TOKENS.has(k) && typeof v === "string") {
@@ -157,14 +181,18 @@ export function applyThemeConfig(cfg: ThemeConfig) {
}
root.dataset.scheme = schemeFromBg(cfg.tokens?.["--color-shell-bg"]);
useThemeStore.setState({ structure: cfg.structure || {}, effects: cfg.effects || [] });
+ forceCompositingRepaint();
}
-export function revertTheme() {
+export function revertTheme(opts?: { silent?: boolean }) {
const root = document.documentElement;
for (const k of _applied) root.style.removeProperty(k);
_applied = [];
root.dataset.scheme = "dark"; // base shell is dark
useThemeStore.setState({ structure: {}, effects: [] });
+ // Skip when called from applyThemeConfig (which repaints once after applying
+ // the new tokens) so a theme switch does not force two reflows.
+ if (!opts?.silent) forceCompositingRepaint();
}
export function setWallpaperForActiveTheme(value: string) {
diff --git a/desktop/src/theme/tokens.css b/desktop/src/theme/tokens.css
index ef21b35c..789e7655 100644
--- a/desktop/src/theme/tokens.css
+++ b/desktop/src/theme/tokens.css
@@ -97,6 +97,21 @@
:root[data-scheme="light"] [class~="hover:bg-white/10"]:hover { background-color: rgba(0, 0, 0, 0.07); }
:root[data-scheme="light"] [class~="hover:bg-white/[0.06]"]:hover { background-color: rgba(0, 0, 0, 0.05); }
+/* Safari backdrop-filter repaint guard
+ ====================================
+ WebKit keeps backdrop-filter elements on cached GPU layers and does not
+ re-rasterize them when the theme custom properties on :root change at
+ runtime, so a dark<->light switch can leave the dock, top bar, modals and
+ widgets rendering black on the live screen (screenshots force a full raster
+ and look fine, which masks the bug). theme-store.ts sets
+ [data-theme-switching] for one frame on every theme apply/revert; dropping
+ the filter for that frame forces WebKit to rebuild each layer against the
+ new tokens. Imperceptible (~1 frame) and a no-op in Chrome/Firefox. */
+:root[data-theme-switching] * {
+ -webkit-backdrop-filter: none !important;
+ backdrop-filter: none !important;
+}
+
/* Wallpaper sizing
----------------
Desktop fills the viewport (cover crops edges if aspect mismatches).
From 865f278d77d6532929cac0ec39d410c476262693 Mon Sep 17 00:00:00 2001
From: jaylfc
Date: Sat, 13 Jun 2026 22:22:19 +0100
Subject: [PATCH 28/57] feat(theme): macOS-dark graphite palette + live neural
wallpaper (#865) (#868)
* feat(theme): macOS-dark graphite palette + live neural wallpaper
Retunes the taOS Dark theme off the blue-indigo base (#1a1b2e) to a neutral
macOS-style graphite charcoal, and replaces the static default wallpaper with
an adaptive live wallpaper component.
Theme tokens (desktop/src/theme/tokens.css @theme):
- shell-bg #1a1b2e -> #1d1d1f, shell-bg-deep #151625 -> #171717
- dock/top bar rgba(22,25,32,.92) -> neutral rgba(29,29,31,.92)
- accent grey #8b92a3 and the traffic lights are unchanged
Live neural wallpaper (NeuralWallpaper.tsx):
- adaptive of drifting nodes + proximity links over a graphite
field, inspired by the original neural wallpaper but neutral. Renders at
native resolution for any aspect ratio (16:10, ultrawide 32:10, mobile
portrait) from one source, so no per-resolution image assets are needed.
- density scales with viewport area; the loop pauses while hidden; honours
prefers-reduced-motion with a static frame.
- "taOS" wordmark overlay is optional, toggled in the wallpaper picker and
persisted locally (default on). The toggle only shows for neural wallpapers.
Wiring:
- theme-store gains a wallpaper "kind" ("image" | "neural"); the new
Neural (Graphite) entry is the taOS Dark default, the original neural PNG
stays selectable as Neural (Classic).
- Desktop.tsx and the mobile shell render the live wallpaper when active and
suppress the CSS background image in that case.
- Wallpaper picker shows a graphite preview swatch for neural wallpapers.
Part of #865. Live wallpapers as a shareable package format + agent authoring
guidelines are a follow-up feature (brainstorm next).
* refactor(theme): generic wallpaper kind + slogan overlay (decouple from "neural")
Per feedback: "neural" should not be the category, and the text should not be
welded to it. Not all animated wallpapers with text will be neural.
- Wallpaper.kind is now "image" | "animated" (was "image" | "neural"); the
specific renderer is a separate `component` field ("neural" is one renderer).
- The wordmark becomes a generic slogan overlay (WallpaperTextOverlay) that
draws centered text above ANY wallpaper, driven by a per-wallpaper
`overlayText` default + a user toggle (showOverlayText, persisted). Colour /
size / style / effects use defaults for now; configurable in a follow-up.
- NeuralWallpaper is now a pure renderer (no embedded text).
- The picker toggle reads "Show slogan ()" and only appears when the
active wallpaper defines a slogan.
- Wallpapers relabelled: "Graphite" (animated, slogan taOS) is the taOS Dark
default; the original PNG is "Classic" (no slogan, text already baked in).
* perf(wallpaper): cache gradients, drop per-link string alloc, reactive reduced-motion
Addresses Gitar review on the neural wallpaper (Pi perf target):
- Precompute the 3 gradients in build() and reuse them every frame instead of
reallocating ~180 gradient objects/sec.
- Draw links with a fixed strokeStyle + per-pair globalAlpha rather than a
toFixed rgba string per pair in the O(n^2) loop; same for node fills.
- prefers-reduced-motion now reacts to live OS changes via a matchMedia change
listener instead of being read once at mount.
---
.gitignore | 1 +
desktop/src/App.tsx | 13 +-
desktop/src/components/Desktop.tsx | 11 +-
desktop/src/components/NeuralWallpaper.tsx | 226 ++++++++++++++++++
desktop/src/components/WallpaperPicker.tsx | 55 ++++-
.../src/components/WallpaperTextOverlay.tsx | 23 ++
desktop/src/stores/theme-store.ts | 69 +++++-
desktop/src/theme/tokens.css | 12 +-
8 files changed, 384 insertions(+), 26 deletions(-)
create mode 100644 desktop/src/components/NeuralWallpaper.tsx
create mode 100644 desktop/src/components/WallpaperTextOverlay.tsx
diff --git a/.gitignore b/.gitignore
index 918432eb..778294a5 100644
--- a/.gitignore
+++ b/.gitignore
@@ -98,3 +98,4 @@ docs/AGENT_HANDOFF.md
docs/audit/
.understand-anything/
docs/agent-jobs/
+.design/
diff --git a/desktop/src/App.tsx b/desktop/src/App.tsx
index 98e10418..39d5130d 100644
--- a/desktop/src/App.tsx
+++ b/desktop/src/App.tsx
@@ -2,6 +2,8 @@ import { useState, useCallback, useEffect } from "react";
import { TopBar } from "@/components/TopBar";
import { Desktop } from "@/components/Desktop";
import { Dock } from "@/components/Dock";
+import { NeuralWallpaper } from "@/components/NeuralWallpaper";
+import { WallpaperTextOverlay } from "@/components/WallpaperTextOverlay";
import { Launchpad } from "@/components/Launchpad";
import { SearchPalette } from "@/components/SearchPalette";
import { ShortcutProvider, useShortcut } from "@/hooks/use-shortcut-registry";
@@ -125,6 +127,11 @@ export function App() {
const wallpaperImage = useThemeStore((s) => s.wallpaperImage);
const wallpaperMobileImage = useThemeStore((s) => s.wallpaperMobileImage);
const wallpaperFallback = useThemeStore((s) => s.wallpaperFallback);
+ const wallpaperKind = useThemeStore((s) => s.wallpaperKind);
+ const wallpaperComponent = useThemeStore((s) => s.wallpaperComponent);
+ const wallpaperOverlayText = useThemeStore((s) => s.wallpaperOverlayText);
+ const showOverlayText = useThemeStore((s) => s.showOverlayText);
+ const isAnimatedWallpaper = wallpaperKind === "animated";
const windows = useProcessStore((s) => s.windows);
const openWindow = useProcessStore((s) => s.openWindow);
const closeWindow = useProcessStore((s) => s.closeWindow);
@@ -288,11 +295,13 @@ export function App() {
-
+
+ {isAnimatedWallpaper && wallpaperComponent === "neural" &&
}
+ {showOverlayText && wallpaperOverlayText &&
}
{/* Install banner — shown in browser mode, hidden in PWA */}
{isBrowserMobile &&
}
-
+
{ setCardSwitcherOpen(false); setSearchOpen((v) => !v); }}
diff --git a/desktop/src/components/Desktop.tsx b/desktop/src/components/Desktop.tsx
index 76f51f8b..7008217a 100644
--- a/desktop/src/components/Desktop.tsx
+++ b/desktop/src/components/Desktop.tsx
@@ -11,6 +11,8 @@ import { SnapOverlay } from "./SnapOverlay";
import { WidgetLayer } from "./WidgetLayer";
import { ContextMenu, type MenuItem } from "./ContextMenu";
import { WallpaperPicker } from "./WallpaperPicker";
+import { NeuralWallpaper } from "./NeuralWallpaper";
+import { WallpaperTextOverlay } from "./WallpaperTextOverlay";
type ContextMenuState = {
x: number;
@@ -23,6 +25,11 @@ export function Desktop() {
const wallpaperImage = useThemeStore((s) => s.wallpaperImage);
const wallpaperMobileImage = useThemeStore((s) => s.wallpaperMobileImage);
const wallpaperFallback = useThemeStore((s) => s.wallpaperFallback);
+ const wallpaperKind = useThemeStore((s) => s.wallpaperKind);
+ const wallpaperComponent = useThemeStore((s) => s.wallpaperComponent);
+ const wallpaperOverlayText = useThemeStore((s) => s.wallpaperOverlayText);
+ const showOverlayText = useThemeStore((s) => s.showOverlayText);
+ const isAnimated = wallpaperKind === "animated";
const { showWidgets, toggleWidgets } = useWidgetStore();
const [contextMenu, setContextMenu] = useState(null);
const [wallpaperPickerOpen, setWallpaperPickerOpen] = useState(false);
@@ -115,10 +122,12 @@ export function Desktop() {
return (
+ {isAnimated && wallpaperComponent === "neural" &&
}
+ {showOverlayText && wallpaperOverlayText &&
}
{windows.map((win) => (
diff --git a/desktop/src/components/NeuralWallpaper.tsx b/desktop/src/components/NeuralWallpaper.tsx
new file mode 100644
index 00000000..a47cb3a7
--- /dev/null
+++ b/desktop/src/components/NeuralWallpaper.tsx
@@ -0,0 +1,226 @@
+import { useEffect, useRef } from "react";
+
+/**
+ * "neural" animated wallpaper renderer — an adaptive canvas of drifting nodes
+ * and proximity links over a graphite field, inspired by the original taOS
+ * neural wallpaper but calmed to a neutral macOS-dark palette. Renders at
+ * native resolution for any aspect ratio (16:10, ultrawide 32:10, mobile
+ * portrait) from one source, so no per-resolution image assets are needed.
+ *
+ * One render component behind the generic animated-wallpaper kind. The optional
+ * slogan is a separate overlay (WallpaperTextOverlay), not part of the
+ * renderer, so it works over any wallpaper.
+ *
+ * Perf: density scales with viewport area, the loop pauses while the tab/app is
+ * hidden, and prefers-reduced-motion renders a single static frame. The canvas
+ * fills its (positioned) parent via ResizeObserver.
+ */
+
+const NODE = "236,237,240"; // soft silver
+const LINK = "150,156,170"; // cool graphite
+const ACCENT = "174,180,196"; // faint highlight on "active" nodes
+const LINK_DIST = 150;
+
+interface Node {
+ x: number;
+ y: number;
+ vx: number;
+ vy: number;
+ r: number;
+ active: boolean;
+ tw: number;
+}
+
+export function NeuralWallpaper() {
+ const canvasRef = useRef
(null);
+
+ useEffect(() => {
+ const canvas = canvasRef.current;
+ if (!canvas) return;
+ const ctx = canvas.getContext("2d");
+ if (!ctx) return;
+
+ const dpr = Math.min(window.devicePixelRatio || 1, 2);
+ let w = 0;
+ let h = 0;
+ let nodes: Node[] = [];
+ let raf = 0;
+ let running = false;
+
+ // Gradients depend only on w/h, so they are built once per resize in build()
+ // and reused every frame rather than reallocated ~180x/sec (Pi GC churn).
+ let bgGrad: CanvasGradient | null = null;
+ let glowGrad: CanvasGradient | null = null;
+ let vignetteGrad: CanvasGradient | null = null;
+
+ const motionQuery = window.matchMedia("(prefers-reduced-motion: reduce)");
+ let reduceMotion = motionQuery.matches;
+
+ function build() {
+ const rect = canvas!.getBoundingClientRect();
+ w = Math.max(1, Math.round(rect.width));
+ h = Math.max(1, Math.round(rect.height));
+ canvas!.width = Math.round(w * dpr);
+ canvas!.height = Math.round(h * dpr);
+ ctx!.setTransform(dpr, 0, 0, dpr, 0, 0);
+
+ bgGrad = ctx!.createLinearGradient(0, 0, w, h);
+ bgGrad.addColorStop(0, "#26262a");
+ bgGrad.addColorStop(0.42, "#1d1d1f");
+ bgGrad.addColorStop(1, "#101011");
+ glowGrad = ctx!.createRadialGradient(w * 0.5, h * 0.46, 0, w * 0.5, h * 0.46, Math.max(w, h) * 0.6);
+ glowGrad.addColorStop(0, "rgba(255,255,255,0.06)");
+ glowGrad.addColorStop(0.5, "rgba(255,255,255,0.015)");
+ glowGrad.addColorStop(1, "rgba(255,255,255,0)");
+ vignetteGrad = ctx!.createRadialGradient(w * 0.5, h * 0.48, Math.min(w, h) * 0.3, w * 0.5, h * 0.48, Math.max(w, h) * 0.75);
+ vignetteGrad.addColorStop(0, "rgba(0,0,0,0)");
+ vignetteGrad.addColorStop(1, "rgba(0,0,0,0.42)");
+
+ const count = Math.max(40, Math.min(260, Math.round((w * h) / 14000)));
+ nodes = Array.from({ length: count }, () => ({
+ x: Math.random() * w,
+ y: Math.random() * h,
+ vx: (Math.random() - 0.5) * 0.22,
+ vy: (Math.random() - 0.5) * 0.22,
+ r: Math.random() * 1.4 + 0.7,
+ active: Math.random() < 0.16,
+ tw: Math.random() * Math.PI * 2,
+ }));
+ }
+
+ function background() {
+ ctx!.fillStyle = bgGrad!;
+ ctx!.fillRect(0, 0, w, h);
+ ctx!.fillStyle = glowGrad!;
+ ctx!.fillRect(0, 0, w, h);
+ }
+
+ function vignette() {
+ ctx!.fillStyle = vignetteGrad!;
+ ctx!.fillRect(0, 0, w, h);
+ }
+
+ const LINK_RGB = `rgb(${LINK})`;
+ const NODE_RGB = `rgb(${NODE})`;
+ const ACCENT_RGB = `rgb(${ACCENT})`;
+
+ function frame(t: number) {
+ background();
+ for (const n of nodes) {
+ n.x += n.vx;
+ n.y += n.vy;
+ if (n.x < -20) n.x = w + 20;
+ else if (n.x > w + 20) n.x = -20;
+ if (n.y < -20) n.y = h + 20;
+ else if (n.y > h + 20) n.y = -20;
+ }
+ // Links: one fixed strokeStyle, per-pair opacity via globalAlpha (no
+ // per-pair rgba string allocation in this O(n^2) hot loop).
+ ctx!.strokeStyle = LINK_RGB;
+ ctx!.lineWidth = 0.7;
+ for (let i = 0; i < nodes.length; i++) {
+ const a = nodes[i]!;
+ for (let j = i + 1; j < nodes.length; j++) {
+ const b = nodes[j]!;
+ const dx = a.x - b.x;
+ const dy = a.y - b.y;
+ const d2 = dx * dx + dy * dy;
+ if (d2 < LINK_DIST * LINK_DIST) {
+ ctx!.globalAlpha = (1 - Math.sqrt(d2) / LINK_DIST) * 0.5;
+ ctx!.beginPath();
+ ctx!.moveTo(a.x, a.y);
+ ctx!.lineTo(b.x, b.y);
+ ctx!.stroke();
+ }
+ }
+ }
+ ctx!.globalAlpha = 1;
+ for (const n of nodes) {
+ const tw = 0.6 + 0.4 * Math.sin(t * 0.0014 + n.tw);
+ if (n.active) {
+ ctx!.fillStyle = ACCENT_RGB;
+ ctx!.globalAlpha = 0.85 * tw;
+ ctx!.shadowColor = `rgba(${ACCENT},0.7)`;
+ ctx!.shadowBlur = 8;
+ } else {
+ ctx!.fillStyle = NODE_RGB;
+ ctx!.globalAlpha = 0.6 * tw;
+ ctx!.shadowBlur = 0;
+ }
+ ctx!.beginPath();
+ ctx!.arc(n.x, n.y, n.r, 0, Math.PI * 2);
+ ctx!.fill();
+ }
+ ctx!.globalAlpha = 1;
+ ctx!.shadowBlur = 0;
+ vignette();
+ raf = requestAnimationFrame(frame);
+ }
+
+ function staticFrame() {
+ background();
+ ctx!.fillStyle = NODE_RGB;
+ ctx!.globalAlpha = 0.6;
+ for (const n of nodes) {
+ ctx!.beginPath();
+ ctx!.arc(n.x, n.y, n.r, 0, Math.PI * 2);
+ ctx!.fill();
+ }
+ ctx!.globalAlpha = 1;
+ vignette();
+ }
+
+ function render() {
+ if (reduceMotion) staticFrame();
+ else start();
+ }
+ function start() {
+ if (running || reduceMotion) return;
+ running = true;
+ raf = requestAnimationFrame(frame);
+ }
+ function stop() {
+ running = false;
+ cancelAnimationFrame(raf);
+ }
+
+ build();
+ render();
+
+ const ro = new ResizeObserver(() => {
+ stop();
+ build();
+ if (reduceMotion) staticFrame();
+ else if (!document.hidden) start();
+ });
+ ro.observe(canvas);
+
+ const onVisibility = () => {
+ if (document.hidden) stop();
+ else start();
+ };
+ document.addEventListener("visibilitychange", onVisibility);
+
+ // React to the OS reduced-motion setting changing mid-session.
+ const onMotionChange = () => {
+ reduceMotion = motionQuery.matches;
+ stop();
+ if (reduceMotion) staticFrame();
+ else if (!document.hidden) start();
+ };
+ motionQuery.addEventListener("change", onMotionChange);
+
+ return () => {
+ stop();
+ ro.disconnect();
+ document.removeEventListener("visibilitychange", onVisibility);
+ motionQuery.removeEventListener("change", onMotionChange);
+ };
+ }, []);
+
+ return (
+
+
+
+ );
+}
diff --git a/desktop/src/components/WallpaperPicker.tsx b/desktop/src/components/WallpaperPicker.tsx
index d91aed5e..74aecbea 100644
--- a/desktop/src/components/WallpaperPicker.tsx
+++ b/desktop/src/components/WallpaperPicker.tsx
@@ -7,7 +7,8 @@ interface Props {
}
export function WallpaperPicker({ open, onClose }: Props) {
- const { wallpaperId, setWallpaper, getWallpapers } = useThemeStore();
+ const { wallpaperId, setWallpaper, getWallpapers, wallpaperOverlayText, showOverlayText, toggleOverlayText } =
+ useThemeStore();
const wallpapers = getWallpapers();
if (!open) return null;
@@ -26,7 +27,7 @@ export function WallpaperPicker({ open, onClose }: Props) {
aria-modal="true"
aria-label="Change Wallpaper"
className="w-full max-w-[500px] max-h-full flex flex-col rounded-xl border border-shell-border-strong overflow-hidden"
- style={{ backgroundColor: "rgba(26, 27, 46, 0.98)" }}
+ style={{ backgroundColor: "rgba(29, 29, 31, 0.98)" }}
onClick={(e) => e.stopPropagation()}
>
@@ -55,15 +56,25 @@ export function WallpaperPicker({ open, onClose }: Props) {
}`}
>
+ className="relative h-24 w-full"
+ style={
+ wp.kind === "animated"
+ ? { background: "radial-gradient(120% 120% at 50% 46%, #2a2a2e 0%, #1d1d1f 45%, #101011 100%)" }
+ : {
+ backgroundImage: wp.image,
+ backgroundColor: wp.fallback,
+ backgroundSize: "cover",
+ backgroundPosition: "center",
+ backgroundRepeat: "no-repeat",
+ }
+ }
+ >
+ {wp.overlayText && (
+
+ {wp.overlayText}
+
+ )}
+
{wp.label}
@@ -75,6 +86,28 @@ export function WallpaperPicker({ open, onClose }: Props) {
))}
+ {wallpaperOverlayText && (
+
+
+ Show slogan ({wallpaperOverlayText})
+
+
+
+
+
+ )}
);
diff --git a/desktop/src/components/WallpaperTextOverlay.tsx b/desktop/src/components/WallpaperTextOverlay.tsx
new file mode 100644
index 00000000..19499a50
--- /dev/null
+++ b/desktop/src/components/WallpaperTextOverlay.tsx
@@ -0,0 +1,23 @@
+/**
+ * Generic wallpaper slogan overlay — centered text drawn above any wallpaper
+ * (animated or image). The text content and whether it shows are driven by the
+ * active wallpaper/theme and a user toggle; this component owns only the
+ * presentation. Colour / size / style / effects use sensible defaults for now
+ * and are intended to become configurable (per wallpaper/theme) in a follow-up.
+ */
+export function WallpaperTextOverlay({ text }: { text: string }) {
+ return (
+
+
+ {text}
+
+
+ );
+}
diff --git a/desktop/src/stores/theme-store.ts b/desktop/src/stores/theme-store.ts
index 297d1871..d362cbfe 100644
--- a/desktop/src/stores/theme-store.ts
+++ b/desktop/src/stores/theme-store.ts
@@ -11,15 +11,34 @@ import { BUILTIN_THEMES } from "@/theme/builtin-themes";
interface Wallpaper {
id: string;
label: string;
- image: string; // desktop background-image
+ image: string; // desktop background-image (empty for animated wallpapers)
mobileImage?: string; // portrait-cropped variant, falls back to `image` if absent
fallback: string; // background-color used as a fallback colour behind the image
+ // "image" = CSS background (url/gradient); "animated" = a live render component
+ // selected by `component`. Defaults to "image" when absent.
+ kind?: "image" | "animated";
+ // Render component id for animated wallpapers (e.g. "neural"). New animated
+ // wallpapers register a renderer and reference it here.
+ component?: string;
+ // Optional default slogan overlaid (centered) on top of this wallpaper. null /
+ // absent = no slogan. The overlay is generic, not tied to any wallpaper kind;
+ // the user can toggle it off. Styling (colour/size/effects) defaults for now.
+ overlayText?: string | null;
}
const WALLPAPERS: Wallpaper[] = [
+ {
+ id: "graphite",
+ label: "Graphite",
+ image: "",
+ fallback: "#141415",
+ kind: "animated",
+ component: "neural",
+ overlayText: "taOS",
+ },
{
id: "default",
- label: "Default",
+ label: "Classic",
image: "url('/static/wallpaper.png')",
mobileImage: "url('/static/wallpaper-mobile.png')",
fallback: "#1a1b2e",
@@ -74,11 +93,29 @@ const WALLPAPERS: Wallpaper[] = [
},
];
+// Default wallpaper for taOS Dark: the animated graphite field.
+const DEFAULT_WP = WALLPAPERS.find((w) => w.id === "graphite") ?? WALLPAPERS[0]!;
+
+// The slogan-overlay toggle persists locally so a clean desktop survives a
+// reload. Best-effort: any storage failure falls back to "on".
+const SLOGAN_KEY = "taos-wallpaper-slogan";
+function loadSloganPref(): boolean {
+ try {
+ return localStorage.getItem(SLOGAN_KEY) !== "off";
+ } catch {
+ return true;
+ }
+}
+
interface ThemeStore {
wallpaperId: string;
wallpaperImage: string;
wallpaperMobileImage: string;
wallpaperFallback: string;
+ wallpaperKind: "image" | "animated";
+ wallpaperComponent: string | null;
+ wallpaperOverlayText: string | null;
+ showOverlayText: boolean;
showDesktopIcons: boolean;
structure: Record
>;
effects: { module: string; params?: Record }[];
@@ -88,15 +125,20 @@ interface ThemeStore {
themeDefaultWallpaper: Record;
setWallpaper: (id: string) => void;
+ toggleOverlayText: () => void;
toggleDesktopIcons: () => void;
getWallpapers: () => Wallpaper[];
}
export const useThemeStore = create((set) => ({
- wallpaperId: "default",
- wallpaperImage: WALLPAPERS[0]!.image,
- wallpaperMobileImage: WALLPAPERS[0]!.mobileImage ?? WALLPAPERS[0]!.image,
- wallpaperFallback: WALLPAPERS[0]!.fallback,
+ wallpaperId: DEFAULT_WP.id,
+ wallpaperImage: DEFAULT_WP.image,
+ wallpaperMobileImage: DEFAULT_WP.mobileImage ?? DEFAULT_WP.image,
+ wallpaperFallback: DEFAULT_WP.fallback,
+ wallpaperKind: DEFAULT_WP.kind ?? "image",
+ wallpaperComponent: DEFAULT_WP.component ?? null,
+ wallpaperOverlayText: DEFAULT_WP.overlayText ?? null,
+ showOverlayText: loadSloganPref(),
showDesktopIcons: true,
structure: {},
effects: [],
@@ -113,10 +155,25 @@ export const useThemeStore = create((set) => ({
wallpaperImage: wp.image,
wallpaperMobileImage: wp.mobileImage ?? wp.image,
wallpaperFallback: wp.fallback,
+ wallpaperKind: wp.kind ?? "image",
+ wallpaperComponent: wp.component ?? null,
+ wallpaperOverlayText: wp.overlayText ?? null,
});
}
},
+ toggleOverlayText() {
+ set((s) => {
+ const next = !s.showOverlayText;
+ try {
+ localStorage.setItem(SLOGAN_KEY, next ? "on" : "off");
+ } catch {
+ // best-effort
+ }
+ return { showOverlayText: next };
+ });
+ },
+
toggleDesktopIcons() {
set((s) => ({ showDesktopIcons: !s.showDesktopIcons }));
},
diff --git a/desktop/src/theme/tokens.css b/desktop/src/theme/tokens.css
index 789e7655..f4868c1a 100644
--- a/desktop/src/theme/tokens.css
+++ b/desktop/src/theme/tokens.css
@@ -1,9 +1,9 @@
@import "tailwindcss";
@theme {
- /* Background layers */
- --color-shell-bg: #1a1b2e;
- --color-shell-bg-deep: #151625;
+ /* Background layers — macOS-dark graphite (warm charcoal, neutral, no indigo) */
+ --color-shell-bg: #1d1d1f;
+ --color-shell-bg-deep: #171717;
--color-shell-surface: rgba(255, 255, 255, 0.04);
--color-shell-surface-hover: rgba(255, 255, 255, 0.06);
--color-shell-surface-active: rgba(255, 255, 255, 0.08);
@@ -24,10 +24,10 @@
--color-accent: #8b92a3;
--color-accent-glow: rgba(139, 146, 163, 0.3);
- /* Dock + top bar — dark gunmetal chrome */
- --color-dock-bg: rgba(22, 25, 32, 0.92);
+ /* Dock + top bar — neutral graphite frosted chrome */
+ --color-dock-bg: rgba(29, 29, 31, 0.92);
--color-dock-border: rgba(255, 255, 255, 0.08);
- --color-topbar-bg: rgba(22, 25, 32, 0.92);
+ --color-topbar-bg: rgba(29, 29, 31, 0.92);
/* Snap zones */
--color-snap-preview: rgba(139, 146, 163, 0.15);
From 421663cf35ecaef665bc3c791fcf65803d7a2bfd Mon Sep 17 00:00:00 2001
From: jaylfc
Date: Sat, 13 Jun 2026 22:23:38 +0100
Subject: [PATCH 29/57] docs(status): #867 + #868 merged to dev; promo/store
program active
---
docs/STATUS.md | 17 ++++++++---------
1 file changed, 8 insertions(+), 9 deletions(-)
diff --git a/docs/STATUS.md b/docs/STATUS.md
index 955df2f0..ed8b86ae 100644
--- a/docs/STATUS.md
+++ b/docs/STATUS.md
@@ -1,28 +1,27 @@
SINGLE SOURCE OF TRUTH for cross-agent handoff.
-Last updated: 2026-06-13 ~21:25 BST, @taOS (active, 5h usage 21% post-reset).
+Last updated: 2026-06-13 ~22:22 BST, @taOS (active, 5h usage 48%).
-▶ WAKE QUEUE (Jay's active queue, resumed post-reset):
-1. THEME #865 DONE-IN-PR: taOS Dark moved off blue-indigo (#1a1b2e) to macOS graphite NEUTRALS (#1d1d1f / #171717), neutral frosted chrome, accent grey kept, Light theme untouched. Shipped with a LIVE adaptive neural wallpaper (canvas, any aspect ratio incl 3840x1200 ultrawide; optional taOS wordmark toggle). PR #868. Also Safari backdrop-filter repaint fix = PR #867. Both need Jay's live Pi visual check (animation + Safari dark<->light are invisible to screenshots). Merge each when bot-review green.
-2. BRAINSTORM (next-up, Jay's call): live-wallpaper PACKAGE format + agent authoring guidelines + store sharing. Prototype proven (neural graphite). See memory [[live-wallpapers]]. Brainstorm after Jay confirms #868 live.
+▶ WAKE QUEUE (Jay's active queue):
+1. THEME #865 MERGED to dev: #867 (Safari backdrop-filter repaint) + #868 (macOS graphite #1d1d1f/#171717 + LIVE adaptive neural wallpaper, generic slogan overlay, any aspect incl 3840x1200 ultrawide). Verified in a local vite build+preview (graphite + neural canvas render, Agents cols aligned). JAY TESTING ON PI (pull dev). Animation + Safari dark<->light need a live look.
+2. PROMO HERO PROGRAM (active, see memory [[promo-hero-initiative]]): build the REAL app seeded w/ mock data for a multi-window hero screenshot (chat + project canvas + store). Mocks approved. Public features to MERGE: App Store FULL redesign (popularity backend = real GitHub stars + Community section), project canvas/mind-map view (net-new), agent desktop window-mgmt API (#18). Mock DATA stays PRIVATE on the local `marketing` branch (never push/merge; MARKETING.md). STANDING: every promo render needs a 5:2 X-article cut. Tasks #13-18.
+3. BRAINSTORM live-wallpaper PACKAGE format + agent authoring guidelines + store sharing (see [[live-wallpapers]]).
3. MOBILE AUDIT: check the Agents app + chat/Messages + composer look right on mobile (Jay flagged); verify the new neural wallpaper + graphite chrome on mobile too.
4. WALLPAPER picker Phase 1 (#864): reorg into Theme-default / Built-in / Your-wallpapers(+upload) sections + a Settings entry point; Phase 2 = Wallhaven KEYLESS browse + optional API-key entry. Mock approved. NOTE: #868 already added a wallpaper "kind" + wordmark toggle to the picker; build on that.
5. DYNAMIC ISLAND v2 (#854): design+mocks approved (island holds agent+search, agent chat bubble replaces side panel + poppable window, search bubble, Mac animations). Build plan then build.
6. GITHUB (#858): Phase 1 connect flow MERGED (#862). Next: Phase 2 time-scoped sharing + consent picker; Phase 3 agent access-request + runtime token injection; Phase 4 fork->PR ops. OAuth app registered (Client ID Ov23licVGSIqagQLXAqb public/in-source; secret stays host-side, NOT in repo; device flow needs no secret).
-Branch tips: master=6394a3ed. dev=7f14fc88 (#863 merged). OPEN PRs baking (merge on green): #867 (Safari backdrop-filter repaint), #868 (macOS-dark graphite + live neural wallpaper, #865).
+Branch tips: master=6394a3ed. dev=865f278d (#867 + #868 merged). Local-only `marketing` branch (private, no upstream; promo mock data; NEVER push/merge).
-Session state: ACTIVE (resumed post-reset, 5h 21%). PRs #867 + #868 open for review; resume the WAKE QUEUE.
+Session state: ACTIVE (5h 48%). #867+#868 merged to dev for Jay's Pi test. taos-website stats secured + merged to main (#5; set STATS_USER/STATS_PASS in Coolify). Next: build the promo/store program (tasks #13-18).
WEBSITE: taos.my live. All 4 taos-website PRs merged (stats/changelog/nav/accessibility).
CI: test suite parallelized via #839 (xdist -n auto). CodeRabbit may be out of credits -- do not merge on a fake rate-limit pass. Use @coderabbitai full review to retrigger; manual review OK for tiny already-reviewed PRs.
OPEN PRs:
-- #868 feat(theme): macOS-dark graphite palette + live neural wallpaper (#865) -- feat/macos-dark-theme; merge on green; needs Jay live Pi check
-- #867 fix(theme): repaint backdrop-filter layers on theme switch (Safari blacked-out) -- fix/safari-theme-repaint; merge on green; Safari-only, verify live
- #846 dependabot esbuild bump -- SUPERSEDED by #849 (already on dev); close it
- #476 DRAFT feat(userspace): App Runtime v1 -- stays DRAFT, not ready to merge
-(merged since last update: #859 kill-switch errors, #860 theme chooser taOS Dark+Light, #863 agents spacing)
+(merged to dev since last update: #867 Safari repaint, #868 macOS-dark + neural wallpaper. taos-website #5 stats-auth merged to main.)
Notable open issues (bugs first):
- #844 rkllama store-UI install chain broken (wrong script + non-interactive false-success) -- unresolved
From 5756489ca1727cea523335bbf01f9d239132be2b Mon Sep 17 00:00:00 2001
From: jaylfc
Date: Sat, 13 Jun 2026 23:14:36 +0100
Subject: [PATCH 30/57] fix(theme): neutralise old-indigo popover + widget
backgrounds to graphite (#869)
The context menu, top-bar dropdown, kill-switch menu, notification toast +
centre, search palette, model browser and mobile bottom nav still hardcoded the
old blue-indigo base (rgba(26,27,46)/(30,31,50)/etc), so they clashed with the
new macOS graphite theme. Point them at var(--color-dock-bg) (the frosted
chrome token) so they match and adapt to the light theme too.
Also neutralise the navy widget card + mobile app-window backgrounds
(rgba(20,20,35)/(30,30,60)) to graphite, keeping the frosted alpha.
Verified in a real build+preview: the desktop context menu now renders
rgba(29,29,31,0.92).
---
desktop/src/components/AgentKillSwitch.tsx | 2 +-
desktop/src/components/ContextMenu.tsx | 2 +-
desktop/src/components/ModelBrowser.tsx | 2 +-
desktop/src/components/NotificationCentre.tsx | 4 ++--
desktop/src/components/NotificationToast.tsx | 2 +-
desktop/src/components/SearchPalette.tsx | 2 +-
desktop/src/components/TopBar.tsx | 2 +-
desktop/src/components/WidgetLayer.tsx | 10 +++++-----
desktop/src/components/mobile/MobileAppWindow.tsx | 2 +-
desktop/src/components/mobile/MobileBottomNav.tsx | 2 +-
10 files changed, 15 insertions(+), 15 deletions(-)
diff --git a/desktop/src/components/AgentKillSwitch.tsx b/desktop/src/components/AgentKillSwitch.tsx
index 4769001c..289f430c 100644
--- a/desktop/src/components/AgentKillSwitch.tsx
+++ b/desktop/src/components/AgentKillSwitch.tsx
@@ -121,7 +121,7 @@ export function AgentKillSwitch() {
align="end"
sideOffset={6}
className="z-50 min-w-[200px] rounded-xl border border-shell-border p-1.5 shadow-2xl backdrop-blur-xl"
- style={{ backgroundColor: "rgba(28,26,44,0.96)" }}
+ style={{ backgroundColor: "var(--color-dock-bg)" }}
>
Stop agents
diff --git a/desktop/src/components/ContextMenu.tsx b/desktop/src/components/ContextMenu.tsx
index 620158df..cda2a77a 100644
--- a/desktop/src/components/ContextMenu.tsx
+++ b/desktop/src/components/ContextMenu.tsx
@@ -100,7 +100,7 @@ export function ContextMenu({ x, y, items, onClose }: Props) {
style={{
left: adjustedX,
top: adjustedY,
- backgroundColor: "rgba(30, 31, 50, 0.95)",
+ backgroundColor: "var(--color-dock-bg)",
backdropFilter: "blur(20px)",
boxShadow: "0 8px 32px rgba(0,0,0,0.5)",
}}
diff --git a/desktop/src/components/ModelBrowser.tsx b/desktop/src/components/ModelBrowser.tsx
index 6fc96a50..049c765c 100644
--- a/desktop/src/components/ModelBrowser.tsx
+++ b/desktop/src/components/ModelBrowser.tsx
@@ -308,7 +308,7 @@ export function ModelBrowser({
>
e.stopPropagation()}
>
{/* Header */}
diff --git a/desktop/src/components/NotificationCentre.tsx b/desktop/src/components/NotificationCentre.tsx
index 3d5a193e..c51350b1 100644
--- a/desktop/src/components/NotificationCentre.tsx
+++ b/desktop/src/components/NotificationCentre.tsx
@@ -34,14 +34,14 @@ export function NotificationCentre() {
style={
isMobile
? {
- backgroundColor: "rgba(26, 27, 46, 0.97)",
+ backgroundColor: "var(--color-dock-bg)",
backdropFilter: "blur(20px)",
boxShadow: "0 12px 48px rgba(0,0,0,0.5)",
top: "calc(env(safe-area-inset-top, 0px) + 52px)",
bottom: "calc(40px + env(safe-area-inset-bottom, 0px) * 0.35 + 16px)",
}
: {
- backgroundColor: "rgba(26, 27, 46, 0.97)",
+ backgroundColor: "var(--color-dock-bg)",
backdropFilter: "blur(20px)",
boxShadow: "0 12px 48px rgba(0,0,0,0.5)",
}
diff --git a/desktop/src/components/NotificationToast.tsx b/desktop/src/components/NotificationToast.tsx
index 58354b63..c40ff3bd 100644
--- a/desktop/src/components/NotificationToast.tsx
+++ b/desktop/src/components/NotificationToast.tsx
@@ -279,7 +279,7 @@ function ToastItem({ notif }: { notif: Notification }) {
return (
diff --git a/desktop/src/components/SearchPalette.tsx b/desktop/src/components/SearchPalette.tsx
index 5c39a08b..dc8c637f 100644
--- a/desktop/src/components/SearchPalette.tsx
+++ b/desktop/src/components/SearchPalette.tsx
@@ -205,7 +205,7 @@ export function SearchPalette({ open, onClose, onOpenApp }: Props) {
e.stopPropagation()}
diff --git a/desktop/src/components/TopBar.tsx b/desktop/src/components/TopBar.tsx
index 7d2e39a5..998879fa 100644
--- a/desktop/src/components/TopBar.tsx
+++ b/desktop/src/components/TopBar.tsx
@@ -52,7 +52,7 @@ function PowerMenu() {
align="end"
sideOffset={6}
className="z-50 min-w-[180px] rounded-xl border border-white/10 bg-shell-surface p-1.5 shadow-2xl backdrop-blur-xl"
- style={{ backgroundColor: "rgba(28,26,44,0.96)" }}
+ style={{ backgroundColor: "var(--color-dock-bg)" }}
>
{
- e.currentTarget.style.background = "rgba(40, 40, 60, 0.85)";
+ e.currentTarget.style.background = "rgba(44, 44, 48, 0.85)";
e.currentTarget.style.transform = "scale(1.1)";
}}
onMouseLeave={(e) => {
- e.currentTarget.style.background = "rgba(20, 20, 35, 0.7)";
+ e.currentTarget.style.background = "rgba(22, 22, 24, 0.7)";
e.currentTarget.style.transform = "scale(1)";
}}
>
@@ -235,7 +235,7 @@ export function WidgetLayer() {
position: "absolute",
bottom: 50,
right: 0,
- background: "rgba(20, 20, 35, 0.9)",
+ background: "rgba(22, 22, 24, 0.9)",
backdropFilter: "blur(16px)",
WebkitBackdropFilter: "blur(16px)",
border: "1px solid rgba(255,255,255,0.15)",
diff --git a/desktop/src/components/mobile/MobileAppWindow.tsx b/desktop/src/components/mobile/MobileAppWindow.tsx
index b36eee97..b4e6cda7 100644
--- a/desktop/src/components/mobile/MobileAppWindow.tsx
+++ b/desktop/src/components/mobile/MobileAppWindow.tsx
@@ -25,7 +25,7 @@ export function MobileAppWindow({ appId, windowId, onClose, onMinimise }: Props)
className="flex items-center px-3 gap-2 shrink-0"
style={{
height: "32px",
- background: "rgba(30, 30, 60, 0.9)",
+ background: "rgba(28, 28, 31, 0.9)",
borderBottom: "1px solid rgba(255,255,255,0.08)",
}}
>
diff --git a/desktop/src/components/mobile/MobileBottomNav.tsx b/desktop/src/components/mobile/MobileBottomNav.tsx
index 7d6127a0..5586af81 100644
--- a/desktop/src/components/mobile/MobileBottomNav.tsx
+++ b/desktop/src/components/mobile/MobileBottomNav.tsx
@@ -14,7 +14,7 @@ export function MobileBottomNav({ onBack, onHome, onSearch, onSwitcher, hasActiv
className="shrink-0 flex items-center justify-around"
style={{
height: 48,
- backgroundColor: "rgba(20, 20, 42, 0.98)",
+ backgroundColor: "var(--color-dock-bg)",
borderTop: "1px solid rgba(255, 255, 255, 0.1)",
backdropFilter: "blur(20px)",
WebkitBackdropFilter: "blur(20px)",
From 4f2259a3042471425d61d23fb0d5de7659608f82 Mon Sep 17 00:00:00 2001
From: jaylfc
Date: Sat, 13 Jun 2026 23:17:37 +0100
Subject: [PATCH 31/57] feat(wallpaper): graphite-regraded neural wallpaper as
default (screenshot-ready) (#870)
* feat(wallpaper): graphite-regraded neural wallpaper as the default (screenshot-ready)
The animated canvas default from #868 didn't read as a neural brain. Ship the
original neural-brain wallpaper regraded to neutral graphite (blue removed,
slight contrast lift, taOS baked into the artwork) as the static default, so
the desktop is screenshot-ready on the new theme. Desktop + mobile assets.
The custom, user/agent-configurable animated wallpaper (tsParticles, with
customisable text/density/speed/colour) is the follow-up foundation; this
static image is what ships now.
* feat(wallpaper): invert the wallpaper with the theme (light/dark variants)
A wallpaper can now declare light-scheme variants (lightImage / lightMobileImage
/ lightFallback). The desktop tracks the active scheme (set from the theme's
shell-bg luminance) and uses the light variant when the theme reads as light,
so the wallpaper inverts with the theme instead of staying dark.
The Graphite wallpaper ships both: the dark neural-brain regrade and an inverted
light version (light field, dark network) baked for the light theme.
---
desktop/src/App.tsx | 10 +++++-
desktop/src/components/Desktop.tsx | 11 +++++-
desktop/src/stores/theme-store.ts | 40 +++++++++++++++++----
static/wallpaper-graphite-light-mobile.png | Bin 0 -> 3654574 bytes
static/wallpaper-graphite-light.png | Bin 0 -> 2158687 bytes
static/wallpaper-graphite-mobile.png | Bin 0 -> 5390861 bytes
static/wallpaper-graphite.png | Bin 0 -> 3229023 bytes
7 files changed, 52 insertions(+), 9 deletions(-)
create mode 100644 static/wallpaper-graphite-light-mobile.png
create mode 100644 static/wallpaper-graphite-light.png
create mode 100644 static/wallpaper-graphite-mobile.png
create mode 100644 static/wallpaper-graphite.png
diff --git a/desktop/src/App.tsx b/desktop/src/App.tsx
index 39d5130d..2c97c28b 100644
--- a/desktop/src/App.tsx
+++ b/desktop/src/App.tsx
@@ -127,11 +127,19 @@ export function App() {
const wallpaperImage = useThemeStore((s) => s.wallpaperImage);
const wallpaperMobileImage = useThemeStore((s) => s.wallpaperMobileImage);
const wallpaperFallback = useThemeStore((s) => s.wallpaperFallback);
+ const wallpaperLightImage = useThemeStore((s) => s.wallpaperLightImage);
+ const wallpaperLightMobileImage = useThemeStore((s) => s.wallpaperLightMobileImage);
+ const wallpaperLightFallback = useThemeStore((s) => s.wallpaperLightFallback);
+ const scheme = useThemeStore((s) => s.scheme);
const wallpaperKind = useThemeStore((s) => s.wallpaperKind);
const wallpaperComponent = useThemeStore((s) => s.wallpaperComponent);
const wallpaperOverlayText = useThemeStore((s) => s.wallpaperOverlayText);
const showOverlayText = useThemeStore((s) => s.showOverlayText);
const isAnimatedWallpaper = wallpaperKind === "animated";
+ const useLightWallpaper = scheme === "light" && !!wallpaperLightImage;
+ const effWallpaperImage = useLightWallpaper ? wallpaperLightImage : wallpaperImage;
+ const effWallpaperMobile = useLightWallpaper ? wallpaperLightMobileImage : wallpaperMobileImage;
+ const effWallpaperFallback = useLightWallpaper ? wallpaperLightFallback : wallpaperFallback;
const windows = useProcessStore((s) => s.windows);
const openWindow = useProcessStore((s) => s.openWindow);
const closeWindow = useProcessStore((s) => s.closeWindow);
@@ -295,7 +303,7 @@ export function App() {
-
+
{isAnimatedWallpaper && wallpaperComponent === "neural" &&
}
{showOverlayText && wallpaperOverlayText &&
}
diff --git a/desktop/src/components/Desktop.tsx b/desktop/src/components/Desktop.tsx
index 7008217a..d37e98df 100644
--- a/desktop/src/components/Desktop.tsx
+++ b/desktop/src/components/Desktop.tsx
@@ -25,11 +25,20 @@ export function Desktop() {
const wallpaperImage = useThemeStore((s) => s.wallpaperImage);
const wallpaperMobileImage = useThemeStore((s) => s.wallpaperMobileImage);
const wallpaperFallback = useThemeStore((s) => s.wallpaperFallback);
+ const wallpaperLightImage = useThemeStore((s) => s.wallpaperLightImage);
+ const wallpaperLightMobileImage = useThemeStore((s) => s.wallpaperLightMobileImage);
+ const wallpaperLightFallback = useThemeStore((s) => s.wallpaperLightFallback);
+ const scheme = useThemeStore((s) => s.scheme);
const wallpaperKind = useThemeStore((s) => s.wallpaperKind);
const wallpaperComponent = useThemeStore((s) => s.wallpaperComponent);
const wallpaperOverlayText = useThemeStore((s) => s.wallpaperOverlayText);
const showOverlayText = useThemeStore((s) => s.showOverlayText);
const isAnimated = wallpaperKind === "animated";
+ // Invert the wallpaper with the theme: use the light variant when present.
+ const useLight = scheme === "light" && !!wallpaperLightImage;
+ const effImage = useLight ? wallpaperLightImage : wallpaperImage;
+ const effMobile = useLight ? wallpaperLightMobileImage : wallpaperMobileImage;
+ const effFallback = useLight ? wallpaperLightFallback : wallpaperFallback;
const { showWidgets, toggleWidgets } = useWidgetStore();
const [contextMenu, setContextMenu] = useState
(null);
const [wallpaperPickerOpen, setWallpaperPickerOpen] = useState(false);
@@ -122,7 +131,7 @@ export function Desktop() {
return (
diff --git a/desktop/src/stores/theme-store.ts b/desktop/src/stores/theme-store.ts
index d362cbfe..c6e1b05c 100644
--- a/desktop/src/stores/theme-store.ts
+++ b/desktop/src/stores/theme-store.ts
@@ -24,17 +24,29 @@ interface Wallpaper {
// absent = no slogan. The overlay is generic, not tied to any wallpaper kind;
// the user can toggle it off. Styling (colour/size/effects) defaults for now.
overlayText?: string | null;
+ // Optional light-scheme variants. When the active theme reads as light, these
+ // are used instead so the wallpaper inverts with the theme. Fall back to the
+ // dark image when absent.
+ lightImage?: string;
+ lightMobileImage?: string;
+ lightFallback?: string;
}
const WALLPAPERS: Wallpaper[] = [
{
id: "graphite",
label: "Graphite",
- image: "",
+ // The original neural-brain wallpaper regraded to neutral graphite (taOS is
+ // baked into the artwork). The animated/custom configurable wallpaper is a
+ // follow-up (tsParticles); this static image is the screenshot-ready default.
+ image: "url('/static/wallpaper-graphite.png')",
+ mobileImage: "url('/static/wallpaper-graphite-mobile.png')",
fallback: "#141415",
- kind: "animated",
- component: "neural",
- overlayText: "taOS",
+ // Inverted variant (light field, dark network) for the light theme.
+ lightImage: "url('/static/wallpaper-graphite-light.png')",
+ lightMobileImage: "url('/static/wallpaper-graphite-light-mobile.png')",
+ lightFallback: "#eef0f3",
+ kind: "image",
},
{
id: "default",
@@ -112,6 +124,12 @@ interface ThemeStore {
wallpaperImage: string;
wallpaperMobileImage: string;
wallpaperFallback: string;
+ // Light-scheme variants (empty when the wallpaper has none). The desktop uses
+ // these when `scheme` is "light", so the wallpaper inverts with the theme.
+ wallpaperLightImage: string;
+ wallpaperLightMobileImage: string;
+ wallpaperLightFallback: string;
+ scheme: "light" | "dark";
wallpaperKind: "image" | "animated";
wallpaperComponent: string | null;
wallpaperOverlayText: string | null;
@@ -135,6 +153,10 @@ export const useThemeStore = create
((set) => ({
wallpaperImage: DEFAULT_WP.image,
wallpaperMobileImage: DEFAULT_WP.mobileImage ?? DEFAULT_WP.image,
wallpaperFallback: DEFAULT_WP.fallback,
+ wallpaperLightImage: DEFAULT_WP.lightImage ?? "",
+ wallpaperLightMobileImage: DEFAULT_WP.lightMobileImage ?? DEFAULT_WP.lightImage ?? "",
+ wallpaperLightFallback: DEFAULT_WP.lightFallback ?? DEFAULT_WP.fallback,
+ scheme: "dark",
wallpaperKind: DEFAULT_WP.kind ?? "image",
wallpaperComponent: DEFAULT_WP.component ?? null,
wallpaperOverlayText: DEFAULT_WP.overlayText ?? null,
@@ -155,6 +177,9 @@ export const useThemeStore = create((set) => ({
wallpaperImage: wp.image,
wallpaperMobileImage: wp.mobileImage ?? wp.image,
wallpaperFallback: wp.fallback,
+ wallpaperLightImage: wp.lightImage ?? "",
+ wallpaperLightMobileImage: wp.lightMobileImage ?? wp.lightImage ?? "",
+ wallpaperLightFallback: wp.lightFallback ?? wp.fallback,
wallpaperKind: wp.kind ?? "image",
wallpaperComponent: wp.component ?? null,
wallpaperOverlayText: wp.overlayText ?? null,
@@ -236,8 +261,9 @@ export function applyThemeConfig(cfg: ThemeConfig) {
_applied.push(k);
}
}
- root.dataset.scheme = schemeFromBg(cfg.tokens?.["--color-shell-bg"]);
- useThemeStore.setState({ structure: cfg.structure || {}, effects: cfg.effects || [] });
+ const scheme = schemeFromBg(cfg.tokens?.["--color-shell-bg"]);
+ root.dataset.scheme = scheme;
+ useThemeStore.setState({ structure: cfg.structure || {}, effects: cfg.effects || [], scheme });
forceCompositingRepaint();
}
@@ -246,7 +272,7 @@ export function revertTheme(opts?: { silent?: boolean }) {
for (const k of _applied) root.style.removeProperty(k);
_applied = [];
root.dataset.scheme = "dark"; // base shell is dark
- useThemeStore.setState({ structure: {}, effects: [] });
+ useThemeStore.setState({ structure: {}, effects: [], scheme: "dark" });
// Skip when called from applyThemeConfig (which repaints once after applying
// the new tokens) so a theme switch does not force two reflows.
if (!opts?.silent) forceCompositingRepaint();
diff --git a/static/wallpaper-graphite-light-mobile.png b/static/wallpaper-graphite-light-mobile.png
new file mode 100644
index 0000000000000000000000000000000000000000..41cd07712855fa293632a0cf24a54ca86559ae29
GIT binary patch
literal 3654574
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