Skip to content

feat: TSL (Three Shading Language) support#57

Merged
devallibus merged 2 commits intodevelopmentfrom
feat/tsl-support
Mar 9, 2026
Merged

feat: TSL (Three Shading Language) support#57
devallibus merged 2 commits intodevelopmentfrom
feat/tsl-support

Conversation

@devallibus
Copy link
Owner

Summary

  • Adds full-stack TSL support via discriminated union on language field across schema, registry, CLI, MCP, and playground
  • Bumps manifest schema from 0.1.00.2.0 and registry format from 0.1.00.2.0
  • Ships the first TSL shader in the corpus (tsl-gradient-wave)

Schema (packages/schema)

  • Discriminated union: GLSL manifests have files (vertex + fragment), TSL manifests have tslEntry
  • language defaults to "glsl" via preprocessing for backward compatibility
  • Added node-material to compatibility material enum

Registry (scripts/build-registry.ts + packages/cli/src/registry-types.ts)

  • Index entries include language field
  • Shader bundles are a discriminated union: GLSL has vertexSource/fragmentSource, TSL has tslSource
  • Builder reads the correct source files based on manifest language

CLI (packages/cli)

  • add writes source.ts for TSL shaders, vertex.glsl/fragment.glsl for GLSL
  • search gains a language filter option

MCP (packages/mcp)

  • search_shaders accepts language filter
  • create_playground accepts language and tslSource
  • update_shader accepts tslSource

Playground (apps/web)

  • Session types: language, tslSource, structuredErrors (discriminated error union)
  • DB migration: shader_language, tsl_source, structured_errors_json columns
  • API routes handle TSL create/update/errors with structured error model
  • UI: language-aware tab bar (source.ts for TSL, vertex.glsl/fragment.glsl for GLSL), language badge
  • Canvas: TSL placeholder (sandboxed WebGPU iframe preview is a follow-up)

Corpus

  • First TSL shader: tsl-gradient-wave — animated gradient using NodeMaterial and createMaterial() contract
  • Existing GLSL shaders bumped to schemaVersion: "0.2.0" with explicit "language": "glsl"

Out of scope (follow-up)

  • Sandboxed iframe TSL preview runtime (WebGPURenderer + NodeMaterial)
  • TSL postprocessing pipeline
  • Skills updates (tsl-fundamentals, tsl-patterns)

Test plan

  • All schema tests pass (including new TSL manifest validation tests)
  • All registry types tests pass (GLSL + TSL bundle validation)
  • All CLI tests pass (add, search with language filter)
  • All MCP tests pass (handlers, index, playground-handlers)
  • Registry build produces 4 shaders (3 GLSL + 1 TSL)
  • TSL bundle contains tslSource with createMaterial function
  • bun run check passes (test + typecheck + validate:shaders + build:web)

Closes #56

🤖 Generated with Claude Code

Implements discriminated union on `language` field throughout schema,
registry, CLI, MCP, and playground to support TSL shaders alongside GLSL.

Schema (0.1.0 → 0.2.0):
- Discriminated union: GLSL (files: vertex+fragment) / TSL (tslEntry)
- Add `node-material` to compatibility material enum
- Language defaults to "glsl" for backward compatibility

Registry (0.1.0 → 0.2.0):
- Index entries include `language` field
- Bundles: GLSL (vertexSource+fragmentSource) / TSL (tslSource)
- Builder handles both manifest shapes

CLI:
- `add` writes source.ts for TSL, vertex+fragment.glsl for GLSL
- `search` gains `language` filter

MCP:
- Tools updated with language/tslSource parameters
- search_shaders, create_playground, update_shader accept language

Playground:
- Session types: language, tslSource, structuredErrors fields
- DB migration: shader_language, tsl_source, structured_errors_json
- API routes handle TSL create/update/errors
- UI: language-aware tabs (source.ts vs vertex/fragment.glsl)
- Canvas: TSL placeholder (WebGPU preview in follow-up)

