From 5948332a87d378917d526ae79287aa21043f0d81 Mon Sep 17 00:00:00 2001 From: pifreak Date: Sat, 12 Sep 2026 21:32:20 +0000 Subject: [PATCH] Fix JAML default filter crash, validate before search, make JAML view usable on phones The shipped default filter used two keys the JAML grammar does not have (spectral, mixedJoker). motely-wasm 26 raises an opaque NativeAOT exception on unknown keys instead of a parse error, so the first Start Search on a fresh install died every time. Fix the keys and gate the search on jaml-lang diagnostics: unknown keys now disable Start and show the line. WebMCP publish re-ran once per registered tool and aborted the previous pass, logging an AbortError for every tool on boot. Batch registration notifies once and a superseded publish exits quietly. In JAML mode the navbar was an empty column (desktop) or a full-width void above the editor (phone). Collapse it there and move the view switcher into the header. Seed-queue arrow hotkeys and header controls no longer act while authoring JAML, and loading a seed list no longer yanks the view back to Blueprint. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01RkbxDfehyheD9RSpWTSDQF --- ___tests___/jamlDefault.spec.ts | 20 ++++++ package-lock.json | 1 + package.json | 1 + src/components/SeedQueue.tsx | 13 ++-- .../blueprint/jamlView/JamlView.tsx | 15 +++- src/components/blueprint/layout/header.tsx | 18 +++-- src/components/blueprint/layout/navbar.tsx | 72 ++++++++++--------- .../blueprint/standardView/index.tsx | 11 +-- src/modules/state/jamlSearchContext.tsx | 6 +- src/modules/state/store.ts | 3 - src/modules/webmcp/register.ts | 2 + src/modules/webmcp/registry.ts | 10 ++- 12 files changed, 115 insertions(+), 57 deletions(-) create mode 100644 ___tests___/jamlDefault.spec.ts diff --git a/___tests___/jamlDefault.spec.ts b/___tests___/jamlDefault.spec.ts new file mode 100644 index 0000000..25548fb --- /dev/null +++ b/___tests___/jamlDefault.spec.ts @@ -0,0 +1,20 @@ +import { describe, expect, it } from "vitest"; +import { Severity, getDiagnostics } from "jaml-lang"; +import { DEFAULT_JAML } from "../src/modules/state/jamlSearchContext"; + +const errors = (jaml: string) => getDiagnostics(jaml).filter((d) => d.severity === Severity.Error); + +describe("DEFAULT_JAML", () => { + it("has no diagnostics", () => { + expect(errors(DEFAULT_JAML)).toEqual([]); + }); + + it("declares nine should clauses", () => { + expect(DEFAULT_JAML.split("should:")[1].match(/^ - /gm)).toHaveLength(9); + }); + + it("the validator rejects the keys that crashed motely-wasm", () => { + const broken = DEFAULT_JAML.replace("spectralCard:", "spectral:").replace("joker: Any", "mixedJoker: Any"); + expect(errors(broken).map((d) => d.message)).toEqual(["Unknown key 'spectral'.", "Unknown key 'mixedJoker'."]); + }); +}); diff --git a/package-lock.json b/package-lock.json index ac0dcfd..9cdee20 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,6 +19,7 @@ "@tanstack/react-query": "^5.85.9", "embla-carousel-react": "^8.6.0", "immer": "^10.1.3", + "jaml-lang": "^3.14.2", "jaml-ui": "^4.6.0", "js-convert-case": "^4.2.0", "js-yaml": "^4.1.1", diff --git a/package.json b/package.json index 5095d3b..e662200 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,7 @@ "@tanstack/react-query": "^5.85.9", "embla-carousel-react": "^8.6.0", "immer": "^10.1.3", + "jaml-lang": "^3.14.2", "jaml-ui": "^4.6.0", "js-convert-case": "^4.2.0", "js-yaml": "^4.1.1", diff --git a/src/components/SeedQueue.tsx b/src/components/SeedQueue.tsx index 1536000..7e32a3e 100644 --- a/src/components/SeedQueue.tsx +++ b/src/components/SeedQueue.tsx @@ -11,14 +11,19 @@ export function SeedQueueHotkeys() { const seeds = useCardStore(state => state.seedQueue.seeds); const index = useCardStore(state => state.seedQueue.index); const engineState = useCardStore(state => state.engineState); + const viewMode = useCardStore(state => state.applicationState.viewMode); const stepSeedQueue = useCardStore(state => state.stepSeedQueue); const jumpSeedQueue = useCardStore(state => state.jumpSeedQueue); + const guarded = (fn: () => void) => () => { + if (viewMode !== 'jaml') fn(); + }; + useHotkeys([ - ["ArrowRight", () => stepSeedQueue(1)], - ["ArrowLeft", () => stepSeedQueue(-1)], - ["Home", () => jumpSeedQueue(0)], - ["End", () => jumpSeedQueue(seeds.length - 1)], + ["ArrowRight", guarded(() => stepSeedQueue(1))], + ["ArrowLeft", guarded(() => stepSeedQueue(-1))], + ["Home", guarded(() => jumpSeedQueue(0))], + ["End", guarded(() => jumpSeedQueue(seeds.length - 1))], ]); useEffect(() => { diff --git a/src/components/blueprint/jamlView/JamlView.tsx b/src/components/blueprint/jamlView/JamlView.tsx index c2d8989..fc78e4a 100644 --- a/src/components/blueprint/jamlView/JamlView.tsx +++ b/src/components/blueprint/jamlView/JamlView.tsx @@ -16,6 +16,7 @@ import { parseJaml, } from "jaml-ui"; import { JamlAesthetic, MotelyDeck, MotelyStake } from "motely-wasm"; +import { Severity, getDiagnostics } from "jaml-lang"; import { useJamlSearch } from "../../../modules/state/jamlSearchContext.tsx"; import type { JamlIdeSearchResult, JimboStatus } from "jaml-ui"; import type { MotelySeedScore } from "motely-wasm"; @@ -164,7 +165,15 @@ export default function JamlView() { } }, [jamlText]); + const jamlErrors = useMemo( + () => getDiagnostics(jamlText) + .filter((d) => d.severity === Severity.Error) + .map((d) => `line ${d.range.start.line + 1}: ${d.message}`), + [jamlText], + ); + const isSearching = status === "running" || status === "booting"; + const startBlocked = !isSearching && jamlErrors.length > 0; const estimate = useMemo(() => { try { @@ -252,10 +261,10 @@ export default function JamlView() { const handleSearch = useCallback(() => { if (isSearching) { workerRef.current?.postMessage({ type: "cancel" } satisfies WorkerRequest); - } else { + } else if (jamlErrors.length === 0) { runSearch(); } - }, [isSearching, runSearch]); + }, [isSearching, jamlErrors, runSearch]); const ideResults: Array = useMemo( () => @@ -326,6 +335,7 @@ export default function JamlView() { ? ` ยท ${Math.round(stats.percentComplete)}%` : ""} + {jamlErrors.map((line) => {line})} {error && {error}} @@ -537,6 +547,7 @@ export default function JamlView() { {isSearching ? "Cancel" : "Start Search"} diff --git a/src/components/blueprint/layout/header.tsx b/src/components/blueprint/layout/header.tsx index dd268d7..04d2ec0 100644 --- a/src/components/blueprint/layout/header.tsx +++ b/src/components/blueprint/layout/header.tsx @@ -1,11 +1,11 @@ -import React from "react"; import {useViewportSize} from "@mantine/hooks"; -import {ActionIcon, AppShell, Box, Burger, Button, Center, Container, CopyButton, Group, Title, Tooltip} from "@mantine/core"; +import {ActionIcon, AppShell, Box, Burger, Button, Center, Container, CopyButton, Group, SegmentedControl, Title, Tooltip} from "@mantine/core"; import {IconMessageChatbot} from "@tabler/icons-react"; import {SeedQueueControls} from "../../SeedQueue.tsx"; import {useCardStore} from "../../../modules/state/store.ts"; import SearchSeedInput from "../../searchInput.tsx"; import {GaEvent} from "../../../modules/useGA.ts"; +import {viewModeOptions} from "./navbar.tsx"; export default function Header() { const { width } = useViewportSize(); @@ -17,13 +17,15 @@ export default function Header() { const toggleOutput = useCardStore(state => state.toggleOutput); const assistantOpened = useCardStore(state => state.applicationState.assistantOpen); const toggleAssistant = useCardStore(state => state.toggleAssistant); + const viewMode = useCardStore(state => state.applicationState.viewMode); + const setViewMode = useCardStore(state => state.setViewMode); return ( - + {viewMode !== 'jaml' && }
@@ -31,10 +33,18 @@ export default function Header() {
+ {viewMode === 'jaml' && ( + setViewMode(value)} + data={viewModeOptions} + /> + )}
- {width > 900 && } + {viewMode !== 'jaml' && width > 600 && } {width > 600 && start && } {width > 700 && start && ( diff --git a/src/components/blueprint/layout/navbar.tsx b/src/components/blueprint/layout/navbar.tsx index d0c334b..53fb7bb 100644 --- a/src/components/blueprint/layout/navbar.tsx +++ b/src/components/blueprint/layout/navbar.tsx @@ -31,6 +31,42 @@ import { SeedQueuePanel } from "../../SeedQueue.tsx"; import { DrawSimulatorModal } from "../../DrawSimulatorModal.tsx"; import { RerollCalculatorModal } from "../../RerollCalculatorModal.tsx"; +export const viewModeOptions = [ + { + value: 'blueprint', + label: ( + + + Blueprint + + ) + }, + { + value: 'jaml', + label: ( + + JAML + JAML + + ) + }, + { + value: 'bp-settings', + label: ( + + + Settings + + ) + } +]; + export default function Navbar() { const theme = useMantineTheme(); const viewMode = useCardStore(state => state.applicationState.viewMode); @@ -83,41 +119,7 @@ export default function Navbar() { fullWidth value={viewMode} onChange={(value: string) => setViewMode(value)} - data={[ - { - value: 'blueprint', - label: ( - - - Blueprint - - ) - }, - { - value: 'jaml', - label: ( - - JAML - JAML - - ) - }, - { - value: 'bp-settings', - label: ( - - - Settings - - ) - } - ]} + data={viewModeOptions} mb="sm" /> diff --git a/src/components/blueprint/standardView/index.tsx b/src/components/blueprint/standardView/index.tsx index ccd8ae6..429286b 100644 --- a/src/components/blueprint/standardView/index.tsx +++ b/src/components/blueprint/standardView/index.tsx @@ -558,6 +558,7 @@ export function Blueprint() { const { width } = useViewportSize(); const settingsOpened = useCardStore(state => state.applicationState.settingsOpen); const outputOpened = useCardStore(state => state.applicationState.asideOpen); + const viewMode = useCardStore(state => state.applicationState.viewMode); const download = useDownloadSeedResults() useEffect(() => { if(typeof window !== 'undefined' && !!download) { @@ -579,10 +580,12 @@ export function Blueprint() { navbar={{ width: { base: '100%', md: 400, lg: 400 }, breakpoint: 'sm', - collapsed: { - desktop: !(width > 1000) && !settingsOpened, - mobile: !settingsOpened - }, + collapsed: viewMode === 'jaml' + ? { desktop: true, mobile: true } + : { + desktop: !(width > 1000) && !settingsOpened, + mobile: !settingsOpened + }, }} padding="md" > diff --git a/src/modules/state/jamlSearchContext.tsx b/src/modules/state/jamlSearchContext.tsx index 24a519a..50d304d 100644 --- a/src/modules/state/jamlSearchContext.tsx +++ b/src/modules/state/jamlSearchContext.tsx @@ -23,7 +23,7 @@ interface JamlSearchContextType extends JamlSearchState { const JamlSearchContext = createContext(undefined); -const DEFAULT_JAML = `name: Blueprint Copy Engine +export const DEFAULT_JAML = `name: Blueprint Copy Engine author: jammy description: Blueprint rare joker with Brainstorm for joker copying synergy deck: Red @@ -44,9 +44,9 @@ should: score: 50 - uncommonJoker: Showman score: 35 - - spectral: Hex + - spectralCard: Hex score: 35 - - mixedJoker: Any + - joker: Any edition: Negative score: 40 - tag: NegativeTag diff --git a/src/modules/state/store.ts b/src/modules/state/store.ts index 9a3928d..92d3981 100644 --- a/src/modules/state/store.ts +++ b/src/modules/state/store.ts @@ -497,9 +497,6 @@ export const useCardStore = create()( set((prev) => { prev.seedQueue.index = clamped; prev.applicationState.start = true; - prev.applicationState.viewMode = prev.applicationState.viewMode === 'jaml' - ? 'blueprint' - : prev.applicationState.viewMode; saveSeedQueue(prev.seedQueue); }, undefined, 'SeedQueue/Jump'); }, diff --git a/src/modules/webmcp/register.ts b/src/modules/webmcp/register.ts index 3c39a12..896d886 100644 --- a/src/modules/webmcp/register.ts +++ b/src/modules/webmcp/register.ts @@ -34,6 +34,7 @@ async function publish() { controller = new AbortController(); const { signal } = controller; for (const tool of listTools()) { + if (signal.aborted) return; try { await ctx.registerTool( { @@ -46,6 +47,7 @@ async function publish() { { signal }, ); } catch (error) { + if (signal.aborted) return; console.warn(`WebMCP: could not register ${tool.name}`, error); } } diff --git a/src/modules/webmcp/registry.ts b/src/modules/webmcp/registry.ts index 129e6ad..9834307 100644 --- a/src/modules/webmcp/registry.ts +++ b/src/modules/webmcp/registry.ts @@ -31,8 +31,14 @@ export function registerTool(tool: BlueprintTool): () => void { } export function registerTools(list: Array): () => void { - const unregister = list.map(registerTool); - return () => unregister.forEach((fn) => fn()); + for (const tool of list) tools.set(tool.name, tool); + notify(); + return () => { + for (const tool of list) { + if (tools.get(tool.name) === tool) tools.delete(tool.name); + } + notify(); + }; } export function listTools(): Array {