diff --git a/.claude/skills/taos-development-skill/SKILL.md b/.claude/skills/taos-development-skill/SKILL.md index 75f7d1dc2..6a6979740 100644 --- a/.claude/skills/taos-development-skill/SKILL.md +++ b/.claude/skills/taos-development-skill/SKILL.md @@ -180,6 +180,25 @@ I have read the CLA Document and I hereby sign the CLA 3. Once code is done and local tests pass, `gh pr ready`. Address review feedback with additional commits on the same branch. The maintainer merges upstream. +### Do not spend turns waiting + +You pay per turn. Spend them on judgement (reading a diff, diagnosing a failure, +deciding whether a green check is trustworthy), not on waiting or repetition. + +- **Do not poll CI in a loop.** `gh pr checks --watch` blocks in one call + and returns when the run finishes. Re-running `gh pr checks` every minute + costs a turn each time and tells you nothing new. (A malformed watch once + exited early and reported success while shards were still running, so read + the final status rather than trusting that the command returned.) +- **If you have hand-verified the same property twice, write the test.** Two + manual checks is the signal. A test costs nothing per run; re-reading costs + every time. +- **Ask the narrow question.** A targeted `grep` or a symbol-level diff against + `origin/dev` usually decides the matter far more cheaply than reading a whole + diff. Work out what single fact settles it, then fetch only that. +- **Do not re-poll a blocked run.** See `action_required` above: surface it and + stop. Repeatedly checking a run that is waiting on a human is pure cost. + ## Post-Push Bot Review Cycle After pushing a PR and marking it ready, automated bots review it. The reliable gate is @@ -429,6 +448,32 @@ Run `scripts/install-git-hooks.sh` to enable local hooks (`.githooks/pre-commit` are available. Most modules use `from __future__ import annotations`. - Code style: match surrounding code, one concern per module - Use `uv` for dependency management and test running: `uv sync --extra dev`, `uv run pytest` +- **Read `CONTRIBUTING.md` sections "Verifying your work" and "Avoiding collisions with + other contributors".** They are the canonical statement of both; this file does not + restate them so the two cannot drift. What follows is only the part specific to + working here as an agent. + +## Verification (agent specifics) + +Green is a claim, not evidence. The repo-level cases are in CONTRIBUTING.md. These are +the ones that bite agents in particular: + +- **Do not report work as done without a PR link.** A branch with commits and no PR is + not delivered. A lane once announced completion on an empty branch; the check now runs + before any completion is claimed, and the same standard applies to you. +- **A PR that passes CI can still be empty.** Before saying a card is finished, look at + the actual diff and confirm it contains the change described. +- **Never infer merge conflicts from `git merge-tree`** against branches you have not + fetched; it produces confident nonsense. Use GitHub's `mergeable_state`, and treat + `unknown` as "not computed yet" and re-query rather than as an answer. +- **Check the exit status of the command that matters**, not the last one in a pipe. + `cmd | tail && echo OK` prints OK when `cmd` failed. +- **An empty fetch is not a match.** If two files that cannot be identical hash the same, + your request failed rather than the contents agreeing. +- **Verify a bot finding against the code before folding it.** Automated review is often + wrong and always confident. Fold what is real, say plainly what is not. +- **Report honestly.** If tests fail, include the output. If a step was skipped, name it. + "Done" means verified. ## Desktop SPA build + test diff --git a/.coderabbit.yaml b/.coderabbit.yaml index 56210ec5c..bab28a109 100644 --- a/.coderabbit.yaml +++ b/.coderabbit.yaml @@ -9,3 +9,28 @@ reviews: base_branches: - master - dev + # The plan has a finite review quota, and it was being spent in the wrong + # places: repeatedly "review limit reached" on real code PRs while lockfile + # bumps consumed reviews. #2127 and #2131 both went unreviewed for this + # reason on the day this was written. + # + # Dependabot PRs are version bumps plus a regenerated lockfile. There is no + # human-authored logic for a reviewer to reason about, and CI already gates + # them, so they are the cheapest thing to stop spending quota on. + ignore_usernames: + - dependabot + - dependabot[bot] + # A draft is explicitly not ready. Reviewing it burns a review that will be + # burned again when it is marked ready. + drafts: false + # Generated artefacts dominate the diff without carrying reviewable intent. + # A single spa-deps bump is a 2000+ line package-lock diff. + path_filters: + - "!**/package-lock.json" + - "!**/uv.lock" + - "!desktop/dist/**" + # Only the BUILT desktop bundle under static/, not all of static/: sw.js and + # the PWA manifests there are human-authored runtime behaviour and exactly + # the kind of change a reviewer should see (Qodo caught the over-broad + # original). + - "!static/desktop/**" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bc76fb9b0..81b95d5dd 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -34,7 +34,15 @@ jobs: # run at once instead of queueing behind the 2 self-hosted boxes. The VPS + # Fedora runners stay free for the kilo lane and GPU/bench work. - test: + # Renamed from "test" to "shards" because sharding changes the check-run names + # this job reports. Branch protection matches required checks by NAME, so the + # sharded jobs report "test (3.12, 1)" etc and the required contexts + # "test (3.12)" / "test (3.13)" would never be reported again. Protection then + # waits forever on checks that cannot exist and blocks every PR, including the + # one that would fix it. The aggregate "test" job below keeps reporting those + # two exact names, so no protection change is needed and there is no window + # where dev is wedged. + shards: runs-on: ubuntu-latest # Measured 2026-07-26 over the last 12 job records: 3.13 runs 31-35 min and # 3.12 runs 38-41 min. The previous comment claimed "~16 min", which had @@ -55,6 +63,15 @@ jobs: # PR + push runs cover the latest two; the daily cron run # below adds 3.11 so the minimum-supported version stays tested. python-version: ${{ github.event_name == 'schedule' && fromJSON('["3.11", "3.12", "3.13"]') || fromJSON('["3.12", "3.13"]') }} + # Shard the suite across parallel jobs. xdist parallelises WITHIN a job, + # but a hosted runner is 2 vCPU, so -n auto caps at 2 workers however + # many tests exist -- and the suite has grown 4845 -> 10250 (#2135). + # Cores per job were the bottleneck, not the tests. + # + # A public repo gets ~20 concurrent jobs free and we were using 2, so + # this costs nothing. 2 versions x 4 shards = 8 jobs on PR/push, and + # 12 on the scheduled run that adds 3.11. + shard: [1, 2, 3, 4] steps: - uses: actions/checkout@v7 @@ -78,20 +95,63 @@ jobs: # not from a fresh unpinned resolve that can pull in a regressed # release (see the fastapi 0.137 incident, #903). - name: Install dependencies - run: uv sync --frozen --extra dev --python ${{ matrix.python-version }} + run: uv sync --frozen --python ${{ matrix.python-version }} # SPA bundle is stubbed by tests/conftest.py — see pytest_configure. # The real build is exercised in the spa-build job below. - - name: Run tests - # Run across all runner cores. The serial suite (4845 tests) took - # ~22 min; -n auto cuts it to a few minutes so "merge on green" is - # practical. Dropped -v so xdist worker output stays readable. - run: uv run --no-sync pytest tests/ --tb=short --ignore=tests/e2e -n auto - + - name: Run tests (shard ${{ matrix.shard }}/4) + # --splits/--group partition the suite deterministically and exactly: + # verified that the 4 groups union to all 10250 collected tests with no + # duplicates and, critically, no drops. A sharding bug that silently + # skipped tests would leave CI green while testing less, which is the + # one failure mode worth checking rather than assuming. + # + # -n auto still uses both cores inside each shard. + run: >- + uv run --no-sync pytest tests/ --tb=short --ignore=tests/e2e + -n auto --splits 4 --group ${{ matrix.shard }} + + # Import smoke check: identical in every shard, so run it once per Python + # version (shard 1 only) rather than in all 4 shards. - name: Verify app starts + if: matrix.shard == 1 run: uv run --no-sync python -c "from tinyagentos.app import create_app; print('OK')" + # Aggregate gate. Reports the exact check names branch protection requires + # ("test (3.12)", "test (3.13)") so the required-context list does not have to + # change when the shard count does. An explicit `name:` is used verbatim by + # GitHub rather than having the matrix values appended, which is what makes + # reproducing the old names possible at all. + # + # `if: always()` is load-bearing. Without it this job is SKIPPED when the + # shards fail, and GitHub treats a skipped required check as satisfied -- so a + # red suite would report a green gate. It must run and fail explicitly. + test: + name: test (${{ matrix.python-version }}) + needs: shards + if: always() + runs-on: ubuntu-latest + strategy: + # Both gates must report their own conclusion. With the default + # fail-fast, the first gate to fail CANCELS its sibling, so a red suite + # reported "test (3.12) failure, test (3.13) cancelled" -- observed on a + # deliberate-failure branch. Cancelled is not success so protection still + # blocks, but which version broke becomes a coin flip, and a required + # check whose conclusion depends on scheduling order is not a gate worth + # trusting. + fail-fast: false + matrix: + python-version: ["3.12", "3.13"] + steps: + - name: Gate on shard results + run: | + echo "shards concluded: ${{ needs.shards.result }}" + if [ "${{ needs.shards.result }}" != "success" ]; then + echo "::error::test shards did not all pass" + exit 1 + fi + lint: runs-on: ubuntu-latest steps: @@ -108,7 +168,7 @@ jobs: enable-cache: true - name: Install dependencies - run: uv sync --frozen --extra dev --python 3.12 + run: uv sync --frozen --python 3.12 - name: Check for syntax errors run: uv run --no-sync python -m compileall tinyagentos/ -q diff --git a/.github/workflows/deleted-symbols-gate.yml b/.github/workflows/deleted-symbols-gate.yml new file mode 100644 index 000000000..239abb1de --- /dev/null +++ b/.github/workflows/deleted-symbols-gate.yml @@ -0,0 +1,41 @@ +name: Deleted symbols gate + +# Detects PRs that silently delete Python symbols (def/class/test names) that +# landed on the target branch after the PR's merge base. A PR cut before +# hardening commits landed on dev would silently delete them when merged -- +# git reports "Automatic merge went well" with no conflict because the PR +# branch simply wins on files dev touched after the branch point. +# +# This check fails such PRs and names the deleted symbols and the commits that +# added them. A "Removes-Intentionally: " trailer in the PR body +# waives named symbols, making deliberate deletions a conscious, auditable act. +# +# See scripts/check_deleted_symbols.py for the implementation. + +on: + pull_request: + branches: [master, dev] + +jobs: + deleted-symbols-gate: + runs-on: ubuntu-latest + permissions: + contents: read + env: + BASE_REF: ${{ github.base_ref }} + steps: + - uses: actions/checkout@v7 + with: + fetch-depth: 0 + + - uses: actions/setup-python@v7 + with: + python-version: "3.12" + + - name: Fetch base branch + run: git fetch origin "$BASE_REF" + + - name: Check for silently deleted symbols + env: + PR_BODY: ${{ github.event.pull_request.body }} + run: python scripts/check_deleted_symbols.py --base "origin/$BASE_REF" diff --git a/.github/workflows/doc-gate.yml b/.github/workflows/doc-gate.yml index 3d0478d66..0ad27c28b 100644 --- a/.github/workflows/doc-gate.yml +++ b/.github/workflows/doc-gate.yml @@ -34,6 +34,9 @@ jobs: - name: Schema-migration guard (#1865) run: python scripts/check_schema_migrations.py + - name: Retrofit-migration guard (#2188) + run: python scripts/check_retrofit_migrations.py + - name: Diff gate (Layer B) env: BASE_REF: ${{ github.base_ref }} diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml index c7ecf11f6..2e0576e06 100644 --- a/.github/workflows/security.yml +++ b/.github/workflows/security.yml @@ -3,11 +3,27 @@ name: Security on: push: branches: [master] + # dev is where work actually lands: every exec PR targets dev, and master only + # receives it later at a release promotion. Auditing master-only meant the + # dependency audit NEVER ran on a pull request, so a vulnerable dependency + # could merge into dev and ship in a beta without anything looking at it. The + # weekly cron still covers master, but that is after the fact and on the wrong + # branch. Verified clean on dev before enabling: pip-audit reports no known + # vulnerabilities, so this does not block the open queue. pull_request: - branches: [master] + branches: [master, dev] schedule: - cron: "0 6 * * 1" +# Mirror ci.yml: this now runs on every dev PR update, so superseded runs must +# cancel or each push leaves an orphaned audit burning runner minutes. Pushes to +# master and dev are deliberately NOT cancelled, so the branch record stays +# complete. (qodo on #2189, verified: ci.yml carries this block and security.yml +# did not, which only started to matter once the dev trigger was added.) +concurrency: + group: security-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: ${{ github.ref != 'refs/heads/master' && github.ref != 'refs/heads/dev' }} + permissions: contents: read diff --git a/.gitignore b/.gitignore index 91b3770ac..1f6dcc81f 100644 --- a/.gitignore +++ b/.gitignore @@ -92,6 +92,9 @@ screenshots.zip data/.auth_user.json data/browser_cookie_key.hex +# Hub identity — runtime state, never track (contains generated keypairs) +data/hub/ + # Vite/TS incremental build artifact — regenerated on every build, must not be tracked # (a tracked copy dirties the tree and blocks the in-app git-pull update). desktop/tsconfig.tsbuildinfo @@ -128,3 +131,48 @@ data/pending-restart.json # Stray dev screenshot artifacts desktop-initial.png venv/ + +# Credential material must never be committable. An agent stored a registry +# token at /opt/taos/secrets/ on 2026-07-27 - correct permissions, but inside the +# deployed git checkout and one "git add -A" away from a public repo. +secrets/ +*.token +*.cred + +# Credential material under data/. On 2026-07-27 three of these sat UNTRACKED in +# the deployed checkout - one 'git add -A' from a public repo. Never committed +# (verified against full history), but untracked is not the same as safe. +# +# Deliberately NOT using 'data/*' deny-all: data/archive, data/sessions and +# data/guides.yaml are legitimately tracked, and a blanket rule would silently +# ignore NEW files there - trading a credential risk for a data-loss-by- +# invisibility one. Enumerate the secrets instead, and keep this list current. +data/.secrets_key +data/.seeded-agent-tokens.json +data/secrets.db* +data/*.key +data/*.token + +# Key material and secrets. Added 2026-07-27 after data/hub/identity.json was +# committed to PR #2043 with signing_private and encryption_private in +# PLAINTEXT. data/hub/ is already covered above; these patterns close the REST +# of the surface, which was open: an identity.json or a *.key written anywhere +# outside data/hub/ had nothing stopping it. Pattern-based on purpose, since a +# per-file allowlist cannot catch a file that does not exist yet. +# Verified against every tracked file: none become ignored. +*.key +*_private.pem +*_private.key +*_private.json +*_private_key* +identity.json +*.p8 +*credentials.json + +# Credential bundles. Added 2026-07-27: @taOS-website-dev ran the same audit +# on its repo and found shapes my first pass (PR #2171) missed. Verified on +# origin/dev: creds.json, my_creds.json and data/creds.json were all NOT +# ignored, while *credentials.json already was. Matching the *creds* stem +# closes the shorter spelling, which is the one people actually type. +# Verified against every tracked file on origin/dev: 0 become ignored. +*creds*.json diff --git a/CHANGELOG.md b/CHANGELOG.md index 571a91743..b1d1efbf2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,91 @@ Versions follow semver beta: `1.0.0-beta.N`, bumped on each dev->master promotio ## [Unreleased] +## [1.0.0-beta.45] - 2026-08-02 + +### Added + +- **Community view**: collaborator stats, leaderboard, and a read-only kanban of + the public board (#2042). +- **Notes/Todo split**: Notes and Todo are now separate apps, final integration + pass (#2033). +- **Hub sealed-envelope relay**: X25519 store-and-forward through taos.my so two + boxes can exchange DMs without a direct connection (#2034), plus the E1 DM + schema migration preparing cross-box messaging (#2047). +- **Library**: storage accounting view (#2099), YouTube and Web ingest processors + with streaming, content-type gates and timeout guards (#2177), a broken + thumbnail now shows a placeholder instead of blank space (#2120), and the + source ingest option is actually wired up (#2117). +- **Memory app**: shows taOSmd running mode, reachability and tier, with a + switch-to-remote control (#1959). +- **Wallpaper picker**: Wallhaven browse via a server-side proxy, sectioned + picker (#1902). +- **Decisions**: agents can ask a Decision and mirror a chat answer back onto the + card, with spoofing, consent and cross-project protections layered on + (#2179 series). +- **Scope requests in the bell**: approve/deny buttons directly on scope-request + notifications (#2107), and `project_tasks_update` so lead agents can edit + their own board cards (#2184). +- **taOStalk groundwork**: typed `content_blocks` and the render dispatcher + (#2154), theme-token migration (#2151). +- **Share destinations**: authorization-filtered `GET /api/share/destinations` + (S2A, #2146). +- **App tiering S1**: registry tier/group/handler fields (#2185). +- **Device-bearer self-service**: push-token rotation and device management on + the device token itself (#2232). +- **Reserved agent names**: `user-`, `human-`, `admin-` and `taos-` prefixes are + rejected at registration (including punctuation/spacing obfuscations), so an + external agent cannot squat an identity that reads as a person or as an + internal taOS agent (#2237). +- Per-user 24h feedback submission cap (#2131). + +### Fixed + +- **Push notifications never routed to the correct app.** Three independent faults + in the service-worker click path meant tapping any notification either opened + the desktop root or did nothing: the backend dropped routing fields before the + SW saw them, the deep-link fallback was always root, and no shell listener + existed for the common mobile-PWA case where the app is already open. Decision + pushes now open Decisions (or the mapped target app) whether or not a window + is already open (#2179). +- **Web push was silently broken**: the VAPID key format made every send fail + (#2166). +- **Wallpapers squashed on square and odd-ratio screens**, in both desktop and + browser modes. +- **SPA entry points missing the auth guard**: every entry point now installs it + (#2174). +- **App Store install gate hardening**: signing failure is fail-closed instead of + silently unsigned (#2050), date-safe manifest canonicalisation and an async + TOCTOU re-check, and the unsigned-manifest policy contradiction is resolved + (#2218). +- **Scope binding**: `project_tasks_create` and file scopes bind to a project on + approval instead of floating globally (#2127). +- Watch-face projection issues (#2230). +- CSRF token is merged into `Request`-object fetch inputs too (#1999). +- Canvas `.tldr` export produces a file tldraw can actually open (#2133). +- GPU arbiter: `drain_tick_seconds` floor clamped and a double capacity wake + removed (#1987). +- Project create enforces name uniqueness and auto-rejects duplicates (#2168). +- Duplicate channel header on rebased share routes (#2169). +- Project lists: entry positioning and missing store wiring. + +### Changed + +- **CI test suite is sharded** across parallel jobs (#2137), the dependency audit + runs on dev PRs (#2189), the test timeout sits above the real suite runtime + (#2134), and CodeRabbit no longer spends review quota on generated diffs + (#2136). +- **Dev dependencies moved to `[dependency-groups]`** so a plain `uv sync` gives + a working test environment (#2217). +- **Doc drift gate**: rules can opt into firing on plain modifications + (`on_modify`), with changelog, agent-manual and contributor-skill coverage + (#2236). +- The Workspace tab no longer embeds a chat pane. +- Security dependency bumps: pillow 12.3.0, dompurify 3.4.12. +- Docs: verification and collision working rules written down (#2163), realtime + A2A connection guide for deployed agents (#2161), agent token storage + hardening (#2159). + ## [1.0.0-beta.44] - 2026-07-26 ### Added diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 53907a338..c994ef046 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,8 +1,8 @@ # Contributing to taOS -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. +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. +> **Note:** The project is in early development. APIs and interfaces may change. That is fine - contributions of all sizes are welcome. --- @@ -10,24 +10,36 @@ Welcome — and thanks for your interest in contributing. taOS is a self-hosted taOS is open source under the **GNU Affero General Public License v3.0 or later** (AGPL-3.0-or-later; see [`LICENSE`](LICENSE)). A separate commercial license is available from jaylfc for uses the AGPL does not grant, for example embedding taOS in a proprietary product or offering it as a hosted service without releasing your modifications under the AGPL (see [`COMMERCIAL-LICENSE.md`](COMMERCIAL-LICENSE.md)). -To keep this sustainable, **all contributors must agree to the Contributor License Agreement ([`CLA.md`](CLA.md))** before their contributions are merged. The CLA grants jaylfc the right to include and **relicense** your contributions under the project's licenses; **you keep ownership of your work**. You sign once — on your first pull request, comment **"I have read the CLA Document and I hereby sign the CLA"** and the CLA check turns green; it then covers all your future contributions. +To keep this sustainable, **all contributors must agree to the Contributor License Agreement ([`CLA.md`](CLA.md))** before their contributions are merged. The CLA grants jaylfc the right to include and **relicense** your contributions under the project's licenses; **you keep ownership of your work**. You sign once - on your first pull request, comment **"I have read the CLA Document and I hereby sign the CLA"** and the CLA check turns green; it then covers all your future contributions. --- ## Getting Started for Contributors ```bash -git clone https://github.com/jaylfc/tinyagentos.git +git clone https://github.com/jaylfc/taOS.git cd tinyagentos -uv sync --extra dev -# Build the desktop SPA — static/desktop/ is gitignored (generated artifact) +uv sync +# Build the desktop SPA - static/desktop/ is gitignored (generated artifact) cd desktop && npm install && npm run build && cd .. uv run pytest tests/ --ignore=tests/e2e -n auto ``` Python 3.10 or later and Node.js 22 or later are required. -For frontend development, use `cd desktop && npm run dev` — Vite serves with hot reload on port 5173. +For frontend development, use `cd desktop && npm run dev` - Vite serves with hot reload on port 5173. + +--- + +## Contributing with an AI coding agent + +If you work on taOS through a coding agent (Claude Code, opencode, Cursor, or similar), point it at **[`.claude/skills/taos-development-skill/SKILL.md`](.claude/skills/taos-development-skill/SKILL.md)**. It ships in the repo, so a clone already has it. + +It is the operational companion to this file: repository layout, the architectural patterns worth knowing before touching a subsystem, the local test gate to run before marking work ready, the fork PR and CI specifics, and the pitfalls that have actually cost contributors time here. This file states the conventions; the skill states how to apply them without tripping over the setup. + +Agents should read both. The skill deliberately does not restate the conventions in this file, so that the two cannot drift out of sync. + +**If you are an AI agent reading this:** load that skill before making changes, and pay particular attention to "Verifying your work" and "Avoiding collisions with other contributors" below. Most problems here have come from trusting a signal that looked like success rather than from writing bad code. --- @@ -63,12 +75,12 @@ Keep pull requests focused. One feature or fix per PR is easier to review. > **Branches:** `master` is the stable branch that installs track, so it only > receives tested changes promoted from `dev`. All contributions target `dev`. -> If you open a PR against `master` by mistake, no problem — we'll retarget it +> If you open a PR against `master` by mistake, no problem - we'll retarget it > to `dev` (the commits and review carry over). ### Documentation -Documentation improvements are always welcome — typo fixes, clarifications, better examples. Open a PR directly. +Documentation improvements are always welcome - typo fixes, clarifications, better examples. Open a PR directly. The taOS agent manual is compiled: edit `docs/agent-manual/` and run `python3 scripts/build-agent-manual.py` to regenerate `docs/taos-agent-manual.md`. @@ -78,7 +90,7 @@ The taOS agent manual is compiled: edit `docs/agent-manual/` and run `python3 sc The catalog lives in `app-catalog/`. Each app has its own directory containing a `manifest.yaml`. -### Step 1 — Create the directory +### Step 1 - Create the directory ``` app-catalog/ @@ -94,7 +106,7 @@ Pick the appropriate category and create a directory named after your app's `id` mkdir app-catalog/agents/my-framework ``` -### Step 2 — Write manifest.yaml +### Step 2 - Write manifest.yaml Use `app-catalog/agents/langroid/manifest.yaml` as a template: @@ -131,7 +143,7 @@ hardware_tiers: All fields except `config_schema` are required. The `hardware_tiers` block controls which hardware profiles see the app as recommended. -### Step 3 — Update catalog.yaml +### Step 3 - Update catalog.yaml Add an entry to `app-catalog/catalog.yaml` under the appropriate section: @@ -143,7 +155,7 @@ Add an entry to `app-catalog/catalog.yaml` under the appropriate section: description: "One-line description matching your manifest" ``` -### Step 4 — Open a PR +### Step 4 - Open a PR Submit a pull request. The CI will run the catalog tests automatically. Include a link to the upstream project in your PR description. @@ -153,7 +165,7 @@ Submit a pull request. The CI will run the catalog tests automatically. Include ### Python -- Follow the patterns already in the codebase — there is no strict linter, but keep it readable +- Follow the patterns already in the codebase - there is no strict linter, but keep it readable - One concern per module; avoid cross-importing between route files - Use `async def` for route handlers; use `await` for all I/O @@ -161,13 +173,13 @@ Submit a pull request. The CI will run the catalog tests automatically. Include The UI is a React SPA (`desktop/`) built with Vite. Static assets are served from `static/desktop/` after `npm run build`. If you are adding a new UI surface: -- Follow existing React patterns in `desktop/src/` — no server-rendered templates for new features +- Follow existing React patterns in `desktop/src/` - no server-rendered templates for new features - ARIA labels are required on interactive elements without visible text labels - One concern per component; keep API calls in dedicated hooks or service files ### Tests -- Use pytest; fixtures live in `tests/conftest.py` — use them +- Use pytest; fixtures live in `tests/conftest.py` - use them - Mirror the module structure: `tinyagentos/routes/agents.py` -> `tests/test_agents.py` - All PRs must pass CI before merge @@ -188,6 +200,12 @@ Do not include AI tool attribution in commit messages. --- +## Design laws + +Every taOS surface is a live surface: new or changed UI must auto-refresh and +carry motion per `docs/design/live-surface-law.md`. PRs adding a static screen +get bounced on review. + ## Testing Run the full test suite: @@ -202,12 +220,37 @@ Run a specific test file: pytest tests/test_catalog_sync.py -v ``` -The project has ~3,590 tests. CI runs against Python 3.12 and 3.13 on every pull request (two matrix jobs). Python 3.11 is added on the nightly scheduled run. A PR cannot be merged until all matrix jobs pass. +The project has ~10,250 tests. CI runs against Python 3.12 and 3.13 on every pull request. Python 3.11 is added on the nightly scheduled run. A PR cannot be merged until all required checks pass. When adding a feature, add tests that cover the new behaviour. When fixing a bug, add a regression test. --- +## Verifying your work + +**A passing signal is a claim, not evidence.** Most bad merges in this project came from something that looked like success without being one. Before trusting a green tick, ask what would have to be true for it to be lying. + +Cases that have actually happened here: + +- **A PR passed every required check while containing only a version bump.** Green proves the suite ran, not that the work happened. +- **A test file reported green while every test in it skipped.** `pytest.importorskip` is the correct way to land tests ahead of the code they cover, and it asserts nothing until that code exists. Once the implementation merges, confirm the skips became real passes. If the import path in the guard does not match what shipped, they skip forever and look identical to passing. +- **`cmd | tail && echo OK` prints OK when `cmd` failed**, because `&&` sees `tail`'s exit status. Check the exit code of the command that matters. +- **Green on a branch with conflicts tested the old base**, not the result of merging. + +Applied to your own changes: + +- **If you add a gate, prove it fails.** A check only ever observed passing is unproven where it counts. Make it go red deliberately once. +- **Verify a review comment against the code before acting on it**, including automated ones. Bot findings are often wrong, and confidently phrased. +- **Report honestly.** If tests fail, say so and include the output. If you skipped a step, name it. "Done" should mean verified, not attempted. + +## Avoiding collisions with other contributors + +- **Check whether an open PR already adds the file you are adding.** Two PRs that both create the same path cannot both merge, and whichever lands first silently decides the design. This has cost duplicated implementations of the same feature more than once. +- **Renaming a CI job orphans its required check.** Branch protection matches required checks by name, so a rename means the old name is never reported again and later PRs block on a check that can no longer exist. Keep the old name reported (an aggregate job whose explicit `name:` reproduces it) or update protection first. +- **Stage explicit paths rather than `git add -A`**, which sweeps in untracked files you did not write. + +--- + ## Documentation gate A gate blocks PRs that add or remove certain feature code without a matching doc update. It only fires on structural changes (a file added or deleted), never on a plain edit, and only for a small set of conservative rules configured in `docs/doc-gate.toml`: @@ -250,7 +293,7 @@ app-catalog/ # YAML manifests for installable apps (108 apps) tests/ # pytest test suite (~3,590 tests) ``` -Routes are registered in `app.py`. Route modules access stores via `request.app.state` (dependency injection set up in the app lifespan) — they do not import stores directly. The frontend is a React SPA; `templates/` is minimal and only used for the agent debugger page. +Routes are registered in `app.py`. Route modules access stores via `request.app.state` (dependency injection set up in the app lifespan) - they do not import stores directly. The frontend is a React SPA; `templates/` is minimal and only used for the agent debugger page. --- diff --git a/README.md b/README.md index e87566e68..32bcee8e1 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ > **A heads-up on the catalogs:** with 100+ apps, 16 frameworks, and a large model catalog, plenty of install manifests have not been exercised on real hardware yet, so some apps, frameworks, and models will fail to install. If one does, [open an issue](https://github.com/jaylfc/taOS/issues) with the name and the error you saw and I will fix the manifest as soon as I can. These reports are genuinely useful, most manifest fixes ship same-day.

- + @@ -249,7 +249,7 @@ Hidden internal gateway that unifies all inference providers behind a single Ope 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. ### Creative Studios -Dedicated studio apps for every kind of project, each a focused, native taOS workspace that runs entirely on your own cluster. Two are bundled (Images, Game Studio); Coding, App, Design, Music, Office, Web, and Video studios are implemented but gated as optional apps (App Studio is taOS's own app builder, so agents and users can build and share new apps). +Dedicated studio apps for every kind of project, each a focused, native taOS workspace that runs entirely on your own cluster. Two are bundled (Images, Game Studio); Coding, App, Design, Music, Office, Web, Video, and Assistant studios are implemented but gated as optional apps (App Studio is taOS's own app builder, so agents and users can build and share new apps).

Images Studio -- generate from a prompt and edit on a local GPU @@ -686,7 +686,7 @@ uv run exo ## Development ```bash -uv sync --extra dev +uv sync uv run pytest tests/ --ignore=tests/e2e -n auto # ~7,400 tests cd desktop && npx vitest run # ~1,900 desktop tests ``` diff --git a/desktop/package-lock.json b/desktop/package-lock.json index 12459b481..b9cf71b26 100644 --- a/desktop/package-lock.json +++ b/desktop/package-lock.json @@ -1,18 +1,18 @@ { "name": "tinyagentos-desktop", - "version": "1.0.0-beta.43", + "version": "1.0.0-beta.44", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "tinyagentos-desktop", - "version": "1.0.0-beta.43", + "version": "1.0.0-beta.44", "dependencies": { "@codemirror/lang-markdown": "^6.5.1", "@codemirror/language-data": "^6.5.2", "@codemirror/state": "^6.7.1", "@codemirror/theme-one-dark": "^6.1.3", - "@codemirror/view": "^6.43.6", + "@codemirror/view": "^6.43.7", "@excalidraw/excalidraw": "^0.18.1", "@excalidraw/mermaid-to-excalidraw": "^2.2.2", "@fortune-sheet/core": "^1.0.4", @@ -28,11 +28,11 @@ "@radix-ui/react-switch": "^1.3.7", "@radix-ui/react-tabs": "^1.1.21", "@radix-ui/react-tooltip": "^1.2.16", - "@tiptap/extension-link": "^3.29.0", - "@tiptap/extension-underline": "^3.29.0", - "@tiptap/pm": "^3.29.0", + "@tiptap/extension-link": "^3.29.2", + "@tiptap/extension-underline": "^3.29.2", + "@tiptap/pm": "^3.29.2", "@tiptap/react": "^3.26.1", - "@tiptap/starter-kit": "^3.29.0", + "@tiptap/starter-kit": "^3.29.2", "@tldraw/assets": "4.5.12", "@tldraw/tldraw": "^4.5.10", "@tonejs/midi": "^2.0.28", @@ -53,11 +53,11 @@ "mathjs": "^15.2.0", "mermaid": "^11.16.0", "modern-screenshot": "^4.7.0", - "motion": "^12.42.2", + "motion": "^12.43.0", "plyr": "^3.8.4", "react": "^19.2.8", "react-dom": "^19.2.8", - "react-grid-layout": "^2.2.3", + "react-grid-layout": "^2.2.4", "react-konva": "^19.2.5", "react-markdown": "^10.1.0", "react-resizable": "^4.0.2", @@ -686,9 +686,9 @@ } }, "node_modules/@codemirror/view": { - "version": "6.43.6", - "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.43.6.tgz", - "integrity": "sha512-EVunGSYN1wz1p75WY1s3Xg7t3i8Yol0kGZGizNdX9BUFgMFILYVe8/u6EVpo7Ff5PwbZuILb4QAq7IZoKzIEQA==", + "version": "6.43.7", + "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.43.7.tgz", + "integrity": "sha512-FZsExxkoxnAN+d9TgqXLg5g4A1oQwzX9WlkOT5i2PKkcW7xx3Bmu0vs90g6fo9Mpdsb/l96dnAraQ8932aO4/g==", "license": "MIT", "dependencies": { "@codemirror/state": "^6.7.0", @@ -1622,6 +1622,12 @@ "node": ">=6" } }, + "node_modules/@excalidraw/excalidraw/node_modules/immutable": { + "version": "4.3.8", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.8.tgz", + "integrity": "sha512-d/Ld9aLbKpNwyl0KiM2CT1WYvkitQ1TSvmRtkcV8FKStiDoA7Slzgjmb/1G2yhKM1p0XeNOieaTbFZmU1d3Xuw==", + "license": "MIT" + }, "node_modules/@excalidraw/excalidraw/node_modules/picomatch": { "version": "2.3.2", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", @@ -6193,9 +6199,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -6213,9 +6216,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -6233,9 +6233,6 @@ "ppc64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -6253,9 +6250,6 @@ "s390x" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -6273,9 +6267,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -6293,9 +6284,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -6522,9 +6510,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -6542,9 +6527,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -6562,9 +6544,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -6582,9 +6561,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -6816,49 +6792,49 @@ } }, "node_modules/@tiptap/core": { - "version": "3.29.0", - "resolved": "https://registry.npmjs.org/@tiptap/core/-/core-3.29.0.tgz", - "integrity": "sha512-A/lrhKpOYtl0V5pmPS00Zps8pgBe1qDOoD9fzsumDSZ3HP8W398C959Jgru75PNFokAya9COPD3iaKP3NWF25g==", + "version": "3.29.2", + "resolved": "https://registry.npmjs.org/@tiptap/core/-/core-3.29.2.tgz", + "integrity": "sha512-oKUkiPUB7noilVYxI9lNzUD4rX17sHub+PYjMfHMWHG9A3nvIy+FdePIVIIhThKWF7ijhr3eIqHY51Bn+GAFtw==", "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/pm": "3.29.0" + "@tiptap/pm": "3.29.2" } }, "node_modules/@tiptap/extension-blockquote": { - "version": "3.29.0", - "resolved": "https://registry.npmjs.org/@tiptap/extension-blockquote/-/extension-blockquote-3.29.0.tgz", - "integrity": "sha512-k8NKHLEYOqre7guObZBeFM04CaTwmEceCmdIrIjd7H9KoHSbRODaBNved6j5G33x/FWMwIF/RQbE2BjWaFSgzA==", + "version": "3.29.2", + "resolved": "https://registry.npmjs.org/@tiptap/extension-blockquote/-/extension-blockquote-3.29.2.tgz", + "integrity": "sha512-ca4OzKDh0yaxg2+Z56bC2QnWsNsFp2YMRfVig1PDXyMVFMNJpLcnhxgq/9btn+xYAlYrj8RymOeCTYREOR6Zjg==", "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "3.29.0", - "@tiptap/pm": "3.29.0" + "@tiptap/core": "3.29.2", + "@tiptap/pm": "3.29.2" } }, "node_modules/@tiptap/extension-bold": { - "version": "3.29.0", - "resolved": "https://registry.npmjs.org/@tiptap/extension-bold/-/extension-bold-3.29.0.tgz", - "integrity": "sha512-BPUJvJ9sCsU3fxao5UJfDJrDuHhMn2hczZwZ9Qs4w1vkAeLGyOYbSejUTJdBYHLoexGiEwgnEeVeZN3C8GaI5g==", + "version": "3.29.2", + "resolved": "https://registry.npmjs.org/@tiptap/extension-bold/-/extension-bold-3.29.2.tgz", + "integrity": "sha512-elYbGxJsYnBb4leqrcjdIJuiG380BcOgN+UUzvOv+qEjfGVzHodFOMBl3qnmD6urYHNu5/qQK2S0qSSRXKCLNQ==", "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "3.29.0" + "@tiptap/core": "3.29.2" } }, "node_modules/@tiptap/extension-bubble-menu": { - "version": "3.29.0", - "resolved": "https://registry.npmjs.org/@tiptap/extension-bubble-menu/-/extension-bubble-menu-3.29.0.tgz", - "integrity": "sha512-aBNI8ebX76ObderRmu/PJ9Q2AGVEuhpg0TBvv2SNbHtmU5kNUMHQI8HxrxKFblJwETAmG6EJ3Ed/5jRxpJmrsg==", + "version": "3.29.2", + "resolved": "https://registry.npmjs.org/@tiptap/extension-bubble-menu/-/extension-bubble-menu-3.29.2.tgz", + "integrity": "sha512-kzcWarcpr031rZu+R3Hzut5uxb3Qj/xxMDEYZIQ3uiq1yb5vEMXj8/SIyWautk6Nu8Rr1leV3rGdR4NzhzyFAA==", "license": "MIT", "optional": true, "dependencies": { @@ -6869,80 +6845,80 @@ "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "3.29.0", - "@tiptap/pm": "3.29.0" + "@tiptap/core": "3.29.2", + "@tiptap/pm": "3.29.2" } }, "node_modules/@tiptap/extension-bullet-list": { - "version": "3.29.0", - "resolved": "https://registry.npmjs.org/@tiptap/extension-bullet-list/-/extension-bullet-list-3.29.0.tgz", - "integrity": "sha512-1045sC5CRn7KD0wluxZeksBwpm2k3LKBUOM3kNwExHAPSV4in4gdarYJ2/WCmIKsmyj2FzUcF4rbpLLSB/dBSg==", + "version": "3.29.2", + "resolved": "https://registry.npmjs.org/@tiptap/extension-bullet-list/-/extension-bullet-list-3.29.2.tgz", + "integrity": "sha512-3bWcCUPbCHv0XttlMdnAtXLNYWx2pblByMgxmGsaP9FU0QnslGXty6A6gHCqI33ygRg1vrA6U5Wtpwbi5aKu5g==", "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/extension-list": "3.29.0" + "@tiptap/extension-list": "3.29.2" } }, "node_modules/@tiptap/extension-code": { - "version": "3.29.0", - "resolved": "https://registry.npmjs.org/@tiptap/extension-code/-/extension-code-3.29.0.tgz", - "integrity": "sha512-Um0BlyunRJ8Fal288Jn7I2n15FRwQcTy3NnKBBrPz3ikwx+yZZ0n9xTozpLMSxXrTq6cRlxLkASp/9FFx9sX9A==", + "version": "3.29.2", + "resolved": "https://registry.npmjs.org/@tiptap/extension-code/-/extension-code-3.29.2.tgz", + "integrity": "sha512-c6W5UGuB7WNLpYocsgRzpO2OOTI4QjaI9jjHRMuty9z+s9DtaYM/HrRLNwVh6MopkHb+i/89Wkv8gCS34fftig==", "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "3.29.0" + "@tiptap/core": "3.29.2" } }, "node_modules/@tiptap/extension-code-block": { - "version": "3.29.0", - "resolved": "https://registry.npmjs.org/@tiptap/extension-code-block/-/extension-code-block-3.29.0.tgz", - "integrity": "sha512-nKm1YSEiPVosjm4qeg6m+6SAKq82rKukAzE/g3M5eVs2OgEb4QIKQvQOmJ+zgGAnpb9Q/JIvcKsGGU1rn9Vnyw==", + "version": "3.29.2", + "resolved": "https://registry.npmjs.org/@tiptap/extension-code-block/-/extension-code-block-3.29.2.tgz", + "integrity": "sha512-w153ct8g6dLiPTdXQ6SOIMxX4SEo5Q50AmjdEEEcJ7ZcYUcde/ScSskLHfOYmyt5ZFAiyEwr121+pux+p3/oAQ==", "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "3.29.0", - "@tiptap/pm": "3.29.0" + "@tiptap/core": "3.29.2", + "@tiptap/pm": "3.29.2" } }, "node_modules/@tiptap/extension-document": { - "version": "3.29.0", - "resolved": "https://registry.npmjs.org/@tiptap/extension-document/-/extension-document-3.29.0.tgz", - "integrity": "sha512-folTlrwmUL+WSnojvfjZ951Gb25Pw8BxjXwhvXFuZXJ92qMfOcezA480kwFuu5nG8pPc/76ezQlz9a86Y5uQJw==", + "version": "3.29.2", + "resolved": "https://registry.npmjs.org/@tiptap/extension-document/-/extension-document-3.29.2.tgz", + "integrity": "sha512-YUamvefLnsqu6124GavVTI7nqcFlQJ12ROB0oSwG69eSBZYNjg1tIs05LFrBxkwf4Xgqd6YzfJ9+FeG428RvzQ==", "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "3.29.0" + "@tiptap/core": "3.29.2" } }, "node_modules/@tiptap/extension-dropcursor": { - "version": "3.29.0", - "resolved": "https://registry.npmjs.org/@tiptap/extension-dropcursor/-/extension-dropcursor-3.29.0.tgz", - "integrity": "sha512-IAY21q7KSXyCXN3u13kfpq0E8D2i3r9nSbsNYlLzkEv0Bpn+pWShZi05A0ubuothDNjdaMa96xaRY0f1y9gDZw==", + "version": "3.29.2", + "resolved": "https://registry.npmjs.org/@tiptap/extension-dropcursor/-/extension-dropcursor-3.29.2.tgz", + "integrity": "sha512-KKno7cU9r1HdR48CRrsDu69/1UjZdoslq/UcE+Kx+tdhAv/aljXMkRSNzGMrBNOBDmHRgS1+58zm21WQWdQzwA==", "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/extensions": "3.29.0" + "@tiptap/extensions": "3.29.2" } }, "node_modules/@tiptap/extension-floating-menu": { - "version": "3.29.0", - "resolved": "https://registry.npmjs.org/@tiptap/extension-floating-menu/-/extension-floating-menu-3.29.0.tgz", - "integrity": "sha512-/rFlN6T70HuT34kFvftKm56UX0IPff7vlII7Sqgs7DprKVYkItiU3lM7+AoeDRzCsWMg4z1yNHj70BPx7+EJ6Q==", + "version": "3.29.2", + "resolved": "https://registry.npmjs.org/@tiptap/extension-floating-menu/-/extension-floating-menu-3.29.2.tgz", + "integrity": "sha512-CBTq4Xs5aGWr65uFCIkKBms796OhmirWf/ax//iJ4fl9IfwqjwFuc2vQs9PmuwpKn9ctDHo2sMTtvyeCvIt5LQ==", "license": "MIT", "optional": true, "funding": { @@ -6951,93 +6927,93 @@ }, "peerDependencies": { "@floating-ui/dom": "^1.0.0", - "@tiptap/core": "3.29.0", - "@tiptap/pm": "3.29.0" + "@tiptap/core": "3.29.2", + "@tiptap/pm": "3.29.2" } }, "node_modules/@tiptap/extension-gapcursor": { - "version": "3.29.0", - "resolved": "https://registry.npmjs.org/@tiptap/extension-gapcursor/-/extension-gapcursor-3.29.0.tgz", - "integrity": "sha512-Jz0zwriGxPMuRjKo6x7WhPa/7pZn0dovyJu/GLarN4T+mMseGNXWwEBsLEKkuumsJVPDEPPFjX+rzEeaFKInPQ==", + "version": "3.29.2", + "resolved": "https://registry.npmjs.org/@tiptap/extension-gapcursor/-/extension-gapcursor-3.29.2.tgz", + "integrity": "sha512-8Q39UR4/Tit759IeW9xZIe3NMwN11GsuA3FLheDyyGn7RrW02HD3HhUDlazE54Ki4HoosjFmChPlN4Ik2ubdRQ==", "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/extensions": "3.29.0" + "@tiptap/extensions": "3.29.2" } }, "node_modules/@tiptap/extension-hard-break": { - "version": "3.29.0", - "resolved": "https://registry.npmjs.org/@tiptap/extension-hard-break/-/extension-hard-break-3.29.0.tgz", - "integrity": "sha512-8qKj0oeuU7IYoG73Lj3W4EC/JS/dFjN5iHmc4yodqppP6qs0o2zsF/2ipceGN/REZq5megPtxRgXu/nxQZ1NYg==", + "version": "3.29.2", + "resolved": "https://registry.npmjs.org/@tiptap/extension-hard-break/-/extension-hard-break-3.29.2.tgz", + "integrity": "sha512-eUW3LN3fq8rXnjEUeI3D2QONYdLsU3yYQm4jxlErs2h4cfwrFjgf19VSUFVmm6LrFbbQ0OnDVPeVLL6iOwDw2w==", "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "3.29.0" + "@tiptap/core": "3.29.2" } }, "node_modules/@tiptap/extension-heading": { - "version": "3.29.0", - "resolved": "https://registry.npmjs.org/@tiptap/extension-heading/-/extension-heading-3.29.0.tgz", - "integrity": "sha512-xWb3QEKo7cp9u6Vzi6oYKODra6jPwziHBv3fJAnxWpRldxtJqfyOVMikWJJFuDjmr+K0qyegzf206pMk+y0bDg==", + "version": "3.29.2", + "resolved": "https://registry.npmjs.org/@tiptap/extension-heading/-/extension-heading-3.29.2.tgz", + "integrity": "sha512-6W4aIy70Mh7BNlbG9zZ5FBLhJhU2UUEzgZJ/jwYSCcB30o8McLxJSEjhtoHiX8R78Ah2/JzBGvIe5olZlbeE4A==", "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "3.29.0" + "@tiptap/core": "3.29.2" } }, "node_modules/@tiptap/extension-highlight": { - "version": "3.29.0", - "resolved": "https://registry.npmjs.org/@tiptap/extension-highlight/-/extension-highlight-3.29.0.tgz", - "integrity": "sha512-4Gcqn8Sg8kAKe+cEHzdXijalV803WUjvloZvlqrTAJmcORFNOipc+r9ylSEmCLb6j0oLc1O7tJXIEX59khNX/A==", + "version": "3.29.2", + "resolved": "https://registry.npmjs.org/@tiptap/extension-highlight/-/extension-highlight-3.29.2.tgz", + "integrity": "sha512-bSeZ1C8OlthyK7BzgtqOGfQymdap9V38W2hbr9KGlhVOIAmTqneOwjm27iFcu7jKqkMArVu6WBYN18Lom6umHQ==", "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "3.29.0" + "@tiptap/core": "3.29.2" } }, "node_modules/@tiptap/extension-horizontal-rule": { - "version": "3.29.0", - "resolved": "https://registry.npmjs.org/@tiptap/extension-horizontal-rule/-/extension-horizontal-rule-3.29.0.tgz", - "integrity": "sha512-u1OgncXkokIuUJQIh4jVQfYHL/6I8VnrokUcCud4eErWvgeLnCbNmVdih4kxIdfmvEyAxXjDwcqf4AB79TxuNg==", + "version": "3.29.2", + "resolved": "https://registry.npmjs.org/@tiptap/extension-horizontal-rule/-/extension-horizontal-rule-3.29.2.tgz", + "integrity": "sha512-8/ZPzbB9X85Mc9/7xVLZupQKBr2UVcQTGr512xtqMW+XkCQRHCph46tRo828YE13IMWI5fWn/FaNCqXG9cULSw==", "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "3.29.0", - "@tiptap/pm": "3.29.0" + "@tiptap/core": "3.29.2", + "@tiptap/pm": "3.29.2" } }, "node_modules/@tiptap/extension-italic": { - "version": "3.29.0", - "resolved": "https://registry.npmjs.org/@tiptap/extension-italic/-/extension-italic-3.29.0.tgz", - "integrity": "sha512-c8gF+zM7yXB9iGxtVCioYecjQ+A2fLlNa1XYBYbTobNhbUo8a11akbTkGNcCIgttwnzwEoI4mB6/PK4yjaQvVg==", + "version": "3.29.2", + "resolved": "https://registry.npmjs.org/@tiptap/extension-italic/-/extension-italic-3.29.2.tgz", + "integrity": "sha512-iH63V/5wsaMnY4Jz0+meaAGhaec4AiOzOduzl6ZZr5IyGhZ1kthyW84ELt0dyLI3hNceAUhaNWc+I7+vX0aoXA==", "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "3.29.0" + "@tiptap/core": "3.29.2" } }, "node_modules/@tiptap/extension-link": { - "version": "3.29.0", - "resolved": "https://registry.npmjs.org/@tiptap/extension-link/-/extension-link-3.29.0.tgz", - "integrity": "sha512-FuLDgE0k1dB4WCBTelw3re9BLmiurgXVSj3d3J4l3sunljyOs941RUU62b7pWP+ePNdGF7RgUMmMAB+58VZCIA==", + "version": "3.29.2", + "resolved": "https://registry.npmjs.org/@tiptap/extension-link/-/extension-link-3.29.2.tgz", + "integrity": "sha512-DcVer5SqrexKCEP6Ip1UPxJUMvcRCCItSv0wxoGytanrimBh2smvcg6X0DWnjlsi5H0updhyl+atYCmmQXUIXA==", "license": "MIT", "dependencies": { "linkifyjs": "^4.3.3" @@ -7047,133 +7023,133 @@ "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "3.29.0", - "@tiptap/pm": "3.29.0" + "@tiptap/core": "3.29.2", + "@tiptap/pm": "3.29.2" } }, "node_modules/@tiptap/extension-list": { - "version": "3.29.0", - "resolved": "https://registry.npmjs.org/@tiptap/extension-list/-/extension-list-3.29.0.tgz", - "integrity": "sha512-k0B/+nIkn4VvHSQ0kP+AzzAmgeOVxKMAdqG4a6qwxp/lR12aJGHlOP92KCjXV2RNPtuwDksJ1RIXrgxdf9WmJg==", + "version": "3.29.2", + "resolved": "https://registry.npmjs.org/@tiptap/extension-list/-/extension-list-3.29.2.tgz", + "integrity": "sha512-WPZ9BHAPT6QeIm1vdVkuoOWvy9a8/EZeJwV2VhU8LXyTAttvzyj4rsbbHyJWvYWlUSTt/QF2AZ2zhKo7u1w3/A==", "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "3.29.0", - "@tiptap/pm": "3.29.0" + "@tiptap/core": "3.29.2", + "@tiptap/pm": "3.29.2" } }, "node_modules/@tiptap/extension-list-item": { - "version": "3.29.0", - "resolved": "https://registry.npmjs.org/@tiptap/extension-list-item/-/extension-list-item-3.29.0.tgz", - "integrity": "sha512-JSz27OIDHWDL7uw28E4W3eaN3H1u+NzJQKZkNXo5Qsvvsx8m/YIQWO9XlhiL3V4TZMjfvlc94lUp5tKW5KwETw==", + "version": "3.29.2", + "resolved": "https://registry.npmjs.org/@tiptap/extension-list-item/-/extension-list-item-3.29.2.tgz", + "integrity": "sha512-s8vBVHHFT0Qpu7CzAZ7S1kYmSiVaDvUNvMNcZUWnxj6VPfiwmx0eXd9FsjePrRCMoM5tFmPnhFTHDxY3D/eZeQ==", "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/extension-list": "3.29.0" + "@tiptap/extension-list": "3.29.2" } }, "node_modules/@tiptap/extension-list-keymap": { - "version": "3.29.0", - "resolved": "https://registry.npmjs.org/@tiptap/extension-list-keymap/-/extension-list-keymap-3.29.0.tgz", - "integrity": "sha512-g14QLZR9fmfJIL+X2R/cVTvQS0Mp7CoenA9PO+8adTjK7hCeB8hA3UAakMB4ekkElOXNOtb16e0wqulDcoqANg==", + "version": "3.29.2", + "resolved": "https://registry.npmjs.org/@tiptap/extension-list-keymap/-/extension-list-keymap-3.29.2.tgz", + "integrity": "sha512-R+3k8OLnxdCH7Xy9ieOwUt5m2Je74u8mikothGmsYVO2Zyq48fIbmZ+X6RBPCu7DBOI2FIUhHEFbKQeDWvDNmA==", "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/extension-list": "3.29.0" + "@tiptap/extension-list": "3.29.2" } }, "node_modules/@tiptap/extension-ordered-list": { - "version": "3.29.0", - "resolved": "https://registry.npmjs.org/@tiptap/extension-ordered-list/-/extension-ordered-list-3.29.0.tgz", - "integrity": "sha512-9TXUDFagkGeu6Bo8L0b2SAVAYrY2Xzd28MXwiDxYvDKMjEGWkkCUpMiEI5Qav3GnfFWLmS3jI8PgUjxIw9wLLQ==", + "version": "3.29.2", + "resolved": "https://registry.npmjs.org/@tiptap/extension-ordered-list/-/extension-ordered-list-3.29.2.tgz", + "integrity": "sha512-ndCunC+UsYOpkOtL7vGnDz21UNa45WUlcO9wMT1fbuYow2QnRhsuMlCWENXI52YPPARWuQ0RDgN7q6TaxPERBg==", "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/extension-list": "3.29.0" + "@tiptap/extension-list": "3.29.2" } }, "node_modules/@tiptap/extension-paragraph": { - "version": "3.29.0", - "resolved": "https://registry.npmjs.org/@tiptap/extension-paragraph/-/extension-paragraph-3.29.0.tgz", - "integrity": "sha512-OtGrkwzqlW+ehW+d2qrQ7VVcP9PbDeQmuT4Ec4yFjf3vR4QO8OVdVfeOcUq52PtkT3kYRxz9fzEiEvjqRm72yA==", + "version": "3.29.2", + "resolved": "https://registry.npmjs.org/@tiptap/extension-paragraph/-/extension-paragraph-3.29.2.tgz", + "integrity": "sha512-7qJj5YTr11vvjNgjDN1ypOfwTovc0QOCYcit/rskeuVgnmQZOZQzC/BbyKLLG7UGnpRLemU/mEGbW9pAqjAXkQ==", "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "3.29.0" + "@tiptap/core": "3.29.2" } }, "node_modules/@tiptap/extension-strike": { - "version": "3.29.0", - "resolved": "https://registry.npmjs.org/@tiptap/extension-strike/-/extension-strike-3.29.0.tgz", - "integrity": "sha512-VzasZkckrEXzmFQT9z3O95rUWy1b0RLFHAC6GUSQHxKk4TpP+Ujfn5+rqmPukMn0eScJyw1Y36jwfJc0VuNR+Q==", + "version": "3.29.2", + "resolved": "https://registry.npmjs.org/@tiptap/extension-strike/-/extension-strike-3.29.2.tgz", + "integrity": "sha512-aEvLAbddUQZ+FukCreV3q4G2HfNI+odE7E9U+wbq6XsSWKyo8/pDu1muz+TFKNre4blSMOQ3JQmw5UeHDKy+fg==", "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "3.29.0" + "@tiptap/core": "3.29.2" } }, "node_modules/@tiptap/extension-text": { - "version": "3.29.0", - "resolved": "https://registry.npmjs.org/@tiptap/extension-text/-/extension-text-3.29.0.tgz", - "integrity": "sha512-AMsf2V7IiGvzbl+LqLLfyWzufazZxHuid1gOnEt3KwolQ9AO7p8WJgHld0PZLxNyEh4SaSqK7b3d3SM9sqrlTw==", + "version": "3.29.2", + "resolved": "https://registry.npmjs.org/@tiptap/extension-text/-/extension-text-3.29.2.tgz", + "integrity": "sha512-Ubko45JWWHe8glBt2PiGNF8hcbys/JNalFhiR7Y1X4iOOtAxAKJJxh3+eq+//NTlGuBPdWGp7zw8EEUp7anjKA==", "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "3.29.0" + "@tiptap/core": "3.29.2" } }, "node_modules/@tiptap/extension-underline": { - "version": "3.29.0", - "resolved": "https://registry.npmjs.org/@tiptap/extension-underline/-/extension-underline-3.29.0.tgz", - "integrity": "sha512-DSPTogdvxmoX/L0U5KgBC9jWffRvYkGiyh1tp28gn+QaARh4iJn79pAZpi1m1bEhal/0Mq5m4ukbiO7FWInwaA==", + "version": "3.29.2", + "resolved": "https://registry.npmjs.org/@tiptap/extension-underline/-/extension-underline-3.29.2.tgz", + "integrity": "sha512-K7XwH/xS/5AIREWQ00VTEf/W5U0olp7j6wwit7cdd/8nHv6h6AGr1+iEApHKoLXWQZLfGQKzJlT9W61LAl+fHA==", "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "3.29.0" + "@tiptap/core": "3.29.2" } }, "node_modules/@tiptap/extensions": { - "version": "3.29.0", - "resolved": "https://registry.npmjs.org/@tiptap/extensions/-/extensions-3.29.0.tgz", - "integrity": "sha512-ltlrm8dDHIgeNj3cOLEdLFMPPVy3TYvWA8ftrrJ44C/L01MBmgFB1f/vkPFYYnAasb2BYyVG6HxAGcTQHo5jHw==", + "version": "3.29.2", + "resolved": "https://registry.npmjs.org/@tiptap/extensions/-/extensions-3.29.2.tgz", + "integrity": "sha512-BCz+FCAChSYtUe4BFj97HEO+nSK+J7GxbJgZG4Hg7DT/gI+hRyeNndU8efiQAx3WGdzsFi3UxRpcF1tTQM7iMQ==", "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "3.29.0", - "@tiptap/pm": "3.29.0" + "@tiptap/core": "3.29.2", + "@tiptap/pm": "3.29.2" } }, "node_modules/@tiptap/pm": { - "version": "3.29.0", - "resolved": "https://registry.npmjs.org/@tiptap/pm/-/pm-3.29.0.tgz", - "integrity": "sha512-4rr3HiZ8kbSNINuWXqKQJLv9fFMypCBN5gOwxoU+D4lEYVblkoM60fA1SlrI2BZVlZQmw2U38XKl5XMtUr55XQ==", + "version": "3.29.2", + "resolved": "https://registry.npmjs.org/@tiptap/pm/-/pm-3.29.2.tgz", + "integrity": "sha512-GCOme7xHaS+DSoaA4CDcAD3l6JyBlvZhvCyfsy2Vp6j8tEoBkZWio7soYVosmlyn7zq8/64VeFZP5s47yfG7fQ==", "license": "MIT", "dependencies": { "prosemirror-changeset": "^2.4.1", @@ -7196,9 +7172,9 @@ } }, "node_modules/@tiptap/react": { - "version": "3.29.0", - "resolved": "https://registry.npmjs.org/@tiptap/react/-/react-3.29.0.tgz", - "integrity": "sha512-HmZf9q5/HXg5M2t09WGEbk16vEBDUGPlkF3eXcQh6yG6PkzMZHq1SP+uA8lWku5H/Ggfmv4XKjI6dEQa5V8bwA==", + "version": "3.29.2", + "resolved": "https://registry.npmjs.org/@tiptap/react/-/react-3.29.2.tgz", + "integrity": "sha512-ZMKgteYmZXnq7C22U9fbCTC6jAF8XlQM5n2K2FLWCdYAlP4FNV3XeQ9hY2a13KSQ0Q3yltWXZlcWBpt5ClxScg==", "license": "MIT", "dependencies": { "@types/use-sync-external-store": "^0.0.6", @@ -7210,12 +7186,12 @@ "url": "https://github.com/sponsors/ueberdosis" }, "optionalDependencies": { - "@tiptap/extension-bubble-menu": "^3.29.0", - "@tiptap/extension-floating-menu": "^3.29.0" + "@tiptap/extension-bubble-menu": "^3.29.2", + "@tiptap/extension-floating-menu": "^3.29.2" }, "peerDependencies": { - "@tiptap/core": "3.29.0", - "@tiptap/pm": "3.29.0", + "@tiptap/core": "3.29.2", + "@tiptap/pm": "3.29.2", "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", "@types/react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0", "react": "^17.0.0 || ^18.0.0 || ^19.0.0", @@ -7232,35 +7208,35 @@ } }, "node_modules/@tiptap/starter-kit": { - "version": "3.29.0", - "resolved": "https://registry.npmjs.org/@tiptap/starter-kit/-/starter-kit-3.29.0.tgz", - "integrity": "sha512-J3jTp3/WXnnL58TCtdMsfeQ2BeycFYrAib6Nbpjd9G0OLUBE7AILiH2HrobRQpvRbAUgD5SW+sBZYOonbWWqNg==", - "license": "MIT", - "dependencies": { - "@tiptap/core": "^3.29.0", - "@tiptap/extension-blockquote": "^3.29.0", - "@tiptap/extension-bold": "^3.29.0", - "@tiptap/extension-bullet-list": "^3.29.0", - "@tiptap/extension-code": "^3.29.0", - "@tiptap/extension-code-block": "^3.29.0", - "@tiptap/extension-document": "^3.29.0", - "@tiptap/extension-dropcursor": "^3.29.0", - "@tiptap/extension-gapcursor": "^3.29.0", - "@tiptap/extension-hard-break": "^3.29.0", - "@tiptap/extension-heading": "^3.29.0", - "@tiptap/extension-horizontal-rule": "^3.29.0", - "@tiptap/extension-italic": "^3.29.0", - "@tiptap/extension-link": "^3.29.0", - "@tiptap/extension-list": "^3.29.0", - "@tiptap/extension-list-item": "^3.29.0", - "@tiptap/extension-list-keymap": "^3.29.0", - "@tiptap/extension-ordered-list": "^3.29.0", - "@tiptap/extension-paragraph": "^3.29.0", - "@tiptap/extension-strike": "^3.29.0", - "@tiptap/extension-text": "^3.29.0", - "@tiptap/extension-underline": "^3.29.0", - "@tiptap/extensions": "^3.29.0", - "@tiptap/pm": "^3.29.0" + "version": "3.29.2", + "resolved": "https://registry.npmjs.org/@tiptap/starter-kit/-/starter-kit-3.29.2.tgz", + "integrity": "sha512-oTu0tysiqk4zgjEtxRHjAQgxUKaAevZwueOWwSWubHdokqp7SpcbE5n9USJv89HKuTUDm3GjnQH6q8HNn/2DsA==", + "license": "MIT", + "dependencies": { + "@tiptap/core": "^3.29.2", + "@tiptap/extension-blockquote": "^3.29.2", + "@tiptap/extension-bold": "^3.29.2", + "@tiptap/extension-bullet-list": "^3.29.2", + "@tiptap/extension-code": "^3.29.2", + "@tiptap/extension-code-block": "^3.29.2", + "@tiptap/extension-document": "^3.29.2", + "@tiptap/extension-dropcursor": "^3.29.2", + "@tiptap/extension-gapcursor": "^3.29.2", + "@tiptap/extension-hard-break": "^3.29.2", + "@tiptap/extension-heading": "^3.29.2", + "@tiptap/extension-horizontal-rule": "^3.29.2", + "@tiptap/extension-italic": "^3.29.2", + "@tiptap/extension-link": "^3.29.2", + "@tiptap/extension-list": "^3.29.2", + "@tiptap/extension-list-item": "^3.29.2", + "@tiptap/extension-list-keymap": "^3.29.2", + "@tiptap/extension-ordered-list": "^3.29.2", + "@tiptap/extension-paragraph": "^3.29.2", + "@tiptap/extension-strike": "^3.29.2", + "@tiptap/extension-text": "^3.29.2", + "@tiptap/extension-underline": "^3.29.2", + "@tiptap/extensions": "^3.29.2", + "@tiptap/pm": "^3.29.2" }, "funding": { "type": "github", @@ -9796,9 +9772,9 @@ "peer": true }, "node_modules/dompurify": { - "version": "3.4.11", - "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.11.tgz", - "integrity": "sha512-zhlUV12GsaRzMsf9q5M254YhA4+VuF0fG+QFqu6aYpoGlKtz+w8//jBcGVYBgQkR5GHjUomejY84AV+/uPbWdw==", + "version": "3.4.12", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.12.tgz", + "integrity": "sha512-zQvGet8Z2sWbQhCmfFz/T5QWH2oBmjnqK3qvOjaqaNLrLEF912WamU+ohnTp0TCep/MFVHpdJuCZEdFOdTnEFg==", "license": "(MPL-2.0 OR Apache-2.0)", "optionalDependencies": { "@types/trusted-types": "^2.0.7" @@ -10030,12 +10006,12 @@ } }, "node_modules/framer-motion": { - "version": "12.42.2", - "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.42.2.tgz", - "integrity": "sha512-5XY9luDiu0oHfHBjpDthFMh0ES+122w6p/papSJBweMkO8Sn+PW2QaEgRblQBpWFnuvZS5qvarpt/hO2pjGmnw==", + "version": "12.43.0", + "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.43.0.tgz", + "integrity": "sha512-1eaL3RvR/kAlbG7UYcpMptEyzPoENO0c6w7ZnB3/hh2vSAz/6uGAFn6fdoqTBguNstf3MsFhJHsD/0DHiclG+g==", "license": "MIT", "dependencies": { - "motion-dom": "^12.42.2", + "motion-dom": "^12.43.0", "motion-utils": "^12.39.0", "tslib": "^2.4.0" }, @@ -10311,12 +10287,6 @@ "url": "https://opencollective.com/immer" } }, - "node_modules/immutable": { - "version": "4.3.9", - "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.9.tgz", - "integrity": "sha512-ObHy4YN7ycwZOUCLI1/6svfyAFu7vL8RhAvVu/bh/RZW9EPlOyDaQ9jDQWCtdqzaXUjgXZCW1migtHE7YI7UGQ==", - "license": "MIT" - }, "node_modules/import-meta-resolve": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/import-meta-resolve/-/import-meta-resolve-4.2.0.tgz", @@ -12167,12 +12137,12 @@ "license": "MIT" }, "node_modules/motion": { - "version": "12.42.2", - "resolved": "https://registry.npmjs.org/motion/-/motion-12.42.2.tgz", - "integrity": "sha512-Atvv11yUKIid41cVrRBDVX5m8tF8kNpExRSlbpt6APClhDjtwQssgFHhQzejxw7/7YYbjHSPKBVbHo05BuJT5Q==", + "version": "12.43.0", + "resolved": "https://registry.npmjs.org/motion/-/motion-12.43.0.tgz", + "integrity": "sha512-BQgQbSa9Hn3/mtbib0MK53y6JSANa+YKUKlaYnWzAVDH424RYQ5LVpV3pNiWH00BA2z4ojsSdMzqT7g2FQwjuQ==", "license": "MIT", "dependencies": { - "framer-motion": "^12.42.2", + "framer-motion": "^12.43.0", "tslib": "^2.4.0" }, "peerDependencies": { @@ -12193,9 +12163,9 @@ } }, "node_modules/motion-dom": { - "version": "12.42.2", - "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.42.2.tgz", - "integrity": "sha512-5gIMWLp/PycBtJRJWRgjxke5n8dlvkSn2DrYW+tr3XcqAZY1xZh6BJyooJXCM8wdfM7wfMjkBJNLge1CKPUIRA==", + "version": "12.43.0", + "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.43.0.tgz", + "integrity": "sha512-azKON4d9S65PEoFUiQTMTgPheEmzf2QngdRc50AKfJp9Q9mmcBVw22c8eMq9k8kxOFHdL7+WZY7N/5F/lwiDag==", "license": "MIT", "dependencies": { "motion-utils": "^12.39.0" @@ -13178,9 +13148,9 @@ } }, "node_modules/react-grid-layout": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/react-grid-layout/-/react-grid-layout-2.2.3.tgz", - "integrity": "sha512-OAEJHBxmfuxQfVtZwRzmsokijGlBgzYIJ7MUlLk/VSa43SaGzu15w5D0P2RDrfX5EvP9POMbL6bFrai/huDzbQ==", + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/react-grid-layout/-/react-grid-layout-2.2.4.tgz", + "integrity": "sha512-Eb57FsgOMYOfsUGrMI1ku/FFR+dPNPrE8qo+3hwZubpqVSy4GO9v52DeX50Tl3JDYAlCypP4rmw7Vrqk/zOIvA==", "license": "MIT", "dependencies": { "clsx": "^2.1.1", diff --git a/desktop/package.json b/desktop/package.json index 2d95c582c..df0d9b92e 100644 --- a/desktop/package.json +++ b/desktop/package.json @@ -1,7 +1,7 @@ { "name": "tinyagentos-desktop", "private": true, - "version": "1.0.0-beta.44", + "version": "1.0.0-beta.45", "type": "module", "scripts": { "dev": "vite", @@ -16,7 +16,7 @@ "@codemirror/language-data": "^6.5.2", "@codemirror/state": "^6.7.1", "@codemirror/theme-one-dark": "^6.1.3", - "@codemirror/view": "^6.43.6", + "@codemirror/view": "^6.43.7", "@excalidraw/excalidraw": "^0.18.1", "@excalidraw/mermaid-to-excalidraw": "^2.2.2", "@fortune-sheet/core": "^1.0.4", @@ -32,11 +32,11 @@ "@radix-ui/react-switch": "^1.3.7", "@radix-ui/react-tabs": "^1.1.21", "@radix-ui/react-tooltip": "^1.2.16", - "@tiptap/extension-link": "^3.29.0", - "@tiptap/extension-underline": "^3.29.0", - "@tiptap/pm": "^3.29.0", + "@tiptap/extension-link": "^3.29.2", + "@tiptap/extension-underline": "^3.29.2", + "@tiptap/pm": "^3.29.2", "@tiptap/react": "^3.26.1", - "@tiptap/starter-kit": "^3.29.0", + "@tiptap/starter-kit": "^3.29.2", "@tldraw/assets": "4.5.12", "@tldraw/tldraw": "^4.5.10", "@tonejs/midi": "^2.0.28", @@ -57,11 +57,11 @@ "mathjs": "^15.2.0", "mermaid": "^11.16.0", "modern-screenshot": "^4.7.0", - "motion": "^12.42.2", + "motion": "^12.43.0", "plyr": "^3.8.4", "react": "^19.2.8", "react-dom": "^19.2.8", - "react-grid-layout": "^2.2.3", + "react-grid-layout": "^2.2.4", "react-konva": "^19.2.5", "react-markdown": "^10.1.0", "react-resizable": "^4.0.2", diff --git a/desktop/src/App.tsx b/desktop/src/App.tsx index 928fa96ed..2667bd20d 100644 --- a/desktop/src/App.tsx +++ b/desktop/src/App.tsx @@ -12,6 +12,7 @@ import { useDeviceMode } from "@/hooks/use-device-mode"; import { useIsPwa } from "@/hooks/use-is-pwa"; import { useThemeStore, restoreActiveTheme, installWebkitRepaintGuards } from "@/stores/theme-store"; import { useOnAuthReady } from "@/hooks/use-on-auth-ready"; +import { usePushClickHandler } from "@/hooks/use-push-click"; import { useProcessStore } from "@/stores/process-store"; import { useDockStore } from "@/stores/dock-store"; import { getApp } from "@/registry/app-registry"; @@ -250,6 +251,8 @@ export function App() { useSessionPersistence(); + usePushClickHandler(openWindow); + // First-run GPU auto-detect: probe the frame rate once and enable Reduce // effects on a struggling device, so low-end hardware is smooth out of the // box (#58). An explicit user choice is always honored and never overridden. diff --git a/desktop/src/app-standalone-main.tsx b/desktop/src/app-standalone-main.tsx index 3e5d0b106..fa5ddee39 100644 --- a/desktop/src/app-standalone-main.tsx +++ b/desktop/src/app-standalone-main.tsx @@ -2,10 +2,13 @@ import { StrictMode } from "react"; import { createRoot } from "react-dom/client"; import { AppStandalone } from "./AppStandalone"; import { AppShell } from "./components/AppShell"; +import { installAuthGuard } from "./lib/auth-guard"; import { restoreActiveTheme, installWebkitRepaintGuards } from "./stores/theme-store"; import { getApp } from "./registry/app-registry"; import "./theme/tokens.css"; +installAuthGuard(); + // Apply the user's persisted theme on boot, same as chat-main.tsx. void restoreActiveTheme(); // WebKit blanks backdrop-filter surfaces when the tab is backgrounded then diff --git a/desktop/src/apps/LibraryApp.storage.test.tsx b/desktop/src/apps/LibraryApp.storage.test.tsx new file mode 100644 index 000000000..e6c7f5d62 --- /dev/null +++ b/desktop/src/apps/LibraryApp.storage.test.tsx @@ -0,0 +1,190 @@ +import { render, screen, fireEvent, waitFor } from "@testing-library/react"; +import { describe, it, expect, vi, afterEach } from "vitest"; +import { LibraryApp } from "./LibraryApp"; + +const MOCK_ITEMS = [ + { + id: "lib-item-1", + title: "YouTube Video", + source_type: "youtube", + source_url: "https://youtube.com/watch?v=1", + source_id: "yt-1", + author: "YT Author", + summary: "Summary 1", + content: "Content 1", + media_path: null, + thumbnail: null, + categories: [], + tags: [], + metadata: {}, + status: "ready", + monitor: { current_interval: 0, frequency: 0, decay_rate: 0, pinned: false, last_poll: null, last_hash: "" }, + created_at: 1700000000, + updated_at: 1700000000, + }, + { + id: "lib-item-2", + title: "Reddit Post", + source_type: "reddit", + source_url: "https://reddit.com/r/test", + source_id: "rp-1", + author: "Redditor", + summary: "Summary 2", + content: "Content 2", + media_path: null, + thumbnail: null, + categories: [], + tags: [], + metadata: {}, + status: "ready", + monitor: { current_interval: 0, frequency: 0, decay_rate: 0, pinned: false, last_poll: null, last_hash: "" }, + created_at: 1700003600, + updated_at: 1700003600, + }, + { + id: "lib-item-3", + title: "GitHub Repo", + source_type: "github", + source_url: "https://github.com/test/repo", + source_id: "gh-1", + author: "Dev", + summary: "Summary 3", + content: "Content 3", + media_path: null, + thumbnail: null, + categories: [], + tags: [], + metadata: {}, + status: "processing", + monitor: { current_interval: 0, frequency: 0, decay_rate: 0, pinned: false, last_poll: null, last_hash: "" }, + created_at: 1700007200, + updated_at: 1700007200, + }, +]; + +const MOCK_AGENTS = [ + { name: "Agent 1", color: "#ff0000" }, + { name: "Agent 2", color: "#00ff00" }, +]; + +function createFetchMock(overrides: Record> = {}) { + return vi.fn((input: RequestInfo | URL) => { + const url = String(input); + if (url.startsWith("/api/knowledge/items")) { + for (const [prefix, response] of Object.entries(overrides)) { + if (url.startsWith(prefix)) return response; + } + return Promise.resolve({ + ok: true, + status: 200, + headers: new Map([["content-type", "application/json"]]), + json: () => Promise.resolve({ items: MOCK_ITEMS, count: MOCK_ITEMS.length }), + } as Response); + } + if (url === "/api/agents") { + return Promise.resolve({ + ok: true, + status: 200, + headers: new Map([["content-type", "application/json"]]), + json: () => Promise.resolve(MOCK_AGENTS), + } as Response); + } + if (url === "/api/knowledge/subscriptions") { + return Promise.resolve({ + ok: true, + status: 200, + headers: new Map([["content-type", "application/json"]]), + json: () => Promise.resolve({ subscriptions: [] }), + } as Response); + } + return Promise.resolve({ + ok: false, + status: 404, + headers: new Map([["content-type", "application/json"]]), + json: () => Promise.resolve({}), + } as Response); + }); +} + +describe("LibraryApp storage view", () => { + afterEach(() => { + vi.unstubAllGlobals(); + vi.restoreAllMocks(); + }); + + it("renders the view mode toggle and switches to storage view", async () => { + vi.stubGlobal("fetch", createFetchMock() as unknown as typeof fetch); + render(); + + await waitFor(() => screen.getByRole("radio", { name: "storage" })); + fireEvent.click(screen.getByRole("radio", { name: "storage" })); + + await waitFor(() => screen.getByText("Storage Accounting"), { timeout: 5000 }); + }); + + it("shows per-source totals sorted by bytes descending", async () => { + vi.stubGlobal("fetch", createFetchMock() as unknown as typeof fetch); + render(); + + await waitFor(() => screen.getByRole("radio", { name: "storage" })); + fireEvent.click(screen.getByRole("radio", { name: "storage" })); + + await waitFor(() => screen.getByText("Storage Accounting"), { timeout: 5000 }); + expect(screen.getAllByText("YouTube").length).toBeGreaterThanOrEqual(1); + expect(screen.getAllByText("Reddit").length).toBeGreaterThanOrEqual(1); + expect(screen.getAllByText("GitHub").length).toBeGreaterThanOrEqual(1); + }); + + it("shows per-item rows sorted by bytes descending", async () => { + vi.stubGlobal("fetch", createFetchMock() as unknown as typeof fetch); + render(); + + await waitFor(() => screen.getByRole("radio", { name: "storage" })); + fireEvent.click(screen.getByRole("radio", { name: "storage" })); + + await waitFor(() => screen.getByText("Storage Accounting"), { timeout: 5000 }); + expect(screen.getByText("YouTube Video")).toBeInTheDocument(); + expect(screen.getByText("Reddit Post")).toBeInTheDocument(); + expect(screen.getByText("GitHub Repo")).toBeInTheDocument(); + }); + + it("does not show paused-at-cap when total is under cap", async () => { + vi.stubGlobal("fetch", createFetchMock() as unknown as typeof fetch); + render(); + + await waitFor(() => screen.getByRole("radio", { name: "storage" })); + fireEvent.click(screen.getByRole("radio", { name: "storage" })); + + await waitFor(() => screen.getByText("Storage Accounting"), { timeout: 5000 }); + expect(screen.queryByText(/Paused at cap/)).toBeNull(); + }); + + it("shows paused-at-cap warning when total exceeds cap", async () => { + const manyItems = Array.from({ length: 60 }, (_, i) => ({ + ...MOCK_ITEMS[0], + id: `overflow-${i}`, + title: `Overflow Item ${i}`, + source_type: "youtube", + created_at: 1700000000 + i, + updated_at: 1700000000 + i, + })); + + const overrides: Record> = { + "/api/knowledge/items": Promise.resolve({ + ok: true, + status: 200, + headers: new Map([["content-type", "application/json"]]), + json: () => Promise.resolve({ items: manyItems, count: manyItems.length }), + } as Response), + }; + + vi.stubGlobal("fetch", createFetchMock(overrides) as unknown as typeof fetch); + render(); + + await waitFor(() => screen.getByRole("radio", { name: "storage" })); + fireEvent.click(screen.getByRole("radio", { name: "storage" })); + + await waitFor(() => screen.getByText("Storage Accounting"), { timeout: 5000 }); + expect(screen.getByText(/Paused at cap/)).toBeInTheDocument(); + }); +}); diff --git a/desktop/src/apps/LibraryApp.tsx b/desktop/src/apps/LibraryApp.tsx index f5e8c52bc..53eef9e9a 100644 --- a/desktop/src/apps/LibraryApp.tsx +++ b/desktop/src/apps/LibraryApp.tsx @@ -96,6 +96,74 @@ const SOURCE_LABELS: Record = { manual: "Manual", }; +interface SourceStorage { + source_type: string; + bytes: number; + item_count: number; +} + +interface ItemStorageRow { + id: string; + title: string; + source_type: string; + bytes: number; +} + +interface StorageViewData { + total_bytes: number; + cap_bytes: number; + paused_at_cap: boolean; + sources: SourceStorage[]; + items: ItemStorageRow[]; +} + +const STORAGE_CAP_BYTES = 50 * 1024 * 1024 * 1024; + +/* ------------------------------------------------------------------ */ +/* Helpers */ +/* ------------------------------------------------------------------ */ + +const fmtBytes = (b: number): string => { + if (b < 1024) return `${b} B`; + if (b < 1024 * 1024) return `${(b / 1024).toFixed(1)} KB`; + if (b < 1024 * 1024 * 1024) return `${(b / (1024 * 1024)).toFixed(1)} MB`; + return `${(b / (1024 * 1024 * 1024)).toFixed(2)} GB`; +}; + +const mockItemBytes = (id: string): number => { + let hash = 0; + for (let i = 0; i < id.length; i++) { + hash = ((hash << 5) - hash) + id.charCodeAt(i); + hash |= 0; + } + return (Math.abs(hash) % 4000 + 100) * 1024 * 1024; +}; + +const deriveMockStorageData = (items: KnowledgeItem[]): StorageViewData => { + const itemRows: ItemStorageRow[] = items.map((item) => ({ + id: item.id, + title: item.title || "Untitled", + source_type: item.source_type, + bytes: mockItemBytes(item.id), + })); + const totalBytes = itemRows.reduce((sum, row) => sum + row.bytes, 0); + const sourceMap = new Map(); + for (const row of itemRows) { + const existing = sourceMap.get(row.source_type) || { bytes: 0, count: 0 }; + sourceMap.set(row.source_type, { bytes: existing.bytes + row.bytes, count: existing.count + 1 }); + } + const sources: SourceStorage[] = Array.from(sourceMap.entries()) + .map(([source_type, data]) => ({ source_type, bytes: data.bytes, item_count: data.count })) + .sort((a, b) => b.bytes - a.bytes); + return { + total_bytes: totalBytes, + cap_bytes: STORAGE_CAP_BYTES, + paused_at_cap: totalBytes >= STORAGE_CAP_BYTES, + sources, + items: itemRows.sort((a, b) => b.bytes - a.bytes), + }; +}; + /* ------------------------------------------------------------------ */ /* Helpers */ /* ------------------------------------------------------------------ */ @@ -160,6 +228,8 @@ export function LibraryApp({ windowId: _windowId }: { windowId: string }) { monitor: null, }); + const [activeView, setActiveView] = useState<"items" | "storage">("items"); + /* ---------- detail state ---------- */ const [snapshots, setSnapshots] = useState([]); const [snapshotsLoading, setSnapshotsLoading] = useState(false); @@ -621,6 +691,21 @@ export function LibraryApp({ windowId: _windowId }: { windowId: string }) { ))} )} +

+ {(["items", "storage"] as const).map((v) => ( + + ))} +
{/* Mobile: search mode + filters inline panel */} @@ -908,6 +993,117 @@ export function LibraryApp({ windowId: _windowId }: { windowId: string }) { ); + /* ---------------------------------------------------------------- */ + /* Storage Accounting View */ + /* ---------------------------------------------------------------- */ + + const storageViewUI = items.length === 0 ? ( +
+
+ Storage Accounting + (mock) +
+
+ No items to account for +
+
+ ) : (() => { + const data = deriveMockStorageData(items); + const totalPct = Math.min(100, (data.total_bytes / data.cap_bytes) * 100); + return ( +
+
+
+ Storage Accounting + (mock) +
+
+
+ + {fmtBytes(data.total_bytes)} used of {fmtBytes(data.cap_bytes)} cap + + {totalPct.toFixed(1)}% +
+
+
= 100 ? "bg-red-500" : totalPct >= 80 ? "bg-amber-500" : "bg-sky-500"}`} + style={{ width: `${totalPct}%` }} + /> +
+ {data.paused_at_cap && ( +
+
+ )} +
+
+
+
+

By source

+ + + + + + + + + + + {data.sources.map((src) => { + const pct = data.total_bytes > 0 ? (src.bytes / data.total_bytes) * 100 : 0; + return ( + + + + + + + ); + })} + +
SourceItemsBytesShare
{SOURCE_LABELS[src.source_type] ?? src.source_type}{src.item_count}{fmtBytes(src.bytes)} +
+
+
+
+
+
+

By item

+ + + + + + + + + + + {data.items.map((item) => { + const pct = data.cap_bytes > 0 ? (item.bytes / data.cap_bytes) * 100 : 0; + return ( + + + + + + + ); + })} + +
TitleSourceBytesShare of cap
{item.title}{SOURCE_LABELS[item.source_type] ?? item.source_type}{fmtBytes(item.bytes)} +
+
+
+
+
+
+
+ ); + })(); + /* ---------------------------------------------------------------- */ /* Detail View UI */ /* ---------------------------------------------------------------- */ @@ -1485,22 +1681,28 @@ export function LibraryApp({ windowId: _windowId }: { windowId: string }) { detailTitle={selectedItem ? (selectedItem.title || "Untitled") : undefined} listWidth={700} list={ - /* List pane: sidebar filters + item list side by side on desktop, - stacked (filters embedded above list) on mobile */ -
- {/* Sidebar — always visible on desktop; hidden on mobile */} - {!isMobile && sidebarUI} - {listViewUI} -
+ activeView === "items" ? ( + /* List pane: sidebar filters + item list side by side on desktop, + stacked (filters embedded above list) on mobile */ +
+ {/* Sidebar — always visible on desktop; hidden on mobile */} + {!isMobile && sidebarUI} + {listViewUI} +
+ ) : ( + storageViewUI + ) } detail={ - detailViewUI ?? ( - !isMobile ? ( -
- {loading ? "Loading..." : items.length === 0 ? "Add items to get started" : "Select an item"} -
- ) : null - ) + activeView === "items" + ? detailViewUI ?? ( + !isMobile ? ( +
+ {loading ? "Loading..." : items.length === 0 ? "Add items to get started" : "Select an item"} +
+ ) : null + ) + : null } /> {categoryManagerUI} diff --git a/desktop/src/apps/MessagesApp.palette.test.ts b/desktop/src/apps/MessagesApp.palette.test.ts new file mode 100644 index 000000000..55d631c24 --- /dev/null +++ b/desktop/src/apps/MessagesApp.palette.test.ts @@ -0,0 +1,24 @@ +import { describe, it, expect } from "vitest"; +import { readFileSync } from "node:fs"; +import { fileURLToPath } from "node:url"; +import { dirname, join } from "node:path"; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const source = readFileSync(join(__dirname, "MessagesApp.tsx"), "utf-8"); + +const HARDCODED = /\b(bg|text|border)-(white|black)(?:\/(?:\d+|\[[\d.]+\]))?\b/g; + +describe("MessagesApp palette token regression", () => { + it("contains no hardcoded white/black palette utilities", () => { + const hits: string[] = []; + for (const line of source.split("\n")) { + if (line.includes("palette-ok")) continue; + let m: RegExpExecArray | null; + while ((m = HARDCODED.exec(line)) !== null) { + hits.push(m[0]); + } + HARDCODED.lastIndex = 0; + } + expect(hits, `hardcoded palette utilities found: ${hits.join(", ")}`).toEqual([]); + }); +}); diff --git a/desktop/src/apps/MessagesApp.tsx b/desktop/src/apps/MessagesApp.tsx index 4ef3db28f..2abd5a4f5 100644 --- a/desktop/src/apps/MessagesApp.tsx +++ b/desktop/src/apps/MessagesApp.tsx @@ -11,8 +11,6 @@ import { Archive, CircleDot, PauseCircle, - MessagesSquare, - Search, AlertTriangle, Loader2, } from "lucide-react"; @@ -41,8 +39,7 @@ import { useThreadPanel } from "@/lib/use-thread-panel"; import { openFilePicker } from "@/shell/file-picker-api"; import { MessageOverflowMenu } from "./chat/MessageOverflowMenu"; import { BottomSheet } from "@/shell/BottomSheet"; -import { PinBadge } from "./chat/PinBadge"; -import { PinnedMessagesPopover, type PinnedMessage } from "./chat/PinnedMessagesPopover"; +import { type PinnedMessage } from "./chat/PinnedMessagesPopover"; import { AllThreadsList } from "./chat/AllThreadsList"; import { ChannelSwitcher } from "./chat/ChannelSwitcher"; import { useChatNotifications } from "./chat/useChatNotifications"; @@ -70,7 +67,6 @@ import { getApp } from "@/registry/app-registry"; import { CodeBlock } from "@/components/CodeBlock"; import ReactMarkdown from "react-markdown"; import remarkGfm from "remark-gfm"; -import { resolveAgentEmoji } from "@/lib/agent-emoji"; import { SearchPanel } from "./chat/SearchPanel"; import { ChannelSidebar } from "./chat/ChannelSidebar"; import { A2aBusMessageView, useBusChannels } from "./chat/A2aBusPanel"; @@ -158,6 +154,44 @@ export function resolveAuthorDisplayState( return "removed"; } +interface TextContentBlock { + kind: "text"; + text: string; +} + +interface ThinkingContentBlock { + kind: "thinking"; + text: string; + collapsed?: boolean; +} + +interface ToolCallContentBlock { + kind: "tool_call"; + call_id: string; + name: string; + input_preview?: string; + status: "running" | "done" | "error"; + result_preview?: string; +} + +interface StatusContentBlock { + kind: "status"; + text: string; +} + +/** + * Structured message content for taOStalk session turns. + * Known kinds are handled by dedicated block components (separate cards); + * any unrecognized kind falls through to the unknown-block fallback in + * renderContent, which is the slice-2 seam. + */ +export type ContentBlock = + | TextContentBlock + | ThinkingContentBlock + | ToolCallContentBlock + | StatusContentBlock + | { kind: string; [key: string]: unknown }; + interface Message { id: string; channel_id: string; @@ -167,6 +201,7 @@ interface Message { /** Parent message id when this message is a thread reply. */ thread_id?: string; content_type?: "text" | "canvas" | string; + content_blocks?: ContentBlock[]; metadata?: { canvas_id?: string; canvas_url?: string; @@ -217,7 +252,32 @@ export function relativeTime(ts: number | string, nowMs: number = Date.now()): s return new Date(ms).toLocaleTimeString(undefined, { hour: "2-digit", minute: "2-digit" }); } -export function renderContent(text: string) { +/** + * Dispatch a single content block to its renderer. Known kinds (text, + * thinking, tool_call, status) are dispatched to dedicated block components + * in separate cards; until those land, they fall through to the unknown + * fallback. This is the slice-2 seam: add a case per kind and return the + * block component. + */ +function renderContentBlock(block: ContentBlock, index: number): React.ReactElement { + switch (block.kind) { + case "text": + case "thinking": + case "tool_call": + case "status": + default: + return ( +
+ unsupported block: {block.kind} +
+ ); + } +} + +export function renderContent(text: string, content_blocks?: ContentBlock[]) { + if (content_blocks && content_blocks.length > 0) { + return content_blocks.map((block, i) => renderContentBlock(block, i)); + } // Split on fenced code blocks first, then apply inline markdown to non-code segments. const result: (string | React.ReactElement)[] = []; const fenceRegex = /```(?:[^\n]*)?\n([\s\S]*?)```/g; @@ -257,7 +317,7 @@ export function renderInline(text: string, keyPrefix: string) { return {children}; } return ( - + {children} ); @@ -265,10 +325,10 @@ export function renderInline(text: string, keyPrefix: string) { ul: ({ node, ...props }) =>