Skip to content

fix(theme-store): use injected get() to break circular self-reference - #2288

Merged
jaylfc merged 33 commits into
jaylfc:devfrom
hognek:feat/theme-store-get-fix2
Aug 9, 2026
Merged

fix(theme-store): use injected get() to break circular self-reference#2288
jaylfc merged 33 commits into
jaylfc:devfrom
hognek:feat/theme-store-get-fix2

Conversation

@hognek

@hognek hognek commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Task: t_71a7eb3a

Replace the useThemeStore.getState() call inside the theme-store create closure with the get() argument injected by zustand's create. This eliminates the TS7022 / TS7006 cascade caused by referencing the store within its own initializer.

Changed:

  • create<ThemeStore>((set) => ({...})) -> create<ThemeStore>((set, get) => ({...}))
  • getWallpapersBySection now uses get() instead of useThemeStore.getState()

Only the in-closure occurrence changes. Module-level getState() calls in exported helpers (outside the creator) keep referencing the hook directly — they are not circular.

Verification:

  • tsc -b --force exits 0 (full uncached typecheck, no TS7022/TS7006)
  • vitest run src/stores/theme-store.test.ts src/stores/__tests__/wallpaper.test.ts: 19/19 pass

Summary by CodeRabbit

  • Refactor
    • Improved theme state handling when generating wallpaper sections.
    • No visible changes to the user experience.

jaylfc and others added 30 commits July 7, 2026 03:11
release: promote 1.0.0-beta.34 to master
release: promote dev to master for v1.0.0-beta.35
chore(license): promote AGPL-3.0 dual-license to master
release: promote 1.0.0-beta.36 to master
release: promote 1.0.0-beta.37 to master
release: promote 1.0.0-beta.38 to master
release: promote dev to master (1.0.0-beta.39)
release: promote dev to master (1.0.0-beta.40)
…jaylfc#1835)

* fix(projects): show consent-flow external agents in the External section (jaylfc#1784)

Approved external CLI agents (grok, kilo) were appearing in the plain Members
list instead of under "External / Connected agents" next to the other connected
agents. The Members panel only classified a member as external when its
member_id matched a registry agent's handle, but the consent flow registers
these agents with an empty handle and adds the project member row keyed by the
canonical id. So the match never fired and they fell through to the main list.

Match external registry agents by canonical id as well as handle (older
identities like the assistant reference by handle, consent-flow agents by
canonical id), and map the "grok" framework, not only "grok-build", to the
Grok label so the badge reads correctly.

* test(secrets): add coverage for the Secrets app (jaylfc#1785)

Covers the mount-time /api/secrets fetch and loading state, masked value
rendering, the empty and failed-fetch fallbacks, reveal and hide via the
per-secret API, add and delete through the dialog, and category filtering.
The GitHub integration is mocked so its on-mount identity fetch does not
interfere with the secrets assertions.

* test(notes): add vitest coverage for NotesApp/TodoApp mounted behavior (jaylfc#1787)

Render NotesApp and TodoApp, mock the /api/notes fetch on mount, and assert real behavior: kind filtering, empty states, detail load on select, and the create flow.

* test(chess): add vitest coverage for ChessApp (jaylfc#1788)

Cover render, legal moves, turn changes, checkmate status, new game reset,
and vs-agent mode, with the on-mount agents fetch mocked.

* test(imageviewer): add vitest coverage for ImageViewerApp (jaylfc#1789)

Render the app and assert real behavior: empty state, file load,
zoom in/out with min/max clamps, 90-degree rotation, reset on new
image, and object URL revocation. Stub fetch and URL.createObjectURL
so on-mount integrations do not interfere.

* fix(desktop): Registry poll no longer resets scroll (jaylfc#1761) (jaylfc#1786)

* fix(desktop): keep Registry panel scroll stable across 5s polls (jaylfc#1761)

Quiet background polls no longer flip loading (which unmounted the list)
and setEntries is a no-op when id/content are unchanged, so scroll and
in-progress interaction are preserved. Add registryEntriesEqual helper
and vitest coverage for the poll no-op path.

* fix(desktop): guard registryEntriesEqual index access and drop em dashes

The poll no-op comparison read a[i]/b[i] without a guard, which fails the
strict noUncheckedIndexedAccess build (spa-build). Add an explicit undefined
guard, and remove the em dashes from the added comments.

* chore(deps): bump the python-deps group with 3 updates (jaylfc#1790)

Updates the requirements on [uvicorn[standard]](https://github.com/Kludex/uvicorn), [croniter](https://github.com/pallets-eco/croniter) and [litellm[proxy]](https://github.com/BerriAI/litellm) to permit the latest version.

Updates `uvicorn[standard]` to 0.51.0
- [Release notes](https://github.com/Kludex/uvicorn/releases)
- [Changelog](https://github.com/Kludex/uvicorn/blob/main/docs/release-notes.md)
- [Commits](Kludex/uvicorn@0.50.0...0.51.0)

Updates `croniter` from 6.2.3 to 6.2.4
- [Release notes](https://github.com/pallets-eco/croniter/releases)
- [Changelog](https://github.com/pallets-eco/croniter/blob/main/CHANGELOG.rst)
- [Commits](pallets-eco/croniter@6.2.3...6.2.4)

Updates `litellm[proxy]` to 1.92.0
- [Release notes](https://github.com/BerriAI/litellm/releases)
- [Commits](https://github.com/BerriAI/litellm/commits)

---
updated-dependencies:
- dependency-name: uvicorn[standard]
  dependency-version: 0.51.0
  dependency-type: direct:production
  dependency-group: python-deps
- dependency-name: croniter
  dependency-version: 6.2.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: python-deps
- dependency-name: litellm[proxy]
  dependency-version: 1.92.0
  dependency-type: direct:production
  dependency-group: python-deps
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* chore(deps): bump the spa-deps group in /desktop with 16 updates (jaylfc#1791)

---
updated-dependencies:
- dependency-name: "@codemirror/state"
  dependency-version: 6.7.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: spa-deps
- dependency-name: "@codemirror/view"
  dependency-version: 6.43.6
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: spa-deps
- dependency-name: "@radix-ui/react-dialog"
  dependency-version: 1.1.19
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: spa-deps
- dependency-name: "@radix-ui/react-dropdown-menu"
  dependency-version: 2.1.20
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: spa-deps
- dependency-name: "@radix-ui/react-select"
  dependency-version: 2.3.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: spa-deps
- dependency-name: "@radix-ui/react-switch"
  dependency-version: 1.3.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: spa-deps
- dependency-name: "@radix-ui/react-tabs"
  dependency-version: 1.1.17
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: spa-deps
- dependency-name: "@radix-ui/react-tooltip"
  dependency-version: 1.2.12
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: spa-deps
- dependency-name: "@tiptap/extension-link"
  dependency-version: 3.27.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: spa-deps
- dependency-name: "@tiptap/extension-underline"
  dependency-version: 3.27.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: spa-deps
- dependency-name: "@tiptap/pm"
  dependency-version: 3.27.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: spa-deps
- dependency-name: "@tiptap/react"
  dependency-version: 3.27.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: spa-deps
- dependency-name: "@tiptap/starter-kit"
  dependency-version: 3.27.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: spa-deps
- dependency-name: "@types/three"
  dependency-version: 0.185.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: spa-deps
- dependency-name: vite
  dependency-version: 8.1.4
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: spa-deps
- dependency-name: vitest
  dependency-version: 4.1.10
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: spa-deps
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* docs(design): account model, free username plus paid chosen subdomains (jaylfc#1792)

* docs(design): Hailo-10H LLM backend, zero-touch install parity with RK3588 (jaylfc#1793)

* docs(design): hub.taos.my local-first P2P social network foundation (jaylfc#1794)

* feat(hailo): reserve port 7836 and map hailo-ollama llm-chat capability (jaylfc#1795)

Slice 1 of the Hailo-10H LLM backend design (docs/design/hailo-llm-backend.md):
add 7836 to RESERVED_PORTS so apps cannot squat the NPU backend port, and
register hailo-ollama with llm-chat in BACKEND_CAPABILITIES. Closes the first
shippable piece of jaylfc#1771.

* feat(account): controller proxy actions for subdomain check/claim/release (slice 3) (jaylfc#1796)

* feat(account): proxy subdomain check/claim/release actions (slice 3)

Add /api/account/subdomains/{check,claim,release} routes to account_proxy.py
that forward to the taos.my subdomain claims service with the session cookie
passthrough. The name field is validated rid-style before it can reach the
upstream URL, so a crafted name cannot inject path/query (SSRF/path-traversal
guard). Implements account design doc slice 3.

* test(account): cover subdomain proxy forwarding, 503, and name validation

Add tests alongside the existing account_proxy suite: forwarding of check
(query name) and claim/release (body name) with cookie passthrough, 503 when
the account service is unconfigured, and 400 on an invalid name with no
upstream call.

* docs(design): Projects app nested elements, one project with typed elements (jaylfc#1797)

* fix(models): VRAM reservation TTL sweep + jaylfc#1766 acceptance coverage (jaylfc#1798)

Completes the remaining jaylfc#1766 work after the jaylfc#1767 hotfix (fail-open on
no-probe hardware, backend-level min_ram_mb gate, asyncio.to_thread probe).

- Reclaim VramReservation entries older than a configurable TTL (default 1h)
  so a hung installer cannot hold capacity until controller restart. Sweep
  runs from reserve(), available_vram(), stats(), and public sweep_stale().
- Extract _estimated_vram_mb() so the rkllama pull gate clearly reads
  requires.backends[].min_ram_mb (max across backends, variant fallback).
- Tests: no-probe + real backend min_ram proceeds (no 503), concurrent
  large reserves on NVIDIA still atomic, TTL reclaim, and route-level
  503 when free VRAM is measurable and insufficient.

Closes jaylfc#1766

* feat(account): frontend types + Account panel split for username/subdomains (slice 4) (jaylfc#1801)

Implements slice 4 of docs/design/account-username-subdomain-model.md.

- account-client: add SubdomainClaim/SubdomainCheck types, Account.username
  and Account.subdomains, deprecate Account.handle; add checkSubdomain,
  claimSubdomain, releaseSubdomain helpers with the same degrade-to-state
  (AuthError, never throw) error style as the auth actions.
- AccountPanel: split the old ReserveHandleCard into a free UsernameCard
  (no taOSgo mention, no .taos.my suffix) and a SubdomainsCard (claim list
  with active/grace badges, inline availability check, release, disabled claim
  UI when unsubscribed). Update the section intro copy.
- Tests: account-client subdomain helper coverage (mocked website endpoints);
  AccountPanel coverage for free-username copy, claim list rendering, disabled
  claim when unsubscribed, and grace badge.

* feat(hailo): slice 2 hailo-ollama installer (jaylfc#1771) (jaylfc#1803)

* feat(hailo): slice 2 hailo-ollama installer (jaylfc#1771)

Implements docs/design/hailo-llm-backend.md slice S2 (section C):
scripts/install-hailo.sh mirrors scripts/install-rknpu.sh structure and
safety contract. Detects Hailo-10H via /dev/hailo0 + lspci/hailortcli,
installs HailoRT (>= 5.1.0 firmware floor) on Raspberry Pi OS, clones
hailo-ollama at a pinned ref remapped to port 7836, installs a systemd
unit with orphan-reap ExecStartPre, and health-waits on /api/tags.

Verification: bash -n, shellcheck, and a non-Hailo host prints the
no-detection notice and exits 0 without touching the system.

* chore(hailo): doc-gate trailer for install-hailo.sh

The installer is specified line by line in docs/design/hailo-llm-backend.md
section C (slice S2), which is already merged on dev.

Docs-Reviewed: implements the merged design doc docs/design/hailo-llm-backend.md section C, no separate doc change needed

* feat(hailo): slice 3 hailo-ollama managed service manifest (jaylfc#1804)

* feat(hailo): slice 3 hailo-ollama managed service manifest

Add app-catalog/services/hailo-ollama/manifest.yaml per the managed-backend
contract (lifecycle.auto_manage, unit, scope=system, health on 7836). The
backend flows through load_managed_backends() with no new plumbing.

Adds a unit test that loads the real manifest and asserts the backend is
returned by load_managed_backends().

Docs-Reviewed: implements the merged design doc docs/design/hailo-llm-backend.md

* fix(hailo): declare the proprietary license in the hailo-ollama manifest

test_services_manifests requires a license key on every service manifest.
The runtime is Hailo proprietary software behind an install-time EULA
acceptance, so the manifest now says exactly that.

Docs-Reviewed: license posture is specified in docs/design/hailo-llm-backend.md security and licensing section

* feat(account): onboarding free username claim step (slice 5) (jaylfc#1805)

Adds a free taOS username claim step to OnboardingScreen, shown right
after local account creation. Clearly labeled free, never gated behind
taOSgo, and the user can always finish (claim is optional and failures
degrade to a Settings pointer). Public subdomain publishing is deferred
to Settings so onboarding never dead-ends on the paid path.

Bounded to OnboardingScreen.tsx plus its test, per the slice plan.

* feat(hub): identity keypair keystore + directory registration proxy (slice 1) (jaylfc#1806)

Implements slice 1 of the hub.taos.my own-your-posts social network design
(docs/design/hub-social-network-foundation.md).

Controller side, the taos.my directory endpoints are the contract (mocked in
tests):

- tinyagentos/hub/identity.py: node keystore that mints an Ed25519 signing key
  and an X25519 encryption key on first use, persists them 0600 under
  <data_dir>/hub/identity.json (mesh_credentials.py pattern: atomic write,
  allowlisted fields, TAOS_DATA_DIR override), and exposes the registerable
  public view, the SHA-256 author fingerprint, and a challenge-proof signer plus
  verifier.
- account_proxy.py: _ACTIONS additions and same-origin routes for hub identity
  register / lookup / rotate, forwarding to /api/hub/identity/* with session
  cookie pass-through; lookup validates the username as an rid-style token before
  it can reach the upstream URL.

Tests: keystore round-trip + 0600 perms + stable fingerprint, proxy forwarding +
503 (unconfigured and unreachable), challenge proof rejects a wrong key, lookup
relays the append-only key log.

* feat(hailo): slice 4 install-time gates for Hailo-10H (per design doc) (jaylfc#1807)

Mirror the Rockchip RKNPU install-time gates for the Hailo-10H NPU across
install.sh, scripts/install-server.sh and scripts/install-worker.sh:
detect /dev/hailo0 with 10H vs 8L discrimination (lspci/hailortcli, with
TAOS_FORCE_HAILO override), chain into scripts/install-hailo.sh under
TAOS_HAILO_SETUP=1, fail-soft on chain failure, and document the new env
vars in each script header. Part of jaylfc#1771.

Docs-Reviewed: implements the merged design doc (doc-gate requires it)

* feat(hailo): slice 5 runtime detection + provider adapter for Hailo-10H (per design doc) (jaylfc#1808)

Add the hailo-ollama backend end to end with no new hardware needed for CI:
- worker probe candidate on the taOS remap port 7836, Ollama-compatible
- OllamaCompatAdapter entry for hailo-ollama
- litellm_config ollama-compat membership extended to hailo-ollama
- provider type registered so auto_register_from_manifest seeds
  local-hailo-ollama on Hailo-10H hardware (mirrors rkllama local-rkllama)
- tests for detection, LiteLLM ollama/<model> prefix, and seed

* feat(hub): profile object + local hub store (slice 2) (jaylfc#1809)

* feat(hub): profile object + local hub store (slice 2)

Implements slice 2 of the hub.taos.my own-your-posts social network design
(docs/design/hub-social-network-foundation.md).

- tinyagentos/hub/store.py: canonical-JSON encode/hash/sign/verify helpers and
  a SQLite HubStore (objects, blobs, authors tables). Objects are canonical-JSON
  encoded, content-addressed by SHA-256 of the canonical bytes excluding the
  signature, and signed by the slice-1 Ed25519 keystore. Profiles are the one
  mutable object with highest-version-wins semantics; put_profile ignores a
  stale (lower-or-equal version) replica so it can never clobber a newer one.
- tinyagentos/routes/hub.py: local API the Hub app consumes. Render the node's
  own profile and create/update it with a version bump, each response carrying
  an explicit degrade state (no-identity / no-profile / ok). The store is opened
  lazily and colocated with the identity keystore under the data dir. Wired into
  routes/__init__.py. No peer networking; directory calls stay in account_proxy.

Tests: canonicalization vectors, sign/verify (good sig verifies, tamper and
wrong key do not), version-wins, object/blob/author store round-trip, and the
profile routes end to end (degrade states, create + version bump, kind
validation, signature check).

* chore(hub): doc-gate trailer for the hub store slice

The profile object and local hub store are specified in
docs/design/hub-social-network-foundation.md (slice 2), merged on dev.

Docs-Reviewed: implements the merged design doc docs/design/hub-social-network-foundation.md slice 2, no separate doc change needed

* feat(hub): follow / friend / circle model + request brokering (slice 3) (jaylfc#1810)

Implement hub social slice 3 from docs/design/hub-social-network-foundation.md:
- signed follow and cache-grant statements (cache-grant stored, not yet
  acted on; the cache worker lands in slice 6),
- friend-request send/accept/decline flows that broker through the
  directory and record the local accepted edge,
- local block (severs every edge to the peer and asks the hub to revoke
  the server-side edge) and mute operations,
- a presence gate that denies lookup without an accepted edge,
- directory proxy entries for requests, presence, and edge revoke.

Tests cover edge authorization (presence denied without an accepted edge),
rate-limit behavior, and block severing the edge.

Docs-Reviewed: implements the merged design doc (doc-gate requires it)

* feat(projects): nested element store, CRUD routes, and task element tags (slice 1) (jaylfc#1811)

* feat(projects): implement nested element store, CRUD routes, and task element tags (slice 1)

Adds project elements (one level of nesting per the design) with a dedicated
store and owner-gated CRUD routes, an element_id tag on tasks with create and
update validation plus list and ready filtering, and the Beads snapshot
carrying the tag. Group/promote and assignment are later slices.

Docs-Reviewed: implements the merged design doc docs/design/projects-nested-elements.md slice 1.

* test(projects): slice 1 element store, CRUD route, and task tag coverage

Adds the element store unit tests and route-level coverage for element CRUD,
tag validation, the 409 delete guard, untag mode, and element_id filtering on
list/ready. Proves an external agent token filters by element with no auth
change.

Docs-Reviewed: implements the merged design doc docs/design/projects-nested-elements.md slice 1.

* feat(projects): kanban element filter bar (slice 2) (jaylfc#1812)

Add a persistent element axis to the kanban board: element client API and
element_id on task types, a pure element filter in boardFiltering, a new
ElementFilterBar rendered from the toolbar (All | element chips | Project-
level), an element badge on cards when the board is unfiltered, and element
fetching wired through useBoardData. Zero-element projects stay untouched
(the bar does not render). Tests added and existing board tests updated.

Docs-Reviewed: implements the merged design doc (doc-gate requires it for scripts/, app-catalog/, tinyagentos/ changes)

* feat(hub): post objects, chain logic, image ingest, composer + own-timeline (slice 4) (jaylfc#1813)

Implements slice 4 of docs/design/hub-social-network-foundation.md: a per-author
hash chain (seq/prev), signed append plus verify and tamper detection, signed
tombstones that drop content while keeping the chain verifiable, and image
ingest that re-encodes and strips EXIF. Adds the local post, timeline, and
delete routes plus the Hub app: a composer with a loud friends-only-by-default
visibility switch and an own-timeline read from the local store. No peer sync
yet (that is slice 5).

Tests cover chain append/verify, tamper detection, tombstone drops content and
keeps the chain verifiable, and EXIF stripped.

Docs-Reviewed: implements the merged design doc (doc-gate requires it for scripts/, app-catalog/, tinyagentos/ changes)

* fix: map violet and red note colors to valid tldraw palette names (jaylfc#1815)

Canvas notes with payload.color violet or red fell back to yellow
because the tldraw COLOR_MAP in NoteShape.tsx was missing those
entries. Added violet, red, and light-blue mappings so the note
shape renders with the correct background color. Added tests to
verify the color strings survive element-to-shape coercion and
that COLOR_MAP has the expected entries.

* docs(readme): External Coding Agents section (bring your own AI team) (jaylfc#1816)

* docs(readme): External Coding Agents section (registry, consent onboarding, kanban + a2a work loop)

The external-agent collaboration flow (access requests approved from the
phone, scoped registry identities, board claim/PR/close loop, a2a
coordination) had no README presence despite being live and proven.

Docs-Reviewed: readme-only change describing the shipped flow documented in docs/design/external-agent-onboarding.md

* docs(readme): reference only docs that exist on dev

doc-gate verifies every mentioned path exists; the project-invite design
doc lives on a branch, so the section links only the onboarding doc.

Docs-Reviewed: readme-only change describing the shipped flow documented in docs/design/external-agent-onboarding.md

* fix(hub): serialize chain appends so racing posts are not orphaned (jaylfc#1817)

next_chain_position read the chain head and put_chain_object inserted with
INSERT OR IGNORE, so two concurrent appends for the same author computed the
same seq and the loser was silently dropped from the chain index while its
body stayed in hub_objects. A per-store asyncio lock now serializes the
read-position-then-insert section in append_post and delete_post; the local
node is the only writer of its own chain, so this closes the race. Regression
test proves three concurrent appends land as seq 1,2,3.

Docs-Reviewed: hardening of the merged design doc docs/design/hub-social-network-foundation.md slice 4, no doc change needed

* fix(canvas): map text elements to visible taos-text shapes (jaylfc#1819)

* fix(canvas): map text elements to visible taos-text shapes

element-to-shape.ts only mapped note/link/image to custom shape types;
text (and mermaid label) fell through to taos-generic whose props only
carry geometry, so the payload never reached a visible label. Add a
taos-text shape util and map kind=text to it, coercing payload.text to
a string with empty-string default so imperfect agent writes still render.

Add tests for text kind mapping, payload coercion, and fallback behavior.

* chore(canvas): doc-gate trailer for text shape

New desktop source (TextShape.tsx) rendering the canvas text kind; no
behavioral doc needed, the canvas element kinds are covered by the design.

Docs-Reviewed: frontend-only canvas fix for the boarded canvas text render bug, no doc change needed

* feat(projects): element overview grid, creation flow, and drill-in navigation (slice 3) (jaylfc#1820)

* feat(projects): element overview grid, creation flow, and drill-in navigation (slice 3)

Implements slice 3 of docs/design/projects-nested-elements.md on the frontend:

- New elements/ registry (types.ts) with the seven known types, their
  icons, and type-driven landing-tab order.
- ElementGrid: overview grid shown when a project has elements, with a
  fixed Project card and an Add element tile; zero-element projects keep
  today's workspace pane untouched (back-compat invariant).
- ElementCard: type icon, name, type label, open/total task counts, owner
  chip, and a recent-activity line.
- ElementCreateDialog: create a single nested element (name, slug, type,
  optional owner) within an existing project.
- CreateProjectDialog: optional second step to seed nested elements;
  skipping yields a project identical to today's.
- ProjectWorkspace: element drill-in scopes the board to the element and
  lands on the type's preferred tab, with a breadcrumb and the element id
  carried on the URL for deep links.
- ProjectBoard/BoardToolbar: accept a scoped element id and hide the
  element filter bar while scoped.

Tests cover the grid, card, create dialog, the zero-element regression,
drill-in with breadcrumb, and the creation-flow step two.

* chore(projects): doc-gate trailer for elements slice 3 UI

New desktop sources under ProjectsApp/elements/ implementing slice 3 of the
merged nested-elements design; frontend-only, no behavioral doc change.

Docs-Reviewed: implements the merged design doc docs/design/projects-nested-elements.md slice 3

* Add confirm guard before video delete (jaylfc#1821)

Docs-Reviewed: frontend change to the shipped Video Studio surface.

* Projects elements slice 4: element-scoped canvas + files (jaylfc#1822)

Add an element_id tag to canvas items (store column + ALTER migration,
element-filtered list and create on the canvas routes), an adopt-existing
element files subfolder helper, untag-on-delete for canvas items, and wire
the frontend so the canvas honors the active element filter and the Files
tab mounts the element subfolder.

Docs-Reviewed: implements the boarded task, frontend/backend change to shipped surface.

* feat(projects): doc-review stamp store + routes (jaylfc#1802 slice 3)

Add per-document review_state machine (awaiting_review/approved/changes_requested)
with actor recording and timestamps, project-scoped agent-token gated routes, and
the desktop stamp badge column plus typed API client.

Docs-Reviewed: doc-review stamp store + routes slice per document-review-surface.md

* feat(projects): add doc-review stamp badge to FilesApp and fix projects.ts types

- Add ReviewBadge component with onClick support for cycling review state
- Add cycleDocReview callback for in-place state transitions
- Wire badge into FileRow (list view) and grid cards (project: locations only)
- Fetch review states on project-location navigation via projectsApi.docReviews.list
- Remove duplicate DocReviewState/DocReview type definitions from projects.ts
- Remove duplicate docReview API block; keep single canonical docReviews namespace
- Use DocReview | DocReviewMissing union for GET response type
- Apply encodeURIComponent to state filter query parameter

Docs-Reviewed: doc-review stamp store + routes slice per document-review-surface.md

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
…#1840)

A re-run of install-server.sh over an existing checkout drops to the
repo-owning user (the 'taos' service user) for the git fetch + reset, to
avoid running git as root inside a user-writable tree. But it only reads
the TOP-LEVEL dir owner. If a prior install was interrupted mid-chown (or
a root step wrote a few paths back), the tree has MIXED ownership: the
owning user then cannot unlink the still-root-owned paths, so the reset
fails with 'unable to unlink old ...: Permission denied' ->
'Could not reset index file to revision origin/master', bricking every
subsequent re-run.

Normalise ownership to the owning user (chown -R, run by root) right
before the update so the reset can rewrite the whole tree. Safe: root
does the chown and git still runs unprivileged.

Reported on #2 (fresh Orange Pi 5 Plus, retry after a partial first run).
Failure class reproduced locally: an unwritable path in the tree yields
the identical unlink-EACCES; normalising the tree makes the reset apply
cleanly.
…sed work

Sync master up to dev. Conflicts resolved:
- auth_middleware.py: keep BOTH the doc-review route allowlist (master jaylfc#1835) and
  the canvas route allowlist (dev identity epic) - additive agent-token scopes.
- project_canvas.py: take dev (epic canvas-actor authz, payload cap, honest actor
  attribution).
- test_routes_projects.py, ProjectWorkspace.tsx, ProjectMembers.test.tsx: take dev
  (superset: deep-link plumbing + exclusive-Lead + members tests).
- install-server.sh: take dev (chown normalise + warn, superset of the master hotfix).
Sync master to dev: land identity epic + audit fixes + all unreleased work
…MA (boot-brick) (jaylfc#1853)

The canvas and task stores put their element_id index in SCHEMA:
  CREATE INDEX ... ON project_canvas_elements(project_id, element_id)
  CREATE INDEX ... ON project_tasks(project_id, element_id)
BaseStore runs SCHEMA (executescript) BEFORE _post_init, so on an existing
pre-element_id database the index creation raised
'no such column: element_id' before the _post_init ALTER could add the column,
crashing controller boot after an upgrade. Reproduced live: the Pi bricked on
startup right after pulling this code (fresh installs were fine because SCHEMA
creates the table WITH element_id, so CI never exercised the migration path).

Move both indexes out of SCHEMA into _post_init, created after the ALTER.
Same class of bug and fix as the registry active-handle index (jaylfc#1841).
Regression test seeds a pre-element_id DB and asserts both stores boot,
migrate the column, and build the index.
…istent repo (jaylfc#1851)

The installer defaulted HAILO_OLLAMA_REPO to hailo-ai/hailo-ollama, which does
not exist (git ls-remote: Repository not found), so every Hailo-10H install
failed at clone. The Hailo-Ollama server is not a standalone repo: it ships
inside hailo-ai/hailo_model_zoo_genai and is built from source there (its
README: an Ollama-compatible API written in C++ on top of HailoRT).

- Point the repo at hailo_model_zoo_genai, pinned to a real commit.
- Replace the Python venv/pip build with the repo's actual cmake flow
  (configure -> build -> install), adding the C++ toolchain + OpenSSL deps.
  cmake --install lands the hailo-ollama binary in /usr/local/bin and manifests
  under /usr/local/share/hailo-ollama.
- Resolve the binary from PATH (system install), not a venv.
- Start the server bare (it has no serve subcommand or --port flag) and set the
  listen port via OLLAMA_HOST (bind 127.0.0.1 on the managed port 7836, off the
  banned default 8000), per the repo's docs/USAGE.rst.

Not hardware-tested locally (no Hailo-10H here); validated by bash -n and the
upstream build docs. doc62fr (jaylfc#1771) tests live on real hardware and reports logs.
Resolve the lifespan conflict properly (the earlier -X theirs dropped
master's doc-review store wiring, leaving DocReviewStore referenced but
unwired -> NameError in the shared test fixture). Keep both feature imports
(DocReviewStore + ProjectInviteStore), take dev's eager state-wiring section
(which already sets both stores), and close both stores on shutdown. Both
stores verified instantiated + init'd + state-set + closed; doc-review +
suggest tests (29) green, version-lock green.
Bumps the uv group with 1 update in the / directory: [mcp](https://github.com/modelcontextprotocol/python-sdk).


Updates `mcp` from 1.27.2 to 1.28.1
- [Release notes](https://github.com/modelcontextprotocol/python-sdk/releases)
- [Changelog](https://github.com/modelcontextprotocol/python-sdk/blob/main/RELEASE.md)
- [Commits](modelcontextprotocol/python-sdk@v1.27.2...v1.28.1)

---
updated-dependencies:
- dependency-name: mcp
  dependency-version: 1.28.1
  dependency-type: indirect
  dependency-group: uv
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Bumps the npm_and_yarn group with 1 update in the /desktop directory: [immutable](https://github.com/immutable-js/immutable-js).


Updates `immutable` from 4.3.8 to 4.3.9
- [Release notes](https://github.com/immutable-js/immutable-js/releases)
- [Changelog](https://github.com/immutable-js/immutable-js/blob/main/CHANGELOG.md)
- [Commits](immutable-js/immutable-js@v4.3.8...v4.3.9)

---
updated-dependencies:
- dependency-name: immutable
  dependency-version: 4.3.9
  dependency-type: indirect
  dependency-group: npm_and_yarn
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
* fix(desktop): dialogs render above windows, and mint shows the URL and PIN (jaylfc#2092)

Two bugs on the same screen, both reported from live use.

Window z-index was an unbounded counter: every open, focus, restore and
recenter incremented nextZIndex forever, while portal overlays sit at a fixed
z-[10001]. After enough focus switches in one session, windows rendered on top
of modal dialogs. The stack is now renumbered to 1..N on each change, so window
z stays far below the overlay layer regardless of session length, and relative
order is preserved by sorting on the existing values first. This affected every
portal overlay, not only the invite dialog.

ProjectMembers closed the invite dialog in its onMinted handler, unmounting it
before the result rendered. The invite URL and PIN are shown exactly once and
cannot be recovered, so a successful mint looked like a silent failure. The
parent now refreshes its member list only and the user closes the dialog once
they have copied the credentials.

* feat(agents): project_tasks_create scope so an external agent can author cards (jaylfc#2098)

An agent holding project_tasks could claim, close and comment on existing
cards but never open one, so an approved grant bought nothing on that route.
Rather than widen project_tasks, which is documented and tested as read plus
lifecycle plus comments (Invariant 2 + 5) and would retroactively grant
authoring to every agent already approved for it, authoring gets its own
narrower scope that an owner opts into per agent.

create_task now authorises through the same _authorize_task_actor as the other
task routes, parameterised on scope, so existence-hiding 404s behave
identically. The middleware allowlist admits POST .../tasks, which lets the
token reach a handler that then verifies JWT, project binding and the narrower
scope; project_tasks alone is still refused.

Tests keep the original invariant (project_tasks alone cannot create) and add
the halves that make it meaningful: the new scope DOES allow authoring, it is
project-bound so a grant on A cannot create on B, and it does not widen member
management. Authorship is attributed to the agent, not the project owner.

* feat(library): item card component with thumbnail, status, artifacts, collection link (jaylfc#2097)

Add LibraryItemCard component per docs/design/library-app.md sections 2-4.
Card shows thumbnail (or placeholder), title, kind badge, media duration,
pipeline status per stage (jobs shape), artifact list (text, transcript,
description, ocr) with preview, link-to-collection action, and a disabled
Download stub until P3. Failure states are always visible -- no silent
empties for missing thumbnails, pipeline stages, artifacts, or errors.

Includes lib/library.ts with types and API client for the library store
(items, artifacts, jobs) and 25 component tests covering pending,
processing, ready, and error states.

* feat(agents): enforce files_read/files_write so member agents can access project Files (jaylfc#2100)

* feat(agents): enforce files_read/files_write scopes so member agents can access project Files

Project-files routes (/api/projects/{slug}/files*, mkdir, trash, stats) had no
membership or scope gate and were absent from the agent middleware allowlist, so
an agent token could not reach them at all while the files_read/files_write
scopes existed but were never enforced. This wires them up, mirroring the canvas
pattern:

- _authorize_files_actor resolves slug -> project and authorizes a session
  owner/admin (unchanged) OR an agent holding files_read (reads) / files_write
  (writes) grant bound to that project. A token bound to another project, or an
  unknown slug, collapses into an existence-hiding 404; a missing scope is 403.
- _AGENT_FILES_ROUTES added to auth_middleware so agent JWTs pass through to
  the routes, which verify the grant.
- InviteAgentDialog offers files_read (default on) and files_write, so an owner
  can grant file access at invite time.

Agents that are project members can now read the project's Files and add files
via the API. Grant creation already grants these scopes generically, and
membership is added via the always-on project_tasks scope.

Adds tests/test_routes_project_files_agent_scope.py (10 cases covering read/write
allow, missing-scope 403, cross-project 404, unknown-slug 404, session owner
unchanged).

* feat(agents): surface Files in the invite bundle + fix agent API-surface docs

- build_connection_bundle now advertises the project Files endpoints and adds a
  Files capability section to the join guide when files_read/files_write are
  granted, plus task_create when project_tasks_create is granted, so a joining
  agent is told the Files API exists and how to reach it (slug-keyed paths).
- docs/agent-coordination.md: drop the non-existent project_doc_review scope,
  add files_read/files_write, project_tasks_create, and decisions_write to the
  agent API-surface list, and correct the doc-gate note (it fires only on file
  add/delete, so it does not catch allowlist edits).
- README: replace the understated read-only agent-surface sentence with the
  real scoped surface (tasks, canvas, files, decisions, a2a).

Verified against VALID_SCOPES / _ALLOWED_SCOPES and the auth_middleware
allowlist. Invite tests pass (36).

* feat(providers): add Nous Portal as a cloud model provider (jaylfc#2102)

Nous Portal (Nous Research) is an OpenAI-compatible inference API serving the
Hermes 4 family and frontier models. Wire it up as a first-class cloud provider
so it can be added from the Providers app instead of a hand-configured
openai-compatible endpoint.

- providers/__init__.py: add 'nous' to ALL_TYPES + CLOUD_TYPES, and map it to
  the OpenAI LiteLLM prefix (api_base set explicitly, like kilocode).
- routes/providers.py: default base URL https://inference-api.nousresearch.com/v1
  and a seed model list (flagship Hermes models) for the case where /v1/models
  cannot be listed without a working credential.
- backend_adapters.py: 'nous' uses the CloudAPIAdapter probe.
- Frontend: add 'nous' to the cloud provider type lists and the Providers app
  metadata (label 'Nous Portal', default URL, description, key placeholder).

Base URL and OpenAI-compatibility verified against Nous Portal docs. Backend
provider suite passes (68); frontend tsc clean.

* feat(desktop): Assistant Studio - a workspace for a personal-assistant agent (jaylfc#2103)

A new studio app where the user picks a registered agent to be their PA and
works out of one hub. Left rail: Overview, Journal, Calendar/time, Tasks, Comms,
Canvas, and a Deliverables (files/reports) area. The PA picker defaults to
Hermes when present and persists the choice. Journal, Tasks, Calendar events and
Deliverables persist locally per PA so switching PA swaps the whole workspace;
Comms opens the live agent chat and Canvas points at the project canvas.

MVP scope: self-contained, no new backend (localStorage-backed), so it is
additive and safe. Accessible (labels, aria-current, keyboard add). Registered
as an optional studio app. Backend wiring (real calendar, PA-scoped board/files)
is a follow-up.

tsc clean; frontend build passes.

* feat(agents): request additional scopes for an existing agent identity (jaylfc#1921)

* feat(agents): request additional scopes for an existing agent identity

Add a scope-request flow so an already-registered agent can gain more
scope grants on its SAME canonical_id, instead of the auth-request flow
which mints a new identity on approval (and 409s on an active-handle
collision).

Endpoints (in routes/agent_auth_requests.py):
- POST /api/agents/registry/{cid}/scope-requests            (create)
- POST /api/agents/registry/{cid}/scope-requests/{id}/approve
- POST /api/agents/registry/{cid}/scope-requests/{id}/deny

Auth (security-critical): creation is gated to the agent's OWN registry
bearer token (sub == canonical_id) OR the owning user / an admin, because
the agent already holds credentials; an anonymous caller can never
escalate an existing identity. The middleware allowlist exposes only the
create path to a registry JWT; approve/deny are owner/admin only.

Approval writes add_grant(cid, scope, project_id) per granted scope
(idempotent via the UNIQUE key), never registers a second identity, and
lets the admin narrow but not widen the requested scopes.
decisions_read/decisions_write are grantable globally or per-project;
project_tasks and canvas scopes still require an explicit project_id.

Adds AgentScopeRequestsStore, a scope-agnostic check_agent_identity
helper, and full route tests. VALID_SCOPES stays in sync with
_ALLOWED_SCOPES.

Fixes jaylfc#1920

* fix(agents): fold scope-request approval security findings (jaylfc#1921)

Addresses the Kilo + CodeRabbit findings on the approve/create scope-request paths:

- Major (project binding): approve_scope_request bound global-capable scopes
  (decisions_*) to effective_project, which fell back to the agent-named
  req.project_id when the operator gave no explicit project_id. Since an agent
  can self-request, that let a global scope bind to any project the operator
  never validated (cross-project escalation). Now grants bind ONLY to the
  operator's explicit body.project_id (None = global); the agent-named value is
  never a binding.
- Atomicity + races: approve_scope_request wrote grants + membership before the
  set_decision flip with no lock, so concurrent approvals could double-grant.
  Wrapped the whole approval in the same per-request _get_approve_lock the
  consent path uses, with a pending re-check inside the lock (grant-before-flip
  is safe under the lock + idempotent add_grant).
- Info leak: create_scope_request now authorizes BEFORE scope-vocabulary
  validation, so an unauthorized caller cannot probe whether a scope name is
  valid.

Adds tests: global scope ignores an agent-supplied project_id (binds global),
and create authorizes before vocab (403 not a 400 vocab leak). 16 tests pass.

* fix(agents): take the per-request lock in deny_scope_request too (jaylfc#1921)

Kilo review: approve_scope_request now serializes concurrent approvals under
_get_approve_lock; deny lacked the same lock, so a concurrent approve+deny of
the same request was unserialized. Wrap the deny body in the same per-req lock
with a pending re-check, matching approve and the sibling consent path.

* fix(apps): register assistant-studio as an installable optional app (jaylfc#2104)

Assistant Studio (jaylfc#2103) shipped in the frontend registry as optional but was
not in the server-side optional-app catalog, so getLaunchableApps hid it (it is
only shown once installed) and POST /api/apps/optional/assistant-studio/install
returned 'not an optional app'. Add it to OPTIONAL_FRONTEND_APPS + the version /
trust / provenance dicts, matching the other Creative Studios.

* fix(shortcuts): resolve the container's real incus project (and start it) for terminal shortcuts (jaylfc#2105)

The agent terminal/TUI shortcuts opened an incus PTY with no --project flag, so
incus used the client's default project (a per-user one like user-999). An agent
whose container lives in a different project (e.g. a legacy container in
'default') failed with 'Failed to fetch instance taos-agent-<name> in project
user-999: Instance not found', even though the container exists. Every other
container op already resolves the real project via _resolve_container_project
(--all-projects); the PTY path was the lone exception.

_open_incus_pty now resolves the container's actual project and passes
--project, and starts the container if it is stopped (incus exec fails on a
non-running instance), so a dev-access shortcut works regardless of project or
run state. Adds a sync _resolve_project_and_state_sync sibling to the async
resolver.

Tests: exec targets the resolved project + stopped container is started; a
running container is not restarted.

* test(scope-requests): add grant-enforcement E2E, tighten approve tolerance, add deny negative test (jaylfc#2108)

Three test gaps filled following PR jaylfc#1921 merge:

1. E2E grant-enforcement test (test_approved_scope_grant_unlocks_route_e2e):
   agent self-requests decisions_write → admin approves → agent uses token
   on POST /api/decisions → assert 200. Proves the full grant-enforcement
   chain is wired end to end. Regression guard for issue jaylfc#2095.

2. Tighten test_agent_cannot_approve_its_own_request:
   replace (401, 403) tolerance with exact 401. The middleware does not pass
   a registry JWT through to the approve handler (only the create path is
   allowlisted), so it falls to the session gate → 401 exactly.

3. Deny negative test (test_agent_cannot_deny_its_own_request):
   same auth model as approve — middleware does not allowlist the deny
   endpoint for registry JWTs, so an agent token on the deny endpoint
   falls through to the session gate → 401.

Co-authored-by: Hogne <227774406+hognek@users.noreply.github.com>

* chore(deps): bump actions/setup-python from 6 to 7 (jaylfc#2109)

Bumps [actions/setup-python](https://github.com/actions/setup-python) from 6 to 7.
- [Release notes](https://github.com/actions/setup-python/releases)
- [Commits](actions/setup-python@v6...v7)

---
updated-dependencies:
- dependency-name: actions/setup-python
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* chore(deps): bump the python-deps group with 2 updates (jaylfc#2110)

Updates the requirements on [matrix-nio](https://github.com/matrix-nio/matrix-nio) and [litellm[proxy]](https://github.com/BerriAI/litellm) to permit the latest version.

Updates `matrix-nio` from 0.25.2 to 0.26.0
- [Changelog](https://github.com/matrix-nio/matrix-nio/blob/main/CHANGELOG.md)
- [Commits](matrix-nio/matrix-nio@0.25.2...0.26.0)

Updates `litellm[proxy]` to 1.93.0
- [Release notes](https://github.com/BerriAI/litellm/releases)
- [Commits](BerriAI/litellm@v1.92.0...v1.93.0)

---
updated-dependencies:
- dependency-name: matrix-nio
  dependency-version: 0.26.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: python-deps
- dependency-name: litellm[proxy]
  dependency-version: 1.93.0
  dependency-type: direct:production
  dependency-group: python-deps
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* chore(release): v1.0.0-beta.44 version bump + changelog (jaylfc#2121)

* chore(release): v1.0.0-beta.44 version bump + changelog

* docs(changelog): complete the beta.44 entry (dialog/mint fix, task-create scope)

* test(desktop): add unit tests for AssistantStudioApp (jaylfc#2116)

* test(desktop): add unit tests for AssistantStudioApp

* test(desktop): await the mount-time agents fetch in the first two Assistant Studio tests

Qodo review: the first two tests rendered the component without awaiting its
mount-time /api/agents fetch, so the resulting setState could land outside
React Testing Library's act() and flake in stricter environments. The later
tests in the same file already waited, so this was an inconsistency as much as
a latent flake. Both now drain the fetch before asserting.

* fix(library): wire up the unused source ingest option (jaylfc#2117)

* fix(library): wire up the unused source ingest option

* fix(library): remove the unused source ingest option instead of sending it

Review found that serializing source only moved the silent drop server-side:
/api/library/ingest accepts file, url and title only, and LibraryStore has no
source column (its source_url is already derived from the url). So the field was
discarded either way, while now looking wired.

The card offered wire-it-in or remove-it and I recommended wire-it-in without
checking the backend, which was wrong. Removing it is the honest fix: no caller
passes source, so nothing breaks, and a dead option no longer implies a feature
that does not exist. Capturing real source metadata is a backend feature, not a
client nit.

* chore(deps): bump the spa-deps group in /desktop with 17 updates (jaylfc#2111)

Bumps the spa-deps group in /desktop with 17 updates:

| Package | From | To |
| --- | --- | --- |
| [@radix-ui/react-dialog](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/dialog) | `1.1.19` | `1.1.23` |
| [@radix-ui/react-dropdown-menu](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/dropdown-menu) | `2.1.20` | `2.1.24` |
| [@radix-ui/react-label](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/label) | `2.1.11` | `2.1.15` |
| [@radix-ui/react-select](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/select) | `2.3.3` | `2.3.7` |
| [@radix-ui/react-slot](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/slot) | `1.3.0` | `1.3.3` |
| [@radix-ui/react-switch](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/switch) | `1.3.3` | `1.3.7` |
| [@radix-ui/react-tabs](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/tabs) | `1.1.17` | `1.1.21` |
| [@radix-ui/react-tooltip](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/tooltip) | `1.2.12` | `1.2.16` |
| [@tiptap/extension-link](https://github.com/ueberdosis/tiptap/tree/HEAD/packages/extension-link) | `3.28.0` | `3.29.0` |
| [@tiptap/extension-underline](https://github.com/ueberdosis/tiptap/tree/HEAD/packages/extension-underline) | `3.28.0` | `3.29.0` |
| [@tiptap/pm](https://github.com/ueberdosis/tiptap/tree/HEAD/packages/pm) | `3.28.0` | `3.29.0` |
| [@tiptap/react](https://github.com/ueberdosis/tiptap/tree/HEAD/packages/react) | `3.28.0` | `3.29.0` |
| [@tiptap/starter-kit](https://github.com/ueberdosis/tiptap/tree/HEAD/packages/starter-kit) | `3.28.0` | `3.29.0` |
| [react](https://github.com/react/react/tree/HEAD/packages/react) | `19.2.7` | `19.2.8` |
| [react-dom](https://github.com/react/react/tree/HEAD/packages/react-dom) | `19.2.7` | `19.2.8` |
| [@playwright/test](https://github.com/microsoft/playwright) | `1.61.1` | `1.62.0` |
| [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/tree/HEAD/packages/plugin-react) | `6.0.3` | `6.0.4` |


Updates `@radix-ui/react-dialog` from 1.1.19 to 1.1.23
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/dialog/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/dialog)

Updates `@radix-ui/react-dropdown-menu` from 2.1.20 to 2.1.24
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/dropdown-menu/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/dropdown-menu)

Updates `@radix-ui/react-label` from 2.1.11 to 2.1.15
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/label/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/label)

Updates `@radix-ui/react-select` from 2.3.3 to 2.3.7
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/select/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/select)

Updates `@radix-ui/react-slot` from 1.3.0 to 1.3.3
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/slot/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/slot)

Updates `@radix-ui/react-switch` from 1.3.3 to 1.3.7
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/switch/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/switch)

Updates `@radix-ui/react-tabs` from 1.1.17 to 1.1.21
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/tabs/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/tabs)

Updates `@radix-ui/react-tooltip` from 1.2.12 to 1.2.16
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/tooltip/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/tooltip)

Updates `@tiptap/extension-link` from 3.28.0 to 3.29.0
- [Release notes](https://github.com/ueberdosis/tiptap/releases)
- [Changelog](https://github.com/ueberdosis/tiptap/blob/main/packages/extension-link/CHANGELOG.md)
- [Commits](https://github.com/ueberdosis/tiptap/commits/v3.29.0/packages/extension-link)

Updates `@tiptap/extension-underline` from 3.28.0 to 3.29.0
- [Release notes](https://github.com/ueberdosis/tiptap/releases)
- [Changelog](https://github.com/ueberdosis/tiptap/blob/main/packages/extension-underline/CHANGELOG.md)
- [Commits](https://github.com/ueberdosis/tiptap/commits/v3.29.0/packages/extension-underline)

Updates `@tiptap/pm` from 3.28.0 to 3.29.0
- [Release notes](https://github.com/ueberdosis/tiptap/releases)
- [Changelog](https://github.com/ueberdosis/tiptap/blob/main/packages/pm/CHANGELOG.md)
- [Commits](https://github.com/ueberdosis/tiptap/commits/v3.29.0/packages/pm)

Updates `@tiptap/react` from 3.28.0 to 3.29.0
- [Release notes](https://github.com/ueberdosis/tiptap/releases)
- [Changelog](https://github.com/ueberdosis/tiptap/blob/main/packages/react/CHANGELOG.md)
- [Commits](https://github.com/ueberdosis/tiptap/commits/v3.29.0/packages/react)

Updates `@tiptap/starter-kit` from 3.28.0 to 3.29.0
- [Release notes](https://github.com/ueberdosis/tiptap/releases)
- [Changelog](https://github.com/ueberdosis/tiptap/blob/main/packages/starter-kit/CHANGELOG.md)
- [Commits](https://github.com/ueberdosis/tiptap/commits/v3.29.0/packages/starter-kit)

Updates `react` from 19.2.7 to 19.2.8
- [Release notes](https://github.com/react/react/releases)
- [Changelog](https://github.com/react/react/blob/main/CHANGELOG.md)
- [Commits](https://github.com/react/react/commits/v19.2.8/packages/react)

Updates `react-dom` from 19.2.7 to 19.2.8
- [Release notes](https://github.com/react/react/releases)
- [Changelog](https://github.com/react/react/blob/main/CHANGELOG.md)
- [Commits](https://github.com/react/react/commits/v19.2.8/packages/react-dom)

Updates `@playwright/test` from 1.61.1 to 1.62.0
- [Release notes](https://github.com/microsoft/playwright/releases)
- [Commits](microsoft/playwright@v1.61.1...v1.62.0)

Updates `@vitejs/plugin-react` from 6.0.3 to 6.0.4
- [Release notes](https://github.com/vitejs/vite-plugin-react/releases)
- [Changelog](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite-plugin-react/commits/plugin-react@6.0.4/packages/plugin-react)

---
updated-dependencies:
- dependency-name: "@radix-ui/react-dialog"
  dependency-version: 1.1.23
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: spa-deps
- dependency-name: "@radix-ui/react-dropdown-menu"
  dependency-version: 2.1.24
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: spa-deps
- dependency-name: "@radix-ui/react-label"
  dependency-version: 2.1.15
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: spa-deps
- dependency-name: "@radix-ui/react-select"
  dependency-version: 2.3.7
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: spa-deps
- dependency-name: "@radix-ui/react-slot"
  dependency-version: 1.3.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: spa-deps
- dependency-name: "@radix-ui/react-switch"
  dependency-version: 1.3.7
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: spa-deps
- dependency-name: "@radix-ui/react-tabs"
  dependency-version: 1.1.21
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: spa-deps
- dependency-name: "@radix-ui/react-tooltip"
  dependency-version: 1.2.16
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: spa-deps
- dependency-name: "@tiptap/extension-link"
  dependency-version: 3.29.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: spa-deps
- dependency-name: "@tiptap/extension-underline"
  dependency-version: 3.29.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: spa-deps
- dependency-name: "@tiptap/pm"
  dependency-version: 3.29.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: spa-deps
- dependency-name: "@tiptap/react"
  dependency-version: 3.29.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: spa-deps
- dependency-name: "@tiptap/starter-kit"
  dependency-version: 3.29.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: spa-deps
- dependency-name: react
  dependency-version: 19.2.8
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: spa-deps
- dependency-name: react-dom
  dependency-version: 19.2.8
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: spa-deps
- dependency-name: "@playwright/test"
  dependency-version: 1.62.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: spa-deps
- dependency-name: "@vitejs/plugin-react"
  dependency-version: 6.0.4
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: spa-deps
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* fix(agents): bind project_tasks_create and files scopes to a project on approval (jaylfc#2127)

* fix(agents): bind project_tasks_create and files scopes to a project on approval

Review of the beta.44 promotion found that _SCOPE_PROJECT_SCOPES listed only
project_tasks and the canvas scopes, so project_tasks_create, files_read and
files_write could be approved with no project_id. The grant was then written
global (project_id=None), and check_agent_scope_for_project only matches a grant
bound to the project, so the operator believed they had granted access while the
agent silently had none. Fails closed, but silently wrong is its own bug.

Also replaces the em dashes in docs/agent-coordination.md with commas and colons
per the house style, and records in project_files.py why the session path
deliberately allows an unknown slug (lazily-created, slug-addressed files tree,
documented by test_list_unknown_slug_returns_empty) while the agent path stays
strict.

42 tests pass (project files, files agent scope, scope requests).

* fix(projects): setting an agent as project lead now sets lead_member_id (jaylfc#2113)

add_agent_to_project wrote role='lead' on the member row but never called
set_lead, which is the only writer of projects.lead_member_id. The member row
is just a label; the pointer column is the actual lead. So the agent read as
lead in the UI while every lead-gated check refused them. This is what happened
to Hermes on taOSrabbit: role='lead', is_lead=0, lead_member_id NULL.

Best-effort like the rest of that block, since the membership and grant already
stand on their own.

* fix(agents): one definition of which scopes require a project binding

Qodo caught that the auth-request approval path still granted files_* and
project_tasks_create globally. The project-scope set existed as three parallel
copies (two function-local, one module-level), and the earlier fix only
corrected the module-level one -- leaving the path an invite actually takes
still writing those grants with project_id=None.

check_agent_scope_for_project only matches a grant bound to that exact project,
so a global grant never matches. The approval returns 200, the operator
believes access was granted, and the agent silently has none.

Now defined once and referenced everywhere; _SCOPE_* are plain aliases rather
than rebuilt literals, since re-listing the members is how the copies drifted.

Adds the regression test that was missing: nothing pinned this set, which is
why three copies could disagree unnoticed. Checks alias identity (not equality)
so a re-introduced copy fails even while it still happens to agree, asserts a
single assignment per name in the module source, and asserts every project-bound
scope is in VALID_SCOPES -- a typo there fails open, granting globally.

* ci: raise the test timeout above the actual suite runtime (jaylfc#2134)

The cap was 45 min with a comment claiming 3.12/3.13 finish in ~16. Measured
over the last 12 job records that is no longer true: 3.13 takes 31-41 min and
3.12 takes 38-41, so the cap sat roughly 4 minutes above the slowest normal run.

Two of those 12 were killed mid-suite with nothing actually wrong, including
the one gating jaylfc#2127. A cap that close to the median does not catch hangs, it
manufactures red PRs, and a timeout kill is indistinguishable from a real
failure until you check the clock against timeout-minutes. That is the worst
property a merge gate can have.

75 keeps a bound on a genuinely hung job while leaving real headroom. The suite
growing from ~16 to ~40 min is its own problem and is filed separately; this
stops it corrupting merge decisions in the meantime.

* feat(feedback): add per-user 24h submission cap (jaylfc#2131)

* feat(feedback): add per-user 24h submission cap

* fix(feedback): make the 24h cap atomic (Qodo)

The cap did a SELECT COUNT then an INSERT as two awaited calls. Every
sequential test passes and the limit still does not hold: concurrent requests
all read the same count, all see room, and all insert. Verified against the
pre-fix code, 8 racers with one slot left all got 201 and took a cap of 20 to
27. A user with a few tabs open trips this without trying, and a scripted
client defeats it outright.

Moved enforcement into the store as a single INSERT ... SELECT ... WHERE
(SELECT COUNT(*) ...) < ?, so SQLite's write lock makes the check and the
insert indivisible, and rowcount says which way it went.

Adds the concurrency test that was missing, plus one asserting a rejected
submission leaves no row behind: a partially-written reject would tighten the
cap on every retry.

* refactor(feedback): drop count_recent, orphaned by the atomic cap

This PR added count_recent for the route to call before inserting. Moving the
cap into create_within_cap left it with no callers anywhere in the tree, so it
is dead on arrival rather than pre-existing code worth keeping.

Removing it also removes the tempting wrong path: a future caller reaching for
count_recent would reintroduce exactly the check-then-insert race the atomic
statement exists to close.

* test: replace always-true assert with issubclass check in test_installer_class_available (jaylfc#1979)

* fix(install): route LXC port allocation through centralized allocator

Replace the standalone _find_free_port() in lxc_installer.py with
allocate_host_port(app_id) from port_allocator.py so the centralized
allocator is the single source of truth for all app host-port
assignments.

- Remove _find_free_port(), socket, and closing imports from lxc_installer
- Import allocate_host_port instead of RESERVED_PORTS
- Accumulate failed ports in exclude set across TOCTOU retry loop
- Fix stale _docker_published_port docstring (DockerInstaller now
  maps {allocated_host_port}:{container_port}, not {p}:{p})
- Add test class verifying _find_free_port is gone and
  allocate_host_port is the only import

Refs: jaylfc#695

* test: strengthen allocator import assertion per Kilo suggestion

Add assert not hasattr(mod, 'RESERVED_PORTS') to verify the old import
is truly removed, not just that allocate_host_port is present.

* test: replace always-true assert with issubclass check in test_installer_class_available

---------

Co-authored-by: Hogne <227774406+hognek@users.noreply.github.com>

* feat(wallpaper): add Wallhaven proxy route + sectioned picker integration (jaylfc#1902)

* feat(wallpaper): add Wallhaven proxy route + browse-online picker section

- Add wallhaven_api_key config field (env-only, never in repo)
- Create GET /api/wallhaven/search proxy route to wallhaven.cc API
- Keyless by default; optional X-API-Key header when WALLHAVEN_API_KEY set
- Handle rate limiting (429), timeouts (504), and Wallhaven errors (502)
- New WallhavenBrowser component: debounced search, thumbnail grid, pagination
- Integrate WallhavenBrowser into WallpaperPicker as collapsible section
- WallpaperPicker: "Browse online" toggle expands search UI, selecting a
  Wallhaven image applies it as a remote wallpaper
- Backend tests (test_wallhaven.py, 12 tests) with respx mocking
- Frontend tests (WallhavenBrowser.test.tsx, 8 tests; WallpaperPicker 14 tests)

Fixes jaylfc#864

* fix(wallpaper): escape CSS url, persist wallpaperIdByTheme, guard JSONResponse, validate categories/purity, move os import

- Escape single-quotes and backslashes in remote wallpaper URLs to prevent
  CSS injection and render breakage (WallpaperPicker.tsx onSelect).
- Route through a state updater that sets wallpaperIdByTheme, and include
  light/mobile/fallback variants so theme switch preserves remote wallpapers.
- Use the received label as wallpaperOverlayText instead of discarding it.
- Guard resp.json() with try/except ValueError and cap response at 1 MB
  (wallhaven.py:71).
- Validate categories/purity with ^[01]{3}$ before forwarding to Wallhaven.
- Move import os as _os from inside load_config to module top (config.py).

* fix(wallpaper): harden CSS url() escaping — escape parens, strip control chars, validate http(s) scheme

Kilo WARNING: CSS url() escaping was incomplete. Only backslashes and
single-quotes were escaped, allowing ')' in query strings to prematurely
terminate url('...') and inject CSS. Now also:

- Escape '(' as %28 and ')' as %29
- Strip control characters (\x00-\x1f, \x7f)
- Validate scheme is http(s) before proceeding

Also removes a duplicate 'Browse online' section left behind during
the sectioned-picker rebase — changes are low-risk and build/vitest pass.

---------

Co-authored-by: Hogne <227774406+hognek@users.noreply.github.com>

* fix(gpu-arbiter): clamp drain_tick_seconds floor + remove double _signal_capacity wake (jaylfc#1987)

* feat(gpu-arbiter): event-driven admission wakeup replacing 2s poll tick (jaylfc#1864 A3)

Replace the hardcoded asyncio.sleep(2) in _process_queue with an
asyncio.Event-based wake path.  The drain loop now blocks on
asyncio.wait_for(self._wake.wait(), timeout=drain_tick_seconds) so a
reservation release or task completion immediately wakes the drain
loop.  The 2 s constant becomes the configurable fallback timeout
(drain_tick_seconds, default 2.0).

- __init__: new drain_tick_seconds param, self._wake Event
- _signal_capacity(): new method — calls self._wake.set()
- _process_queue: event-driven wait + clear, fallback timeout
- _release_reservation: calls _signal_capacity on actual release
- _run_gpu_task finally: calls _signal_capacity on completion
- tests/test_gpu_arbiter_wakeup.py: 2 new tests
  * test_release_triggers_immediate_drain (60 s tick, < 0.5 s admit)
  * test_poll_tick_still_drains_without_signal (0.1 s tick)

* fix(gpu-arbiter): clamp drain_tick_seconds floor + remove double _signal_capacity wake

Kilo review on jaylfc#1986:
1. Clamp drain_tick_seconds to >= 0.01 in __init__ to prevent
   asyncio.wait_for(timeout=0) ValueError crash in _process_queue.
2. Remove redundant _signal_capacity() in _run_gpu_task finally —
   _release_reservation already calls it at line 250, making the
   line-448 call a double-wake on every task completion.

32/32 GPU arbiter tests pass.

---------

Co-authored-by: Hogne <227774406+hognek@users.noreply.github.com>

* fix(peer): address Kilo WARNINGs — os.environ leak, design doc, rate limiter docs (jaylfc#2032)

* fix(peer): address Kilo WARNINGs — align design doc, FIXME rate limiter

- Design doc: outbound_token comment now matches code (plaintext; deferred
  to post-MVP) instead of misleading 'encrypted at rest'.
- Rate limiter: add explicit FIXME for shared-store backing across workers,
  document per-worker aggregate limit semantics.

Note: monkeypatch.setenv fixture fix was already applied in upstream merge
of jaylfc#2025, so this commit carries only the two remaining warnings.

* fix(peer): address 5 Kilo findings — centralized auth, nonce replay, rate-limit LRU, prune commit, token docs

Fix #1 (WARNING): Document outbound_token plaintext threat model in
contacts_store schema comment — token must be presented on outbound
requests; at-rest encryption deferred to post-MVP.

Fix #2 (WARNING): Centralized /api/peer/ authentication via router-level
_peer_auth_dep dependency. Previously EXEMPT_PREFIXES bypassed all auth
middleware and correctness depended on every route calling
_authenticate_peer. Now every route under /api/peer/ gets bearer-auth
automatically; route handlers read contact_id from request.state.

Fix jaylfc#3 (SUGGESTION): Commit the opportunistic nonce prune in its own
transaction so a replay (IntegrityError) rollback does not undo it.

Fix jaylfc#4 (SUGGESTION): Add record_nonce(…, kind='ack') to /api/peer/ack
for replay protection. A replayed ack now returns 409 Conflict, matching
the /inbox and /chat contract.

Fix jaylfc#5 (SUGGESTION): Add LRU fallback eviction to the rate limiter.
When all 2000+ entries have active windows (no expired entries to sweep),
the oldest entry is evicted to prevent unbounded dict growth under
sustained load.

---------

Co-authored-by: Hogne <227774406+hognek@users.noreply.github.com>

* fix(notifications): show approve/deny for scope-request notifications in bell (jaylfc#2107)

* fix(notifications): show approve/deny buttons for agent_scope_requests in bell and toast

PR jaylfc#1921 (agent scope-requests) emits notifications with source
'agent_scope_requests' but NotificationCentre and NotificationToast
only rendered ConsentActions for source === 'auth_requests'. Scope-
request approve/deny was invisible in the UI.

- Branch source check in NotificationCentre and NotificationToast to
  also accept 'agent_scope_requests'
- ConsentActions now accepts optional source + canonicalId props and
  routes approve/deny to the scope-request endpoints when source is
  'agent_scope_requests' (/api/agents/registry/{id}/scope-requests/...)
- consentPayload() extracts canonical_id from notification data for
  the scope-request endpoint path
- Backward compatible: source defaults to 'auth_requests' for existing
  callers (DecisionsApp, tests)

Ref: jaylfc#1921

* fix: guard missing canonicalId for scope-request consent actions

Per CodeRabbit review: the canonicalId ?? requestId fallback could produce
malformed URLs like /registry/{requestId}/scope-requests/... when
canonical_id is missing from the notification data. Now explicitly
rejects scope-request approve/deny with a clear error when the
canonicalId is absent.

---------

Co-authored-by: Hogne <227774406+hognek@users.noreply.github.com>

* fix(csrf): merge CSRF token into effectiveInit for Request inputs (bot-fix for jaylfc#1991) (jaylfc#1999)

* fix(csrf): merge CSRF token into effectiveInit for Request inputs, not rebuild

- Compute effective method from init?.method || input.method
- Merge token into effectiveInit headers instead of rebuilding Request
  (avoids body stream consumption and respects init-provided headers/method)
- Update test to inspect init.headers instead of Request.headers

Addresses Kilo finding (Request inputs excluded from CSRF) and
CodeRabbit finding (broken token injection when init overrides
method/headers + body stream consumption).

Refs: PR jaylfc#1991

* fix(csrf): merge Request headers with init headers instead of choosing one

Kilo's WARNING on this PR was correct. `init?.headers || input.headers` picks
one source, so whenever a caller supplied headers via BOTH the Request and the
init, the Request's own headers were discarded entirely:

    fetch(new Request(url, {headers: {Authorization}}), {method, headers})

lost the Authorization header. init also wins at the fetch layer for a Request
plus init, so nothing downstream restored it.

Now both are merged, init winning on conflict, which matches how fetch itself
resolves the two.

The regression test is verified to fail on the pre-fix code, and it exposes the
bug as slightly worse than reported: with the old line, that call lost the CSRF
token too, so the header this PR exists to attach was itself dropped in exactly
the case it was being extended to cover.

Test assertions live inside the existing it() block on purpose: installAuthGuard
has a module-level `installed` flag, so a second install in a fresh it() is a
no-op and window.fetch would be the bare spy rather than the wrapper. I lost
time to that before spotting it, hence the note.

---------

Co-authored-by: Hogne <227774406+hognek@users.noreply.github.com>
Co-authored-by: jaylfc <jaylfc25@gmail.com>

* fix(canvas): make the .tldr export a file tldraw can actually open (jaylfc#2133)

* fix(canvas): make the .tldr export a file tldraw can actually open

The .tldr snapshot is the data-recovery escape hatch for jaylfc#2132: it is what a
user falls back on to get their canvas out and open it elsewhere. It did not
work, and nothing tested that it did.

Two independent faults, both verified against tldraw 4.5.12 rather than assumed:

1. Wrong container entirely. We emitted {schema, store{}}, the shape of an
   in-memory store snapshot. A .tldr FILE is
   {tldrawFileFormatVersion, schema, records[]}. tldraw's tldrawFileValidator
   requires all three, so parseTldrawJsonFile threw and returned notATldrawFile
   -- a flat refusal to open, before it ever looked at the content. The declared
   schemaVersion 2 also lacked the required sequences dict.

2. Shapes stock tldraw cannot render. note/link/image were typed taos-note /
   taos-link / taos-image, our own shape utils. Nobody else's tldraw has them.
   On the live Pi that is 55 of 64 live elements. The rest were geo shapes
   carrying taos_* keys in props, which tldraw rejects as unknown props.

Fixed by emitting the real envelope with a serialized schema captured from
tldraw, mapping every kind onto native note/text/geo shapes, and moving taOS
provenance into meta, which tldraw carries through untouched.

Deliberately does NOT pass through a user_shape's literal tldraw_shape blob,
despite that being the highest-fidelity option. One invalid record makes tldraw
reject the WHOLE file, so a single stale blob would cost the user their entire
board in the one file whose only job is recovery. That risk is concrete here:
we declare a fixed schema constant, so tldraw runs no migration on a blob
written by an older version. The lossless copy belongs in the canonical JSON
export instead. The raw blob stays untouched in the DB payload either way.

Index keys are generated properly rather than hardcoded to "a1": tldraw
validates them, so "a10" is rejected outright (a fraction may not end in 0),
and reusing one key discards z-order.

Also sets Content-Disposition, without which the browser renders the JSON
inline and the user never gets a file.

Verified on real data, not just fixtures: all three live Pi boards exported and
loaded into a stock tldraw with every shape intact (22/22, 25/25, 17/17).

* test(canvas): update the two tests that pinned the old .tldr shape

Both asserted "store" in body, which encoded the bug this branch fixes: the
in-memory {schema, store{}} store-snapshot shape rather than the
{tldrawFileFormatVersion, schema, records[]} file envelope tldraw actually
accepts. They now assert the file format and read shapes out of records.

My miss: these live under tests/projects/ and I only ran the files I had
touched, so CI found them instead of me. Swept the tree for any other consumer
of the old shape and there are none.

* fix(canvas): make every field read in the .tldr export non-fatal

CodeRabbit and Qodo independently caught the same bug, and they were right.
_element_text did payload.get(...), but the `or {}` guard only catches falsy
payloads: a truthy non-dict (a list or bare string decoded from the DB) reached
.get() and raised AttributeError. el["id"], el["x"], el["kind"] could equally
raise KeyError on an absent column.

Any of those aborts _build_tldraw_snapshot for the WHOLE project. This is the
recovery export, so it runs precisely on the boards whose rows are already
ragged, and the failure mode was that one odd row costs the user every other
shape they were trying to rescue. That directly contradicts the rule this file
already states, which makes it a bug in the code rather than in the intent.

Every read now degrades to a safe default instead of raising. A string payload
becomes the shape's label, since if that is all the row holds it is the most
useful thing to show. _num_or excludes bool deliberately: bool is an int
subclass, so True would otherwise sail through as a width of 1.0 and silently
misplace a shape rather than fall back.

Verified against tldraw's real parser, not just unit tests: the live Pi board
with malformed rows spliced in exports 67 elements and loads all 67 shapes in a
stock tldraw.

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: hognek <hognek@gmail.com>
Co-authored-by: Hogne <227774406+hognek@users.noreply.github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Conflicts resolved to dev (canonical). Master-only content audited rather
than blindly kept: the whole divergence is ONE feature - doc-review stamps
(jaylfc#1835: store, routes, middleware allowlist, FilesApp UI) that never
reached dev but runs on every deployed install - retained deliberately.
The merge had also produced byte-identical DUPLICATE definitions of
_AGENT_FILES_ROUTES and _AGENT_SCOPE_REQUEST_ROUTES (master's stale copies
shadowing dev's); deduped to dev's single copies. Reconciling doc-review
into dev proper is carded separately.
…esolution

FilesApp's doc-review UI (master-only, retained) imports DocReviewState and
calls projects.docReviews; the dev-side sweep of desktop/src/lib/projects.ts
had dropped master's client block, so tsc failed. Ported the docReviews block
and its three types; master's redundant docReview (singular) duplicate block
was deliberately NOT ported. SPA builds clean locally.
release: promote dev to master for v1.0.0-beta.45
Resolves the routes/__init__.py doc-review router registration to dev's
canonical single CSRF-protected mount (master carried a stale double
registration without CSRF from the beta.45 conflict residue).
…iver

The gate reads github.event.pull_request.body from the event payload, so a
workflow re-run replays the ORIGINAL body and never sees a waiver added by
editing the PR afterwards. An empty commit raises a fresh synchronize event
carrying the current body; the tree is unchanged, so the promote-tree-identity
assertion (diff vs dev empty) still holds.
release: promote dev to master for v1.0.0-beta.46
Replace the useThemeStore.getState() call inside the create closure with
the get() second argument of zustand's create. Eliminates the TS7022 /
TS7006 cascade caused by referencing the store within its own initializer.
Only the in-closure occurrence changes; module-level getState() calls
(outside the creator) keep referencing the store hook directly.
@github-actions

github-actions Bot commented Aug 4, 2026

Copy link
Copy Markdown

👋 Thanks for the PR! This one targets master, which is our
stable branch (it's what live installs track). Please retarget it to
dev — click Edit next to the PR title and change the base
branch dropdown from master to dev. Your commits and any review
carry over, nothing is lost.

See CONTRIBUTING.md for the branch model.

@gitar-bot

gitar-bot Bot commented Aug 4, 2026

Copy link
Copy Markdown

Gitar is working

Gitar

@qodo-code-review

Copy link
Copy Markdown

PR Summary by Qodo

Fix theme store circular self-reference by using zustand-injected get()

🐞 Bug fix 🕐 10-20 Minutes

Grey Divider

AI Description

• Use zustand’s injected get() inside the store initializer to avoid self-referencing the store.
• Eliminate TS7022/TS7006 typecheck failures without changing external helper patterns.
Diagram

graph TD
  UI["Theme consumers"] --> Hook["useThemeStore"] --> Init["create<ThemeStore>(...) initializer"]
  Init --> GetFn["injected get()"] --> Selector["getWallpapersBySection()"] --> Data["WALLPAPERS"]
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Move derived logic to a pure helper taking state as an argument
  • ➕ Keeps store initializer free of any intra-store reads
  • ➕ Makes derivations easier to unit test in isolation
  • ➖ Adds indirection and may duplicate state-shape typing at call sites
  • ➖ Doesn’t address other potential circular reads if added later
2. Define store creator with an explicit `StateCreator` type alias
  • ➕ Can reduce inference cascades in more complex stores
  • ➕ Makes the intended set/get API explicit in types
  • ➖ More boilerplate for a single call-site issue
  • ➖ Doesn’t change the underlying circular reference risk if getState() is still used inside initializer

Recommendation: Keep the PR’s approach. Using zustand’s injected get() is the idiomatic fix for avoiding circular self-references during store initialization, and it minimizes surface-area change while directly addressing the TS7022/TS7006 cascade.

Files changed (1) +2 / -2

Bug fix (1) +2 / -2
theme-store.tsUse zustand-injected 'get()' inside store initializer +2/-2

Use zustand-injected 'get()' inside store initializer

• Updates the zustand 'create' initializer signature to accept 'get' and switches 'getWallpapersBySection' to read state via 'get()' rather than 'useThemeStore.getState()'. This prevents referencing the store hook within its own initialization and avoids the resulting TypeScript inference/error cascade.

desktop/src/stores/theme-store.ts

@kilo-code-bot

kilo-code-bot Bot commented Aug 4, 2026

Copy link
Copy Markdown

Code Review Summary

Status: No New Issues Found | Recommendation: Merge

Files Reviewed (1 files)
  • desktop/src/stores/theme-store.ts - No new issues in this incremental diff
Previous Review Summaries (2 snapshots, latest commit 04574bb)

Current summary above is authoritative. Previous snapshots are kept for context only.

Previous review (commit 04574bb)

Status: No New Issues Found | Recommendation: Merge

Files Reviewed (1 files)
  • desktop/src/stores/theme-store.ts - No new issues in this incremental diff

Previous review (commit 1743609)

Status: No Issues Found | Recommendation: Merge

Files Reviewed (1 files)
  • desktop/src/stores/theme-store.ts

Reviewed by step-3.7-flash · Input: 44.2K · Output: 2.5K · Cached: 257.2K

@qodo-code-review

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (0) 📘 Rule violations (0) 📜 Skill insights (1)

Context used
✅ Compliance rules (platform): 35 rules

Grey Divider


Remediation recommended

1. No new regression test 📜 Skill insight ▣ Testability
Description
This PR fixes a bug in useThemeStore initialization by switching to the injected get() accessor,
but it does not add any new test case that would fail prior to the fix and pass after it. Without a
regression test, this typecheck/circular-reference issue may be reintroduced unnoticed.
Code

desktop/src/stores/theme-store.ts[229]

+export const useThemeStore = create<ThemeStore>((set, get) => ({
Relevance

●● Moderate

Tests are frequently accepted when suggested, but no clear precedent enforcing new regression test
for every bugfix.

PR-#1542
PR-#507

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
PR Compliance ID 2185285 requires that bug-fix PRs add at least one regression test. The diff shows
only the fix in desktop/src/stores/theme-store.ts (switching the create closure to accept get
and using get() in getWallpapersBySection) with no accompanying test change in the provided PR
diff.

desktop/src/stores/theme-store.ts[229-229]
desktop/src/stores/theme-store.ts[305-307]
Skill: taos-development-skill

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The PR is a bug fix but does not include a new regression test that would catch the circular self-reference/type inference failure (TS7022/TS7006) if it reappears.

## Issue Context
The change replaces `useThemeStore.getState()` inside the zustand `create` closure with the injected `get()` to avoid self-referencing the store during initialization.

## Fix Focus Areas
- desktop/src/stores/theme-store.test.ts[1-200]
- desktop/src/stores/theme-store.ts[229-307]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

To customize comments, go to the Qodo configuration screen, or learn more in the docs.

Qodo Logo

}

export const useThemeStore = create<ThemeStore>((set) => ({
export const useThemeStore = create<ThemeStore>((set, get) => ({

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Remediation recommended

1. No new regression test 📜 Skill insight ▣ Testability

This PR fixes a bug in useThemeStore initialization by switching to the injected get() accessor,
but it does not add any new test case that would fail prior to the fix and pass after it. Without a
regression test, this typecheck/circular-reference issue may be reintroduced unnoticed.
Agent Prompt
## Issue description
The PR is a bug fix but does not include a new regression test that would catch the circular self-reference/type inference failure (TS7022/TS7006) if it reappears.

## Issue Context
The change replaces `useThemeStore.getState()` inside the zustand `create` closure with the injected `get()` to avoid self-referencing the store during initialization.

## Fix Focus Areas
- desktop/src/stores/theme-store.test.ts[1-200]
- desktop/src/stores/theme-store.ts[229-307]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

@jaylfc

jaylfc commented Aug 4, 2026

Copy link
Copy Markdown
Owner

Reviewed. ON-SPEC, arming. Correct fix and the right idiom.

getWallpapersBySection called useThemeStore.getState() from INSIDE the store's own initializer, which is a circular self-reference: the module binding is not initialised while create() is still running, so the call depends on module-evaluation order rather than on anything the code controls. Swapping to zustand's injected get() is the standard fix and is semantically identical for callers - same state object, no behaviour change - while removing the cycle.

Verified rather than eyeballed: 191 tests across all 16 store suites pass with the change, including restore-theme.test.ts which is the one that would notice if getWallpapersBySection started returning a different shape.

No changelog needed (internal correctness fix, no user-visible behaviour change) and doc-gate agrees - it is not red. CI is otherwise clean on this one.

Auto-merge armed on green.

@jaylfc
jaylfc enabled auto-merge (squash) August 4, 2026 10:54
@coderabbitai

coderabbitai Bot commented Aug 4, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The theme store initializer now receives Zustand’s get accessor. Wallpaper-section generation uses this accessor to read current store state.

Changes

Theme store state access

Layer / File(s) Summary
Use initializer-scoped state access
desktop/src/stores/theme-store.ts
The store initializer accepts get. getWallpapersBySection uses get() instead of useThemeStore.getState().

Estimated code review effort: 1 (Trivial) | ~5 minutes

Suggested reviewers: jaylfc

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: using the injected Zustand get() function to remove the theme-store circular self-reference.
✨ Finishing Touches 💡 1
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@jaylfc jaylfc closed this Aug 9, 2026
auto-merge was automatically disabled August 9, 2026 12:17

Pull request was closed

@jaylfc jaylfc reopened this Aug 9, 2026
@qodo-code-review

Copy link
Copy Markdown

ⓘ Qodo reviews are paused because the subscription is no longer active. Ask your workspace admin to reactivate the subscription to resume reviews. Manage billing

@jaylfc

jaylfc commented Aug 9, 2026

Copy link
Copy Markdown
Owner

recreate-cla

@hognek

hognek commented Aug 9, 2026

Copy link
Copy Markdown
Contributor Author

Pushed empty commit to retrigger CLA check.

@hognek

hognek commented Aug 9, 2026

Copy link
Copy Markdown
Contributor Author

I have read the CLA Document and I hereby sign the CLA

@jaylfc

jaylfc commented Aug 9, 2026

Copy link
Copy Markdown
Owner

recreate-cla

@hognek
hognek changed the base branch from master to dev August 9, 2026 14:52
@hognek

hognek commented Aug 9, 2026

Copy link
Copy Markdown
Contributor Author

recreate-cla

1 similar comment
@jaylfc

jaylfc commented Aug 9, 2026

Copy link
Copy Markdown
Owner

recreate-cla

@jaylfc jaylfc closed this Aug 9, 2026
@jaylfc jaylfc reopened this Aug 9, 2026
@jaylfc

jaylfc commented Aug 9, 2026

Copy link
Copy Markdown
Owner

Refreshing CI against the current base (dev moved 14 times since these checks ran). The cla check cannot structurally appear on this PR (fork PRs do not fire pull_request_target on synchronize, and the comment-triggered run binds to the default branch); hognek's CLA signature is on record in cla-signatures, so this will merge on fresh green with a documented gate exception.

@jaylfc

jaylfc commented Aug 9, 2026

Copy link
Copy Markdown
Owner

Merging with the cla check absent, documented exception: fork PRs cannot fire pull_request_target on synchronize (measured earlier today), and the comment-triggered run binds to the default branch, so cla is structurally unable to appear here. hognek's CLA signature is verified in the cla-signatures branch (signatures/version1/cla.json), which satisfies the check's actual purpose. All other required checks are green on the current head after the close/reopen refresh; base drift since those runs is one docs-only merge (#2327).

@jaylfc
jaylfc merged commit 782ac24 into jaylfc:dev Aug 9, 2026
35 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants