diff --git a/frontend/src/app/Main.tsx b/frontend/src/app/Main.tsx index 43fed04a..74910bfc 100644 --- a/frontend/src/app/Main.tsx +++ b/frontend/src/app/Main.tsx @@ -29,6 +29,7 @@ import { setPanelMode, disableOnboardingAfterCrash } from '@/shared/state/onboar const Analytics = React.lazy(() => import('./pages/Analytics/Analytics')); const OnboardingV3Root = React.lazy(() => import('./components/OnboardingV3/OnboardingV3Root')); +const SignInRequiredGate = React.lazy(() => import('./components/overlays/SignInRequiredGate')); const OnboardingRoot = React.lazy(() => import('./components/Onboarding').then((m) => ({ default: m.OnboardingRoot })), ); @@ -547,6 +548,9 @@ const ThemedApp: React.FC = () => { + + + diff --git a/frontend/src/app/components/overlays/SignInDialog.tsx b/frontend/src/app/components/overlays/SignInDialog.tsx index 55a2498e..bfdf5eb4 100644 --- a/frontend/src/app/components/overlays/SignInDialog.tsx +++ b/frontend/src/app/components/overlays/SignInDialog.tsx @@ -24,7 +24,7 @@ type Stage = 'choose' | 'email_form' | 'code_form'; const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/; -export default function SignInDialog({ onClose, initialStage = 'choose' }: { onClose: () => void; initialStage?: Stage }): JSX.Element { +export default function SignInDialog({ onClose, initialStage = 'choose', mandatory = false }: { onClose: () => void; initialStage?: Stage; mandatory?: boolean }): JSX.Element { const tokens = useClaudeTokens(); const dispatch = useAppDispatch(); const proxyUrl = useAppSelector( @@ -156,7 +156,9 @@ export default function SignInDialog({ onClose, initialStage = 'choose' }: { onC return ( - - - + {!mandatory && ( + + + + )} {stage === 'code_form' ? ( <> s.settings.loaded); + const userId = useAppSelector((s) => s.settings.data.user_id ?? null); + const onboardingActive = useAppSelector((s) => s.onboardingV3.flowActive); + + if (!shouldRequireSignIn({ settingsLoaded, userId, onboardingActive })) return null; + + return undefined} />; +} diff --git a/frontend/src/app/components/overlays/shouldRequireSignIn.test.ts b/frontend/src/app/components/overlays/shouldRequireSignIn.test.ts new file mode 100644 index 00000000..8248dc53 --- /dev/null +++ b/frontend/src/app/components/overlays/shouldRequireSignIn.test.ts @@ -0,0 +1,35 @@ +// Run: node --test frontend/src/app/components/overlays/shouldRequireSignIn.test.ts +import { test } from 'node:test'; +import assert from 'node:assert/strict'; +import { shouldRequireSignIn } from './shouldRequireSignIn.ts'; + +const base = { settingsLoaded: true, userId: null as string | null, onboardingActive: false }; + +test('the veteran this exists for: settings loaded, no account, no onboarding', () => { + assert.equal(shouldRequireSignIn(base), true); +}); + +test('a signed-in user is never walled', () => { + assert.equal(shouldRequireSignIn({ ...base, userId: 'u-1' }), false); +}); + +test('nothing shows until settings are read, so a launch does not flash a login wall', () => { + assert.equal(shouldRequireSignIn({ ...base, settingsLoaded: false }), false); +}); + +test('a backend that never answers leaves the app usable rather than bricked', () => { + // settingsLoaded stays false forever in that case; the wall must stay down, not go up. + assert.equal(shouldRequireSignIn({ settingsLoaded: false, userId: null, onboardingActive: false }), false); +}); + +test('onboarding owns the screen, so the gate stands down while its own sign-in beat runs', () => { + assert.equal(shouldRequireSignIn({ ...base, onboardingActive: true }), false); +}); + +test('a fresh install that finishes onboarding signed in stays down afterwards', () => { + assert.equal(shouldRequireSignIn({ settingsLoaded: true, userId: 'u-2', onboardingActive: false }), false); +}); + +test('an empty-string user id is not an account', () => { + assert.equal(shouldRequireSignIn({ ...base, userId: '' }), true); +}); diff --git a/frontend/src/app/components/overlays/shouldRequireSignIn.ts b/frontend/src/app/components/overlays/shouldRequireSignIn.ts new file mode 100644 index 00000000..bdf28451 --- /dev/null +++ b/frontend/src/app/components/overlays/shouldRequireSignIn.ts @@ -0,0 +1,18 @@ +// Whether the mandatory sign-in wall should be up right now. +// +// Split out of the component so the branch that can lock every user out of the app is testable +// without a React harness. See SignInRequiredGate.tsx for why the wall exists at all. +export interface SignInGateState { + settingsLoaded: boolean; + userId: string | null; + onboardingActive: boolean; +} + +export function shouldRequireSignIn({ settingsLoaded, userId, onboardingActive }: SignInGateState): boolean { + // Fails OPEN until settings are read: a backend that never answers must not brick a local-first + // app behind a wall the user's own saved account would have taken down. + if (!settingsLoaded) return false; + // Onboarding carries its own sign-in beat and its own curtain; stacking a second one hides both. + if (onboardingActive) return false; + return !userId; +} diff --git a/linter/config/config.json b/linter/config/config.json index c15653e0..301b0886 100644 --- a/linter/config/config.json +++ b/linter/config/config.json @@ -20,7 +20,7 @@ "eslint-knip": "Node tooling deferred to a later pass.", "classes": "Placeholder check, not wired up. endpoints: orphaned-endpoint triage deferred.", "max-file-lines-exceptions": "Grandfather list of pre-existing >300-line files (existing debt, not new). Paths updated after the folder-tree restructure moved several of them. The two manager/prompt/* entries are from the agent_manager decomposition: prompt_context.py aggregates the system-prompt context builders and attachments.py is one cohesive 230-line attachment resolver; both are single-responsibility and a few lines over, not splittable without an artificial seam.", - "max-folder-items-exceptions": "Exact-path allow for folders intentionally over the cap. The rule trips at >7 (7 items is fine, the 8th tips it), so only genuinely 8+ folders are listed. backend/ and backend/apps are FastAPI feature-package registries (each child is an app mounted in main.py); agents/ aggregates agent subsystems; agents/manager/ is the agent_manager god-object decomposition (cohesive AgentManager mixins + standalone run helpers + the streaming/permissions/prompt/session subtrees), conventionally flat like agents/ and core/ since its standalone helpers are heterogeneous and don't group cleanly; agents/manager/streaming and agents/manager/session are flat peer collections of one-module-per-concern handlers; core/, tools_lib/, tests/ are conventionally flat. Frontend: app/pages is the page registry, AgentChat/ChatInput/Settings-sections/Onboarding are organizational parents, and shared/state (Redux slices) plus hooks/steps/mcp-cards/Views are flat peer collections. scripts/, electron/, linter/checks/ are flat tool dirs. These replaced blanket .lintignore-max-folder-items sentinels (backend, frontend, scripts, electron, linter/checks) so the rule still catches NEW unplanned bloat everywhere else. Kept as whole-subtree sentinels on purpose: debugger/ (self-contained injected sub-tool with its own Vite GUI), webapp_template (Vite scaffold payload), and vendored mcp-bundles. 2026-07 desktop-shell additions: Dashboard canvas/cards/desktop + hooks/interaction + hooks/lifecycle, AgentChat bubbles/tool-ui, and shared/styles are flat peer collections (one component or hook per concern) that crossed 7 as the redesign surface grew. frontend/src/toolui carries a whole-subtree .lintignore: vendored tool-ui component library (pierre), same treatment as mcp-bundles. openswarm-edge/app is the edge's flat one-module-per-concern set (routing, bundles, inject, ratelimit, sandbox, and the vendored code_safety gate); it crossed 7 when the sandbox's static gate was split out to mirror the desktop file byte for byte. AgentChat/parsing joined when the narration/deliverable classifier landed: it is the same flat one-module-per-parser collection as the rest of that subtree. 2026-08-03 browser merge: agents/browser is the flat one-module-per-concern browser tier (40 modules) that arrived whole from eric/browser-merged; .github/workflows crossed 7 when the packaged-smoke and intel-verify workflows landed; frontend/ is a package root, not a code folder.", + "max-folder-items-exceptions": "Exact-path allow for folders intentionally over the cap. The rule trips at >7 (7 items is fine, the 8th tips it), so only genuinely 8+ folders are listed. backend/ and backend/apps are FastAPI feature-package registries (each child is an app mounted in main.py); agents/ aggregates agent subsystems; agents/manager/ is the agent_manager god-object decomposition (cohesive AgentManager mixins + standalone run helpers + the streaming/permissions/prompt/session subtrees), conventionally flat like agents/ and core/ since its standalone helpers are heterogeneous and don't group cleanly; agents/manager/streaming and agents/manager/session are flat peer collections of one-module-per-concern handlers; core/, tools_lib/, tests/ are conventionally flat. Frontend: app/pages is the page registry, AgentChat/ChatInput/Settings-sections/Onboarding are organizational parents, and shared/state (Redux slices) plus hooks/steps/mcp-cards/Views are flat peer collections. scripts/, electron/, linter/checks/ are flat tool dirs. These replaced blanket .lintignore-max-folder-items sentinels (backend, frontend, scripts, electron, linter/checks) so the rule still catches NEW unplanned bloat everywhere else. Kept as whole-subtree sentinels on purpose: debugger/ (self-contained injected sub-tool with its own Vite GUI), webapp_template (Vite scaffold payload), and vendored mcp-bundles. 2026-07 desktop-shell additions: Dashboard canvas/cards/desktop + hooks/interaction + hooks/lifecycle, AgentChat bubbles/tool-ui, and shared/styles are flat peer collections (one component or hook per concern) that crossed 7 as the redesign surface grew. frontend/src/toolui carries a whole-subtree .lintignore: vendored tool-ui component library (pierre), same treatment as mcp-bundles. openswarm-edge/app is the edge's flat one-module-per-concern set (routing, bundles, inject, ratelimit, sandbox, and the vendored code_safety gate); it crossed 7 when the sandbox's static gate was split out to mirror the desktop file byte for byte. AgentChat/parsing joined when the narration/deliverable classifier landed: it is the same flat one-module-per-parser collection as the rest of that subtree. 2026-08-03 browser merge: agents/browser is the flat one-module-per-concern browser tier (40 modules) that arrived whole from eric/browser-merged; .github/workflows crossed 7 when the packaged-smoke and intel-verify workflows landed; frontend/ is a package root, not a code folder. components/overlays is the flat one-component-per-overlay collection; it crossed 7 when the mandatory sign-in gate landed as component + pure predicate + its test.", "import-cycles": "Flags RUNTIME circular imports only (SCC>1). Skips type-only imports (import type / export type) and dynamic import() since neither runs at module init, which is why the idiomatic Redux store<->hooks type cycle is not flagged. Frontend alias resolution comes from import-cycle-aliases. Zero cycles today; the check keeps it that way.", "ruff + pyright": "Ported from Haik's linter (haik/feat/ingest). ruff is narrowed to F401/F811/F841 (unused imports/redefs/locals) and intentionally DROPS Haik's ARG001/ARG002 (unused args): our SDK-callback signatures require unused params (can_use_tool/pre_tool_hook take a `context` they don't use) and we ban the `_unused` prefix, so ARG is noise here. pyright runs Haik's existence-only config (typeCheckingMode off) with reportAttributeAccessIssue ENABLED: the AgentManager behavior classes now inherit a typing-only AgentManagerProtocol base (manager/AgentManagerProtocol.py) that declares the composed __init__ state + cross-class methods, so the checker sees self.sessions etc. from inside a mixin. pyright caught real bugs: a dangling `_conns` ref + TWO broken lazy imports (`_load_all`/`_load` from outputs.py, renamed to load_all/load in workspace_io but the import sites weren't updated — App Builder workspace seeding/name-sync was silently failing in a try/except). The one grandfathered SURFACE file (handle_assistant_message) is the SDK-optional try/except-import boundary (TextBlock=object fallback defeats isinstance narrowing). Both grandfather pre-existing debt by file; the refactor surface is clean. Requires `ruff` + `pyright` on PATH (added to requirements-dev.txt); pyright's config expects the venv at backend/.venv.", "no-underscore-names + p-private": "Convention checks ported verbatim from Haik's linter (haik/feat/ingest): no-underscore-names bans leading-underscore names (a dead-code-tooling blind spot; use p_ for private), p-private enforces that p_-prefixed names are accessed only inside their owning file/class (cross-file/class use means the name should be public). Backend Python only. The exception lists grandfather pre-existing debt that landed with the workflows/analytics forward-ports (eric's 'don't mass-migrate untouched files' rule); the agent_manager refactor surface is clean. NOTE: Haik's full linter (his branch also adds pyright + ruff and runs a different enabled set) should eventually supersede this; these two were lifted to enforce the p_ conventions on eric/dev now. browser_cookies.py and its Windows round-trip test are excepted for `_fields_` only: a ctypes.Structure protocol name required by the ctypes metaclass, not our naming.", @@ -163,7 +163,8 @@ "backend/apps/onboarding/usage/browser_cookies.py", "backend/tests/test_browser_agent_loop.py", "backend/tests/test_browser_skills.py", - "frontend/src/shared/state/settingsSlice.ts" + "frontend/src/shared/state/settingsSlice.ts", + "frontend/src/app/components/overlays/SignInDialog.tsx" ], "max-folder-items": [ "backend", @@ -208,7 +209,8 @@ "backend/apps/workflows/cloud", "backend/apps/agents/browser", ".github/workflows", - "frontend" + "frontend", + "frontend/src/app/components/overlays" ], "no-nested-imports": [], "import-cycles": [],