Settings split pass 5: extract Developer Options (SettingsView under 9.5k) - #151
Merged
Conversation
The Settings activity log was ~14 useState declarations, 6 mirror refs, two fetchers and four effects spread across two distant regions of SettingsView (the state at ~1018, the logic at ~2577). Every one of them was read by exactly one accordion, so the whole subsystem moves into lib/use-activity-log.ts rather than being drilled through props. The JSX is deliberately untouched: the hook's fields are renamed back to their old `activity*` names on destructure, so all four diff hunks land above the render (the first `return (` is at line 5094). That keeps this commit reviewable as "did the logic move intact?" without also having to diff 375 lines of markup. The follow-up commit moves the markup and drops the renames. Behaviour is preserved line-for-line, including the parts that look odd but are load-bearing: - `applyActivityQueryParams` still reads state directly for the manual load and refs for the poll — that split is why polling doesn't drift from the visible filter. - `pollActivityLog` keeps its empty dep array and the eslint-disable that explains it; the effect keys off a `logLoaded` boolean rather than the array so a successful prepend doesn't reset the interval. - Polling failures stay silent. Only user-initiated loads report, now via an `onLoadError` callback so the hook doesn't own the toast or its i18n key. One deliberate change: a local named `window` inside the params helper would have shadowed the global, so it is `activeWindow` here. The pre-existing `window` parameter on `timeWindowToSince` is left alone. SettingsView 10,951 -> 10,679. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The developer section was 914 lines of JSX rendering four independent sub-panels behind eight separate flags. It becomes DeveloperSection (a composition shell) plus AiDebugLogPanel, ActivityLogPanel, ActivityRowDetail and AiTimeoutsPanel. Two decisions here extend the contract in ./settings/README.md rather than quietly departing from it: **Sub-block flags move into the panel that owns them.** The contract says SettingsView is "the map" of what sections exist — that is about whether a *card* exists, and SettingsView still owns `flag.devopts.visible`. Which sub-blocks a card shows is the card's own business. Drilling the other eight down would have meant eight booleans whose names no reader could match back to a flag key. **State read by exactly one section moves with it.** ActivityLogPanel calls useActivityLog directly and so takes a single prop. `debugLogging` looked equally local but round-trips through the AI settings blob, so it stays in SettingsView and is passed down. The test is whether anything outside the section reads the state, not whether it feels local. Also moved, because both sides now need them: fmtDate / fmtRelativeTime / fmtDuration and the DateT / TimeT aliases to ./settings/format.ts, AiDebugLogRow to ./settings/types.ts, and validateAiTimeout + makeAiTimeoutBlurHandler into AiTimeoutsPanel (nothing else used them). SettingsView 10,679 -> 9,404. Across both commits in this pass: 10,951 -> 9,404, a 1,547-line reduction. Verification — a typecheck is not sufficient for this kind of move, so: - Rendered #developer with all eight dev-opts flags forced on, on origin/main and on this branch, and diffed the normalised outerHTML. Byte-identical at 265,719 chars, zero console errors on both sides. - Every useTranslations namespace and key in the new files resolved against locales/en.json. This caught three wrong namespaces (ai_debug_log vs ai_debug, activity_log.types vs activity_types, flag_presets vs presets) that typechecked perfectly happily — the hazard is now written into the README's verification section. - typecheck, lint, 441 unit tests, i18n parity at 4,763 keys. The i18n-text-literals baseline moves three entries from SettingsView.tsx to DeveloperSection.tsx. That is the same pre-existing untranslated text in a new file, not new debt — regenerated rather than marked `i18n-exempt`, so it stays on the books. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This was referenced Aug 2, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Pass 5 — Developer Options
SettingsView.tsx10,951 → 9,404 (−1,547). Two commits, each independently reviewable.The developer section was 914 lines of JSX rendering four independent sub-panels behind eight separate flags — bigger than earlier passes, and not splittable in the middle without leaving an incoherent state. So it lands as one section in two stages.
1.
refactor(settings): hoist the activity-log subsystem into a hookThe activity log was ~14 state values, 6 mirror refs, two fetchers and four effects spread across two distant regions of the file (state at ~1018, logic at ~2577). Every external reference was its own declaration or its own effects — nothing else read it — so it moves wholesale into
lib/use-activity-log.ts.The JSX is untouched in this commit. The hook's fields are renamed back to their old
activity*names on destructure, so all four diff hunks land above the firstreturn (at line 5094. That makes it reviewable as "did the logic move intact?" without also diffing 375 lines of markup.Behaviour is preserved including the parts that look odd but are load-bearing: the params helper still reads state for the manual load and refs for the poll (that split is why polling doesn't drift from the visible filter), the poll keeps its empty dep array, and the effect keys off a boolean rather than the array so a prepend doesn't reset the interval.
2.
refactor(settings): extract Developer Options into five componentsDeveloperSection(shell) +AiDebugLogPanel+ActivityLogPanel+ActivityRowDetail+AiTimeoutsPanel.Two decisions extend the contract in
app/components/settings/README.mdrather than quietly departing from it:flag.devopts.visible. Which sub-blocks a card shows is the card's business. Drilling the other eight down meant eight booleans whose names no reader could match back to a flag key.ActivityLogPanelcalls the hook directly and takes a single prop.debugLogginglooked equally local but round-trips through the AI settings blob, so it stays in SettingsView and is passed down. The test is whether anything outside the section reads it, not whether it feels local.Verification
A typecheck is not sufficient for this kind of move, so:
#developeronorigin/mainand on this branch with all eight dev-opts flags forced on, and diffed the normalisedouterHTML. Byte-identical at 265,719 chars, zero console errors on both sides.useTranslationsnamespace and key in the new files resolved againstlocales/en.json. This caught three wrong namespaces —ai_debug_logvsai_debug,activity_log.typesvsactivity_types,flag_presetsvspresets— all of which typechecked perfectly happily and would have thrown at render. That hazard is now written into the README's verification section, because it applies to every remaining pass.One thing to know
The
i18n-text-literalsbaseline moves three entries fromSettingsView.tsxtoDeveloperSection.tsx. That is the same pre-existing untranslated text in a new file, not new debt. The detector keys on path, so a moved string reads as new — I regenerated the baseline rather than adding ani18n-exemptmarker, so it stays recorded as debt instead of being silently blessed.Follow-ups (not in this PR)
activity*prefixes insideActivityLogPanelare now redundant. They survived so the markup is a verbatim copy, which is what makes the HTML diff meaningful. Dropping them is mechanical and noted in the file.import-history(1,342 lines) using the same two-stage shape, thenai-summaries,reset,wayback-import,backup,sync-status.Branched from
mainand targetsmaindirectly — no stacking, per #150.🤖 Generated with Claude Code