W15-A17: settings URL subtabs + 6 theme palettes - #217
Conversation
URL-addressable subtabs (8): #settings/general, /providers, /agents, /mcp, /printers, /environment, /updates, /about — clicks rewrite via replaceState, hashchange + popstate re-sync the subtab so deep links and back/forward navigate cleanly. Named theme palettes (6): default, cyberpunk, matrix, tron, industrial-forge, aurora-operator. Each palette ships all 13 --h3d-color-* vars; live-preview applies them immediately, Save persists to localStorage[h3d.theme.palette]. Backend /api/settings/themes is owned by A20 — palette is preview-only until it ships. ThemeProvider re-applies the named palette after every light/dark flip; the `default` id is a transparent no-op so light-mode users keep white backgrounds. WCAG AA verified for every palette: fg:bg >= 15.83 muted:bg >= 5.55 primary:bg >= 6.41 Tests: 21 new unit tests (8 hash routes + 13 palette guarantees) plus 6 Playwright preview screenshots under tests/visual/__snapshots__/. Sources: - WCAG 2.1 contrast: https://www.w3.org/WAI/WCAG21/Understanding/contrast-minimum.html - VS Code color customizations: https://code.visualstudio.com/docs/getstarted/themes Task: W15-A17-SETTINGS-THEMES-2026-05-10 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
📝 WalkthroughWalkthroughThis PR adds a named-palette color theming system for the UI and extends Settings navigation from 4 to 8 subtabs with URL-hash–driven routing. It includes six theme palettes (default, cyberpunk, matrix, tron, industrial-forge, aurora-operator), palette persistence/application logic, a palette picker in GeneralSubtab, comprehensive WCAG contrast validation, and visual/unit tests. ChangesPalette & Routing Unified Feature
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Code Review
This pull request introduces a named theme palette system and implements URL-hash-based routing for settings subtabs. Feedback focuses on the live preview mechanism in the General subtab, identifying issues where the 'default' palette fails to revert styles, previews desync during light/dark mode toggles, and previewed styles leak when navigating away. The reviewer suggested using the theme hook to coordinate base variables and adding a cleanup effect to ensure consistent behavior.
| import { | ||
| applyPalette, | ||
| DEFAULT_PALETTE_ID, | ||
| isNamedPaletteId, | ||
| NAMED_PALETTES, | ||
| PALETTE_STORAGE_KEY, | ||
| readStoredPaletteId, | ||
| writeStoredPaletteId, | ||
| type NamedPaletteId, | ||
| } from "../../theme/palettes"; |
There was a problem hiding this comment.
To correctly handle the live preview coordination and cleanup, you'll need to import useTheme and themeCssVars from the theme module. This allows the component to re-apply base variables when switching back to the 'default' palette or when the underlying light/dark mode changes.
| import { | |
| applyPalette, | |
| DEFAULT_PALETTE_ID, | |
| isNamedPaletteId, | |
| NAMED_PALETTES, | |
| PALETTE_STORAGE_KEY, | |
| readStoredPaletteId, | |
| writeStoredPaletteId, | |
| type NamedPaletteId, | |
| } from "../../theme/palettes"; | |
| import { | |
| applyPalette, | |
| DEFAULT_PALETTE_ID, | |
| isNamedPaletteId, | |
| NAMED_PALETTES, | |
| PALETTE_STORAGE_KEY, | |
| readStoredPaletteId, | |
| writeStoredPaletteId, | |
| useTheme, | |
| themeCssVars, | |
| type NamedPaletteId, | |
| } from "../../theme"; |
| useEffect(() => { | ||
| applyPalette(palette); | ||
| }, [palette]); |
There was a problem hiding this comment.
The current live preview implementation has three issues:
- Broken 'Default' Preview: Since
applyPalette(DEFAULT_PALETTE_ID)is a no-op for CSS variables, clicking 'Default' after a named palette won't actually revert the colors in the UI. - Theme Toggle Desync: If the user toggles light/dark mode while previewing a named palette, the preview is lost because
ThemeProviderre-applies the saved palette. - Leaky Preview: Navigating away from the Settings tab without saving leaves the previewed palette active on the entire application until a page reload.
Using useTheme to coordinate with the base theme and adding a cleanup effect solves these issues.
const { resolvedTheme } = useTheme();
// Live preview: every palette change re-applies the CSS variables.
// We coordinate with the base theme to ensure 'default' correctly reverts
// and that the preview survives light/dark mode toggles.
useEffect(() => {
if (palette === DEFAULT_PALETTE_ID) {
const vars = themeCssVars(resolvedTheme);
const root = document.documentElement;
for (const [name, value] of Object.entries(vars)) {
root.style.setProperty(name, value);
}
root.dataset.h3dPalette = DEFAULT_PALETTE_ID;
} else {
applyPalette(palette);
}
}, [palette, resolvedTheme]);
// Cleanup: restore the actually persisted palette when navigating away
// to prevent the preview from 'leaking' to the rest of the app.
useEffect(() => {
return () => {
applyPalette(readStoredPaletteId());
};
}, []);
| if (palette.id === DEFAULT_PALETTE_ID) { | ||
| // No-op for CSS vars — keep whatever ThemeProvider has set so | ||
| // light/dark mode and the default look stay consistent. | ||
| return palette.id; | ||
| } |
There was a problem hiding this comment.
The 'no-op' behavior for DEFAULT_PALETTE_ID is problematic when applyPalette is called for live previews (e.g., in GeneralSubtab). If a named palette was previously applied, its inline styles will persist because this branch doesn't clear or overwrite them. While this works within the applyTheme flow (which sets base variables first), it creates a bug for other callers. Consider documenting this requirement or allowing applyPalette to accept base variables for resetting.
There was a problem hiding this comment.
Actionable comments posted: 3
🧹 Nitpick comments (1)
03_implementation/ui/src/components/settings/SettingsPage.tsx (1)
71-84: 💤 Low valueDev check only verifies length, not key membership.
The mismatch check compares array lengths but won't catch if
SUBTABScontains a typo or different key thanSETTINGS_SUBTAB_KEYS. Consider validating key membership for stronger protection during development.♻️ Suggested improvement
if (__isDev && SUBTABS.length !== SETTINGS_SUBTAB_KEYS.length) { // eslint-disable-next-line no-console console.warn( `[SettingsPage] SUBTABS (${SUBTABS.length}) and SETTINGS_SUBTAB_KEYS (${SETTINGS_SUBTAB_KEYS.length}) length mismatch`, ); } +if (__isDev) { + const subtabKeys = new Set(SUBTABS.map((s) => s.key)); + const storeKeys = new Set(SETTINGS_SUBTAB_KEYS); + const missing = SETTINGS_SUBTAB_KEYS.filter((k) => !subtabKeys.has(k)); + if (missing.length > 0) { + // eslint-disable-next-line no-console + console.warn(`[SettingsPage] SUBTABS missing keys from SETTINGS_SUBTAB_KEYS: ${missing.join(", ")}`); + } +}🤖 Prompt for 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. In `@03_implementation/ui/src/components/settings/SettingsPage.tsx` around lines 71 - 84, The current dev-only guard only compares lengths; change it to also validate membership between SUBTABS and SETTINGS_SUBTAB_KEYS by computing the set difference both ways (items in SUBTABS not in SETTINGS_SUBTAB_KEYS and vice versa) and, if any differences exist, emit a console.warn that includes the offending keys; keep the existing __isDev guard and the descriptive message but augment it to list missing/extra keys to catch typos or mismatches for symbols SUBTABS and SETTINGS_SUBTAB_KEYS in SettingsPage.tsx.
🤖 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 `@03_implementation/ui/src/components/settings/GeneralSubtab.tsx`:
- Around line 148-150: Summary: Selecting the "default" palette currently is a
no-op because applyPalette("default") doesn't undo previously set CSS variables;
implement a proper reset path so the live preview truly reverts. Fix: update
applyPalette (and its consumer in GeneralSubtab.tsx where useEffect watches
palette) so that when palette === "default" it removes or clears any
palette-specific CSS custom properties (or reapplies the baseline/theme CSS)
instead of doing nothing; alternatively add a resetPalette/clearPalette helper
called from the useEffect when palette === "default". Reference symbols:
applyPalette, palette, GeneralSubtab.tsx useEffect (the effect block that
currently calls applyPalette(palette)).
In `@03_implementation/ui/src/theme/palettes/index.ts`:
- Around line 113-117: The early return when palette.id === DEFAULT_PALETTE_ID
leaves previously-applied inline CSS variables on the document, so update that
branch to remove/reset managed palette CSS vars before returning: when hitting
the DEFAULT_PALETTE_ID case call or implement a small helper (e.g.,
clearManagedCssVars or remove CSS vars by iterating
PALETTE_CSS_VARS/MANAGED_CSS_VARS and calling
document.documentElement.style.removeProperty(varName)) and then return
palette.id; reference DEFAULT_PALETTE_ID and palette.id to locate the
conditional and ensure all palette-specific CSS custom properties are cleared.
- Around line 70-84: The type guard is vulnerable to prototype properties
because NAMED_PALETTE_BY_ID is built as a plain object and isNamedPaletteId uses
the `in` operator; rebuild NAMED_PALETTE_BY_ID using a prototype-less object
(use Object.create(null) as the initial accumulator in the NAMED_PALETTES.reduce
that constructs NAMED_PALETTE_BY_ID) and change isNamedPaletteId to check
ownership with Object.prototype.hasOwnProperty.call(NAMED_PALETTE_BY_ID,
String(value)) (also ensure you coerce value to string before the hasOwnProperty
check) so only own keys qualify as NamedPaletteId.
---
Nitpick comments:
In `@03_implementation/ui/src/components/settings/SettingsPage.tsx`:
- Around line 71-84: The current dev-only guard only compares lengths; change it
to also validate membership between SUBTABS and SETTINGS_SUBTAB_KEYS by
computing the set difference both ways (items in SUBTABS not in
SETTINGS_SUBTAB_KEYS and vice versa) and, if any differences exist, emit a
console.warn that includes the offending keys; keep the existing __isDev guard
and the descriptive message but augment it to list missing/extra keys to catch
typos or mismatches for symbols SUBTABS and SETTINGS_SUBTAB_KEYS in
SettingsPage.tsx.
🪄 Autofix (Beta)
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: defaults
Review profile: CHILL
Plan: Pro
Run ID: 1674c250-90f1-4776-bf0b-3b00b342b169
⛔ Files ignored due to path filters (6)
03_implementation/ui/tests/visual/__snapshots__/palette-preview/aurora-operator.pngis excluded by!**/*.png03_implementation/ui/tests/visual/__snapshots__/palette-preview/cyberpunk.pngis excluded by!**/*.png03_implementation/ui/tests/visual/__snapshots__/palette-preview/default.pngis excluded by!**/*.png03_implementation/ui/tests/visual/__snapshots__/palette-preview/industrial-forge.pngis excluded by!**/*.png03_implementation/ui/tests/visual/__snapshots__/palette-preview/matrix.pngis excluded by!**/*.png03_implementation/ui/tests/visual/__snapshots__/palette-preview/tron.pngis excluded by!**/*.png
📒 Files selected for processing (17)
03_implementation/ui/src/app/store.ts03_implementation/ui/src/components/settings/GeneralSubtab.tsx03_implementation/ui/src/components/settings/SettingsPage.tsx03_implementation/ui/src/theme/ThemeProvider.tsx03_implementation/ui/src/theme/index.ts03_implementation/ui/src/theme/palettes/aurora-operator.ts03_implementation/ui/src/theme/palettes/cyberpunk.ts03_implementation/ui/src/theme/palettes/default.ts03_implementation/ui/src/theme/palettes/index.ts03_implementation/ui/src/theme/palettes/industrial-forge.ts03_implementation/ui/src/theme/palettes/matrix.ts03_implementation/ui/src/theme/palettes/tron.ts03_implementation/ui/src/theme/themeBootstrap.ts03_implementation/ui/tests/unit/SettingsPage.test.tsx03_implementation/ui/tests/unit/palettes.test.ts03_implementation/ui/tests/visual/palettes-preview.spec.ts03_implementation/ui/tests/visual/palettes.playwright.config.ts
| useEffect(() => { | ||
| applyPalette(palette); | ||
| }, [palette]); |
There was a problem hiding this comment.
Default palette does not actually revert live preview.
At Line 149, applyPalette("default") is a no-op, so previously stamped non-default CSS vars remain active. That means users can’t truly preview/reset to baseline by selecting default without a reload/theme reapply.
Proposed fix
+import { themeCssVars } from "../../theme/tokens";
...
useEffect(() => {
- applyPalette(palette);
+ if (palette === DEFAULT_PALETTE_ID) {
+ const resolved = document.documentElement.dataset.h3dTheme === "light" ? "light" : "dark";
+ for (const [k, v] of Object.entries(themeCssVars(resolved))) {
+ document.documentElement.style.setProperty(k, v);
+ }
+ document.documentElement.dataset.h3dPalette = DEFAULT_PALETTE_ID;
+ return;
+ }
+ applyPalette(palette);
}, [palette]);🤖 Prompt for 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.
In `@03_implementation/ui/src/components/settings/GeneralSubtab.tsx` around lines
148 - 150, Summary: Selecting the "default" palette currently is a no-op because
applyPalette("default") doesn't undo previously set CSS variables; implement a
proper reset path so the live preview truly reverts. Fix: update applyPalette
(and its consumer in GeneralSubtab.tsx where useEffect watches palette) so that
when palette === "default" it removes or clears any palette-specific CSS custom
properties (or reapplies the baseline/theme CSS) instead of doing nothing;
alternatively add a resetPalette/clearPalette helper called from the useEffect
when palette === "default". Reference symbols: applyPalette, palette,
GeneralSubtab.tsx useEffect (the effect block that currently calls
applyPalette(palette)).
| export const NAMED_PALETTE_BY_ID: Record<NamedPaletteId, NamedPalette> = Object.freeze( | ||
| NAMED_PALETTES.reduce( | ||
| (acc, p) => { | ||
| acc[p.id] = p; | ||
| return acc; | ||
| }, | ||
| {} as Record<NamedPaletteId, NamedPalette>, | ||
| ), | ||
| ); | ||
|
|
||
| export const DEFAULT_PALETTE_ID: NamedPaletteId = "default"; | ||
|
|
||
| export function isNamedPaletteId(value: unknown): value is NamedPaletteId { | ||
| return typeof value === "string" && value in NAMED_PALETTE_BY_ID; | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
cat -n 03_implementation/ui/src/theme/palettes/index.ts | head -100Repository: Ghenghis/Hermes3D
Length of output: 4396
🏁 Script executed:
cat -n 03_implementation/ui/src/theme/palettes/index.ts | sed -n '100,140p'Repository: Ghenghis/Hermes3D
Length of output: 2131
🏁 Script executed:
rg "readStoredPaletteId|writeStoredPaletteId|applyPalette" --type ts --type tsxRepository: Ghenghis/Hermes3D
Length of output: 89
🏁 Script executed:
rg "readStoredPaletteId|writeStoredPaletteId|applyPalette" -t tsRepository: Ghenghis/Hermes3D
Length of output: 3507
🏁 Script executed:
cat -n 03_implementation/ui/tests/unit/palettes.test.tsRepository: Ghenghis/Hermes3D
Length of output: 6491
🏁 Script executed:
python3 << 'EOF'
# Test the prototype-chain vulnerability
obj = {}
print(f"'__proto__' in obj: {'__proto__' in obj}")
print(f"'constructor' in obj: {'constructor' in obj}")
print(f"obj.hasOwnProperty('__proto__'): {hasattr(obj, '__proto__')}")
# Simulate what happens in JavaScript
import json
dict_obj = {}
print(f"\nIn Python dict: '__proto__' in dict_obj = {'__proto__' in dict_obj}")
# The concern is about JavaScript's prototype chain
print("\nIn JavaScript, a plain object {} has inherited properties from Object.prototype:")
print("- '__proto__' would be inherited")
print("- 'constructor' would be inherited")
print("- 'toString' would be inherited")
print("\nUsing 'in' operator checks both own and inherited properties")
print("Using hasOwnProperty checks only own properties")
EOFRepository: Ghenghis/Hermes3D
Length of output: 495
🏁 Script executed:
cat -n 03_implementation/ui/src/theme/palettes/index.ts | sed -n '82,84p'Repository: Ghenghis/Hermes3D
Length of output: 228
Harden palette-id validation to prevent prototype-chain properties from passing the type guard.
Line 83 uses value in NAMED_PALETTE_BY_ID, which on a plain object accepts inherited prototype properties like __proto__ and constructor. Although applyPalette has a fallback that prevents runtime crashes, the type guard itself becomes unreliable—isNamedPaletteId("__proto__") would return true despite "__proto__" not being a valid NamedPaletteId. This violates the type guard contract and could cause type-safety issues if the fallback is removed or other code relies on the guard's accuracy.
Use Object.create(null) to eliminate the prototype chain, and Object.prototype.hasOwnProperty.call() to check only own properties:
Suggested fix
-export const NAMED_PALETTE_BY_ID: Record<NamedPaletteId, NamedPalette> = Object.freeze(
+export const NAMED_PALETTE_BY_ID: Readonly<Record<NamedPaletteId, NamedPalette>> = Object.freeze(
NAMED_PALETTES.reduce(
(acc, p) => {
acc[p.id] = p;
return acc;
},
- {} as Record<NamedPaletteId, NamedPalette>,
+ Object.create(null) as Record<NamedPaletteId, NamedPalette>,
),
);
@@
export function isNamedPaletteId(value: unknown): value is NamedPaletteId {
- return typeof value === "string" && value in NAMED_PALETTE_BY_ID;
+ return (
+ typeof value === "string" &&
+ Object.prototype.hasOwnProperty.call(NAMED_PALETTE_BY_ID, value)
+ );
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| export const NAMED_PALETTE_BY_ID: Record<NamedPaletteId, NamedPalette> = Object.freeze( | |
| NAMED_PALETTES.reduce( | |
| (acc, p) => { | |
| acc[p.id] = p; | |
| return acc; | |
| }, | |
| {} as Record<NamedPaletteId, NamedPalette>, | |
| ), | |
| ); | |
| export const DEFAULT_PALETTE_ID: NamedPaletteId = "default"; | |
| export function isNamedPaletteId(value: unknown): value is NamedPaletteId { | |
| return typeof value === "string" && value in NAMED_PALETTE_BY_ID; | |
| } | |
| export const NAMED_PALETTE_BY_ID: Readonly<Record<NamedPaletteId, NamedPalette>> = Object.freeze( | |
| NAMED_PALETTES.reduce( | |
| (acc, p) => { | |
| acc[p.id] = p; | |
| return acc; | |
| }, | |
| Object.create(null) as Record<NamedPaletteId, NamedPalette>, | |
| ), | |
| ); | |
| export const DEFAULT_PALETTE_ID: NamedPaletteId = "default"; | |
| export function isNamedPaletteId(value: unknown): value is NamedPaletteId { | |
| return ( | |
| typeof value === "string" && | |
| Object.prototype.hasOwnProperty.call(NAMED_PALETTE_BY_ID, value) | |
| ); | |
| } |
🤖 Prompt for 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.
In `@03_implementation/ui/src/theme/palettes/index.ts` around lines 70 - 84, The
type guard is vulnerable to prototype properties because NAMED_PALETTE_BY_ID is
built as a plain object and isNamedPaletteId uses the `in` operator; rebuild
NAMED_PALETTE_BY_ID using a prototype-less object (use Object.create(null) as
the initial accumulator in the NAMED_PALETTES.reduce that constructs
NAMED_PALETTE_BY_ID) and change isNamedPaletteId to check ownership with
Object.prototype.hasOwnProperty.call(NAMED_PALETTE_BY_ID, String(value)) (also
ensure you coerce value to string before the hasOwnProperty check) so only own
keys qualify as NamedPaletteId.
| if (palette.id === DEFAULT_PALETTE_ID) { | ||
| // No-op for CSS vars — keep whatever ThemeProvider has set so | ||
| // light/dark mode and the default look stay consistent. | ||
| return palette.id; | ||
| } |
There was a problem hiding this comment.
Reset managed CSS vars when switching back to default.
On Line 113, default returns early without removing previously applied non-default inline overrides. If a user previews a palette and then picks default, the old palette vars can remain active until another base-theme write happens.
Suggested fix
root.dataset.h3dPalette = palette.id;
if (palette.id === DEFAULT_PALETTE_ID) {
- // No-op for CSS vars — keep whatever ThemeProvider has set so
- // light/dark mode and the default look stay consistent.
+ // Remove palette overrides so base ThemeProvider vars are visible again.
+ for (const name of Object.keys(defaultPalette.palette.cssVars)) {
+ root.style.removeProperty(name);
+ }
return palette.id;
}🤖 Prompt for 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.
In `@03_implementation/ui/src/theme/palettes/index.ts` around lines 113 - 117, The
early return when palette.id === DEFAULT_PALETTE_ID leaves previously-applied
inline CSS variables on the document, so update that branch to remove/reset
managed palette CSS vars before returning: when hitting the DEFAULT_PALETTE_ID
case call or implement a small helper (e.g., clearManagedCssVars or remove CSS
vars by iterating PALETTE_CSS_VARS/MANAGED_CSS_VARS and calling
document.documentElement.style.removeProperty(varName)) and then return
palette.id; reference DEFAULT_PALETTE_ID and palette.id to locate the
conditional and ensure all palette-specific CSS custom properties are cleared.
Summary
#settings/<sub>) with clean replaceState + hashchange round-triph3d.theme.paletteapplyPalette()intheme/palettes/; ThemeProvider re-applies after every light/dark flip;defaultis a transparent no-op so light mode keeps white backgroundsSubtabs verified
#settings/general,/providers,/agents,/mcp,/printers,/environment,/updates,/abouttests/unit/SettingsPage.test.tsx(it.each block + 5 additional behaviour tests)Palette WCAG AA contrast (all PASS)
Threshold: body text (
fg,muted) >= 4.5; UI/large (primary) >= 3.0. Verified at compile time viatests/unit/palettes.test.ts.Self-audit
npx tsc --noEmitPASS (0 errors)npm run buildPASS (vite production build, 1.36s)npx vitest runPASS — 139 tests / 4 skipped (was 118 / 4; +21 new)npx playwright test --config tests/visual/palettes.playwright.config.tsPASS — 6/6 palette screenshots written totests/visual/__snapshots__/palette-preview/Coordination
store.tschange is additive (newSETTINGS_SUBTAB_KEYS,settingsSubtabFromHash, genericsubtabFromHash). A18 (Voice) can addvoiceSubtabFromHashnext to it without conflict.theme/palettes/is a new directory; ThemeProvider edit is a single re-apply call. No file overlaps with A18 / A19 / A11-A16 lanes perhermes_list_locks.Honest fallback
Backend
/api/settings/themes(A20 lane) does not exist yet. Palette is preview-only: changes persist vialocalStorageand survive reload viathemeBootstrap. Banner text in the Settings UI states this explicitly.Sources cited
workbench.colorCustomizations)Hermes lock
claude-w15-a17-settingsW15-A17-SETTINGS-THEMES-2026-05-10Test plan
#settings/<sub>URL on the running GUI; the matching subtab renders.Co-Authored-By: Claude Opus 4.7 (1M context) noreply@anthropic.com
Summary by CodeRabbit
Release Notes
New Features
Tests