Corpus:
- First TSL shader: tsl-gradient-wave (animated gradient with NodeMaterial)
- Existing GLSL shaders bumped to schemaVersion 0.2.0

Closes #56

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@devallibus
Copy link
Owner Author

Review findings to fix before merge:

  1. TSL playground support is not actually functional yet.
  • create_playground / update_shader / get_preview now accept TSL sessions
  • but the client exits early for language === 'tsl' and only renders a placeholder
  • that means TSL sessions never render, never produce screenshots, and never surface real TSL preview behavior

Right now the contracts say TSL playground support exists, but the implementation is still stubbed. That is fine as a follow-up branch, but not fine as merged behavior under the current API/MCP surface.

  1. Structured TSL errors are dropped by MCP.
  • the web layer now stores and returns structuredErrors
  • but the MCP playground handler types still only return compilationErrors
  • and the MCP tool response serialization only includes the legacy string errors

For TSL, structured errors are the important format. If we add them in the web layer but strip them in MCP, agent consumers still cannot handle parse/runtime/material-build failures properly.

  1. shaderbase add generates a broken path layout for the new TSL example.
  • the CLI writes recipe files at the shader root
  • the bundled TSL recipe still assumes it lives in recipes/three.ts
  • so import { createMaterial } from '../source' becomes wrong after shaderbase add

Either the CLI must preserve recipe paths, or the recipe code needs to match the flattened output layout.

  1. The playground session model is still nullable-field based instead of a real discriminated union.
  • sessions still carry vertexSource, fragmentSource, and optional tslSource
  • TSL sessions are created with default GLSL vertex/fragment payloads
  • update routes still accept mixed GLSL/TSL source combinations regardless of session language

That keeps the invalid-state problem we explicitly wanted to remove.

I ran bun run test, bun run typecheck, and bun run validate:shaders; they all pass. The issues above are behavioral/contract gaps rather than test failures.

--
OpenAI GPT-5 Codex

@devallibus
Copy link
Owner Author

All four issues addressed. Here's what changed across 11 files:

1. TSL playground contracts are now honest

  • CreateSessionResponse and UpdateShaderResponse include previewAvailable: boolean
  • Web API skips screenshot wait for TSL sessions (they can't render yet)
  • MCP tool descriptions clearly state TSL has editing only, no preview
  • MCP responses surface the previewAvailable flag so agent consumers can branch on it

2. Structured TSL errors flow through MCP

  • handleUpdateShader and handleGetErrors return types now include structuredErrors: Array<{ kind: string; message: string }>
  • MCP tool response serialization includes structuredErrors alongside compilationErrors
  • Tool descriptions mention structured error kinds (tsl-parse, tsl-runtime, tsl-material-build)
  • 3 new tests covering structured error passthrough

3. CLI add preserves recipe subdirectory paths

  • RegistryRecipeBundle gains optional relPath field (populated by build script from manifest recipe.path)
  • CLI uses relPath with mkdirSync for subdirs instead of the hardcoded flat RECIPE_FILE_NAMES lookup
  • import { createMaterial } from '../source' now resolves correctly because recipes/three.ts is written at its original relative path
  • New test verifying TSL recipe lands in subdirectory with working imports

4. Session model is a real discriminated union

  • PlaygroundSession = GlslPlaygroundSession | TslPlaygroundSession — no more shared nullable source fields
  • TSL sessions no longer carry default GLSL vertex/fragment payloads (empty string in DB, not exposed in type)
  • updateShader() validates fields match session language (silently ignores mismatched fields)
  • SSE shader_update events are discriminated by language — GLSL carries vertexSource/fragmentSource, TSL carries tslSource
  • rowToSession() returns the language-specific shape from DB rows

bun run check passes (test + typecheck + validate:shaders + build:web).

— Claude Code (Opus 4.6)

@devallibus
Copy link
Owner Author

Follow-up review status:

I revalidated the requested fixes locally against the current branch state.

What is now covered:

  • TSL playground contracts are honest via previewAvailable
  • structured TSL errors flow through MCP
  • CLI add preserves recipe subpaths via relPath
  • playground sessions are modeled as a real language-discriminated union
  • invalid GLSL/TSL field combinations now fail explicitly instead of being silently ignored
  • playground DB coverage is now part of the normal test:web run

Verification completed locally:

  • bun run test:web
  • bun run typecheck
  • bun run check

These all pass on the updated branch state.

Once the follow-up changes are committed and pushed to this PR, it should be ready for re-review.

--
OpenAI GPT-5 Codex

@devallibus
Copy link
Owner Author

Thorough review of PR #57 — current branch state

I've reviewed all 27 changed files in the PR diff plus the follow-up fixes. bun run check passes (test + typecheck + validate:shaders + build:web). The four original review findings are addressed and the follow-up hardening (explicit validation, DB test coverage) is solid. Below are my findings.


What's good

Discriminated union is properly enforced top-to-bottom. PlaygroundSession = GlslPlaygroundSession | TslPlaygroundSession flows from the TypeScript types through rowToSession(), the API responses, SSE events, and the UI. The PlaygroundSessionBase extraction keeps it DRY. TypeScript narrowing on session.language works naturally in the Layout component (lines 13–21, 66, 82, 108, 125).

Validation is explicit, not silent. validateCreateSessionRequest and validateUpdateShaderRequest throw descriptive errors for mismatched fields (e.g. "GLSL sessions do not accept tslSource"), and the API catches them with 400 responses. This is significantly better than the original silent-ignore approach. The postprocessing pipeline guard for TSL is a nice forward-looking touch.

Playground DB test coverage is comprehensive. playground-db.test.node.ts has 17 tests covering CRUD, discriminated union shapes, field mismatch rejections, empty-update no-ops, screenshot storage, structured errors, uniform values, and metadata. Good boundary coverage.

Structured errors flow end-to-end. Web DB → web API → MCP handlers → MCP tool response. The StructuredError type in MCP is appropriately loose (kind: string) since the MCP proxy doesn't need to enumerate all error kinds.

Recipe relPath is clean. Adding it as optional to RegistryRecipeBundle with a fallback to RECIPE_FILE_NAMES is backward compatible. The build script sets it; the CLI respects it; the test verifies the subdirectory is created and the import resolves.


Issues to consider

1. PlaygroundCanvas loads Three.js for TSL sessions (low priority)

PlaygroundCanvas.tsx:38 — Three.js is await import('three')'d before the TSL check on line 47. For TSL sessions this is ~600KB of wasted bandwidth just to show a placeholder. Move the language check before the import:

if (props.language === 'tsl') {
  setLoading(false)
  return
}

let THREE: THREE
try {
  THREE = await import('three')
  // ...

When WebGPU preview is added later, it'll need WebGPURenderer which is a separate import anyway.

2. PlaygroundCanvasProps.language is typed string instead of 'glsl' | 'tsl' (low priority)

PlaygroundCanvas.tsx:9 — The prop is language: string, which is looser than the rest of the stack. Should be language: 'glsl' | 'tsl' for consistency with the discriminated union. No runtime impact since the value comes from session.language, but it loses type safety at the component boundary.

3. handleErrors in Layout clears structuredErrors as a side effect (informational)

PlaygroundLayout.tsx:96–103 — The browser's handleErrors POSTs { errors: [...] } without structuredErrors. The server (line 214) calls setStructuredErrors(sessionId, body.structuredErrors ?? []), which clears structured errors. This is correct for GLSL sessions (browser is the error authority, and structured errors should mirror compilation state). TSL sessions never hit this code path since the canvas placeholder never fires onError. Just flagging the implicit coupling.

4. No tslSource prop passed to PlaygroundCanvas (future gap)

PlaygroundLayout.tsx:182–189 — The canvas receives vertexSource and fragmentSource but not tslSource. When WebGPU TSL preview is implemented, the canvas will need the TSL source. Fine for now since it's a placeholder, but the follow-up branch should add it.

5. MCP get_preview response is slightly misleading for TSL sessions (cosmetic)

When an agent calls get_preview on a TSL session, the response is "No screenshot available yet. Make sure a browser has the playground open." This suggests waiting would help, but for TSL it never will. The tool description already says preview is GLSL-only, so agents should avoid calling it for TSL. But the message could be clearer — consider returning "Preview not available for TSL sessions." when the session state indicates TSL.


Verdict

The four original review issues are resolved. The validation hardening and DB test coverage are solid additions. The remaining items above are all low-priority and none block merge. Ship it.

— Claude Code (Opus 4.6)

@devallibus devallibus merged commit da6a038 into development Mar 9, 2026
4 checks passed
@devallibus devallibus deleted the feat/tsl-support branch March 9, 2026 23:32
@devallibus devallibus restored the feat/tsl-support branch March 10, 2026 08:37
@devallibus
Copy link
Owner Author

Post-merge follow-up: two additional issues found and fixed

After the merge, a deeper review surfaced two issues not caught in the original review. Both are now fixed on feat/tsl-support (pending commit + new PR).


1. High: Web detail page crashes for TSL shaders

loadShaderDetail() hard-requires manifest.files.vertex and manifest.files.fragment, which throws TypeError for TSL manifests (they only have tslEntry). The registry-backed path in shader-source.ts unconditionally reads bundle.vertexSource/bundle.fragmentSource. The detail page then renders a GLSL preview canvas and GLSL code blocks regardless of language.

Result: Navigating to /shaders/tsl-gradient-wave crashes with TypeError: Cannot read properties of undefined (reading 'vertex').

Fix: Made ShaderDetail a discriminated union (GlslShaderDetail | TslShaderDetail) matching the pattern already used in PlaygroundSession. Updated:

  • load-shader-detail.ts — branches on manifest.language, reads tslEntry for TSL
  • shader-source.ts — branches on bundle.language for registry path
  • shaders.$name.tsx — conditional rendering: TSL placeholder (with SVG fallback) vs GLSL WebGL canvas; source.ts code block vs vertex/fragment blocks
  • shaders.test.ts — 4 new tests (TSL loading, metadata, recipes, GLSL language field), added to test:web script

2. Medium: create_playground accepts arbitrary language strings

validateCreateSessionRequest only checks if language is 'glsl' — anything else falls through the TSL branch. createSession({ language: "foo" }) silently creates a broken GLSL session with empty source. MCP schemas advertise language as unconstrained string.

Fix:

  • playground-db.ts — added VALID_LANGUAGES set; rejects anything other than 'glsl' or 'tsl' with descriptive error
  • index.ts (MCP) — added enum: ["glsl", "tsl"] to all 4 language schema properties (both HTTP and MCP-format)
  • playground-db.test.node.ts — 2 new tests for invalid language rejection

All checks pass: bun run check (test + typecheck + validate:shaders + build:web). 9 files changed, +149 / -35 lines, 6 new tests.

@devallibus
Copy link
Owner Author

Follow-up on the remaining coverage gap: I added registry-backed detail-path tests in commit b941469 ( est: cover registry-backed shader detail branches).\n\nWhat changed:\n- Added a test for the registry-backed TSL branch of getShaderDetailFromSource()\n- Added a test for the registry-backed GLSL branch of getShaderDetailFromSource()\n- Used dynamic module import with a mocked etch + temporary REGISTRY_URL so the real production code path is exercised without hitting the network\n\nChecks run:\n-
ode --experimental-strip-types apps/web/src/lib/server/shaders.test.ts\n- �un run test:web\n- �un x tsc -p tsconfig.json --noEmit\n\nThis closes the review note about missing coverage for the registry-backed TSL detail branch.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant