Skip to content

feat: show tiny.place only to users with an identity - #5439

Merged
senamakel merged 7 commits into
tinyhumansai:mainfrom
YellowSnnowmann:feat/gate-tinyplace-visibility-5424
Aug 13, 2026
Merged

senamakel merged 7 commits into
tinyhumansai:mainfrom
YellowSnnowmann:feat/gate-tinyplace-visibility-5424

Conversation

@YellowSnnowmann

@YellowSnnowmann YellowSnnowmann commented Aug 7, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • tiny.place is being removed from the app after 31 August 2026. This shows its entry points only to users who already set up a tiny.place identity and gives them a persistent notice pointing to tiny.place. No code is removed — visibility + notice only.
  • New useTinyPlaceIdentity (cached self-identity check) + useNavTabs, which hides the agent-world tab for users without an identity in both nav renderers.
  • Agent-world route guarded (confirmed non-holders redirected to chat; direct links preserved for holders); the Brain Orchestration sub-tab chip is hidden and ?tab=orchestration redirects to welcome for non-holders.
  • New non-dismissable TinyPlaceSunsetNotice with a link out to tiny.place, shown on the tiny.place surfaces.
  • tinyplaceSunset.* copy added to every supported locale.

Problem

Someone without a tiny.place identity never really started, so they shouldn't be shown an entry point that's about to disappear; identity-holders need a clear path to keep using tiny.place. The app had no way to answer "has this user set up tiny.place?" at the UI layer.

Solution

The authoritative signal is the orchestration.self_identity RPC: a non-empty agentId means the wallet-backed identity exists (a locked/unconfigured wallet or a rejection is the "never set up" case → fail closed to hidden). The result is fetched once per session and cached, then read by the nav filter, the agent-world route guard, the Brain orchestration gate, and the notice. Routes stay registered so identity-holders' bookmarks keep working.

Submission Checklist

  • Tests added or updated (happy path + failure/edge) — identity hook (incl. reject path), nav filter, notice + link, route guard; 13 tests pass
  • Diff coverage ≥ 80% — new hooks/notice/shell changes covered by Vitest (frontend-only diff)
  • Coverage matrix updated — N/A: visibility-gating change, no new feature IDs
  • All affected feature IDs listed under Related — N/A: no matrix rows affected
  • No new external network dependencies introduced
  • Manual smoke checklist updated — N/A: no release-cut surface touched
  • Linked issue closed via Closes #NNN

Impact

  • Frontend-only (desktop app UI). No Rust/backend changes.
  • CI note: the local pre-push hook runs full Rust clippy over the core + Tauri shell, which can't run in a fresh worktree without the vendored submodules (incl. the large CEF submodule) initialized. This diff touches zero Rust, so that check is not applicable here; frontend format/lint/typecheck/i18n and the changed-file tests all pass locally, and CI's Rust lanes correctly skip for this frontend-only diff.

Related


AI Authored PR Metadata (required for Codex/Linear PRs)

N/A — not a Codex/Linear-authored PR.

Validation Run

  • pnpm --filter openhuman-app format:check
  • pnpm typecheck
  • Focused tests: useTinyPlaceIdentity, useNavTabs, TinyPlaceSunsetNotice, AgentWorldShell (13 pass)
  • Rust fmt/check (if changed) — N/A: no Rust changed
  • Tauri fmt/check (if changed) — N/A: no Rust changed

Summary by CodeRabbit

  • New Features

    • Added identity-aware access to TinyPlace features and navigation.
    • Users without a TinyPlace identity are redirected to Chat and no longer see the TinyPlace tab.
    • Added a non-dismissible notice about TinyPlace’s transition and removal after August 31, 2026, with a link to tiny.place.
    • Added translated notice content across supported languages.
  • Bug Fixes

    • Improved identity detection with caching, retry handling, and recovery when the app regains focus.
    • Preserved access while identity checks are still loading.

tiny.place is being removed from the app after 31 August 2026. Show its entry
points only to users who already set up a tiny.place identity and give them a
persistent notice pointing to tiny.place.

- Add useTinyPlaceIdentity (cached self-identity check) and useNavTabs, which
  hides the agent-world tab for users without an identity in both nav renderers.
- Guard the agent-world route (redirect confirmed non-holders to chat, direct
  links preserved for holders) and hide the Brain orchestration sub-tab.
- Add a non-dismissable TinyPlaceSunsetNotice with a link out to tiny.place on
  the tiny.place surfaces.
- Add tinyplaceSunset.* copy to every supported locale.

Closes tinyhumansai#5424
@coderabbitai

coderabbitai Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: d0ee00c5-1980-4404-8033-9b60fa109727

📥 Commits

Reviewing files that changed from the base of the PR and between 2e90d8b and bc55c6d.

📒 Files selected for processing (2)
  • app/src/hooks/useNavTabs.test.ts
  • app/src/pages/Brain.tsx
🚧 Files skipped from review as they are similar to previous changes (2)
  • app/src/hooks/useNavTabs.test.ts
  • app/src/pages/Brain.tsx

📝 Walkthrough

Walkthrough

The PR adds shared TinyPlace identity detection, hides TinyPlace navigation without an identity, redirects unauthorized direct access, and adds a translated sunset notice with a link to https://tiny.place.

Changes

TinyPlace access lifecycle

Layer / File(s) Summary
Shared TinyPlace identity state
app/src/hooks/useTinyPlaceIdentity.ts, app/src/hooks/useTinyPlaceIdentity.test.ts
The hook queries self-identity, shares results, fails closed on errors, retries failures, and responds to window focus.
Identity-based navigation filtering
app/src/hooks/useNavTabs.ts, app/src/hooks/useNavTabs.test.ts, app/src/components/layout/shell/*.tsx, app/src/components/layout/shell/*.test.tsx
Navigation hides the agent-world tab when the user has no TinyPlace identity.
TinyPlace sunset notice
app/src/agentworld/TinyPlaceSunsetNotice.tsx, app/src/agentworld/TinyPlaceSunsetNotice.test.tsx, app/src/utils/links.ts, app/src/lib/i18n/*.ts
The non-dismissible notice displays translated sunset messaging and opens https://tiny.place.
Direct surface access and notice integration
app/src/agentworld/AgentWorldShell.tsx, app/src/agentworld/AgentWorldShell.test.tsx, app/src/pages/Brain.tsx, app/src/pages/__tests__/Brain.test.tsx
Agent-world and orchestration surfaces redirect users without an identity and show the sunset notice to identity holders.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant Navigation
  participant useTinyPlaceIdentity
  participant TinyPlaceSurface
  participant TinyPlace
  User->>Navigation: open application navigation
  Navigation->>useTinyPlaceIdentity: read identity state
  useTinyPlaceIdentity-->>Navigation: return identity status
  Navigation-->>User: show or hide agent-world tab
  User->>TinyPlaceSurface: open direct TinyPlace route
  TinyPlaceSurface->>useTinyPlaceIdentity: read identity state
  useTinyPlaceIdentity-->>TinyPlaceSurface: return identity status
  TinyPlaceSurface-->>User: render surface or redirect to /chat
  User->>TinyPlace: activate sunset notice CTA
  TinyPlaceSurface->>TinyPlace: open external TinyPlace URL
Loading

Suggested labels: feature, agent

Poem

I twitch my nose at identity’s gate,
TinyPlace tabs now know their state.
A sunset banner points the way,
“Visit tiny.place” is what we say.
The rabbit approves this tidy route. 🐇

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Linked Issues check ❓ Inconclusive The changes address [#5424] functional requirements, but the required diff-coverage threshold is not verifiable from the provided summaries. Provide CI or coverage results confirming at least 80% changed-lines coverage.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the primary change: restricting tiny.place visibility to users with an identity.
Out of Scope Changes check ✅ Passed The implementation, tests, translations, navigation changes, and link constant all support the requirements in [#5424].

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.

The agent-world tab is now gated on a tiny.place identity (tinyhumansai#5424), so the
existing SidebarNav / CollapsedNavRail tests — which assert the full nav and
Tiny.Place active state — must mock useTinyPlaceIdentity as present. The gate
itself is covered by useNavTabs.test.ts.
@YellowSnnowmann
YellowSnnowmann marked this pull request as ready for review August 7, 2026 10:39
@YellowSnnowmann
YellowSnnowmann requested a review from a team August 7, 2026 10:39

@greptile-apps greptile-apps Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Your trial has ended. Reactivate Greptile to resume code reviews.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 21017f7a80

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread app/src/hooks/useTinyPlaceIdentity.ts
… caching it

A rejected self_identity call (wallet/keyring locked at startup, or a transient
relay error) previously published ready/false and latched, so a real identity
holder stayed hidden from tiny.place — nav tab gone, direct links redirected —
until an app restart. Distinguish a resolved answer (terminal) from a transient
rejection: on rejection stay fail-closed but retry with a bounded backoff, and
re-check immediately on window focus (e.g. after the user unlocks their wallet),
so a one-time startup failure never locks a holder out for the session.

@greptile-apps greptile-apps Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Your trial has ended. Reactivate Greptile to resume code reviews.

@coderabbitai coderabbitai Bot added agent Built-in agents, prompts, orchestration, and agent runtime in src/openhuman/agent/. feature Net-new user-facing capability or product behavior. labels Aug 10, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@app/src/lib/i18n/bn.ts`:
- Around line 7193-7196: Update only the tinyplaceSunset.title translation to
use the product name “Tiny Place” instead of the domain spelling, while
preserving the existing tiny.place references in tinyplaceSunset.message and
tinyplaceSunset.cta.

In `@app/src/lib/i18n/es.ts`:
- Line 7344: Update the tinyplaceSunset.title translation to use the product
name “Tiny Place” instead of the domain-style “tiny.place”; leave the related
message and CTA translations unchanged so domain references remain “tiny.place”.

In `@app/src/lib/i18n/id.ts`:
- Around line 7232-7235: Update the `tinyplaceSunset.title` UI label to use the
product name “Tiny Place” with the existing Indonesian wording, while preserving
`tiny.place` unchanged in `tinyplaceSunset.message` and `tinyplaceSunset.cta`
where it denotes the external domain.

In `@app/src/pages/Brain.tsx`:
- Around line 115-120: Remove the redirect useEffect in Brain and derive the
ready, non-identity orchestration condition from activeTab, tinyplaceStatus, and
hasTinyplaceIdentity. After all hooks and before the main JSX return, return
<Navigate to="/brain" replace /> for that condition so OrchestrationView never
mounts; add a Brain test covering a confirmed non-holder at ?tab=orchestration.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: a05ec0a0-ace4-43a0-9d77-fbd3e33d1bd7

📥 Commits

Reviewing files that changed from the base of the PR and between c7e15ba and f4cc35d.

📒 Files selected for processing (28)
  • app/src/agentworld/AgentWorldShell.test.tsx
  • app/src/agentworld/AgentWorldShell.tsx
  • app/src/agentworld/TinyPlaceSunsetNotice.test.tsx
  • app/src/agentworld/TinyPlaceSunsetNotice.tsx
  • app/src/components/layout/shell/CollapsedNavRail.test.tsx
  • app/src/components/layout/shell/CollapsedNavRail.tsx
  • app/src/components/layout/shell/SidebarNav.test.tsx
  • app/src/components/layout/shell/SidebarNav.tsx
  • app/src/hooks/useNavTabs.test.ts
  • app/src/hooks/useNavTabs.ts
  • app/src/hooks/useTinyPlaceIdentity.test.ts
  • app/src/hooks/useTinyPlaceIdentity.ts
  • app/src/lib/i18n/ar.ts
  • app/src/lib/i18n/bn.ts
  • app/src/lib/i18n/de.ts
  • app/src/lib/i18n/en.ts
  • app/src/lib/i18n/es.ts
  • app/src/lib/i18n/fr.ts
  • app/src/lib/i18n/hi.ts
  • app/src/lib/i18n/id.ts
  • app/src/lib/i18n/it.ts
  • app/src/lib/i18n/ko.ts
  • app/src/lib/i18n/pl.ts
  • app/src/lib/i18n/pt.ts
  • app/src/lib/i18n/ru.ts
  • app/src/lib/i18n/zh-CN.ts
  • app/src/pages/Brain.tsx
  • app/src/utils/links.ts

Comment thread app/src/lib/i18n/bn.ts Outdated
Comment thread app/src/lib/i18n/es.ts Outdated
Comment thread app/src/lib/i18n/id.ts Outdated
Comment thread app/src/pages/Brain.tsx Outdated
…ct-name notice title

Address CodeRabbit review on tinyhumansai#5439.

- Brain: redirect a confirmed non-holder away from ?tab=orchestration in the
  render phase (return <Navigate to="/brain" replace />) instead of a
  post-commit effect, so OrchestrationView — and its tiny.place RPCs — never
  mount even once for a gated user. The in-flight 'loading' window and holders
  are untouched. Adds Brain tests for the confirmed-non-holder redirect and the
  loading window.
- i18n: the sunset notice title is a UI product label, so use the product name
  'Tiny Place' (matching nav.agentWorld / agentWorld.world.title) across all 14
  locales; the message and CTA keep the 'tiny.place' domain spelling. CodeRabbit
  flagged only bn/es/id — applied consistently to every locale, English source
  included.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@app/src/pages/__tests__/Brain.test.tsx`:
- Around line 224-236: Add a hoisted render spy for OrchestrationView and a
useLocation-based pathname probe to the test setup. Update the confirmed
non-holder test to assert OrchestrationView was never rendered and that the
final pathname is /brain, while retaining the existing welcome-tab assertion.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 78ca8195-7750-48d6-99b5-48b27950aaea

📥 Commits

Reviewing files that changed from the base of the PR and between f4cc35d and a005cd0.

📒 Files selected for processing (16)
  • app/src/lib/i18n/ar.ts
  • app/src/lib/i18n/bn.ts
  • app/src/lib/i18n/de.ts
  • app/src/lib/i18n/en.ts
  • app/src/lib/i18n/es.ts
  • app/src/lib/i18n/fr.ts
  • app/src/lib/i18n/hi.ts
  • app/src/lib/i18n/id.ts
  • app/src/lib/i18n/it.ts
  • app/src/lib/i18n/ko.ts
  • app/src/lib/i18n/pl.ts
  • app/src/lib/i18n/pt.ts
  • app/src/lib/i18n/ru.ts
  • app/src/lib/i18n/zh-CN.ts
  • app/src/pages/Brain.tsx
  • app/src/pages/__tests__/Brain.test.tsx
🚧 Files skipped from review as they are similar to previous changes (15)
  • app/src/lib/i18n/pt.ts
  • app/src/lib/i18n/en.ts
  • app/src/lib/i18n/ko.ts
  • app/src/lib/i18n/de.ts
  • app/src/lib/i18n/fr.ts
  • app/src/lib/i18n/es.ts
  • app/src/lib/i18n/bn.ts
  • app/src/lib/i18n/id.ts
  • app/src/lib/i18n/zh-CN.ts
  • app/src/lib/i18n/pl.ts
  • app/src/lib/i18n/ru.ts
  • app/src/lib/i18n/hi.ts
  • app/src/lib/i18n/it.ts
  • app/src/lib/i18n/ar.ts
  • app/src/pages/Brain.tsx

Comment thread app/src/pages/__tests__/Brain.test.tsx
…brain

Address CodeRabbit follow-up on tinyhumansai#5439: queryByTestId only checks final DOM, so a
regression that mounts OrchestrationView and redirects from an effect would still
pass. Add a hoisted render spy on OrchestrationView (assert not called) and a
useLocation pathname probe (assert final route is /brain) to the confirmed
non-holder test, alongside the existing welcome-tab assertion.
coderabbitai[bot]
coderabbitai Bot previously approved these changes Aug 10, 2026

@tinysweeper tinysweeper Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Requesting changes: 1 lane(s) blocking, worst finding is high.

Fix or reply to the findings below and push. The next review clears this automatically once they are gone — you should not need to dismiss anything by hand.

             $0.0148 · 167,502 in / 46,707 out · 136,638 cached (82%) · z-ai/glm-5.2, deepseek/deepseek-v4-pro
critique:    $0.0093 · 70,322 in  / 31,790 out · 57,141 cached (81%)  · z-ai/glm-5.2, deepseek/deepseek-v4-pro
security:    $0.0029 · 64,174 in  / 7,035 out  · 54,972 cached (86%)  · z-ai/glm-5.2
tests:       $0.0014 · 16,161 in  / 4,457 out  · 12,629 cached (78%)  · z-ai/glm-5.2
description: $0.0013 · 16,845 in  / 3,425 out  · 11,896 cached (71%)  · z-ai/glm-5.2

Comment thread app/src/hooks/useNavTabs.test.ts Outdated
import { useNavTabs } from './useNavTabs';
import type { TinyPlaceIdentityState } from './useTinyPlaceIdentity';

let identity: TinyPlaceIdentityState = { status: 'ready', hasIdentity: false };

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

priority high critique likely

vi.mock factory references a non-mock-prefixed variable

The vi.mock factory references identity, a module-level variable that is not prefixed with mock. Vitest hoists vi.mock calls above all imports and initialization, and its factory is only permitted to close over out-of-scope variables whose names start with mock (e.g. mockIdentity); any other reference triggers a hoisting/reference error at setup time. Because identity is declared after the (hoisted) vi.mock and lacks the required prefix, every test in this file will fail before running, e.g. with “Cannot access ‘identity’ before initialization” or the equivalent Vitest message. Rename the variable to mockIdentity (and update the assignments in beforeEach/each test) so the factory can legally reference it.

[RULE] vi.mock factories may only reference out-of-scope variables prefixed with mock ·

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

The "every test will fail before running" claim is not accurate — the file passed in CI (Frontend Checks → Vitest) and locally (vitest run src/hooks/useNavTabs.test.ts → 3/3). Vitest v4 evaluates the factory's () => identity lazily at call time (inside a test), not at hoist/factory-eval time, so the out-of-scope reference never triggers the error. That said, the repo convention for controllable mock state is vi.hoisted (see Brain.test.tsx), so in bc55c6d I switched to a hoisted identityRef to match it and remove the ambiguity.

Comment thread app/src/pages/Brain.tsx
// tiny.place surface must not fire even once. Holders and the in-flight
// `loading` window are left untouched.
const { status: tinyplaceStatus, hasIdentity: hasTinyplaceIdentity } = useTinyPlaceIdentity();
const shouldRedirectFromOrchestration =

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

priority medium critique uncertain

Loading window lets OrchestrationView mount for a confirmed non-holder

The comment states the goal is that "OrchestrationView never mounts for a confirmed non-holder — its RPCs and tiny.place surface must not fire even once." But the guard only fires once tinyplaceStatus === 'ready'. During the in-flight loading window the redirect is skipped, so a stale ?tab=orchestration deep link causes <OrchestrationView /> to mount and run its RPCs for a user who is later confirmed to have no identity. The redirect then unmounts it after the fact, so the RPCs have already fired once — exactly what the comment says must not happen. If the "must not fire even once" invariant is real, the loading case needs handling (e.g. treat loading on a stale orchestration link as "do not mount OrchestrationView yet" rather than "mount it").

[RULE] , ·

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Good catch on the comment — it overstated the invariant, fixed in bc55c6d. The behaviour is intentional and matches the AgentWorldShell route guard: a confirmed non-holder (status === 'ready' && !hasIdentity) never mounts OrchestrationView (render-phase <Navigate>), while the in-flight loading window renders optimistically so a holder — the common case — never sees a flash. Making loading withhold the mount would regress every holder to protect a rare stale non-holder deep link, so I kept the optimistic behaviour and reworded the comment to describe that trade-off accurately rather than claim "must not fire even once".

@tinysweeper

tinysweeper Bot commented Aug 10, 2026

Copy link
Copy Markdown

What this change touches

29 files, +613 -18 across 7 components. The code graph knows nothing about these files yet — normal for newly added files, and a cold index otherwise.

flowchart LR
  n0["app/src/hooks<br/>4 files +277 -0"]:::changed
  n1["app/src/agentworld<br/>4 files +147 -1"]:::changed
  n2["app/src/pages/__tests__<br/>1 file +65 -1"]:::changed
  n3["app/src/lib/i18n<br/>14 files +56 -0"]:::changed
  n4["app/src/pages<br/>1 file +38 -12"]:::changed
  n5["app/src/components/layout/shell<br/>4 files +27 -4"]:::changed
  n6["app/src/utils<br/>1 file +3 -0"]:::changed
  classDef changed fill:#0d4429,stroke:#238636,color:#e6edf3
  classDef impacted fill:#161b22,stroke:#6e7681,color:#c9d1d9
  classDef flagged fill:#5a1e02,stroke:#d93f0b,color:#ffffff
  classDef blocking fill:#67060c,stroke:#f85149,color:#ffffff
Loading

Green: changed. Grey: untouched, reached through an import or a call. Orange: has findings. Red: has a finding that blocks the merge.

Component Files Lines Findings
app/src/hooks changed 4 +277 -0
app/src/agentworld changed 4 +147 -1
app/src/pages/__tests__ changed 1 +65 -1
app/src/lib/i18n changed 14 +56 -0
app/src/pages changed 1 +38 -12
app/src/components/layout/shell changed 4 +27 -4
app/src/utils changed 1 +3 -0
Changed files

app/src/hooks

  • app/src/hooks/useNavTabs.test.ts
  • app/src/hooks/useNavTabs.ts
  • app/src/hooks/useTinyPlaceIdentity.test.ts
  • app/src/hooks/useTinyPlaceIdentity.ts

app/src/agentworld

  • app/src/agentworld/AgentWorldShell.test.tsx
  • app/src/agentworld/AgentWorldShell.tsx
  • app/src/agentworld/TinyPlaceSunsetNotice.test.tsx
  • app/src/agentworld/TinyPlaceSunsetNotice.tsx

app/src/pages/__tests__

  • app/src/pages/__tests__/Brain.test.tsx

app/src/lib/i18n

  • app/src/lib/i18n/ar.ts
  • app/src/lib/i18n/bn.ts
  • app/src/lib/i18n/de.ts
  • app/src/lib/i18n/en.ts
  • app/src/lib/i18n/es.ts
  • app/src/lib/i18n/fr.ts
  • app/src/lib/i18n/hi.ts
  • app/src/lib/i18n/id.ts
  • app/src/lib/i18n/it.ts
  • app/src/lib/i18n/ko.ts
  • app/src/lib/i18n/pl.ts
  • app/src/lib/i18n/pt.ts
  • …and 2 more files

app/src/pages

  • app/src/pages/Brain.tsx

app/src/components/layout/shell

  • app/src/components/layout/shell/CollapsedNavRail.test.tsx
  • app/src/components/layout/shell/CollapsedNavRail.tsx
  • app/src/components/layout/shell/SidebarNav.test.tsx
  • app/src/components/layout/shell/SidebarNav.tsx

app/src/utils

  • app/src/utils/links.ts

tinysweeper 0.1.0

@tinysweeper tinysweeper Bot added the priority: p1 Next. Wrong behaviour a user will hit, or a security weakness behind a condition. label Aug 10, 2026
…ng comment

Address tinysweeper review on tinyhumansai#5439.

- useNavTabs.test.ts: switch the controllable identity mock to vi.hoisted (the
  repo convention, as in Brain.test.tsx). The prior module-scoped `let identity`
  already passed (Vitest v4 evaluates the factory's `() => identity` lazily at
  call time, not at hoist time), but the hoisted ref matches the established
  pattern and removes any ambiguity.
- Brain.tsx: the orchestration redirect comment overstated the invariant. A
  *confirmed* non-holder never mounts OrchestrationView, but the in-flight
  `loading` window renders optimistically — matching the AgentWorldShell route
  guard so a holder never sees a flash. Reworded to describe that trade-off
  accurately; behaviour is unchanged and intentional.
@YellowSnnowmann

Copy link
Copy Markdown
Collaborator Author

@tinysweeper — both findings are addressed in bc55c6d23 (replies on each thread):

1. useNavTabs.test.ts — vi.mock references non-mock variable (high).
The "every test will fail before running" claim isn't accurate: the file passes in CI (Frontend Checks → Vitest) and locally (vitest run src/hooks/useNavTabs.test.ts → 3/3). Vitest v4 evaluates the factory's () => identity lazily at call time, not at hoist time, so the out-of-scope reference never errors. I still switched it to a hoisted identityRef (vi.hoisted) to match the repo convention (as in Brain.test.tsx) and remove the ambiguity.

2. Brain.tsx — loading window lets OrchestrationView mount (medium).
Correct that the comment overstated the invariant — fixed. The behaviour is intentional and mirrors the AgentWorldShell route guard: a confirmed non-holder (status === 'ready' && !hasIdentity) never mounts OrchestrationView via the render-phase <Navigate>, while the in-flight loading window renders optimistically so a holder (the common case) never sees a flash. Making loading withhold the mount would regress every holder to guard a rare stale non-holder deep link, so I kept the optimistic behaviour and reworded the comment to describe that trade-off accurately rather than claim "must not fire even once".

Please re-review when you get a chance so the critique lane can clear.

@tinysweeper tinysweeper Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

The previously-blocking findings are resolved. Clearing the changes request.

             $0.0052 · 47,973 in / 18,132 out · 36,981 cached (77%) · z-ai/glm-5.2
critique:    $0.0009 · 8,265 in  / 3,270 out  · 6,468 cached (78%)  · z-ai/glm-5.2
security:    $0.0008 · 6,024 in  / 2,883 out  · 4,676 cached (78%)  · z-ai/glm-5.2
tests:       $0.0023 · 16,494 in / 8,469 out  · 12,664 cached (77%) · z-ai/glm-5.2
description: $0.0012 · 17,190 in / 3,510 out  · 13,173 cached (77%) · z-ai/glm-5.2

@tinysweeper tinysweeper Bot added priority: p3 Whenever. Cosmetic, a nicety, or a cleanup with no user visible effect. and removed priority: p1 Next. Wrong behaviour a user will hit, or a security weakness behind a condition. labels Aug 10, 2026

@M3gA-Mind M3gA-Mind left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

PR #5439 — feat: show tiny.place only to users with an identity

Walkthrough

Independent human-directed review. This PR adds a module-cached useTinyPlaceIdentity hook over orchestration.self_identity and applies it at four gates: the primary nav tab (both renderers via a new useNavTabs), the /agent-world/* route, the Brain ?tab=orchestration sub-tab, plus a non-dismissible sunset notice and copy in all 14 locales. The shape is right — one shared signal, gating at registration/render sites rather than deleting routes, tests that fail if the gate is reverted.

I re-read the current state of the files (post d4eb8ece / a005cd02 / 2e90d8b4 / bc55c6d2) rather than the original diff, and verified the four previously-raised points independently: the backoff + refocus retry, the render-phase Brain guard, the Tiny Place vs tiny.place locale fix, and the Brain render-spy test. All four hold. What follows is only what those 23 bot reviews did not cover — chiefly one finding that changes the premise the hook is built on, and one entry point the gate missed.

I ran the seven affected test files locally: 48/48 pass.

Changes

File Summary
app/src/hooks/useTinyPlaceIdentity.ts New module-singleton identity check; resolved answer is terminal, rejection retried on a 2s/5s/10s backoff plus a window focus recheck
app/src/hooks/useNavTabs.ts NAV_TABS filtered to drop agent-world for non-holders
app/src/components/layout/shell/{SidebarNav,CollapsedNavRail}.tsx Both nav renderers switched from NAV_TABS to useNavTabs()
app/src/agentworld/AgentWorldShell.tsx Route guard (<Navigate to="/chat" replace /> for confirmed non-holders) + mounts the sunset notice
app/src/agentworld/TinyPlaceSunsetNotice.tsx Non-dismissible UpsellBanner with an openUrl(TINYPLACE_URL) CTA
app/src/pages/Brain.tsx Orchestration chip hidden for non-holders; render-phase <Navigate to="/brain"> for a confirmed non-holder on ?tab=orchestration
app/src/utils/links.ts TINYPLACE_URL constant
app/src/lib/i18n/{14 locales}.ts tinyplaceSunset.{title,message,cta}
5 test files Hook (incl. reject + retry + refocus), nav filter, notice, route guard, Brain gate

Actionable comments (6)

🛑 Blockers

None.

⚠️ Major

1. app/src/hooks/useTinyPlaceIdentity.ts:50-80 — the "no identity" case is a rejection, not a resolved empty agentId, so resolved never latches for non-holders

This is the premise the whole hook rests on, and I think it's inverted. Tracing the RPC core-side:

orchestration_self_identityhandle_tinyplace_signal_key_status (src/openhuman/tinyplace/manifest.rs:3195) → global_state().client().await?tinyplace_signer_seed()secret_material(WalletChain::Solana)Err(WALLET_NOT_CONFIGURED_MESSAGE) when no wallet exists (src/openhuman/web3/wallet/ops.rs:747-754). And require_signer (manifest.rs:2925-2932) returns Err("no signer configured — unlock wallet to manage Signal keys"). When a signer does exist, agent_id = signer.agent_id() is the wallet address — non-empty.

So { agentId: '' } is essentially unreachable in production. The real discriminator is reject vs resolve, and the "never set up tiny.place" majority — the exact population this PR exists to serve — lands in the catch, which the hook classifies as transient. Consequences:

  • resolved never becomes true for a non-holder, so onFocus (line 105) is never short-circuited: every window focus re-arms a fresh 4-call chain (t=0, +2s, +7s, +17s) for the whole session. Alt-tab twenty times, eighty failing RPCs. Each one does a core-side config load + wallet-state file lock.
  • This is the pattern this repo already got burned by — coreRpcClient.ts:253-259: "an unbounded retry loop against an absent method produced ~9k Sentry events/day from a single client (#5157)". Not Sentry-visible here (callCoreRpc doesn't capture), but the churn is the same shape.
  • The module doc (line 12-13, "A resolved call (identity present or absent) is terminal and cached") and the PR body ("fetched once per session and cached") are both accurate only for holders.

Suggested change — bound the total attempts across the session, so a permanently wallet-less user settles instead of re-arming on focus:

// before
async function attemptLoad(attempt: number) {
  if (resolved || inFlight) return;
  ...
    const delay = RETRY_DELAYS_MS[attempt];
    if (delay !== undefined && !retryTimer) { ... }

// after
const MAX_ATTEMPTS = RETRY_DELAYS_MS.length + 1; // session-wide, not per-chain
let attempts = 0;

async function attemptLoad(attempt: number) {
  if (resolved || inFlight || attempts >= MAX_ATTEMPTS) return;
  attempts += 1;
  ...
    // (in catch) schedule only while the session budget allows
    const delay = RETRY_DELAYS_MS[attempt];
    if (delay !== undefined && !retryTimer && attempts < MAX_ATTEMPTS) { ... }
    else console.debug('[tinyplace-gate] identity unresolved after %d attempts — staying hidden', attempts);

Longer term the clean fix is core-side: have self_identity (or a small dedicated method) answer "is there an identity?" with Ok(false) for an absent wallet instead of an Err, so the frontend can tell "no" from "couldn't ask". Worth a follow-up issue either way, because as written the frontend genuinely cannot distinguish them.

2. app/src/components/settings/panels/AgentAccessPanel.tsx:394-413 — missed entry point: the tiny.place autopilot toggle is still shown to non-holders

Six doors are gated; this is the seventh. Settings → Agent access renders an "Autonomous tiny.place agent" section whose only condition is autopilotJobId — the presence of the seeded cron job. That job is backfilled at boot for every user (src/openhuman/cron/seed.rs:119-123, "boot seed — backfilling tinyplace_autopilot (disabled, opt-in)"), so autopilotJobId is non-null regardless of identity.

Net effect after this PR: a user with no tiny.place identity has the tab hidden, the route bounced and the Brain chip removed — and can still switch on an autonomous tiny.place agent from Settings for a feature they cannot open and that is being removed. (The panel isn't in this diff, which is presumably how it was missed.)

Suggested change:

// AgentAccessPanel.tsx
+ import { useTinyPlaceIdentity } from '../../../hooks/useTinyPlaceIdentity';
+ const { hasIdentity: hasTinyplaceIdentity } = useTinyPlaceIdentity();

- {autopilotJobId && (
+ {autopilotJobId && hasTinyplaceIdentity && (
    <SettingsSection title={t('settings.agentAccess.tinyplaceAutopilot.title')} >

Worth a test in the panel's spec mirroring useNavTabs.test.ts — the two-branch shape is already there to copy.

3. app/src/agentworld/AgentWorldShell.tsx:35-37 and app/src/pages/Brain.tsx:199-201 — a transient failure evicts a holder who is already on the surface, and the successful retry can't bring them back

The retry fixed the nav-tab half of the transient-failure problem, but not the routing half. Sequence for a real identity holder whose wallet is locked at startup (the motivating case in the hook's own doc):

  1. They open on /agent-world (bookmark, deep link, or restored window state). status === 'loading' → renders optimistically. Good.
  2. The RPC rejects → publish({ status: 'ready', hasIdentity: false }).
  3. status === 'ready' && !hasIdentity<Navigate to="/chat" replace />. They are now on /chat, and because it's replace the /agent-world history entry is gone.
  4. 2s later the retry succeeds → hasIdentity: true. The tab reappears in the nav; their location does not come back.

So the doc's claim (line 18-19) that "a one-time startup failure never locks a holder out until an app restart" holds for the nav entry point but not for where they actually were. The same applies to Brain.tsx:199 for ?tab=orchestration.

The fix follows from #1: make a transient error its own state, and let the route guards stay permissive on it while the nav stays fail-closed. Redirect only on a definitive answer.

// useTinyPlaceIdentity.ts
- status: 'loading' | 'ready';
+ /** `error` = we could not ask (transient). Gates that hide should hide;
+  *  gates that *evict* must not act on it. */
+ status: 'loading' | 'ready' | 'error';
...
-    publish({ status: 'ready', hasIdentity: false });   // in catch
+    publish({ status: 'error', hasIdentity: false });

AgentWorldShell and Brain then need no change at all — both already test status === 'ready', so 'error' falls through to rendering — and useNavTabs keeps hiding on hasIdentity === false. Add a case to AgentWorldShell.test.tsx for { status: 'error', hasIdentity: false } asserting no redirect.

💡 Refactor / suggestion

4. app/src/components/walkthrough/walkthroughSteps.ts:162-168 — onboarding step 10 points at a nav item that new users no longer have

Step 10 targets [data-walkthrough="tab-agent-world"]. The walkthrough runs immediately after onboarding (setWalkthroughPending() on wizard completion), and a freshly-onboarded user is by definition not an identity holder — so that node is absent for essentially every run of the tour. It doesn't hang: react-joyride logs Target not mounted, records a target_not_found failure, emits EVENTS.TARGET_NOT_FOUND and auto-advances (react-joyride/dist/index.mjs:1249-1259). But the step, and its walkthrough.steps.agentWorldTab.* copy in 14 locales, is now dead weight that also teaches a feature being removed.

Suggested change — make the step list identity-aware the same way the nav is:

// AppWalkthrough.tsx
- const steps = useMemo(() => createWalkthroughSteps(navigate, t), [navigate, t]);
+ const { hasIdentity } = useTinyPlaceIdentity();
+ const steps = useMemo(
+   () => createWalkthroughSteps(navigate, t).filter(
+     s => hasIdentity || s.target !== '[data-walkthrough="tab-agent-world"]'
+   ),
+   [navigate, t, hasIdentity]
+ );

5. app/src/agentworld/TinyPlaceSunsetNotice.tsx:23UpsellBanner truncates message to one line, and the removal date is at the end of the sentence

UpsellBanner renders the message as <p className="text-xs … truncate"> (components/upsell/UpsellBanner.tsx:68) — white-space: nowrap + ellipsis, inside a min-w-0 flex child. The English copy is ~90 chars and the payload (after 31 August 2026) is the last clause, so it's the first thing clipped; de/ru/pl/bn are longer still, and the Brain orchestration surface shares width with the sidebar. A sunset notice whose date can be ellipsed away isn't doing its job.

Suggested change — give the banner an opt-out and use it here:

// UpsellBanner.tsx
- <p className={`text-xs ${styles.text} truncate`}>{message}</p>
+ <p className={`text-xs ${styles.text} ${wrapMessage ? '' : 'truncate'}`}>{message}</p>

// TinyPlaceSunsetNotice.tsx
  <UpsellBanner variant="info"  rounded={false} dismissible={false}
+   wrapMessage

Also worth role="status" on the wrapper (line 19) so a non-dismissible notice is announced, and aria-hidden="true" on the banner's decorative icon.

6. app/src/hooks/useTinyPlaceIdentity.ts — the new gate has no debug logging

AGENTS.md is explicit: "Default to verbose diagnostics on new/changed flows… Log entry/exit, branches, external calls, retries/timeouts, state transitions, errors… Changes lacking logging are incomplete." The neighbours follow it — [brain] in Brain.tsx:105,158,163, [routes] orchestration-redirect: in AppRoutes.tsx:56,77. This hook has zero log lines, so "why is tiny.place hidden for this user?" is not answerable from a support log — and given #1, that's the question you'll actually get.

+ const LOG = '[tinyplace-gate]';
  const identity = await orchestrationClient.selfIdentity();
+ console.debug('%s identity resolved hasIdentity=%s', LOG, identity.agentId.trim().length > 0);
- } catch {
+ } catch (err) {
+   console.debug('%s identity check failed attempt=%d — hidden, retrying', LOG, attempt, err);

(Privacy-safe: log the boolean and the attempt number, never agentId.)

Nitpicks (6)

  • app/src/pages/Brain.tsx:256 — the orchestration chip gate is the only gate with no test: deleting the ...(hasTinyplaceIdentity ? […] : []) spread wouldn't fail anything (Brain.test.tsx covers the redirect, not the chip). One assertion on the nav items would close it.
  • app/src/agentworld/TinyPlaceSunsetNotice.test.tsx:26-30 — the "not dismissible" test is vacuous: UpsellBanner requires both dismissible and onDismiss to render the close button, and onDismiss is never passed, so the assertion passes even if dismissible={true}. Assert on the prop, or pass a dummy onDismiss in a control case.
  • app/src/hooks/useNavTabs.ts:18 — hides the tab while status === 'loading' too, so a holder gets a nav layout shift when the RPC settles, while the route guard is optimistic in the opposite direction. Consider hasIdentity || status === 'loading' for consistency (or state in the doc comment that the asymmetry is deliberate).
  • app/src/hooks/useTinyPlaceIdentity.ts:42retryTimer is module-scoped and never cleared when the last consumer unmounts, so a pending retry fires an RPC with nothing listening. Harmless in the desktop shell (the nav is always mounted) but it's a real leak in any other host.
  • app/src/agentworld/TinyPlaceSunsetNotice.tsx:10 — a deprecation notice built on components/upsell/UpsellBanner reads oddly; the variant: 'info' path also renders a warning-triangle icon (pre-existing in that component, not this PR).
  • app/src/hooks/useTinyPlaceIdentity.ts:60 — given #1, identity.agentId.trim().length > 0 is defensive-only; a comment saying so would stop the next reader from assuming the empty-string case is a live path.

Questions for the author (2)

  • app/src/hooks/useTinyPlaceIdentity.ts:60agentId is the wallet address (tinyplace/mod.rs:22-26: "the wallet key becomes the tiny.place identity"), so hasIdentity really means "a wallet exists and its Solana key is derivable", not "this user used tiny.place". self_identity also returns discoverable / card-published / key-published. Is a wallet holder who never published a tiny.place card intended to keep the tab — and conversely, is a tiny.place user whose wallet is merely locked meant to be treated as a non-holder until a retry lands?
  • Is a restartApp-less identity flip in scope? The hook's cache is process-lifetime and unkeyed by user. Desktop A→B restarts (CoreStateProvider.tsx:326-338), but that file explicitly handles the non-restart flip ("same-process session attach or web where restartApp is a no-op") by clearing the thread slice, and lib/composio/connectionCache.ts scopes its module cache by OPENHUMAN_ACTIVE_USER_ID for the same reason. If those paths matter, this cache wants the same treatment.

Verified / looks good

  • i18n, all 14 locales: tinyplaceSunset.{title,message,cta} present in every file (3/3 keys × 14), every value genuinely translated with no English leakage, Tiny Place used for the product label and tiny.place for the domain, no U+2014 em dashes. Nothing left behind.
  • Prior review points re-verified against current code, not the bot replies: the bounded backoff + refocus recheck exist and are wired (useTinyPlaceIdentity.ts:37,65-71,104-111); inFlight is set synchronously before the first await, so N hook consumers sharing one focus event cannot double-fire, and StrictMode's double effect is a no-op; the Brain guard is genuinely render-phase (Brain.tsx:199) and Brain.test.tsx proves non-mount with a render spy plus a /brain pathname probe.
  • Tests are not vacuous: reverting the useNavTabs filter, the AgentWorldShell guard, the Brain redirect or the notice each breaks a specific assertion. 48/48 pass locally across the seven affected files.
  • Fail-closed direction is sound: I could not find a path where an error or in-flight state resolves to visible for a user with no identity — hasIdentity starts false and only a successful non-empty agentId sets it true. The gaps above are all on the other side (a holder wrongly hidden or evicted), which is the right side to fail on.
  • Routes stay registered, so holders' deep links and the /orchestration/brain?tab=orchestration → settings tasks redirect chain all funnel through the one Brain gate rather than needing four.

// Transient: stay fail-closed (hidden) but schedule a bounded retry so a
// real holder is not locked out for the whole session by a startup blip.
publish({ status: 'ready', hasIdentity: false });
const delay = RETRY_DELAYS_MS[attempt];

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

⚠️ major — the "no identity" case is a rejection, not a resolved empty agentId, so resolved never latches for the majority of users.

Tracing the RPC core-side: orchestration_self_identityhandle_tinyplace_signal_key_status (src/openhuman/tinyplace/manifest.rs:3195) → global_state().client().await?tinyplace_signer_seed()secret_material(WalletChain::Solana)Err(WALLET_NOT_CONFIGURED_MESSAGE) when no wallet exists (src/openhuman/web3/wallet/ops.rs:747-754). require_signer (manifest.rs:2925-2932) likewise returns Err("no signer configured — unlock wallet…"). When a signer does exist, agent_id = signer.agent_id() is the wallet address — never empty.

So { agentId: '' } is effectively unreachable, and the "never set up tiny.place" population lands in this catch, which the hook classifies as transient. Because resolved stays false, the onFocus guard on line 105 never short-circuits: every window focus re-arms a fresh 4-call chain (t=0, +2s, +7s, +17s) for the whole session, each attempt costing a core-side config load + wallet-state file lock. That is the shape coreRpcClient.ts:253-259 already warns about — "an unbounded retry loop against an absent method produced ~9k Sentry events/day from a single client (#5157)" (not Sentry-visible here, but the same churn).

The module doc on line 12-13 and the PR body's "fetched once per session and cached" are therefore true only for holders.

Suggested change — bound total attempts session-wide, not per chain:

// before
async function attemptLoad(attempt: number) {
  if (resolved || inFlight) return;
  ...
    const delay = RETRY_DELAYS_MS[attempt];
    if (delay !== undefined && !retryTimer) { ... }

// after
const MAX_ATTEMPTS = RETRY_DELAYS_MS.length + 1; // session-wide
let attempts = 0;

async function attemptLoad(attempt: number) {
  if (resolved || inFlight || attempts >= MAX_ATTEMPTS) return;
  attempts += 1;
  ...
    const delay = RETRY_DELAYS_MS[attempt];
    if (delay !== undefined && !retryTimer && attempts < MAX_ATTEMPTS) { ... }
    else console.debug('[tinyplace-gate] unresolved after %d attempts — staying hidden', attempts);

The clean long-term fix is core-side: answer "is there an identity?" with Ok(false) for an absent wallet instead of Err, so the frontend can tell "no" from "couldn't ask". As written it genuinely cannot.

// in flight the surface renders optimistically so a holder never sees a flash.
const { status, hasIdentity } = useTinyPlaceIdentity();
if (status === 'ready' && !hasIdentity) {
return <Navigate to="/chat" replace />;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

⚠️ major — a transient failure evicts an identity holder who is already on this surface, and the successful retry cannot bring them back.

The backoff added in d4eb8ece fixed the nav-tab half of the transient-failure problem, not the routing half. For a real holder whose wallet is locked at startup (the motivating case in the hook's own doc comment):

  1. They open on /agent-world (bookmark, deep link, restored window). status === 'loading' → renders optimistically. ✅
  2. The RPC rejects → publish({ status: 'ready', hasIdentity: false }).
  3. This line fires <Navigate to="/chat" replace /> — and because it is replace, the /agent-world history entry is gone.
  4. ~2s later the retry succeeds → hasIdentity: true. The nav tab reappears; their location does not.

So useTinyPlaceIdentity.ts:18-19 ("a one-time startup failure never locks a holder out until an app restart") holds for the entry point but not for where the user actually was. Brain.tsx:199 has the same behaviour for ?tab=orchestration.

Suggested change — give a transient error its own state, so gates that hide still hide but gates that evict stay permissive:

// useTinyPlaceIdentity.ts
- status: 'loading' | 'ready';
+ /** `error` = we could not ask (transient). Hide on it; never evict on it. */
+ status: 'loading' | 'ready' | 'error';
...
-    publish({ status: 'ready', hasIdentity: false });   // in catch
+    publish({ status: 'error', hasIdentity: false });

Neither this file nor Brain.tsx then needs a change — both already test status === 'ready', so 'error' falls through to rendering — and useNavTabs keeps hiding on hasIdentity === false. Worth adding a { status: 'error', hasIdentity: false } case to AgentWorldShell.test.tsx asserting no redirect.

export function useNavTabs(): NavTab[] {
const { hasIdentity } = useTinyPlaceIdentity();
return useMemo(
() => NAV_TABS.filter(tab => tab.id !== 'agent-world' || hasIdentity),

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

⚠️ major — missed entry point: the tiny.place autopilot toggle in Settings is still shown to non-holders.

Six doors are gated by this PR (nav ×2, /agent-world/*, the Brain chip, ?tab=orchestration, and the legacy /orchestration + settings-tasks redirects that funnel into it). This is the seventh:

app/src/components/settings/panels/AgentAccessPanel.tsx:394-413 renders an "Autonomous tiny.place agent" section whose only condition is autopilotJobId — i.e. the seeded cron job exists. That job is backfilled at boot for every user (src/openhuman/cron/seed.rs:119-123"boot seed — backfilling tinyplace_autopilot (disabled, opt-in)"), so the condition is identity-independent.

Net effect after this PR: a user with no tiny.place identity has the tab hidden, the route bounced and the Brain chip removed — and can still switch on an autonomous tiny.place agent from Settings, for a surface they cannot open and that is being removed. (The panel isn't in this diff, which is presumably how it slipped.)

Suggested change:

// AgentAccessPanel.tsx
+ import { useTinyPlaceIdentity } from '../../../hooks/useTinyPlaceIdentity';
+ const { hasIdentity: hasTinyplaceIdentity } = useTinyPlaceIdentity();

- {autopilotJobId && (
+ {autopilotJobId && hasTinyplaceIdentity && (
    <SettingsSection title={t('settings.agentAccess.tinyplaceAutopilot.title')} >

Plus a two-branch test in the panel spec, mirroring useNavTabs.test.ts.

const companionActive = useAppSelector(selectCompanionSessionActive);

const tabs = useMemo(() => NAV_TABS.map(tab => ({ ...tab, label: t(tab.labelKey) })), [t]);
const navTabs = useNavTabs();

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

💡 suggestion — the onboarding walkthrough still points at this now-conditional tab.

components/walkthrough/walkthroughSteps.ts:162-168 (step 10) targets [data-walkthrough="tab-agent-world"]. The tour runs immediately after onboarding (setWalkthroughPending() on wizard completion), and a freshly-onboarded user is by definition not an identity holder — so that node is absent for essentially every run of the tour.

It doesn't hang: react-joyride logs Target not mounted, records a target_not_found failure, emits EVENTS.TARGET_NOT_FOUND and auto-advances (react-joyride/dist/index.mjs:1249-1259), and ACTIONS.PREV decrements correctly too. But the step — plus its walkthrough.steps.agentWorldTab.* copy in 14 locales — is now dead for most users, and it teaches a feature that is being removed.

Suggested change — make the step list identity-aware the same way the nav now is:

// AppWalkthrough.tsx
- const steps = useMemo(() => createWalkthroughSteps(navigate, t), [navigate, t]);
+ const { hasIdentity } = useTinyPlaceIdentity();
+ const steps = useMemo(
+   () => createWalkthroughSteps(navigate, t).filter(
+     s => hasIdentity || s.target !== '[data-walkthrough="tab-agent-world"]'
+   ),
+   [navigate, t, hasIdentity]
+ );

<UpsellBanner
variant="info"
title={t('tinyplaceSunset.title')}
message={t('tinyplaceSunset.message')}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

💡 suggestionUpsellBanner truncates message to a single line, and the removal date is the last clause.

UpsellBanner renders the message as <p className="text-xs … truncate"> (components/upsell/UpsellBanner.tsx:68) — white-space: nowrap + ellipsis, inside a min-w-0 flex child. The English copy is ~90 chars and ends with after 31 August 2026, so the payload is the first thing clipped; de/ru/pl/bn are longer, and the Brain orchestration surface shares width with the sidebar. A sunset notice whose date can be ellipsed away isn't doing its job.

Suggested change — add an opt-out and use it here:

// UpsellBanner.tsx
- <p className={`text-xs ${styles.text} truncate`}>{message}</p>
+ <p className={`text-xs ${styles.text} ${wrapMessage ? '' : 'truncate'}`}>{message}</p>

// TinyPlaceSunsetNotice.tsx
  <UpsellBanner variant="info"  rounded={false} dismissible={false}
+   wrapMessage

Also worth role="status" on the wrapper div (line 19) so a non-dismissible notice is announced, and aria-hidden="true" on the banner's decorative icon.

}

/** Kick a load if one isn't already settled, in flight, or scheduled. */
function ensureLoad() {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

💡 suggestion — the new gate ships with no debug logging.

AGENTS.md is explicit: "Default to verbose diagnostics on new/changed flows… Log entry/exit, branches, external calls, retries/timeouts, state transitions, errors… Changes lacking logging are incomplete." The neighbouring code follows it — [brain] in Brain.tsx:105,158,163, [routes] orchestration-redirect: in AppRoutes.tsx:56,77. This hook has zero log lines, so "why is tiny.place hidden for this user?" isn't answerable from a support log — and given the reject-vs-resolve issue flagged above, that is the question you will actually get.

+ const LOG = '[tinyplace-gate]';
  const identity = await orchestrationClient.selfIdentity();
+ console.debug('%s identity resolved hasIdentity=%s', LOG, identity.agentId.trim().length > 0);
- } catch {
+ } catch (err) {
+   console.debug('%s identity check failed attempt=%d — hidden, retrying', LOG, attempt, err);

Privacy-safe: the boolean and the attempt number only, never agentId.

Two smaller things on this same function while you're here:

  • retryTimer (line 42) is module-scoped and never cleared when the last consumer unmounts, so a pending retry fires an RPC with nothing listening. Harmless in the desktop shell (the nav is always mounted), but it is a real leak in any other host.
  • identity.agentId.trim().length > 0 (line 60) is defensive-only in practice — agent_id comes from signer.agent_id(), so an empty string is unreachable once a signer exists. A one-line comment saying so would stop the next reader assuming it's a live branch.

Comment thread app/src/pages/Brain.tsx
// TinyPlace multi-agent orchestration, folded back under Brain
// from the former top-level `/orchestration` tab. Hidden from
// users without a tiny.place identity (#5424).
...(hasTinyplaceIdentity

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

nitpick (test coverage) — this is the only gate in the PR with no test behind it.

Brain.test.tsx covers the ?tab=orchestration redirect and the loading window, but nothing asserts the chip itself is absent for a non-holder: delete this ...(hasTinyplaceIdentity ? […] : []) spread and the suite still passes. Since TwoPaneNav is already mocked in that spec, one assertion closes it:

it('hides the orchestration chip for a non-holder', async () => {
  tinyplaceIdentityRef.current = { status: 'ready', hasIdentity: false };
  graphExportMock.mockResolvedValue(makeGraph(0));
  await act(async () => { renderWithProviders(<Brain />, { initialEntries: ['/'] }); });
  expect(screen.queryByText('brain.tabs.orchestration')).not.toBeInTheDocument();
});

Related, and worth a decision rather than a fix: the chip (and useNavTabs) hide while status === 'loading', whereas the route guards render optimistically in that same window. A holder therefore gets a nav layout shift when the RPC settles. Either hasIdentity || status === 'loading' for symmetry, or say in the doc comment that the asymmetry is deliberate (hide-by-default for advertising, permissive for navigation) — right now the two read as an oversight.

it('is not dismissible — no dismiss control is rendered', () => {
render(<TinyPlaceSunsetNotice />);

expect(screen.queryByRole('button', { name: 'common.dismiss' })).toBeNull();

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

nitpick — this assertion is vacuous.

UpsellBanner renders the close button only when both dismissible and onDismiss are truthy (components/upsell/UpsellBanner.tsx:80). TinyPlaceSunsetNotice never passes onDismiss, so this query returns null even if dismissible were flipped to true — the test cannot fail for the reason it claims to check.

Either assert the prop directly, or make the control case real:

// prove the query would find a dismiss button when one exists,
// so its absence in TinyPlaceSunsetNotice means something
it('is not dismissible — no dismiss control is rendered', () => {
  render(<TinyPlaceSunsetNotice />);
  expect(screen.queryByRole('button', { name: 'common.dismiss' })).toBeNull();
  // control: the same query does find one when the banner is dismissible
  render(<UpsellBanner variant="info" title="t" message="m" dismissible onDismiss={() => {}} />);
  expect(screen.getByRole('button', { name: 'common.dismiss' })).toBeInTheDocument();
});

@senamakel
senamakel merged commit c644486 into tinyhumansai:main Aug 13, 2026
32 of 37 checks passed
senamakel pushed a commit to nocstah/openhuman that referenced this pull request Sep 11, 2026
…ct-name notice title\n\nAddress CodeRabbit review on tinyhumansai#5439.\n\n- Brain: redirect a confirmed non-holder away from ?tab=orchestration in the\n  render phase (return <Navigate to="/brain" replace />) instead of a\n  post-commit effect, so OrchestrationView — and its tiny.place RPCs — never\n  mount even once for a gated user. The in-flight 'loading' window and holders\n  are untouched. Adds Brain tests for the confirmed-non-holder redirect and the\n  loading window.\n- i18n: the sunset notice title is a UI product label, so use the product name\n  'Tiny Place' (matching nav.agentWorld / agentWorld.world.title) across all 14\n  locales; the message and CTA keep the 'tiny.place' domain spelling. CodeRabbit\n  flagged only bn/es/id — applied consistently to every locale, English source\n  included.\n
senamakel pushed a commit to nocstah/openhuman that referenced this pull request Sep 11, 2026
…brain\n\nAddress CodeRabbit follow-up on tinyhumansai#5439: queryByTestId only checks final DOM, so a\nregression that mounts OrchestrationView and redirects from an effect would still\npass. Add a hoisted render spy on OrchestrationView (assert not called) and a\nuseLocation pathname probe (assert final route is /brain) to the confirmed\nnon-holder test, alongside the existing welcome-tab assertion.\n
senamakel pushed a commit to nocstah/openhuman that referenced this pull request Sep 11, 2026
…ng comment\n\nAddress tinysweeper review on tinyhumansai#5439.\n\n- useNavTabs.test.ts: switch the controllable identity mock to vi.hoisted (the\n  repo convention, as in Brain.test.tsx). The prior module-scoped `let identity`\n  already passed (Vitest v4 evaluates the factory's `() => identity` lazily at\n  call time, not at hoist time), but the hoisted ref matches the established\n  pattern and removes any ambiguity.\n- Brain.tsx: the orchestration redirect comment overstated the invariant. A\n  *confirmed* non-holder never mounts OrchestrationView, but the in-flight\n  `loading` window renders optimistically — matching the AgentWorldShell route\n  guard so a holder never sees a flash. Reworded to describe that trade-off\n  accurately; behaviour is unchanged and intentional.\n
senamakel added a commit to nocstah/openhuman that referenced this pull request Sep 11, 2026
…inyplace-visibility-5424\n\nfeat: show tiny.place only to users with an identity\n
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agent Built-in agents, prompts, orchestration, and agent runtime in src/openhuman/agent/. feature Net-new user-facing capability or product behavior. priority: p3 Whenever. Cosmetic, a nicety, or a cleanup with no user visible effect.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Show tiny.place only to users who already set up an identity

3 participants