{"owner":"Snapchat","repo":"Valdi","hasSkills":true,"totalSkillsCount":46,"totalTokensCount":28825,"categories":["anthropic-skill","cursor-rule","root-instruction","plugin-manifest"],"hasMcp":false,"mcpConfig":null,"found":["ai-skills/skills/valdi-animations/skill.md","ai-skills/skills/valdi-bazel/skill.md","ai-skills/skills/valdi-compiler/skill.md","ai-skills/skills/valdi-async/skill.md","ai-skills/skills/valdi-component-dedup/skill.md","ai-skills/skills/valdi-android/skill.md","ai-skills/skills/valdi-cpp-runtime/skill.md","ai-skills/skills/valdi-custom-view/skill.md","ai-skills/skills/valdi-component-tests/skill.md","ai-skills/skills/valdi-gestures/skill.md","ai-skills/skills/valdi-inspect/skill.md","ai-skills/skills/valdi-error-handling/skill.md","ai-skills/skills/valdi-ios/skill.md","ai-skills/skills/valdi-localization/skill.md","ai-skills/skills/valdi-migrate/skill.md","ai-skills/skills/valdi-mock-breakdown/skill.md","ai-skills/skills/valdi-mock-iterate/skill.md","ai-skills/skills/valdi-mock-to-module/skill.md","ai-skills/skills/valdi-navigation/skill.md","ai-skills/skills/valdi-overview/skill.md","ai-skills/skills/valdi-perf/skill.md","ai-skills/skills/valdi-polyglot-module/skill.md","ai-skills/skills/valdi-pr-review/skill.md","ai-skills/skills/valdi-preview/skill.md","ai-skills/skills/valdi-profile/skill.md","ai-skills/skills/valdi-setup/skill.md","ai-skills/skills/valdi-testing/skill.md","ai-skills/skills/valdi-tsx/skill.md",".cursorrules","AGENTS.md","tools/api_surface/baseline.json","tools/ci/AGENTS.md","tools/valdi_web_devtools/README.md","tools/valdi_web_devtools/package.json","tools/valdi_web_devtools/tsconfig.json","tools/ci/compiler_goldens/README.md","ai-skills/skills/valdi-async/tests/README.md","ai-skills/skills/valdi-async/tests/tsconfig.json","ai-skills/skills/valdi-component-tests/tests/README.md","ai-skills/skills/valdi-component-tests/tests/tsconfig.json","ai-skills/skills/valdi-migrate/tests/README.md","ai-skills/skills/valdi-migrate/tests/tsconfig.json","ai-skills/skills/valdi-perf/tests/README.md","ai-skills/skills/valdi-perf/tests/tsconfig.json","ai-skills/skills/valdi-tsx/tests/README.md","ai-skills/skills/valdi-tsx/tests/tsconfig.json"],"skills":{"ai-skills/skills/valdi-animations/skill.md":"# Valdi Animations\n\nImperative animation API for animating element attributes and state changes.\n\n## When to use\n\nWhen animating layout changes, view attributes, or state transitions. Almost all element attributes can be animated in Valdi.\n\n## Key concepts\n\nValdi uses an **imperative animation model** inspired by iOS's `animateWithDuration:animations:`. You call `this.animate()` or `this.animatePromise()` with options and a block of mutations — all changes inside the block are animated automatically.\n\n**Not CSS transitions.** There is no `transition` property, no `@keyframes`, no `animation-duration`. Animations are triggered programmatically.\n\n## animate and animatePromise\n\nBoth methods are available on any `Component`:\n\n```tsx\n// ✅ Fire-and-forget animation\nthis.animate({ duration: 0.3 }, () => {\n  this.myLabel.setAttribute('font', 'title');\n  this.myContainer.setAttribute('width', 200);\n});\n\n// ✅ Wait for animation to complete\nawait this.animatePromise({ duration: 0.5, curve: AnimationCurve.EaseOut }, () => {\n  this.setState({ expanded: true });\n});\n// Animation finished — safe to do next step\n```\n\n## AnimationOptions\n\nThree curve types: preset, custom bezier, and spring.\n\n```tsx\nimport { AnimationCurve } from 'valdi_core/src/AnimationOptions';\n\n// Preset curves\n{ duration: 0.3, curve: AnimationCurve.Linear }\n{ duration: 0.3, curve: AnimationCurve.EaseIn }\n{ duration: 0.3, curve: AnimationCurve.EaseOut }\n{ duration: 0.3, curve: AnimationCurve.EaseInOut }  // default\n\n// Custom bezier control points\n{ duration: 0.4, controlPoints: [0.25, 0.1, 0.25, 1.0] }\n\n// Spring animation (no duration — spring physics determine timing)\n{ stiffness: 300, damping: 30 }\n\n// Crossfade (alpha transition between old and new state)\n{ duration: 0.3, crossfade: true }\n\n// Continue from current animation state\n{ duration: 0.3, beginFromCurrentState: true }\n\n// Completion callback\n{ duration: 0.3, completion: (wasCancelled) => { /* done */ } }\n```\n\n## Animating element attributes via ElementRef\n\n```tsx\nimport { ElementRef } from 'valdi_core/src/ElementRef';\n\nexport class MyComponent extends Component {\n  private titleRef = new ElementRef();\n  private containerRef = new ElementRef();\n\n  onRender(): void {\n    <view ref={this.containerRef} width={100}>\n      <label ref={this.titleRef} value=\"Hello\" font={systemFont(14)} />\n    </view>;\n  }\n\n  private expand = () => {\n    // ✅ Animate attribute changes via ElementRef\n    this.animate({ duration: 0.3 }, () => {\n      this.titleRef.setAttribute('font', systemBoldFont(17));\n      this.containerRef.setAttribute('width', 300);\n    });\n  };\n}\n```\n\n## Animating state changes\n\n```tsx\ninterface State {\n  expanded: boolean;\n  text: string;\n}\n\nexport class MyComponent extends StatefulComponent<{}, State> {\n  state = { expanded: false, text: 'Collapsed' };\n\n  onRender(): void {\n    <view width={this.state.expanded ? 300 : 100} height={this.state.expanded ? 200 : 50}>\n      <label value={this.state.text} />\n    </view>;\n  }\n\n  private toggle = () => {\n    // ✅ setStateAnimated — renders with new state, animates all resulting mutations\n    this.setStateAnimated(\n      { expanded: !this.state.expanded, text: this.state.expanded ? 'Collapsed' : 'Expanded' },\n      { duration: 0.3, curve: AnimationCurve.EaseInOut },\n    );\n  };\n}\n```\n\n## Spring animations\n\n```tsx\n// ✅ Bouncy spring — lower damping = more bounce\nthis.animate({ stiffness: 300, damping: 15 }, () => {\n  this.cardRef.setAttribute('scale', 1.0);\n});\n\n// ✅ Stiff spring — high damping = smooth settle\nthis.animate({ stiffness: 500, damping: 40 }, () => {\n  this.panelRef.setAttribute('translateY', 0);\n});\n```\n\n## Animating text and attributed text\n\nThe imperative `animate()` / `setStateAnimated()` API above does not drive per-character or per-word text reveals. For those, use the **text-animation surface**: an `AttributedText` chunk carries an `animationTransform`, and a `<textanimationgroup>` container coordinates one shared timeline across all of its descendant `<label>` / `<textview>` elements.\n\n`animationTransform` fields (`valdi_tsx/src/AttributedText.d.ts`, `AttributedTextAnimationTransform`):\n\n| Field | Meaning |\n|-------|---------|\n| `translationY` | Initial vertical offset (logical px); animates back to the normal position |\n| `scale` | Initial scale; animates back to 1 |\n| `opacity` | Initial opacity; animates back to 1 |\n| `duration` | Seconds for each part to reach its normal appearance |\n| `timeOffsetBetweenParts` | Delay (seconds) staggered between consecutive parts (0 = all at once) |\n| `partPattern` | Native regex; each non-overlapping match becomes a separately animated part (unmatched text stays static). Omit to animate the whole chunk as one part |\n| `key` | Stable animation identity; preserves progress across view-pool recycling. Change it to restart the animation |\n\n```tsx\nimport { AttributedTextBuilder } from 'valdi_core/src/utils/AttributedTextBuilder';\n\n// ✅ Build attributed text with a per-chunk animation transform\nprivate animatedText(text: string) {\n  return new AttributedTextBuilder()\n    .append(text, {\n      animationTransform: {\n        duration: 0.45,\n        key: `run-${this.state.runId}`,   // change key to replay\n        opacity: 0,\n        translationY: 8,\n        timeOffsetBetweenParts: 0.08,      // stagger each part\n      },\n    })\n    .build();\n}\n\n// ✅ <textanimationgroup> runs ONE shared timeline across every descendant\n//    label/textview, even nested ones\nonRender(): void {\n  <textanimationgroup>\n    <label value={this.animatedText('First line joins the timeline.')} />\n    <view>\n      <label value={this.animatedText('So does this nested one.')} />\n    </view>\n    <textview value={this.animatedText('And the textview animates last.')} />\n  </textanimationgroup>;\n}\n```\n\n`<textanimationgroup>` is a container (`CommonView` + `ContainerTemplateElement`) with no animation-specific attributes of its own — its only job is to group descendants onto a single coordinated timeline. Without it, each animated chunk runs on its own timeline.\n\n## Common patterns\n\n### Show/hide with fade\n\n```tsx\nprivate show = () => {\n  this.animate({ duration: 0.2 }, () => {\n    this.overlayRef.setAttribute('opacity', 1);\n  });\n};\n\nprivate hide = () => {\n  this.animate({ duration: 0.2 }, () => {\n    this.overlayRef.setAttribute('opacity', 0);\n  });\n};\n```\n\n### Sequential animations\n\n```tsx\nprivate async runSequence() {\n  await this.animatePromise({ duration: 0.2 }, () => {\n    this.step1Ref.setAttribute('opacity', 1);\n  });\n  await this.animatePromise({ duration: 0.3 }, () => {\n    this.step2Ref.setAttribute('translateY', 0);\n  });\n}\n```\n\n## Common mistakes\n\n```tsx\n// ❌ WRONG — CSS-style transitions don't exist\n<view style={{ transition: 'all 0.3s ease' }} />  // ❌\n<view className=\"animate-fade-in\" />               // ❌\n\n// ❌ WRONG — React-style animation libraries\nimport { animated, useSpring } from 'react-spring';  // ❌\n\n// ❌ WRONG — Calling setState inside animate (use setStateAnimated instead)\nthis.animate({ duration: 0.3 }, () => {\n  this.setState({ expanded: true });  // ❌ Won't animate the re-render\n});\n\n// ✅ Correct — use setStateAnimated for state-driven animations\nthis.setStateAnimated({ expanded: true }, { duration: 0.3 });\n\n// ❌ WRONG — Duration in milliseconds (it's seconds)\nthis.animate({ duration: 300 }, () => { ... });  // ❌ This is 300 seconds!\n\n// ✅ Correct — Duration in seconds\nthis.animate({ duration: 0.3 }, () => { ... });\n```\n\n## CoreUI animation components\n\nThe `coreui` library includes pre-built animation components:\n\n| Component | Purpose |\n|-----------|---------|\n| `AnimatableVisibilityView` | Animate show/hide transitions |\n| `AnimationShimmer` | Loading shimmer effect |\n| `AnimationSliding` | Slide in/out animations |\n| `AnimationSquishy` | Press-and-squish feedback |\n| `AnimationRotate` | Continuous rotation |\n| `VerticallyOpenableComponent` | Expand/collapse vertically |\n\nImport from `coreui/src/components/animation/`.\n","ai-skills/skills/valdi-bazel/skill.md":"# Bazel Build System Rules\n\n**Applies to**: `BUILD.bazel`, `*.bzl` files in `/bzl/`, `WORKSPACE`, `MODULE.bazel`\n\n## Overview\n\nValdi uses Bazel as its build system. Bazel provides reproducible, incremental builds across all platforms.\n\n## Key Commands\n\n```bash\n# Build everything\nbazel build //...\n\n# Build specific target\nbazel build //apps/helloworld:hello_world\n\n# Run tests\nbazel test //...\n\n# Clean (use sparingly - cache is valuable!)\nbazel clean\n```\n\n## Important Notes\n\n1. **Use `bazel`** for all build commands\n2. **The CLI wraps Bazel** - `valdi` commands use bazel under the hood\n3. **Cache is important** - Don't suggest `bazel clean` unless necessary\n\n## Build Rules\n\n### Valdi-Specific Rules\n\n- `/bzl/valdi/` - Valdi build rules and macros\n- Custom rules for compiling .tsx to .valdimodule\n- Platform-specific build transitions\n\n### Common Targets\n\n```python\nload(\"@aspect_rules_ts//ts:defs.bzl\", \"ts_project\")\n\n# Valdi application\nvaldi_application(\n    name = \"my_app\",\n    root_component_path = \"App@my_app/src/MyApp\",\n    title = \"My App\",\n    version = \"1.0.0\",\n    deps = [\"//apps/my_app/src/valdi/my_app\"],\n)\n\n# Web polyglot views — MUST be ts_project, never filegroup\nts_project(\n    name = \"my_module_web\",\n    srcs = glob(\n        [\"web/**/*.ts\", \"src/**/*.d.ts\"],\n        exclude = [\"web/**/*.d.ts\"],\n    ),\n    allow_js = True,\n    composite = True,\n    transpiler = \"tsc\",\n    tsconfig = \"web/tsconfig.json\",\n)\n\n# Valdi module with platform deps\nvaldi_module(\n    name = \"my_module\",\n    srcs = glob([\"src/**/*.ts\", \"src/**/*.tsx\"]) + [\"tsconfig.json\"],\n    ios_deps = [\":my_ios_views\"],          # objc_library\n    macos_deps = [\":my_macos_views\"],      # objc_library (or omit to share ios_deps)\n    android_deps = [\":my_android_views\"],  # valdi_android_library\n    web_deps = [\":my_module_web\"],         # ts_project (never filegroup)\n    deps = [\n        \"//src/valdi_modules/src/valdi/valdi_core\",\n    ],\n)\n```\n\n## Conventions\n\n### File Naming\n\n- `BUILD.bazel` not `BUILD` (explicit extension)\n- `.bzl` for Starlark macros and rules\n\n### Targets\n\n- Use descriptive target names\n- One main target per BUILD file usually matches directory name\n\n### Dependencies\n\n- Be explicit about dependencies\n- Don't rely on transitive deps implicitly\n- Use visibility to control access\n\n## Platform Builds\n\n```bash\n# Build and install iOS app\nvaldi install ios\n\n# Build and install Android app\nvaldi install android\n```\n\n## Configuration\n\n- `.bazelrc` - Build flags and configurations\n- `MODULE.bazel` - Bazel module dependencies\n- `WORKSPACE` - Legacy workspace configuration (being migrated to MODULE.bazel)\n\n## Common Issues\n\n1. **Missing dependencies** - Add to `deps` in BUILD.bazel\n2. **Cache issues** - Try `bazel clean --expunge` (last resort)\n3. **Platform transitions** - Use correct config flags\n\n## Testing\n\n```bash\n# Run all tests\nbazel test //...\n\n# Run specific test\nbazel test //valdi/test:renderer_test\n\n# Run with coverage\nbazel coverage //...\n```\n\n## More Information\n\n- Bazel docs: https://bazel.build\n- Valdi build rules: `bzl/valdi/npm/README.md`\n- Framework docs: `/AGENTS.md`\n","ai-skills/skills/valdi-compiler/skill.md":"# Valdi Compiler Rules\n\n**Applies to**: Swift files in `/compiler/compiler/`\n\n## Overview\n\nThe Valdi compiler consists of two parts:\n\n1. **Compiler (Swift)**: Transforms TypeScript/TSX into `.valdimodule` files\n2. **Companion (TypeScript/Node.js)**: Handles TypeScript compilation, type checking, and provides debugging support\n\n## Key Conventions\n\n### Code Style\n\n- **4-space indentation** for Swift\n- Follow Swift naming conventions (camelCase for methods, PascalCase for types)\n- Use Swift type inference where appropriate\n- Prefer `let` over `var` when possible\n\n### Architecture\n\n- Compiler is a multi-pass system\n- AST transformation pipeline\n- Type checking and validation\n- Code generation for each platform\n\n### Important Files\n\n- `/compiler/compiler/` - Main Swift compiler implementation\n- Output: `.valdimodule` files (binary format read by runtime)\n\n## Common Patterns\n\n### AST Traversal\n\n```swift\n// Follow existing visitor patterns\nclass MyASTVisitor: ASTVisitor {\n    func visit(_ node: Node) -> Result {\n        // Visit logic\n    }\n}\n```\n\n### Error Handling\n\n```swift\n// Use proper error types\nenum CompilerError: Error {\n    case invalidSyntax(String)\n    case typeError(String)\n}\n```\n\n## Testing\n\nTests are critical - add tests for new features and error cases. Run the test suite with `bzl test //compiler/compiler/...` (the build script below does not run tests).\n\n## Build\n\n### Using the update script (recommended):\n\n```bash\ncd compiler/compiler\n./scripts/update_compiler_bazel.sh -o bin/compiler\n```\n\nThis script:\n- Builds the compiler with Bazel (`//compiler/compiler:local_valdi_compiler`); no Swift toolchain install is required\n- Builds the compiler for the correct architecture (on macOS, lipos arm64 + x86_64 into a universal binary and codesigns it)\n- Copies the binary to `bin/compiler/macos/valdi_compiler` (or `linux/valdi_compiler`)\n- Handles platform differences (macOS vs Linux)\n\n### Alternative: Using Xcode\n\nOpen `compiler/compiler/Compiler/Package.swift` in Xcode, let it resolve dependencies, then build.\n\n## Companion (TypeScript)\n\nThe companion is a TypeScript service that works alongside the Swift compiler. It handles TypeScript compilation, type checking, and provides debugging support.\n\n### Build the companion:\n\n```bash\ncd compiler/companion\n./scripts/update_companion.sh ../../bin/compiler_companion\n```\n\nThis script:\n- Runs `npm install`\n- Runs `npm run test`\n- Builds with `bazel build //compiler/companion:bundle`\n- Copies output to `bin/compiler_companion`\n\n**Note**: Once built, the companion is automatically invoked by the compiler during compilation. You don't need to run it manually - it's part of the compiler process.\n\n## Important Notes\n\n1. **Performance matters** - Compiler speed affects developer experience\n2. **Error messages** - Make them helpful and actionable\n3. **Backward compatibility** - Don't break existing .valdimodule files\n4. **Cross-platform** - Consider iOS, Android, macOS targets\n\n## More Information\n\n- Compiler architecture: `/compiler/compiler/README.md`\n- Framework docs: `/AGENTS.md`\n","ai-skills/skills/valdi-async/skill.md":"# Valdi Async & Lifecycle Safety\n\nAsync operations that complete after a component is destroyed will call `setState()`\non a dead component — Valdi logs a `console.error` and drops the update (a silent\nno-op, not a thrown error). The fix is always to cancel or guard before the callback\nfires.\n\n## HTTPClient: Store, Cancel, Repeat\n\n`HTTPClient` methods return `CancelablePromise<HTTPResponse>`. Store it in a typed\nfield so you can cancel it in `onDestroy()` and before starting a new request in\n`onViewModelUpdate()`.\n\n```typescript\nimport { Component, StatefulComponent } from 'valdi_core/src/Component';\nimport { CancelablePromise } from 'valdi_core/src/CancelablePromise';\nimport { HTTPClient } from 'valdi_http/src/HTTPClient';\nimport { HTTPResponse } from 'valdi_http/src/HTTPTypes';\n\ninterface UserState {\n  user: { name: string } | null;\n  loading: boolean;\n}\n\nclass UserProfile extends StatefulComponent<UserProfileViewModel, UserState> {\n  state: UserState = { user: null, loading: true };\n\n  private client = new HTTPClient('https://api.example.com');\n  private request?: CancelablePromise<HTTPResponse>;\n\n  onCreate(): void {\n    this.fetchUser(this.viewModel.userId);\n  }\n\n  onViewModelUpdate(previous?: UserProfileViewModel): void {\n    if (this.viewModel.userId !== previous?.userId) {\n      this.request?.cancel?.();          // Cancel in-flight before starting new one\n      this.fetchUser(this.viewModel.userId);\n    }\n  }\n\n  onDestroy(): void {\n    this.request?.cancel?.();            // Always clean up\n  }\n\n  private fetchUser(id: string): void {\n    this.request = this.client.get(`/users/${id}`);\n    this.request.then(response => {\n      const user = JSON.parse(new TextDecoder().decode(response.body));\n      this.setState({ user, loading: false });\n    });\n  }\n\n  onRender(): void {\n    if (this.state.loading) {\n      <spinner />;\n      return;\n    }\n    <label value={this.state.user?.name ?? ''} />;\n  }\n}\n```\n\n**Important:** `cancel` is an optional method on `CancelablePromise` — always use\n`?.cancel?.()`, not `?.cancel()`.\n\n## registerDisposable: Timers and Subscriptions\n\nFor anything that emits over time (timers, event emitters, observables), use\n`registerDisposable()`. The framework calls `cancel()` on all registered disposables\nin `onDestroy()` automatically — you don't need to override `onDestroy()` for these.\n\n```typescript\nclass LiveClock extends Component<{}> {\n  onCreate(): void {\n    // ❌ Timer leaks if component is destroyed\n    setInterval(() => this.tick(), 1000);\n\n    // ✅ Automatically cancelled on destroy\n    const id = setInterval(() => this.tick(), 1000);\n    this.registerDisposable({ cancel: () => clearInterval(id) });\n  }\n\n  private tick = () => { /* ... */ };\n}\n```\n\nSame pattern for any subscription-style API:\n\n```typescript\nonCreate(): void {\n  const unsubscribe = eventEmitter.on('change', this.handleChange);\n  this.registerDisposable({ cancel: unsubscribe });\n}\n```\n\n## isDestroyed() Guard for Plain Promises\n\nWhen using `async/await` or plain `Promise` chains that can't be cancelled, guard\n`setState()` with `isDestroyed()` before calling it:\n\n```typescript\nprivate async loadData(): Promise<void> {\n  const result = await someAsyncOperation();\n\n  if (this.isDestroyed()) return;   // Component unmounted while awaiting\n\n  this.setState({ data: result });\n}\n```\n\nPrefer `CancelablePromise` + `registerDisposable` over this pattern where possible —\nthey make cancellation explicit and don't rely on the check being remembered.\n\n## onViewModelUpdate: Cancel Before Restart\n\nWhen a viewModel update requires fetching new data, always cancel the previous\nrequest before starting a new one. Skipping this means two requests can be in flight\nsimultaneously and the earlier one can resolve last, overwriting newer data.\n\n```typescript\nonViewModelUpdate(previous?: MyViewModel): void {\n  if (this.viewModel.query !== previous?.query) {\n    this.searchRequest?.cancel?.();           // ← required\n    this.searchRequest = this.client.get(`/search?q=${this.viewModel.query}`);\n    this.searchRequest.then(response => {\n      this.setState({ results: parse(response) });\n    });\n  }\n}\n```\n\n## Observable Subscriptions\n\nFor RxJS or observable-based APIs, prefer `registerDisposable` for one-off subscriptions\nor the `Subscription` class for managing multiple subscriptions as a group:\n\n```typescript\nimport { Subscription } from 'rxjs';\n\nclass FeedComponent extends StatefulComponent<FeedViewModel, FeedState> {\n  private subscription = new Subscription();\n\n  onCreate(): void {\n    // Add multiple subscriptions to the group\n    this.subscription.add(\n      this.viewModel.feedItems$.subscribe({ next: items => this.setState({ items }) })\n    );\n    this.subscription.add(\n      this.viewModel.loading$.subscribe({ next: loading => this.setState({ loading }) })\n    );\n  }\n\n  onDestroy(): void {\n    this.subscription?.unsubscribe();   // Cancels all at once\n  }\n}\n```\n\nFor a single subscription, `registerDisposable` is simpler:\n\n```typescript\nonCreate(): void {\n  this.registerDisposable(\n    this.viewModel.counter$.subscribe({ next: count => this.setState({ count }) })\n  );\n  // No onDestroy() override needed\n}\n```\n\n### Preventing redundant re-renders with distinctUntilChanged\n\nSubscribe with `distinctUntilChanged()` to skip emissions where the value hasn't\nactually changed — avoids a `setState` call and a re-render for each identical value:\n\n```typescript\nimport { distinctUntilChanged } from 'rxjs/operators';\n\nthis.subscription.add(\n  this.viewModel.title$.pipe(distinctUntilChanged()).subscribe({\n    next: title => this.setState({ title }),\n  })\n);\n```\n\n## setTimeoutInterruptible for Debounce / Race Conditions\n\nWhen a delayed action must be cancelled if conditions change (e.g. search debounce,\nretry delay), use `setTimeoutInterruptible` rather than a bare `setTimeout`:\n\n```typescript\nimport { setTimeoutInterruptible } from 'valdi_core/src/SetTimeout';\n\n// setTimeoutInterruptible returns a number (timer ID) — cancel with clearTimeout()\nclass SearchBar extends StatefulComponent<SearchViewModel, SearchState> {\n  private debounceId?: number;\n\n  onViewModelUpdate(previous?: SearchViewModel): void {\n    if (this.viewModel.query !== previous?.query) {\n      clearTimeout(this.debounceId);   // Cancel any pending debounce\n      this.debounceId = setTimeoutInterruptible(() => {\n        this.fetchResults(this.viewModel.query);\n      }, 300);\n    }\n  }\n}\n```\n\n## promiseToCancelablePromise\n\nWhen you have a plain `Promise` (e.g. from a third-party library) that needs to participate in Valdi's cancellation system, wrap it with `promiseToCancelablePromise`:\n\n```typescript\nimport { CancelablePromise, promiseToCancelablePromise } from 'valdi_core/src/CancelablePromise';\n\nclass MyComponent extends StatefulComponent<MyViewModel, MyState> {\n  private request?: CancelablePromise<string>;\n\n  onCreate(): void {\n    const rawPromise: Promise<string> = thirdPartyApi.fetchData();\n    this.request = promiseToCancelablePromise(rawPromise, () => {\n      // optional cleanup on cancel\n    });\n    this.request.then(data => {\n      if (this.isDestroyed()) return;\n      this.setState({ data });\n    });\n  }\n\n  onDestroy(): void {\n    this.request?.cancel?.();\n  }\n}\n```\n\nPrefer `CancelablePromise` directly (e.g. `HTTPClient`) when available — `promiseToCancelablePromise` is the bridge for external APIs.\n\n## Import Paths\n\n```typescript\nimport { CancelablePromise, promiseToCancelablePromise } from 'valdi_core/src/CancelablePromise';\nimport { HTTPClient } from 'valdi_http/src/HTTPClient';\nimport { HTTPResponse } from 'valdi_http/src/HTTPTypes';\nimport { setTimeoutInterruptible } from 'valdi_core/src/SetTimeout';\nimport { Subscription } from 'rxjs';\nimport { distinctUntilChanged } from 'rxjs/operators';\n```\n","ai-skills/skills/valdi-component-dedup/skill.md":"# Valdi Component Deduplication\n\nFind and extract duplicated UI patterns across Valdi modules into shared components.\n\n## When to use\n\nWhen you want to identify components that multiple modules have independently implemented, then extract them into a shared library so future modules can reuse them instead of reinventing.\n\n## Running the analysis\n\n```bash\n# Full scan of all modules\nvaldi analyze duplicates\n\n# Scope to one module\nvaldi analyze duplicates --module my_module\n\n# Machine-readable output\nvaldi analyze duplicates --json\n\n# Only check for a specific pattern\nvaldi analyze duplicates --pattern async\n\n# Custom scan roots and shared libraries\nvaldi analyze duplicates --scan-root src/modules --shared-lib valdi_widgets\nvaldi analyze duplicates --scan-root src/modules --shared-lib my_shared_lib --shared-lib another_lib\n```\n\n### CLI flags\n\n| Flag | Effect |\n|---|---|\n| `--module <name>` | Scope to one module |\n| `--json` | Machine-readable JSON output |\n| `--threshold <0-1>` | Similarity threshold (default 0.6) |\n| `--pattern <name>` | Only detect: `async`, `button`, `shimmer` |\n| `--output <path>` | Write JSON report to file |\n| `--verbose` | Show per-file detail |\n| `--scan-root <path>` | Root directory to scan (repeatable) |\n| `--shared-lib <name>` | Shared component library name (repeatable) |\n\n## Understanding the report\n\nThe report has two sections:\n\n**Known Patterns** — pre-built detectors for the most common duplication:\n- **Async State Container**: Components that implement loading→error/content state machines\n- **Button Wrapper**: Thin wrappers around a shared button with hardcoded sizing/coloring\n- **Shimmer / Skeleton Layout**: Custom loading placeholder components\n\n**Similar Clusters** — generic similarity analysis that finds groups of components with overlapping ViewModel props, render trees, and import signatures.\n\nEach cluster shows:\n- Number of files and average similarity score\n- Shared ViewModel props across the cluster\n- A suggested extraction approach\n\n## Extraction workflow\n\nOnce you identify a cluster worth extracting:\n\n### 1. Read the candidates\n\nRead each file in the cluster. Identify:\n- The shared ViewModel props (these become the new component's ViewModel)\n- The shared render structure (this becomes the new component's `onRender`)\n- Module-specific variations (these become optional props or render callbacks)\n\n### 2. Check for an existing shared component to extend\n\nBefore creating anything new, search the shared library for a component that already solves part of the problem. Often the right fix is adding props, a preset enum, or a render callback to an existing component rather than building from scratch.\n\n```bash\n# Search the shared library for related components\ngrep -rl \"Button\\|Shimmer\\|AsyncContent\" <your-shared-lib>/src/components/\n```\n\nIf an existing component covers 70%+ of the use cases, extend it:\n- Add optional props for the missing behavior\n- Add a preset or variant enum if consumers just need different default combos\n- Add a render callback slot if consumers need to inject custom UI\n\nOnly create a new component when nothing in the shared library is close enough to extend.\n\n### 3. Design or extend the shared component\n\n**Extending an existing component** (preferred):\n\n```typescript\n// Add a preset to an existing button component\nexport enum CoreButtonPreset {\n  PRIMARY_CTA,\n  SECONDARY_ACTION,\n  PILL_ACTION,\n}\n\n// Add an optional prop to the existing ViewModel\ninterface CoreButtonViewModel {\n  // ... existing props ...\n  preset?: CoreButtonPreset;\n}\n```\n\n**Creating a new component** (when nothing exists to extend):\n\n```typescript\nimport { Component } from 'valdi_core/src/Component';\n\ninterface SharedComponentViewModel {\n  commonProp: string;\n  optionalVariant?: VariantType;\n  renderCustomSection?: () => void;\n}\n\nexport class SharedComponent extends Component<SharedComponentViewModel> {\n  onRender(): void {\n    <view>\n      <label value={this.viewModel.commonProp} />;\n      {this.viewModel.renderCustomSection?.()};\n    </view>;\n  }\n}\n```\n\n### 4. Place it in the shared library\n\nIf you created a new component, place it in your project's shared component library\n(e.g. `<shared-lib>/src/components/<category>/`). Follow your project's conventions\nfor directory structure and naming.\n\n### 5. Migrate consumers\n\nFor each file in the cluster:\n1. Replace the local component class with an import of the shared one\n2. Map the old ViewModel props to the new shared ViewModel\n3. Move module-specific rendering into render callbacks\n4. Delete the old local component file\n5. Run `valdi agent-check --module <name>` to verify\n\n### 6. Verify\n\n```bash\n# Re-run analysis — the cluster should shrink or disappear\nvaldi analyze duplicates --pattern async\n\n# Build and test affected modules\nvaldi agent-check --module module_a\nvaldi agent-check --module module_b\n```\n\n## Common extraction patterns\n\n### Async State Container\n\nThe most common duplication. Components that manage loading→loaded/error states:\n\n```typescript\nimport { StatefulComponent } from 'valdi_core/src/Component';\nimport { promiseToCancelablePromise } from 'valdi_core/src/CancelablePromise';\n\ninterface AsyncStateContainerViewModel<T> {\n  resultProvider: () => PromiseLike<T>;\n  renderLoading: () => void;\n  renderResult: (data: T) => void;\n  renderError: (error: string, retry: () => void) => void;\n  renderEmpty?: () => void;\n}\n\ntype AsyncState<T> = { phase: 'loading' } | { phase: 'result'; data: T } | { phase: 'error'; message: string };\n\nexport class AsyncStateContainer<T> extends StatefulComponent<AsyncStateContainerViewModel<T>, { state: AsyncState<T> }> {\n  state = { state: { phase: 'loading' } as AsyncState<T> };\n\n  onCreate(): void {\n    this.load();\n  }\n\n  onRender(): void {\n    const s = this.state.state;\n    switch (s.phase) {\n      case 'loading': this.viewModel.renderLoading(); break;\n      case 'result': this.viewModel.renderResult(s.data); break;\n      case 'error': this.viewModel.renderError(s.message, () => this.load()); break;\n    }\n  }\n\n  private load(): void {\n    this.setState({ state: { phase: 'loading' } });\n    const cancelable = promiseToCancelablePromise(\n      Promise.resolve(this.viewModel.resultProvider()),\n      () => {},\n    );\n    this.registerDisposable(() => cancelable.cancel?.());\n    cancelable.then(\n      data => { this.setState({ state: { phase: 'result', data } }); },\n      error => { this.setState({ state: { phase: 'error', message: (error as Error).message } }); },\n    );\n  }\n}\n```\n\n### Button Presets\n\nInstead of per-module button wrappers, use preset enums:\n\n```typescript\n// Instead of a new wrapper component, add presets to the existing shared button\nenum ButtonPreset {\n  PRIMARY_CTA,\n  SECONDARY_ACTION,\n  PILL_ACTION,\n}\n\nfunction presetProps(preset: ButtonPreset): { sizing: ButtonSizing; coloring: ButtonColoring } {\n  switch (preset) {\n    case ButtonPreset.PRIMARY_CTA: return { sizing: ButtonSizing.XL, coloring: ButtonColoring.PRIMARY_BRAND };\n    case ButtonPreset.SECONDARY_ACTION: return { sizing: ButtonSizing.LARGE, coloring: ButtonColoring.SECONDARY };\n    case ButtonPreset.PILL_ACTION: return { sizing: ButtonSizing.SMALL, coloring: ButtonColoring.TERTIARY };\n  }\n}\n```\n\n### Shimmer Layout\n\nConfigurable skeleton loading placeholder:\n\n```typescript\nimport { Component } from 'valdi_core/src/Component';\n\ninterface ShimmerRow {\n  widthRatio: number;\n  height: number;\n  borderRadius?: number;\n}\n\ninterface ShimmerLayoutViewModel {\n  rows: ShimmerRow[];\n  spacing?: number;\n}\n\nexport class ShimmerLayout extends Component<ShimmerLayoutViewModel> {\n  onRender(): void {\n    const spacing = this.viewModel.spacing ?? 8;\n    <view>\n      {this.viewModel.rows.forEach((row, i) => {\n        <AnimationShimmer\n          width={`${row.widthRatio * 100}%`}\n          height={row.height}\n          borderRadius={row.borderRadius ?? 4}\n          marginTop={i > 0 ? spacing : 0}\n        />;\n      })}\n    </view>;\n  }\n}\n```\n\n## Tips\n\n- Start with the highest-severity known patterns — they have the most impact\n- Extract the simplest shared interface first, then iterate to handle edge cases\n- Use render callbacks for module-specific UI instead of trying to generalize everything\n- Run `valdi analyze duplicates --json --output report.json` to save a baseline, then re-run after extraction to measure impact\n","ai-skills/skills/valdi-android/skill.md":"# Android Runtime Rules\n\n**Applies to**: Kotlin/Java files in `/valdi/src/valdi/android/` and related Android runtime code\n\n## Overview\n\nThe Valdi Android runtime bridges TypeScript/Valdi components to native Android views. It's implemented in Kotlin and C++ (via JNI).\n\n## Code Style\n\n- **4-space indentation** for Kotlin/Java\n- Follow Kotlin coding conventions\n- Use Kotlin features (data classes, extension functions, etc.)\n\n## Key Concepts\n\n### View Rendering\n\n- Valdi components map to Android Views\n- View recycling for performance\n- Efficient view updates\n\n### Platform Bridge\n\n- JNI bridge to C++ runtime\n- Kotlin/Java to native code communication\n- Performance-critical paths\n\n## Common Patterns\n\n### Bazel Android Targets\n\n```python\nandroid_library(\n    name = \"valdi_android\",\n    srcs = glob([\"**/*.kt\", \"**/*.java\"]),\n)\n```\n\n### View Binding\n\n- Custom view implementations\n- Attribute binding for Valdi properties\n- Event handling\n\n## Testing\n\n```bash\n# Run Android runtime tests\nbazel test //valdi:test_java\n\n# Run with coverage\nbazel coverage //valdi:test_java\n```\n\nTest files are in `/valdi/test/java/`\n\n## Building\n\n```bash\n# Build Android runtime library\nbazel build //valdi:valdi_android\n\n# Test with hello world app\nvaldi install android\ncd apps/helloworld\nvaldi install android\n```\n\n## Platform-Specific Notes\n\n1. **API Level** - Be mindful of minimum API level\n2. **Android NDK** - C++ integration via NDK\n3. **Permissions** - Handle runtime permissions appropriately\n4. **Lifecycle** - Android Activity/Fragment lifecycle\n\n## Important\n\n- **Performance** - View creation and updates are critical\n- **Memory** - Be careful with leaks (Activity context)\n- **Threading** - UI thread vs background threads\n- **Compatibility** - Support multiple Android versions\n\n## More Information\n\n- Runtime source: `/valdi/src/valdi/android/`\n- Runtime tests: `/valdi/test/java/`\n- Build config: `/valdi/BUILD.bazel`\n- Framework docs: `/AGENTS.md`\n","ai-skills/skills/valdi-cpp-runtime/skill.md":"# C++ Runtime Rules\n\n**Applies to**: C++ files in `/valdi/`, `/valdi_core/`, `/snap_drawing/`\n\n## Overview\n\nValdi's runtime and layout engine are implemented in C++ for cross-platform performance. This code runs on iOS, Android, macOS, but not web.\n\n**Note**: `/libs/` contains shared utility libraries (crypto, logging, image processing) used across the codebase, but not the core runtime itself.\n\n## Code Style\n\n- **4-space indentation**\n- Follow existing C++ style in the codebase\n- Use smart pointers appropriately\n- Prefer const correctness\n\n## Key Concepts\n\n### Layout Engine\n\n- Uses **Yoga** (Facebook's Flexbox implementation) for layout\n- Cross-platform layout calculations\n- Performance-critical code\n- RTL (right-to-left) support built-in\n- Yoga source: `/third-party/yoga/`\n\n### Memory Management\n\n- Be careful with memory ownership\n- Use RAII patterns\n- Consider platform-specific memory constraints (mobile)\n\n### Platform Abstractions\n\n- Code must work on iOS, Android, macOS\n- Use platform-agnostic APIs where possible\n- Platform-specific code goes in appropriate subdirectories\n\n## Common Patterns\n\n### Djinni Generated Code\n\n- Some C++ code is generated from `.djinni` interface files\n- **Don't modify generated code** - change the .djinni file instead\n- Generated files typically in `generated-src/` directories\n\n### Native Bindings Exposed to JS\n\nThe runtime exposes selected native (C++) classes and functions to the JS engine so\nTypeScript can call into performance-critical native code directly.\n\n- **`Base64` namespace** (`coreutils/src/Base64.ts`) is a good example of a\n  native-backed binding: the TS surface (`fromByteArray(uint8, { urlSafe })`,\n  `toByteArray(b64)`, `byteLength(b64)`) delegates to native encode/decode\n  (`Base64Native`) rather than doing the work in JS. Use this shape when a hot codec\n  or transform belongs in native code but needs a clean JS entry point.\n- **`WeakRef`** is supported by the runtime (recently added). JS-side weak references\n  hold their referent without preventing garbage collection — useful for caches and\n  back-references that must not keep objects alive.\n\n**V8 caveat:** some native class bindings (e.g. `Base64` and `Unicode`) are wired for\nQuickJS and may **throw on the V8 runtime**. Don't assume a native binding is present\non every JS engine — guard or feature-check when the same code can run under V8.\n\n### Performance\n\n- This is performance-critical code\n- Profile before optimizing\n- Consider cache locality\n- Be mindful of allocations in hot paths\n\n## Testing\n\n```bash\n# Run all C++ runtime tests\nbazel test //valdi:test\n\n# Run specific test suites\nbazel test //valdi:test_runtime       # Runtime tests\nbazel test //valdi:test_integration   # Integration tests\nbazel test //valdi:test_snap_drawing  # Snap drawing tests\n```\n\n## Platform-Specific Notes\n\n### iOS\n- Objective-C++ bridge in `/valdi/src/valdi/ios/`\n- Metal for GPU rendering\n- UIKit view integration\n\n### Android\n- JNI bridge in `/valdi/src/valdi/android/`\n- NDK integration\n- Native view rendering\n\n### macOS\n- Platform-specific code in `/valdi/src/valdi/macos/`\n- Desktop runtime support\n\n### Build\n\n```bash\n# Build runtime for specific platform\nbazel build //valdi:valdi_ios\nbazel build //valdi:valdi_android\nbazel build //valdi:valdi_macos\n```\n\n## Important\n\n1. **Cross-platform first** - Code must work on all platforms\n2. **Performance critical** - UI rendering and layout\n3. **Memory efficiency** - Mobile devices have constraints\n4. **Thread safety** - Consider concurrency\n\n## More Information\n\n- Runtime overview: `/valdi/README.md`\n- Core bindings: `/valdi_core/README.md`\n- Framework docs: `/AGENTS.md`\n","ai-skills/skills/valdi-custom-view/skill.md":"# Custom View Rules\n\n**Applies to**: `**/*.tsx` files using `<custom-view>` elements.\n\n## `<custom-view>` Element\n\n`<custom-view>` renders a platform-native view inside a Valdi component. Each platform resolves the view by class name.\n\n```typescript\n<custom-view\n  androidClass=\"com.snap.modules.my_module.MyNativeView\"\n  iosClass=\"SCMyNativeView\"\n  macosClass=\"SCMyNativeView\"\n  webClass=\"MyWebViewClassName\"\n  width=\"100%\"\n  height={120}\n/>\n```\n\n## Class Attributes\n\n| Attribute | Platform | Resolution |\n|-----------|----------|------------|\n| `androidClass` | Android | Reflection via `ReflectionViewFactory`; needs `@RegisterAttributesBinder` |\n| `iosClass` | iOS | ObjC class name; must be linked via `ios_deps` |\n| `macosClass` | macOS | `NSClassFromString()`; must be linked via `macos_deps` |\n| `webClass` | Web | Looked up in `WebViewClassRegistry`; registered via `webPolyglotViews` export |\n\n## Platform Resolution\n\n- **Android**: The view class needs a single-arg `(Context)` constructor. An `@RegisterAttributesBinder`-annotated binder is discovered from assets at runtime.\n- **iOS**: The ObjC class must be a `UIView` subclass linked into the binary via `ios_deps`.\n- **macOS**: Falls through to `iosClass` when `macosClass` is not specified — both in TypeScript (`JSXBootstrap.ts`) and C++ (`ViewNode.cpp`). Only specify `macosClass` when the macOS view is different from iOS.\n- **Web**: The `webClass` name is matched against the `WebViewClassRegistry`. Register by exporting `webPolyglotViews` from a typed TypeScript web polyglot entry file compiled via `ts_project` (see `valdi-polyglot-module` skill). Factories return an `AttributeHandler` with `changeAttribute(name, value)` to receive attribute updates.\n\n## viewFactory Pattern\n\nInstead of resolving by class name, you can pass a `ViewFactory` object directly. This is useful when the factory is provided by a parent component or constructed dynamically:\n\n```typescript\nimport { ViewFactory } from 'valdi_tsx/src/ViewFactory';\n\ninterface MyViewModel {\n  viewFactory?: ViewFactory;\n}\n\nclass MyComponent extends Component<MyViewModel> {\n  onRender(): void {\n    if (this.viewModel.viewFactory) {\n      <custom-view style={styles.container} viewFactory={this.viewModel.viewFactory} />;\n    }\n  }\n}\n```\n\n`viewFactory` and `*Class` attributes are mutually exclusive — use one or the other. Specifying both on the same `<custom-view>` is a compile error (the JSXProcessor rejects it). If a view needs a `viewFactory` on one platform and a registered class on another, branch into two sibling `<custom-view>` elements, each with a single mechanism. Prefer gating on whether a `viewFactory` is provided (`if (viewFactory) { … } else { …class… }`) over `Device.isWeb()`: the factory is often supplied by native too, so an `isWeb` branch that routes native to classes can break it. Only keep a platform class you've confirmed actually exists.\n\n## macOS Attribute Binding\n\nmacOS custom views can bind attributes (including callbacks) from TSX using `+bindAttributes:`. The view manager calls this class method at registration time.\n\n```objc\n#import \"valdi/macos/SCValdiMacOSAttributesBinder.h\"\n#import \"valdi/macos/SCValdiMacOSFunction.h\"\n\n@interface SCMyView () {\n    SCValdiMacOSFunction *_onMyCallback;\n}\n@end\n\n@implementation SCMyView\n- (void)valdi_setOnMyCallback:(id)value { _onMyCallback = value; }\n\n+ (void)bindAttributes:(SCValdiMacOSAttributesBinder *)attributesBinder {\n    [attributesBinder bindUntypedAttribute:@\"onMyCallback\"\n                invalidateLayoutOnChange:NO\n                                selector:@selector(valdi_setOnMyCallback:)];\n}\n@end\n```\n\n**Binder methods:**\n- `bindUntypedAttribute:invalidateLayoutOnChange:selector:` — binds any attribute (strings, numbers, functions)\n- `bindColorAttribute:invalidateLayoutOnChange:selector:` — binds color attributes specifically\n\n**Invoking callbacks:** Call `[_onMyCallback performWithParameters:@[@{@\"key\": @\"value\"}]]` — the NSArray/NSDictionary is auto-converted to JS objects.\n\n## Keyboard Input on macOS\n\nValdi doesn't have built-in keyboard event support. Use a polyglot `<custom-view>` with an NSView that:\n1. Overrides `acceptsFirstResponder` → `YES`\n2. Overrides `keyDown:` to map key codes to callback invocations\n3. Calls `[self.window makeFirstResponder:self]` in `viewDidMoveToWindow` to grab focus\n4. Has **non-zero dimensions** (wrap visible content inside it)\n\nSee the `valdi-polyglot-module` skill for a complete macOS attribute binding example with BUILD.bazel wiring.\n\n## Common Mistakes\n\n- Using `<custom-view>` without checking the platform — wrap in `Device.isAndroid()` / `Device.isIOS()` / `Device.isDesktop()` if not all platforms are supported\n- Forgetting to link native implementations — the class name string alone isn't enough; the native code must be compiled and linked via platform `_deps` in BUILD.bazel\n- Wrong package name in `androidClass` — must match the Kotlin/Java package exactly\n- Specifying `macosClass` when it's the same as `iosClass` — omit it and let the fallthrough handle it\n- Using a `filegroup` for `web_deps` — always use `ts_project` with `transpiler = \"tsc\"`\n- Creating a 0×0 custom-view for keyboard focus — it won't become first responder; wrap content inside it\n- **Duplicate ObjC class names across modules** — if two modules define the same native class (e.g. both have `SCKeyboardView`), the linker will fail with \"duplicate symbol\" errors. Always use a unique class name per module (e.g. prefix with module name: `SCSnakeGameKeyboardView`)\n","ai-skills/skills/valdi-component-tests/skill.md":"# Write Valdi Component Tests\n\nWrite unit tests for a Valdi component using standard Valdi test suite patterns.\n\n## Steps\n\n### 1. Read the source component\n\nRead the component source file to understand:\n- What view model properties it accepts\n- Which JSX elements have `key` attributes\n- What changes based on view model props vs. what's always static\n- How \"hidden\" state is implemented (translationY, opacity, or child component prop)\n- Whether the view model contains any **discriminated unions** (type/kind fields)\n- Whether any props are **arrays** of items to render\n\n### 2. Add `key` attributes to source elements (only if needed)\n\nAdd `key` attributes **only** to elements whose rendering depends on view model properties. Never add keys to static content.\n\nGood candidates for keys:\n- The outer container when `hidden` controls `translationY` or `opacity`\n- An `<image>` element whose `src` comes from a view model URL prop\n- An element inside a `when(condition, ...)` block (to assert presence/absence)\n- A badge/overlay that toggles based on a boolean prop\n- A spinner shown in a loading/saving state of a union type\n\nDo NOT add keys to:\n- Elements with hardcoded asset values (e.g., `src={SIGIcon.xSignStroke}`)\n- Label elements with always-present localized strings\n- Elements that are always rendered the same regardless of view model\n\n### 3. Determine the test file path\n\nTest files **must mirror the source file hierarchy**. For example:\n- Source: `src/categories/CollectionComponent.tsx` → Test: `test/categories/CollectionComponentTest.spec.tsx`\n- Source: `src/home_page/OptionPreviewView.tsx` → Test: `test/home_page/OptionPreviewViewTest.spec.tsx`\n- Source: `src/MyComponent.tsx` → Test: `test/MyComponentTest.spec.tsx`\n\n### 4. Write the test file\n\n**Imports:**\n```tsx\nimport { MyComponent } from 'my_module/src/MyComponent';\nimport { componentGetElements } from 'foundation/test/util/componentGetElements';\nimport { componentTypeFind } from 'foundation/test/util/componentTypeFind';\nimport { elementKeyFind } from 'foundation/test/util/elementKeyFind';\nimport { elementTypeFind } from 'foundation/test/util/elementTypeFind';\nimport { findNodeWithKey } from 'foundation/test/util/findNodeWithKey';\nimport { tapNodeWithKey } from 'foundation/test/util/tapNodeWithKey';\nimport 'jasmine/src/jasmine';\nimport { IRenderedElementViewClass } from 'valdi_test/test/IRenderedElementViewClass';\nimport { IComponentTestDriver, valdiIt } from 'valdi_test/test/JSXTestUtils';\nimport { ImageView, View } from 'valdi_tsx/src/NativeTemplateElements';\n```\n\nOnly import what you actually use. `View` is needed when you call `getAttribute` on a non-image element (e.g., for `onTap`, `onVisibilityChanged`). `ImageView` is needed for `src` attribute access.\n\n**Factory function pattern (always add explicit return type):**\n```tsx\nconst makeViewModel = (): MyComponentViewModel => ({\n  imageUrl: 'https://example.com/image.png',\n  isVisible: true,\n  onTap: fail.bind(null, 'onTap should not be called'),\n});\n```\n\nUse `fail.bind` for callbacks in the factory default — tests that need to assert on a callback should declare their own spy and pass it explicitly, rather than relying on the factory.\n\n**Render pattern:**\n```tsx\nconst nodes = driver.render(() => {\n  <MyComponent {...viewModel} />;\n});\n```\n\n### 5. Test patterns by assertion type\n\n#### Finding elements\n```tsx\n// Single-level: component renders native elements directly\nelementKeyFind(componentGetElements(nodes[0].component!), 'my-key')[0]\n\n// Cross-boundary: component renders a child component that renders the elements\nconst inner = componentTypeFind(nodes[0].component as MyComponent, InnerComponent)[0];\nelementKeyFind(componentGetElements(inner), 'my-key')[0]\n\n// Typed (for typed attribute access without casting):\nelementKeyFind<View>(componentGetElements(nodes[0].component!), 'container')[0]\nelementKeyFind<ImageView>(componentGetElements(nodes[0].component!), 'image')[0]\n\n// By element type (e.g. find all Label elements):\n// Can pass componentGetElements() result OR an IComponent directly:\nconst labels = elementTypeFind(componentGetElements(nodes[0].component!), IRenderedElementViewClass.Label);\nconst labels2 = elementTypeFind(nodes[0].component!, IRenderedElementViewClass.Label); // equivalent\nexpect(labels[0]?.getAttribute('value')).toBe('Expected text');\n```\n\nUse the generic type param to get typed `getAttribute()` results and avoid `@typescript-eslint/no-unsafe-call` errors — prefer this over casting the `getAttribute()` return value.\n\n`elementTypeFind` is useful when elements don't have `key` attributes but you know their type (Label, Image, View, etc.). It returns all elements of that type in render order.\n\n#### Hidden via `translationY`\n```tsx\n// hidden=false\nexpect(elementKeyFind<View>(componentGetElements(nodes[0].component!), 'container')[0]?.getAttribute('translationY')).toBe(0);\n\n// hidden=true\nexpect(elementKeyFind<View>(componentGetElements(nodes[0].component!), 'container')[0]?.getAttribute('translationY')).toBe(850);\n```\n\n#### Hidden via `opacity` on native view\n```tsx\n// visible\nexpect(elementKeyFind<View>(componentGetElements(nodes[0].component!), 'my-view')[0]?.getAttribute('opacity')).toBe(1);\n\n// hidden\nexpect(elementKeyFind<View>(componentGetElements(nodes[0].component!), 'my-view')[0]?.getAttribute('opacity')).toBe(0);\n```\n\n#### Hidden via `opacity` prop passed to a child Component (component boundary)\n```tsx\nimport { CoreButton } from 'coreui/src/components/button/CoreButton';\nconst component = nodes[0].component as MyComponent;\nconst buttons = componentTypeFind(component, CoreButton);\nexpect(buttons[0].viewModel.opacity).toBe(1); // or 0\n```\n\n#### View model URL bound to image `src`\n```tsx\nexpect(elementKeyFind<ImageView>(componentGetElements(nodes[0].component!), 'my-image')[0]?.getAttribute('src')).toBe('https://example.com/image.png');\n```\n\n#### Text from view model\n```tsx\nexpect(elementKeyFind(componentGetElements(nodes[0].component!), 'my-label')[0]?.getAttribute('value')).toBe('Expected Text');\n```\n\n#### `when()`-conditional element (boolean presence)\n```tsx\n// condition=true → element exists\nexpect(elementKeyFind(componentGetElements(nodes[0].component!), 'my-element')[0]).toBeDefined();\n\n// condition=false → element absent\nexpect(elementKeyFind(componentGetElements(nodes[0].component!), 'my-element')[0]).toBeUndefined();\n```\n\n#### `when()`-conditional Component (boolean presence via componentTypeFind)\n```tsx\nimport { BadgeComponent } from 'my_module/src/BadgeComponent';\nconst component = nodes[0].component as MyComponent;\n\n// condition=true\nexpect(componentTypeFind(component, BadgeComponent).length).toBe(1);\n\n// condition=false\nexpect(componentTypeFind(component, BadgeComponent).length).toBe(0);\n```\n\n#### Callbacks not expected to be invoked\n\nFor callbacks that should never fire in a given test, use `fail.bind(null, '...')` instead of `jasmine.createSpy`. This causes the test to immediately fail with a clear message if the callback is accidentally triggered:\n\n```tsx\n<MyComponent\n  onSelect={fail.bind(null, 'onSelect should not be called')}\n  onLoad={onLoad}\n/>\n```\n\n`fail.bind(null, 'message')` is a plain function call (not an inline lambda), so it satisfies `jsx-no-lambda`. It is assignable to any callback type since TypeScript allows functions with fewer parameters.\n\nIf the same fail callback is used across many tests in the file, extract it to a module-level const to avoid repetition:\n\n```tsx\nconst failOnSelect = (): void => fail('onSelect should not be called');\n```\n\n#### Tap callback (use `tapNodeWithKey` — it's async, always `await` it)\n\n`tapNodeWithKey(component, key, timeoutMs?, intervalMs?)` accepts `IComponent | IRenderedElement`.\n\n```tsx\nconst onTap = jasmine.createSpy('onTap');\nconst nodes = driver.render(() => {\n  <MyComponent onTap={onTap} />;\n});\nawait tapNodeWithKey(nodes[0].component!, 'my-button');\nexpect(onTap).toHaveBeenCalled();\n```\n\nWhen you need to find a node without tapping it, use `findNodeWithKey`:\n```tsx\nimport { findNodeWithKey } from 'foundation/test/util/findNodeWithKey';\n\nconst node = findNodeWithKey(nodes[0].component!, 'my-button')[0];\nexpect(node).toBeDefined();\n```\n\nWhen the callback receives arguments (e.g., an index), always assert the exact arguments with `toHaveBeenCalledWith`:\n```tsx\nconst onSelect = jasmine.createSpy('onSelect');\n// ... render and trigger ...\nexpect(onSelect).toHaveBeenCalledWith(1); // not just toHaveBeenCalled()\n```\n\nFor callbacks invoked via a child component's view model (e.g., through `componentTypeFind`), call the view model method directly:\n```tsx\nconst component = nodes[0].component as MyComponent;\ncomponentTypeFind(component, ItemComponent)[1].viewModel.onTap();\nexpect(onSelect).toHaveBeenCalledWith(1);\n```\n\n#### `onTap` retrieved via `getAttribute` (non-tappable element pattern)\n```tsx\n// Use elementKeyFind<View> so getAttribute('onTap') is typed — no cast needed\nconst el = elementKeyFind<View>(componentGetElements(nodes[0].component!), 'my-element')[0];\nel?.getAttribute('onTap')?.();\nexpect(onTap).toHaveBeenCalled();\n```\n\n#### `onVisibilityChanged` callback\n\n`View.onVisibilityChanged` signature is `(isVisible: boolean, eventTime: EventTime)` where `EventTime = number`. Always pass both args when invoking:\n\n```tsx\nconst el = elementKeyFind<View>(componentGetElements(nodes[0].component!), 'container')[0];\nel?.getAttribute('onVisibilityChanged')?.(true, 0);\n```\n\n**Asserting the callback:** depends on how the component wires the handler:\n\n- If the component **wraps** the viewModel callback (e.g., `onVisibilityChanged={(v) => vm.onVisibilityChanged(v)}`), `toHaveBeenCalledWith(true)` works:\n  ```tsx\n  expect(onVisibilityChanged).toHaveBeenCalledWith(true);\n  ```\n\n- If the component **directly assigns** the viewModel callback (e.g., `onVisibilityChanged={this.viewModel.onVisibilityChanged}`), the spy receives both args. If the spy is typed as `(isVisible: boolean) => void`, `toHaveBeenCalledWith(true, ...)` is a TypeScript error. Check the first arg via `calls`:\n  ```tsx\n  const spy = viewModel.onVisibilityChanged as jasmine.Spy;\n  expect(spy).toHaveBeenCalled();\n  expect(spy.calls.mostRecent().args[0]).toBe(true);\n  ```\n  If the spy is an untyped `jasmine.createSpy()`, you can use `toHaveBeenCalledWith(true, 0)` directly.\n\n### 6. Discriminated union state testing\n\nWhen a view model contains a discriminated union (e.g., `type: 'LOADING' | 'CONTENT' | 'ERROR'`), **test every branch**. For each state:\n1. Assert which elements/components ARE rendered\n2. Assert which elements/components from OTHER states are NOT rendered\n\n```tsx\n// LOADING state: spinner present, action button absent\nvaldiIt('Verify spinner is shown in loading state', async driver => {\n  const nodes = driver.render(() => {\n    <MyComponent button={{ type: ButtonType.LOADING }} />;\n  });\n  expect(elementKeyFind(componentGetElements(nodes[0].component!), 'loading-spinner')[0]).toBeDefined();\n  expect(elementKeyFind(componentGetElements(nodes[0].component!), 'action-button')[0]).toBeUndefined();\n});\n\n// CONTENT state: action button present, no spinner\nvaldiIt('Verify action button is shown in content state', async driver => {\n  const nodes = driver.render(() => {\n    <MyComponent button={{ type: ButtonType.PURCHASE, price: '$9.99', onTap }} />;\n  });\n  expect(elementKeyFind(componentGetElements(nodes[0].component!), 'action-button')[0]).toBeDefined();\n  expect(elementKeyFind(componentGetElements(nodes[0].component!), 'loading-spinner')[0]).toBeUndefined();\n});\n```\n\n### 7. Array view model testing\n\nWhen a component renders a list from an array prop, test three cases:\n1. **Empty array** — assert 0 items render\n2. **Single item** — assert 1 item renders\n3. **Multiple items** — assert item count matches array length\n\nUse `componentTypeFind(component, ItemComponent)` or `elementKeyFind` with indexed keys (e.g., `tile-0`, `tile-1`) to count rendered items.\n\n```tsx\nvaldiIt('Verify no items render when array is empty', async driver => {\n  const emptyOptions: OptionViewModel[] = [];\n  const nodes = driver.render(() => {\n    <MyComponent items={emptyOptions} />;\n  });\n  const component = nodes[0].component as MyComponent;\n  expect(componentTypeFind(component, ItemComponent).length).toBe(0);\n});\n\nvaldiIt('Verify item count matches array length', async driver => {\n  const threeItems: OptionViewModel[] = [makeItem('a'), makeItem('b'), makeItem('c')];\n  const nodes = driver.render(() => {\n    <MyComponent items={threeItems} />;\n  });\n  const component = nodes[0].component as MyComponent;\n  expect(componentTypeFind(component, ItemComponent).length).toBe(3);\n});\n```\n\nNote: Extract array literals to local `const` variables before using in JSX (required by `@snapchat/valdi/jsx-no-lambda`).\n\n### 8. Component boundary traversal\n\nWhen a component's `onRender()` only renders child components (not native elements), `componentGetElements(component)` returns `[]`. You must get the child component first:\n\n```tsx\nimport { componentGetElements } from 'foundation/test/util/componentGetElements';\nimport { componentTypeFind } from 'foundation/test/util/componentTypeFind';\nimport { elementKeyFind } from 'foundation/test/util/elementKeyFind';\n\nconst component = nodes[0].component as OuterComponent;\nconst inner = componentTypeFind(component, InnerComponent)[0];\nconst container = elementKeyFind<View>(componentGetElements(inner), 'container')[0];\nexpect(container?.getAttribute('translationY')).toBe(0);\n```\n\n### 9. Extract render and find helpers for readability\n\nFor larger test files, extract repeated render + find logic into helper functions. This keeps individual tests focused:\n\n```tsx\n// Extract rendering\nconst renderComponent = (driver: IComponentTestDriver, overrides?: Partial<MyComponentViewModel>) => {\n  const vm = { ...makeViewModel(), ...overrides };\n  return driver.render(() => { <MyComponent {...vm} />; })[0].component as MyComponent;\n};\n\n// Extract finding\nconst getImage = (component: MyComponent) =>\n  elementKeyFind<ImageView>(componentGetElements(component), 'image')[0];\n\n// Tests become clean:\nvaldiIt('Verify imageUrl is bound', async driver => {\n  expect(getImage(renderComponent(driver))?.getAttribute('src')).toBe('https://example.com/image.png');\n});\n```\n\n### 10. Lint rules to follow\n\n- **`explicit-function-return-type`**: Always add explicit return types to factory functions: `const makeViewModel = (): MyViewModel => ({...})`\n- **`jsx-no-lambda`**: Never assign inline array literals directly in JSX props. Extract to a local `const` first:\n  ```tsx\n  // WRONG\n  <MyComponent items={[makeItem('a')]} />;\n\n  // CORRECT\n  const items: ItemViewModel[] = [makeItem('a')];\n  <MyComponent items={items} />;\n  ```\n- **`no-unsafe-call`**: Use the generic type parameter on `elementKeyFind<T>` to get typed `getAttribute()` results rather than casting: `elementKeyFind<View>(...)` gives typed access to `onTap`, `onVisibilityChanged`, etc.\n- **`import/order`**: Keep imports sorted alphabetically by path.\n\n### 11. Key principle\n\n**Only assert on things that change based on view model props.** Every test should have a clear \"when X is Y, then Z\" story. If the UI looks the same regardless of the prop, skip it.\n\nFor union types, a test that only verifies \"the component renders without error\" in a given state is not sufficient — assert the meaningful structural difference that state introduces.\n\n## Example test file structure\n\n```tsx\nimport { MyComponent } from 'my_module/src/MyComponent';\nimport { MyComponentViewModel } from 'my_module/src/MyComponentViewModel';\nimport { ChildComponent } from 'my_module/src/ChildComponent';\nimport { componentGetElements } from 'foundation/test/util/componentGetElements';\nimport { componentTypeFind } from 'foundation/test/util/componentTypeFind';\nimport { elementKeyFind } from 'foundation/test/util/elementKeyFind';\nimport { tapNodeWithKey } from 'foundation/test/util/tapNodeWithKey';\nimport 'jasmine/src/jasmine';\nimport { IComponentTestDriver, valdiIt } from 'valdi_test/test/JSXTestUtils';\nimport { ImageView, View } from 'valdi_tsx/src/NativeTemplateElements';\n// elementTypeFind + IRenderedElementViewClass for finding elements by type (no key needed):\n// import { elementTypeFind } from 'foundation/test/util/elementTypeFind';\n// import { IRenderedElementViewClass } from 'valdi_test/test/IRenderedElementViewClass';\n\nconst makeViewModel = (): MyComponentViewModel => ({\n  imageUrl: 'https://example.com/image.png',\n  isVisible: true,\n  onTap: fail.bind(null, 'onTap should not be called'),\n});\n\ndescribe('MyComponentTest', () => {\n  valdiIt('Verify visible when isVisible is true', async driver => {\n    const nodes = driver.render(() => {\n      <MyComponent {...makeViewModel()} />;\n    });\n    expect(elementKeyFind<View>(componentGetElements(nodes[0].component!), 'container')[0]?.getAttribute('opacity')).toBe(1);\n  });\n\n  valdiIt('Verify hidden when isVisible is false', async driver => {\n    const nodes = driver.render(() => {\n      <MyComponent {...{ ...makeViewModel(), isVisible: false }} />;\n    });\n    expect(elementKeyFind<View>(componentGetElements(nodes[0].component!), 'container')[0]?.getAttribute('opacity')).toBe(0);\n  });\n\n  valdiIt('Verify imageUrl is bound to image src', async driver => {\n    const nodes = driver.render(() => {\n      <MyComponent {...makeViewModel()} />;\n    });\n    expect(elementKeyFind<ImageView>(componentGetElements(nodes[0].component!), 'image')[0]?.getAttribute('src')).toBe('https://example.com/image.png');\n  });\n\n  valdiIt('Verify onTap is called when tapped', async driver => {\n    const onTap = jasmine.createSpy('onTap');\n    const nodes = driver.render(() => {\n      <MyComponent {...{ ...makeViewModel(), onTap }} />;\n    });\n    await tapNodeWithKey(nodes[0].component!, 'button');\n    expect(onTap).toHaveBeenCalled();\n  });\n\n  valdiIt('Verify ChildComponent is present when condition is true', async driver => {\n    const nodes = driver.render(() => {\n      <MyComponent {...{ ...makeViewModel(), showChild: true }} />;\n    });\n    const component = nodes[0].component as MyComponent;\n    expect(componentTypeFind(component, ChildComponent).length).toBe(1);\n  });\n\n  valdiIt('Verify ChildComponent is absent when condition is false', async driver => {\n    const nodes = driver.render(() => {\n      <MyComponent {...{ ...makeViewModel(), showChild: false }} />;\n    });\n    const component = nodes[0].component as MyComponent;\n    expect(componentTypeFind(component, ChildComponent).length).toBe(0);\n  });\n});\n```\n","ai-skills/skills/valdi-gestures/skill.md":"# Valdi Gestures\n\nTouch and gesture handling: tap, long press, drag, pinch, and rotate.\n\n## When to use\n\nWhen handling user touch interactions beyond basic `onTap` — drag-to-reorder, pinch-to-zoom, long press menus, custom gesture recognition.\n\n## Key concepts\n\nValdi gesture handling is **attribute-based on `<view>` elements**. Each gesture type has three attributes:\n- `on<Gesture>` — the callback\n- `on<Gesture>Disabled` — boolean to disable the gesture (the gesture is enabled by default once a callback is set; there is no `<gesture>Enabled` attribute)\n- `on<Gesture>Predicate` — function returning boolean, controls whether the gesture should fire\n\nThe topmost visible element with a matching gesture wins. Conflicting gestures on parent elements are suppressed.\n\n## TouchEvent interface\n\nAll gesture callbacks receive a `TouchEvent` (or extension of it):\n\n```tsx\ninterface TouchEvent {\n  x: number;          // X relative to the element\n  y: number;          // Y relative to the element\n  absoluteX: number;  // X relative to root Valdi view\n  absoluteY: number;  // Y relative to root Valdi view\n  pointerCount: number;\n  pointerLocations: Pointer[];\n}\n```\n\n## Basic gestures\n\n### onTap\n\n```tsx\n// ✅ Simple tap handler\n<view onTap={this.handleTap}>\n  <label value=\"Tap me\" />\n</view>\n\nprivate handleTap = (event: TouchEvent) => {\n  console.log(`Tapped at ${event.x}, ${event.y}`);\n};\n```\n\n### onDoubleTap\n\n```tsx\n// ✅ Double tap to zoom\n<view onDoubleTap={this.handleDoubleTap}>\n  <image src={this.viewModel.photo} />\n</view>\n```\n\n### onLongPress\n\n```tsx\n// ✅ Long press with custom duration\n<view\n  onLongPress={this.handleLongPress}\n  longPressDuration={0.5}  // seconds before trigger\n>\n  <label value=\"Hold me\" />\n</view>\n```\n\n## Continuous gestures\n\n### onDrag\n\n```tsx\n// ✅ Drag to move — fires continuously during the drag\n<view onDrag={this.handleDrag}>\n  <label value=\"Drag me\" />\n</view>\n\nprivate handleDrag = (event: DragEvent) => {\n  // event extends TouchEvent with drag-specific data\n  this.cardRef.setAttribute('translateX', event.x);\n  this.cardRef.setAttribute('translateY', event.y);\n};\n```\n\n### onPinch\n\n```tsx\n// ✅ Pinch to zoom — requires 2+ pointers\n<view onPinch={this.handlePinch}>\n  <image src={this.viewModel.photo} ref={this.imageRef} />\n</view>\n\nprivate handlePinch = (event: PinchEvent) => {\n  this.imageRef.setAttribute('scale', event.scale);\n};\n```\n\n### onRotate\n\n```tsx\n// ✅ Rotate with two fingers — requires 2+ pointers\n<view onRotate={this.handleRotate}>\n  <image src={this.viewModel.sticker} ref={this.stickerRef} />\n</view>\n```\n\n## Simultaneous gestures\n\n`onDrag`, `onPinch`, and `onRotate` can run simultaneously. This enables multitouch interactions like pinch-zoom-rotate on a photo:\n\n```tsx\n// ✅ Combined pinch + drag + rotate on same element\n<view\n  onDrag={this.handleDrag}\n  onPinch={this.handlePinch}\n  onRotate={this.handleRotate}\n>\n  <image src={this.viewModel.photo} ref={this.photoRef} />\n</view>\n```\n\n## Touch area extension\n\nExpand the tappable area beyond the visible bounds:\n\n```tsx\n// ✅ Small button with larger touch target\n<view\n  onTap={this.handleTap}\n  touchAreaExtension={10}  // extends hit area by 10pt in each direction\n  width={24} height={24}\n>\n  <image src={SIGIcon.checkmarkFill} />\n</view>\n```\n\n## Touch delay in scroll views\n\nInside a `<scroll>`, touches are delayed to avoid conflicts with scrolling:\n\n```tsx\n// ✅ Delay touch recognition inside scrollable content\n<view\n  onTouch={this.handleTouch}\n  onTouchDelayDuration={0.15}  // seconds before onTouch fires during scroll\n/>\n```\n\n## Gesture predicates\n\nControl whether a gesture should activate:\n\n```tsx\n// ✅ Only allow drag when in edit mode\n<view\n  onDrag={this.handleDrag}\n  onDragPredicate={() => this.state.editMode}\n/>\n```\n\n## onTouch (raw)\n\nLow-level touch events that fire on all elements in the hierarchy:\n\n```tsx\n// ✅ Raw touch tracking — fires for all touch phases\n<view onTouch={this.handleTouch}>\n  <label value=\"Touch me\" />\n</view>\n\nprivate handleTouch = (event: TouchEvent) => {\n  // Fires on begin, move, end, cancel\n};\n```\n\nUnlike other gestures, `onTouch` propagates to parents/siblings — it doesn't suppress other handlers.\n\n## Common mistakes\n\n```tsx\n// ❌ WRONG — React-style event handlers\n<div onClick={handler} />        // ❌ No <div>, no onClick\n<button onMouseDown={handler} /> // ❌ No mouse events\n\n// ❌ WRONG — addEventListener\nelement.addEventListener('touchstart', handler);  // ❌\n\n// ❌ WRONG — Gesture on <layout> (use <view>)\n<layout onTap={handler} />  // ❌ Gestures go on <view> elements\n\n// ✅ Correct — gestures on <view>\n<view onTap={handler}>\n  <layout>\n    <label value=\"Content\" />\n  </layout>\n</view>\n```\n","ai-skills/skills/valdi-inspect/skill.md":"# valdi-inspect\n\nUse this skill when you need to inspect a **live running Valdi app** — to see what components are on screen, traverse the rendered virtual node tree, capture element screenshots, or check JS heap usage.\n\n## When to use\n\n- User asks \"what's on screen?\", \"what component is rendering here?\", \"what contexts are active?\"\n- You need to diagnose a layout or rendering issue in a running app\n- You want to verify a component tree change after a hot-reload\n- User asks about memory usage or wants a JS heap dump\n\n## Prerequisites\n\nThe Valdi app must be **running on a connected device or emulator**. No hot-reloader required — the CLI connects directly to the app's DebuggerService.\n\n**Port reference** — determined by how the app is built, not the OS:\n\n| Target | Port | When |\n|--------|------|------|\n| iOS in-app (e.g. Snapchat) | **13592** (default) | `isStandalone = false` |\n| Android in-app | **13592** (default) | `isStandalone = false` |\n| macOS Valdi app | **13591** | `isStandalone = true` |\n| Standalone runner / CLI runner | **13591** | `isStandalone = true` |\n\nFor **Android**, `adb forward` runs automatically. For **iOS simulator**, connect directly to `localhost`.\n\nAll commands output **JSON by default** (for AI parsing). Add `--pretty` for human-readable output.\n\n---\n\n## Commands\n\n### `valdi inspect status`\n\nCheck whether the Valdi daemon is reachable and how many devices are connected.\n\n```\nvaldi inspect status\nvaldi inspect status --pretty\n```\n\nOutput: `{ connected: bool, port: number, connectedDevices: number, portName: string }`\n\n---\n\n### `valdi inspect devices`\n\nList all devices connected to the Valdi daemon.\n\n```\nvaldi inspect devices\nvaldi inspect devices --pretty\n```\n\nOutput: `[{ client_id, platform, application_id }, ...]`\n\nUse `client_id` values with `--client` in other commands. If only one device is connected, other commands auto-select it.\n\n---\n\n### `valdi inspect select`\n\nInteractively choose a device and save it to `~/.valdi-inspect.json`. Subsequent commands use this device without prompting.\n\n```\nvaldi inspect select\n```\n\n---\n\n### `valdi inspect contexts`\n\nList all active root components (contexts) on the device. A **context** is one mounted Valdi component tree — typically one per screen or overlay.\n\n```\nvaldi inspect contexts\nvaldi inspect contexts --pretty\nvaldi inspect contexts --client <client_id>\n```\n\nOutput: `[{ id: string, rootComponentName: string }, ...]`\n\nThe `id` is the **contextId** used by `tree` and `snapshot`. If there is only one context, commands that take a contextId will auto-select it.\n\n---\n\n### `valdi inspect tree [contextId]`\n\nGet the full rendered virtual node tree for a context. If `contextId` is omitted, auto-selects the single context or prompts when there are multiple.\n\n```\nvaldi inspect tree\nvaldi inspect tree <contextId>\nvaldi inspect tree <contextId> --pretty\nvaldi inspect tree <contextId> --max-depth <n>\n```\n\nOutput: a nested tree where each node has:\n- `tag`: element type (e.g. `\"view\"`, `\"label\"`, `\"image\"`, or a component name)\n- `key`: the Valdi TSX key assigned in source\n- `element.id`: numeric element ID (use this for `snapshot`)\n- `element.frame`: `{ x, y, width, height }` in screen coordinates\n- `element.attributes`: rendered prop values (style, value, etc.)\n- `children`: array of child nodes (same shape)\n- `_childrenTrimmed`: count of trimmed children when `--max-depth` is used\n\n**Tip**: Use `--max-depth 3` or `--max-depth 4` first to get a summary, then drill in.\n\n---\n\n### `valdi inspect snapshot <elementId> [contextId]`\n\nCapture a screenshot of a specific element and save it as a PNG. If `contextId` is omitted, auto-selects or prompts.\n\n```\nvaldi inspect snapshot <elementId>\nvaldi inspect snapshot <elementId> <contextId>\nvaldi inspect snapshot <elementId> <contextId> --output /tmp/my-snap.png\n```\n\nOutput: `{ path: \"/var/folders/.../valdi-snapshot-<elementId>.png\" }`\n\nThe PNG is written to disk. Read it with the `Read` tool to view it visually.\n\n**Getting elementId**: Use `element.id` from the `tree` output (a number, e.g. `1`, `5`, `42`).\n\n#### Using `--key` instead of numeric IDs (recommended)\n\nInstead of looking up numeric element IDs from the tree, use the `--key` flag to resolve by component tag name or element `key` prop:\n\n```\nvaldi inspect snapshot --key PreviewRoot\nvaldi inspect snapshot --key weather-card --port 13591\n```\n\nThe `--key` flag searches the component tree for a matching tag name or `key` prop and resolves it to the element ID automatically.\n\n**This is stable across hot-reloads** — element IDs change on every reload, but component names and `key` props stay the same. Use `--key` instead of numeric IDs to avoid re-querying the tree after each hot-reload cycle.\n\nCustom keys can be set on any element in TSX:\n```tsx\n<view key=\"weather-card\" style={styles.card}>\n```\n\n---\n\n### `valdi inspect heap`\n\nDump JS heap statistics for the connected device.\n\n```\nvaldi inspect heap\nvaldi inspect heap --pretty\nvaldi inspect heap --gc\n```\n\nOptions:\n- `--gc`: run garbage collection before dumping (more accurate free memory reading)\n\nOutput: `{ memoryUsageBytes: number, heapDumpJSON: string }`\n\n`heapDumpJSON` is a serialized heap snapshot in V8 format. Note: heap dumps can crash small/toy apps — this works best on full production apps.\n\n---\n\n## Common workflows\n\n### \"What is rendering on screen right now?\"\n\n```\nvaldi inspect tree --pretty\n```\n\n(contextId is auto-selected if there's only one)\n\n### \"Show me what a specific component looks like\"\n\n1. Run `valdi inspect tree --pretty` to find the node — note its `element.id`\n2. Run `valdi inspect snapshot <element.id>` to capture it\n3. Read the PNG with the `Read` tool\n\n### \"Diagnose a layout issue at position X,Y\"\n\nRun `valdi inspect tree --pretty` and look for `element.frame` values overlapping with the target position.\n\n### \"Check memory usage\"\n\n```\nvaldi inspect heap --pretty\n```\n\n### \"Verify a hot-reload applied correctly\"\n\nRun `valdi inspect tree` before and after the edit, compare the JSON output.\n\n---\n\n## Options reference\n\n| Option | Commands | Description |\n|--------|----------|-------------|\n| `--port <n>` | all | Daemon TCP port (default: 13592 mobile, 13591 standalone) |\n| `--client <id>` | contexts, tree, snapshot, heap | Target a specific connected device |\n| `--pretty` | devices, status, contexts, tree, heap | Human-readable output |\n| `--max-depth <n>` | tree | Trim tree to N levels deep |\n| `--output <path>` | snapshot | Custom PNG output path |\n| `--gc` | heap | GC before heap dump |\n\n---\n\n## Troubleshooting\n\n**\"Valdi daemon not running on port 13592\"** — Make sure the app is running. macOS Valdi apps and standalone runners use port 13591: add `--port 13591`.\n\n**\"No devices connected\"** — Make sure the Valdi app is running. For Android, ensure a device or emulator is connected via `adb`.\n\n**\"Timeout waiting for device response\"** — The app may be frozen or the JS runtime may be busy. Try again after a moment.\n","ai-skills/skills/valdi-error-handling/skill.md":"# Valdi Error Handling\n\nError handling, promise rejection, and lifecycle safety patterns.\n\n## When to use\n\nWhen dealing with async operations, network requests, component lifecycle boundaries, or any code that can fail at runtime.\n\n## Key concepts\n\nValdi runs on a JavaScript runtime (Hermes/V8) with native platform integration. Unhandled errors crash the app. The primary error sources are:\n\n1. **Async operations** completing after component destruction\n2. **Network failures** from HTTPClient\n3. **Uncaught promise rejections**\n4. **Type mismatches** from native/JS boundary\n\n## CancelablePromise — the first line of defense\n\nMost Valdi errors stem from async work completing after a component is destroyed. `CancelablePromise` + `registerDisposable` prevents this entirely:\n\n```tsx\nimport { promiseToCancelablePromise } from 'valdi_core/src/CancelablePromise';\n\n// ✅ Safe async — cancelled on component destruction\nonCreate() {\n  const cancelable = promiseToCancelablePromise(this.fetchData(), () => {});\n  this.registerDisposable(() => cancelable.cancel?.());\n  cancelable.then(\n    data => { this.setState({ data, loading: false }); },\n    error => { this.setState({ error: error.message, loading: false }); },\n  );\n}\n```\n\n```tsx\n// ❌ WRONG — Raw promise with no lifecycle safety\nonCreate() {\n  this.fetchData().then(data => {\n    this.setState({ data });  // Logs a console.error and no-ops if destroyed\n  });\n}\n```\n\nSee the `valdi-async` skill for comprehensive CancelablePromise patterns.\n\n## HTTPClient error handling\n\n`HTTPClient` is an instance, not static. Methods return `CancelablePromise<HTTPResponse>`.\n\n```tsx\nimport { HTTPClient } from 'valdi_http/src/HTTPClient';\n\nprivate httpClient = new HTTPClient('https://api.example.com');\n\n// ✅ Handle network errors with lifecycle safety\nprivate fetchData = () => {\n  const cancelable = this.httpClient.get('/data');\n  this.registerDisposable(() => cancelable.cancel?.());\n  cancelable.then(\n    response => {\n      if (response.statusCode !== 200) {\n        this.setState({ error: `HTTP ${response.statusCode}`, loading: false });\n        return;\n      }\n      const decoder = new TextDecoder();\n      const data = JSON.parse(decoder.decode(response.body));\n      this.setState({ data, loading: false });\n    },\n    error => {\n      this.setState({ error: (error as Error).message, loading: false });\n    },\n  );\n};\n```\n\n`HTTPResponse` shape: `{ statusCode: number, headers: StringMap<string>, body?: Uint8Array }`. Decode `body` with `TextDecoder` before parsing.\n\n## isDestroyed() guard\n\nFor callbacks that can't use CancelablePromise (e.g., native callbacks, timers):\n\n```tsx\n// ✅ Guard against use-after-destroy\nprivate onNativeCallback = (result: string) => {\n  if (this.isDestroyed()) return;\n  this.setState({ result });\n};\n```\n\n`isDestroyed()` is a method — always call with parentheses.\n\n## try/catch in async methods\n\n```tsx\nimport { promiseToCancelablePromise } from 'valdi_core/src/CancelablePromise';\n\n// ✅ Catch errors from async operations\nprivate loadProfile = () => {\n  const cancelable = promiseToCancelablePromise(\n    (async () => {\n      try {\n        const profile = await this.api.getProfile();\n        return { profile, error: undefined };\n      } catch (e) {\n        return { profile: undefined, error: (e as Error).message };\n      }\n    })(),\n    () => {},\n  );\n  this.registerDisposable(() => cancelable.cancel?.());\n\n  cancelable.then(result => {\n    this.setState({\n      profile: result.profile,\n      error: result.error,\n      loading: false,\n    });\n  });\n};\n```\n\n## Error states in UI\n\n```tsx\n// ✅ Show error state to the user\nonRender(): void {\n  if (this.state.loading) {\n    <AnimationShimmer width=\"100%\" height={200} />;\n    return;\n  }\n\n  if (this.state.error) {\n    <view alignItems=\"center\" justifyContent=\"center\" padding={16}>\n      <label value=\"Something went wrong\" font={systemBoldFont(16)} />\n      <label value={this.state.error} color=\"#999\" />\n      <CoreButton text=\"Retry\" onTap={this.handleRetry} />\n    </view>;\n    return;\n  }\n\n  <label value={this.state.data.name} />;\n}\n```\n\n## Defensive ViewModel access\n\nViewModels come from parent components and may have optional fields:\n\n```tsx\n// ✅ Defensive access with defaults\nonRender(): void {\n  const title = this.viewModel.title ?? 'Untitled';\n  const items = this.viewModel.items ?? [];\n\n  <view>\n    <label value={title} />\n    {items.forEach(item => { <label value={item.name} />; })}\n  </view>;\n}\n```\n\n## Timer cleanup\n\n```tsx\n// ✅ Use setTimeoutDisposable for automatic cleanup\nonCreate() {\n  this.setTimeoutDisposable(this.tick, 1000);\n}\n```\n\n```tsx\n// ✅ Manual cleanup for setInterval\nprivate timerId?: number;\n\nonCreate() {\n  this.timerId = setInterval(this.tick, 1000);\n}\n\nonDestroy() {\n  if (this.timerId !== undefined) {\n    clearInterval(this.timerId);\n  }\n}\n```\n\nOr use `registerDisposable` with a disposable wrapper — see `valdi-async` skill.\n\n## Common mistakes\n\n```tsx\n// ❌ WRONG — HTTPClient is not static\nconst data = await HTTPClient.send(request);  // No static send() method\n\n// ✅ Correct — instantiate then call methods\nconst client = new HTTPClient('https://api.example.com');\nconst cancelable = client.get('/data');\n\n// ❌ WRONG — .catch() is not available on CancelablePromise\ncancelable.catch(error => { ... });  // CancelablePromise only has .then()\n\n// ✅ Correct — use second argument of .then() for error handling\ncancelable.then(\n  response => { /* success */ },\n  error => { /* failure */ },\n);\n\n// ❌ WRONG — React error boundaries don't exist\n<ErrorBoundary fallback={<Error />}>  // Not a Valdi concept\n  <MyComponent />\n</ErrorBoundary>\n\n// ❌ WRONG — window.onerror / global error handlers\nwindow.addEventListener('error', handler);  // No window object\n\n// ❌ WRONG — isDestroyed without parentheses\nif (this.isDestroyed) return;  // It's a method, not a property\n\n// ✅ Correct\nif (this.isDestroyed()) return;\n\n// ❌ WRONG — Passing CancelablePromise directly to registerDisposable\nthis.registerDisposable(cancelable);  // Wrong type\n\n// ✅ Correct — wrap cancel in a callback\nthis.registerDisposable(() => cancelable.cancel?.());\n```\n\n## Summary\n\n| Pattern | When to use |\n|---------|------------|\n| `promiseToCancelablePromise` + `registerDisposable` | Any async work in a component |\n| `isDestroyed()` guard | Native callbacks, external event handlers |\n| `try/catch` | Operations that can throw |\n| Error state in UI | User-facing failure recovery |\n| `onDestroy` cleanup | Timers, subscriptions, manual resources |\n| `setTimeoutDisposable` | One-shot timers (auto-cleaned) |\n","ai-skills/skills/valdi-ios/skill.md":"# valdi-ios\n\nPath: `ai-skills/skills/valdi-ios/skill.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Snapchat/Valdi/HEAD/ai-skills/skills/valdi-ios/skill.md)","ai-skills/skills/valdi-localization/skill.md":"# valdi-localization\n\nPath: `ai-skills/skills/valdi-localization/skill.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Snapchat/Valdi/HEAD/ai-skills/skills/valdi-localization/skill.md)","ai-skills/skills/valdi-migrate/skill.md":"# valdi-migrate\n\nPath: `ai-skills/skills/valdi-migrate/skill.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Snapchat/Valdi/HEAD/ai-skills/skills/valdi-migrate/skill.md)","ai-skills/skills/valdi-mock-breakdown/skill.md":"# valdi-mock-breakdown\n\nPath: `ai-skills/skills/valdi-mock-breakdown/skill.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Snapchat/Valdi/HEAD/ai-skills/skills/valdi-mock-breakdown/skill.md)","ai-skills/skills/valdi-mock-iterate/skill.md":"# valdi-mock-iterate\n\nPath: `ai-skills/skills/valdi-mock-iterate/skill.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Snapchat/Valdi/HEAD/ai-skills/skills/valdi-mock-iterate/skill.md)","ai-skills/skills/valdi-mock-to-module/skill.md":"# valdi-mock-to-module\n\nPath: `ai-skills/skills/valdi-mock-to-module/skill.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Snapchat/Valdi/HEAD/ai-skills/skills/valdi-mock-to-module/skill.md)","ai-skills/skills/valdi-navigation/skill.md":"# valdi-navigation\n\nPath: `ai-skills/skills/valdi-navigation/skill.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Snapchat/Valdi/HEAD/ai-skills/skills/valdi-navigation/skill.md)","ai-skills/skills/valdi-overview/skill.md":"# valdi-overview\n\nPath: `ai-skills/skills/valdi-overview/skill.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Snapchat/Valdi/HEAD/ai-skills/skills/valdi-overview/skill.md)","ai-skills/skills/valdi-perf/skill.md":"# valdi-perf\n\nPath: `ai-skills/skills/valdi-perf/skill.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Snapchat/Valdi/HEAD/ai-skills/skills/valdi-perf/skill.md)","ai-skills/skills/valdi-polyglot-module/skill.md":"# valdi-polyglot-module\n\nPath: `ai-skills/skills/valdi-polyglot-module/skill.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Snapchat/Valdi/HEAD/ai-skills/skills/valdi-polyglot-module/skill.md)","ai-skills/skills/valdi-pr-review/skill.md":"# valdi-pr-review\n\nPath: `ai-skills/skills/valdi-pr-review/skill.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Snapchat/Valdi/HEAD/ai-skills/skills/valdi-pr-review/skill.md)","ai-skills/skills/valdi-preview/skill.md":"# valdi-preview\n\nPath: `ai-skills/skills/valdi-preview/skill.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Snapchat/Valdi/HEAD/ai-skills/skills/valdi-preview/skill.md)","ai-skills/skills/valdi-profile/skill.md":"# valdi-profile\n\nPath: `ai-skills/skills/valdi-profile/skill.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Snapchat/Valdi/HEAD/ai-skills/skills/valdi-profile/skill.md)","ai-skills/skills/valdi-setup/skill.md":"# valdi-setup\n\nPath: `ai-skills/skills/valdi-setup/skill.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Snapchat/Valdi/HEAD/ai-skills/skills/valdi-setup/skill.md)","ai-skills/skills/valdi-testing/skill.md":"# valdi-testing\n\nPath: `ai-skills/skills/valdi-testing/skill.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Snapchat/Valdi/HEAD/ai-skills/skills/valdi-testing/skill.md)","ai-skills/skills/valdi-tsx/skill.md":"# valdi-tsx\n\nPath: `ai-skills/skills/valdi-tsx/skill.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Snapchat/Valdi/HEAD/ai-skills/skills/valdi-tsx/skill.md)",".cursorrules":"# Cursor IDE Native Rules\n\nPath: `.cursorrules`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Snapchat/Valdi/HEAD/.cursorrules)","AGENTS.md":"# AI Agent Protocol & Instructions\n\nPath: `AGENTS.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Snapchat/Valdi/HEAD/AGENTS.md)","tools/api_surface/baseline.json":"# Api_surface Skill\n\nPath: `tools/api_surface/baseline.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Snapchat/Valdi/HEAD/tools/api_surface/baseline.json)","tools/ci/AGENTS.md":"# Ci Skill\n\nPath: `tools/ci/AGENTS.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Snapchat/Valdi/HEAD/tools/ci/AGENTS.md)","tools/valdi_web_devtools/README.md":"# valdi_web_devtools Documentation\n\nPath: `tools/valdi_web_devtools/README.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Snapchat/Valdi/HEAD/tools/valdi_web_devtools/README.md)","tools/valdi_web_devtools/package.json":"# Valdi_web_devtools Skill\n\nPath: `tools/valdi_web_devtools/package.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Snapchat/Valdi/HEAD/tools/valdi_web_devtools/package.json)","tools/valdi_web_devtools/tsconfig.json":"# Valdi_web_devtools Skill\n\nPath: `tools/valdi_web_devtools/tsconfig.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Snapchat/Valdi/HEAD/tools/valdi_web_devtools/tsconfig.json)","tools/ci/compiler_goldens/README.md":"# compiler_goldens Documentation\n\nPath: `tools/ci/compiler_goldens/README.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Snapchat/Valdi/HEAD/tools/ci/compiler_goldens/README.md)","ai-skills/skills/valdi-async/tests/README.md":"# Tests Skill\n\nPath: `ai-skills/skills/valdi-async/tests/README.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Snapchat/Valdi/HEAD/ai-skills/skills/valdi-async/tests/README.md)","ai-skills/skills/valdi-async/tests/tsconfig.json":"# Tests Skill\n\nPath: `ai-skills/skills/valdi-async/tests/tsconfig.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Snapchat/Valdi/HEAD/ai-skills/skills/valdi-async/tests/tsconfig.json)","ai-skills/skills/valdi-component-tests/tests/README.md":"# Tests Skill\n\nPath: `ai-skills/skills/valdi-component-tests/tests/README.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Snapchat/Valdi/HEAD/ai-skills/skills/valdi-component-tests/tests/README.md)","ai-skills/skills/valdi-component-tests/tests/tsconfig.json":"# Tests Skill\n\nPath: `ai-skills/skills/valdi-component-tests/tests/tsconfig.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Snapchat/Valdi/HEAD/ai-skills/skills/valdi-component-tests/tests/tsconfig.json)","ai-skills/skills/valdi-migrate/tests/README.md":"# Tests Skill\n\nPath: `ai-skills/skills/valdi-migrate/tests/README.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Snapchat/Valdi/HEAD/ai-skills/skills/valdi-migrate/tests/README.md)","ai-skills/skills/valdi-migrate/tests/tsconfig.json":"# Tests Skill\n\nPath: `ai-skills/skills/valdi-migrate/tests/tsconfig.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Snapchat/Valdi/HEAD/ai-skills/skills/valdi-migrate/tests/tsconfig.json)","ai-skills/skills/valdi-perf/tests/README.md":"# Tests Skill\n\nPath: `ai-skills/skills/valdi-perf/tests/README.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Snapchat/Valdi/HEAD/ai-skills/skills/valdi-perf/tests/README.md)","ai-skills/skills/valdi-perf/tests/tsconfig.json":"# Tests Skill\n\nPath: `ai-skills/skills/valdi-perf/tests/tsconfig.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Snapchat/Valdi/HEAD/ai-skills/skills/valdi-perf/tests/tsconfig.json)","ai-skills/skills/valdi-tsx/tests/README.md":"# Tests Skill\n\nPath: `ai-skills/skills/valdi-tsx/tests/README.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Snapchat/Valdi/HEAD/ai-skills/skills/valdi-tsx/tests/README.md)","ai-skills/skills/valdi-tsx/tests/tsconfig.json":"# Tests Skill\n\nPath: `ai-skills/skills/valdi-tsx/tests/tsconfig.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Snapchat/Valdi/HEAD/ai-skills/skills/valdi-tsx/tests/tsconfig.json)"},"files":{"ai-skills/skills/valdi-animations/skill.md":"# Valdi Animations\n\nImperative animation API for animating element attributes and state changes.\n\n## When to use\n\nWhen animating layout changes, view attributes, or state transitions. Almost all element attributes can be animated in Valdi.\n\n## Key concepts\n\nValdi uses an **imperative animation model** inspired by iOS's `animateWithDuration:animations:`. You call `this.animate()` or `this.animatePromise()` with options and a block of mutations — all changes inside the block are animated automatically.\n\n**Not CSS transitions.** There is no `transition` property, no `@keyframes`, no `animation-duration`. Animations are triggered programmatically.\n\n## animate and animatePromise\n\nBoth methods are available on any `Component`:\n\n```tsx\n// ✅ Fire-and-forget animation\nthis.animate({ duration: 0.3 }, () => {\n  this.myLabel.setAttribute('font', 'title');\n  this.myContainer.setAttribute('width', 200);\n});\n\n// ✅ Wait for animation to complete\nawait this.animatePromise({ duration: 0.5, curve: AnimationCurve.EaseOut }, () => {\n  this.setState({ expanded: true });\n});\n// Animation finished — safe to do next step\n```\n\n## AnimationOptions\n\nThree curve types: preset, custom bezier, and spring.\n\n```tsx\nimport { AnimationCurve } from 'valdi_core/src/AnimationOptions';\n\n// Preset curves\n{ duration: 0.3, curve: AnimationCurve.Linear }\n{ duration: 0.3, curve: AnimationCurve.EaseIn }\n{ duration: 0.3, curve: AnimationCurve.EaseOut }\n{ duration: 0.3, curve: AnimationCurve.EaseInOut }  // default\n\n// Custom bezier control points\n{ duration: 0.4, controlPoints: [0.25, 0.1, 0.25, 1.0] }\n\n// Spring animation (no duration — spring physics determine timing)\n{ stiffness: 300, damping: 30 }\n\n// Crossfade (alpha transition between old and new state)\n{ duration: 0.3, crossfade: true }\n\n// Continue from current animation state\n{ duration: 0.3, beginFromCurrentState: true }\n\n// Completion callback\n{ duration: 0.3, completion: (wasCancelled) => { /* done */ } }\n```\n\n## Animating element attributes via ElementRef\n\n```tsx\nimport { ElementRef } from 'valdi_core/src/ElementRef';\n\nexport class MyComponent extends Component {\n  private titleRef = new ElementRef();\n  private containerRef = new ElementRef();\n\n  onRender(): void {\n    <view ref={this.containerRef} width={100}>\n      <label ref={this.titleRef} value=\"Hello\" font={systemFont(14)} />\n    </view>;\n  }\n\n  private expand = () => {\n    // ✅ Animate attribute changes via ElementRef\n    this.animate({ duration: 0.3 }, () => {\n      this.titleRef.setAttribute('font', systemBoldFont(17));\n      this.containerRef.setAttribute('width', 300);\n    });\n  };\n}\n```\n\n## Animating state changes\n\n```tsx\ninterface State {\n  expanded: boolean;\n  text: string;\n}\n\nexport class MyComponent extends StatefulComponent<{}, State> {\n  state = { expanded: false, text: 'Collapsed' };\n\n  onRender(): void {\n    <view width={this.state.expanded ? 300 : 100} height={this.state.expanded ? 200 : 50}>\n      <label value={this.state.text} />\n    </view>;\n  }\n\n  private toggle = () => {\n    // ✅ setStateAnimated — renders with new state, animates all resulting mutations\n    this.setStateAnimated(\n      { expanded: !this.state.expanded, text: this.state.expanded ? 'Collapsed' : 'Expanded' },\n      { duration: 0.3, curve: AnimationCurve.EaseInOut },\n    );\n  };\n}\n```\n\n## Spring animations\n\n```tsx\n// ✅ Bouncy spring — lower damping = more bounce\nthis.animate({ stiffness: 300, damping: 15 }, () => {\n  this.cardRef.setAttribute('scale', 1.0);\n});\n\n// ✅ Stiff spring — high damping = smooth settle\nthis.animate({ stiffness: 500, damping: 40 }, () => {\n  this.panelRef.setAttribute('translateY', 0);\n});\n```\n\n## Animating text and attributed text\n\nThe imperative `animate()` / `setStateAnimated()` API above does not drive per-character or per-word text reveals. For those, use the **text-animation surface**: an `AttributedText` chunk carries an `animationTransform`, and a `<textanimationgroup>` container coordinates one shared timeline across all of its descendant `<label>` / `<textview>` elements.\n\n`animationTransform` fields (`valdi_tsx/src/AttributedText.d.ts`, `AttributedTextAnimationTransform`):\n\n| Field | Meaning |\n|-------|---------|\n| `translationY` | Initial vertical offset (logical px); animates back to the normal position |\n| `scale` | Initial scale; animates back to 1 |\n| `opacity` | Initial opacity; animates back to 1 |\n| `duration` | Seconds for each part to reach its normal appearance |\n| `timeOffsetBetweenParts` | Delay (seconds) staggered between consecutive parts (0 = all at once) |\n| `partPattern` | Native regex; each non-overlapping match becomes a separately animated part (unmatched text stays static). Omit to animate the whole chunk as one part |\n| `key` | Stable animation identity; preserves progress across view-pool recycling. Change it to restart the animation |\n\n```tsx\nimport { AttributedTextBuilder } from 'valdi_core/src/utils/AttributedTextBuilder';\n\n// ✅ Build attributed text with a per-chunk animation transform\nprivate animatedText(text: string) {\n  return new AttributedTextBuilder()\n    .append(text, {\n      animationTransform: {\n        duration: 0.45,\n        key: `run-${this.state.runId}`,   // change key to replay\n        opacity: 0,\n        translationY: 8,\n        timeOffsetBetweenParts: 0.08,      // stagger each part\n      },\n    })\n    .build();\n}\n\n// ✅ <textanimationgroup> runs ONE shared timeline across every descendant\n//    label/textview, even nested ones\nonRender(): void {\n  <textanimationgroup>\n    <label value={this.animatedText('First line joins the timeline.')} />\n    <view>\n      <label value={this.animatedText('So does this nested one.')} />\n    </view>\n    <textview value={this.animatedText('And the textview animates last.')} />\n  </textanimationgroup>;\n}\n```\n\n`<textanimationgroup>` is a container (`CommonView` + `ContainerTemplateElement`) with no animation-specific attributes of its own — its only job is to group descendants onto a single coordinated timeline. Without it, each animated chunk runs on its own timeline.\n\n## Common patterns\n\n### Show/hide with fade\n\n```tsx\nprivate show = () => {\n  this.animate({ duration: 0.2 }, () => {\n    this.overlayRef.setAttribute('opacity', 1);\n  });\n};\n\nprivate hide = () => {\n  this.animate({ duration: 0.2 }, () => {\n    this.overlayRef.setAttribute('opacity', 0);\n  });\n};\n```\n\n### Sequential animations\n\n```tsx\nprivate async runSequence() {\n  await this.animatePromise({ duration: 0.2 }, () => {\n    this.step1Ref.setAttribute('opacity', 1);\n  });\n  await this.animatePromise({ duration: 0.3 }, () => {\n    this.step2Ref.setAttribute('translateY', 0);\n  });\n}\n```\n\n## Common mistakes\n\n```tsx\n// ❌ WRONG — CSS-style transitions don't exist\n<view style={{ transition: 'all 0.3s ease' }} />  // ❌\n<view className=\"animate-fade-in\" />               // ❌\n\n// ❌ WRONG — React-style animation libraries\nimport { animated, useSpring } from 'react-spring';  // ❌\n\n// ❌ WRONG — Calling setState inside animate (use setStateAnimated instead)\nthis.animate({ duration: 0.3 }, () => {\n  this.setState({ expanded: true });  // ❌ Won't animate the re-render\n});\n\n// ✅ Correct — use setStateAnimated for state-driven animations\nthis.setStateAnimated({ expanded: true }, { duration: 0.3 });\n\n// ❌ WRONG — Duration in milliseconds (it's seconds)\nthis.animate({ duration: 300 }, () => { ... });  // ❌ This is 300 seconds!\n\n// ✅ Correct — Duration in seconds\nthis.animate({ duration: 0.3 }, () => { ... });\n```\n\n## CoreUI animation components\n\nThe `coreui` library includes pre-built animation components:\n\n| Component | Purpose |\n|-----------|---------|\n| `AnimatableVisibilityView` | Animate show/hide transitions |\n| `AnimationShimmer` | Loading shimmer effect |\n| `AnimationSliding` | Slide in/out animations |\n| `AnimationSquishy` | Press-and-squish feedback |\n| `AnimationRotate` | Continuous rotation |\n| `VerticallyOpenableComponent` | Expand/collapse vertically |\n\nImport from `coreui/src/components/animation/`.\n","ai-skills/skills/valdi-bazel/skill.md":"# Bazel Build System Rules\n\n**Applies to**: `BUILD.bazel`, `*.bzl` files in `/bzl/`, `WORKSPACE`, `MODULE.bazel`\n\n## Overview\n\nValdi uses Bazel as its build system. Bazel provides reproducible, incremental builds across all platforms.\n\n## Key Commands\n\n```bash\n# Build everything\nbazel build //...\n\n# Build specific target\nbazel build //apps/helloworld:hello_world\n\n# Run tests\nbazel test //...\n\n# Clean (use sparingly - cache is valuable!)\nbazel clean\n```\n\n## Important Notes\n\n1. **Use `bazel`** for all build commands\n2. **The CLI wraps Bazel** - `valdi` commands use bazel under the hood\n3. **Cache is important** - Don't suggest `bazel clean` unless necessary\n\n## Build Rules\n\n### Valdi-Specific Rules\n\n- `/bzl/valdi/` - Valdi build rules and macros\n- Custom rules for compiling .tsx to .valdimodule\n- Platform-specific build transitions\n\n### Common Targets\n\n```python\nload(\"@aspect_rules_ts//ts:defs.bzl\", \"ts_project\")\n\n# Valdi application\nvaldi_application(\n    name = \"my_app\",\n    root_component_path = \"App@my_app/src/MyApp\",\n    title = \"My App\",\n    version = \"1.0.0\",\n    deps = [\"//apps/my_app/src/valdi/my_app\"],\n)\n\n# Web polyglot views — MUST be ts_project, never filegroup\nts_project(\n    name = \"my_module_web\",\n    srcs = glob(\n        [\"web/**/*.ts\", \"src/**/*.d.ts\"],\n        exclude = [\"web/**/*.d.ts\"],\n    ),\n    allow_js = True,\n    composite = True,\n    transpiler = \"tsc\",\n    tsconfig = \"web/tsconfig.json\",\n)\n\n# Valdi module with platform deps\nvaldi_module(\n    name = \"my_module\",\n    srcs = glob([\"src/**/*.ts\", \"src/**/*.tsx\"]) + [\"tsconfig.json\"],\n    ios_deps = [\":my_ios_views\"],          # objc_library\n    macos_deps = [\":my_macos_views\"],      # objc_library (or omit to share ios_deps)\n    android_deps = [\":my_android_views\"],  # valdi_android_library\n    web_deps = [\":my_module_web\"],         # ts_project (never filegroup)\n    deps = [\n        \"//src/valdi_modules/src/valdi/valdi_core\",\n    ],\n)\n```\n\n## Conventions\n\n### File Naming\n\n- `BUILD.bazel` not `BUILD` (explicit extension)\n- `.bzl` for Starlark macros and rules\n\n### Targets\n\n- Use descriptive target names\n- One main target per BUILD file usually matches directory name\n\n### Dependencies\n\n- Be explicit about dependencies\n- Don't rely on transitive deps implicitly\n- Use visibility to control access\n\n## Platform Builds\n\n```bash\n# Build and install iOS app\nvaldi install ios\n\n# Build and install Android app\nvaldi install android\n```\n\n## Configuration\n\n- `.bazelrc` - Build flags and configurations\n- `MODULE.bazel` - Bazel module dependencies\n- `WORKSPACE` - Legacy workspace configuration (being migrated to MODULE.bazel)\n\n## Common Issues\n\n1. **Missing dependencies** - Add to `deps` in BUILD.bazel\n2. **Cache issues** - Try `bazel clean --expunge` (last resort)\n3. **Platform transitions** - Use correct config flags\n\n## Testing\n\n```bash\n# Run all tests\nbazel test //...\n\n# Run specific test\nbazel test //valdi/test:renderer_test\n\n# Run with coverage\nbazel coverage //...\n```\n\n## More Information\n\n- Bazel docs: https://bazel.build\n- Valdi build rules: `bzl/valdi/npm/README.md`\n- Framework docs: `/AGENTS.md`\n","ai-skills/skills/valdi-compiler/skill.md":"# Valdi Compiler Rules\n\n**Applies to**: Swift files in `/compiler/compiler/`\n\n## Overview\n\nThe Valdi compiler consists of two parts:\n\n1. **Compiler (Swift)**: Transforms TypeScript/TSX into `.valdimodule` files\n2. **Companion (TypeScript/Node.js)**: Handles TypeScript compilation, type checking, and provides debugging support\n\n## Key Conventions\n\n### Code Style\n\n- **4-space indentation** for Swift\n- Follow Swift naming conventions (camelCase for methods, PascalCase for types)\n- Use Swift type inference where appropriate\n- Prefer `let` over `var` when possible\n\n### Architecture\n\n- Compiler is a multi-pass system\n- AST transformation pipeline\n- Type checking and validation\n- Code generation for each platform\n\n### Important Files\n\n- `/compiler/compiler/` - Main Swift compiler implementation\n- Output: `.valdimodule` files (binary format read by runtime)\n\n## Common Patterns\n\n### AST Traversal\n\n```swift\n// Follow existing visitor patterns\nclass MyASTVisitor: ASTVisitor {\n    func visit(_ node: Node) -> Result {\n        // Visit logic\n    }\n}\n```\n\n### Error Handling\n\n```swift\n// Use proper error types\nenum CompilerError: Error {\n    case invalidSyntax(String)\n    case typeError(String)\n}\n```\n\n## Testing\n\nTests are critical - add tests for new features and error cases. Run the test suite with `bzl test //compiler/compiler/...` (the build script below does not run tests).\n\n## Build\n\n### Using the update script (recommended):\n\n```bash\ncd compiler/compiler\n./scripts/update_compiler_bazel.sh -o bin/compiler\n```\n\nThis script:\n- Builds the compiler with Bazel (`//compiler/compiler:local_valdi_compiler`); no Swift toolchain install is required\n- Builds the compiler for the correct architecture (on macOS, lipos arm64 + x86_64 into a universal binary and codesigns it)\n- Copies the binary to `bin/compiler/macos/valdi_compiler` (or `linux/valdi_compiler`)\n- Handles platform differences (macOS vs Linux)\n\n### Alternative: Using Xcode\n\nOpen `compiler/compiler/Compiler/Package.swift` in Xcode, let it resolve dependencies, then build.\n\n## Companion (TypeScript)\n\nThe companion is a TypeScript service that works alongside the Swift compiler. It handles TypeScript compilation, type checking, and provides debugging support.\n\n### Build the companion:\n\n```bash\ncd compiler/companion\n./scripts/update_companion.sh ../../bin/compiler_companion\n```\n\nThis script:\n- Runs `npm install`\n- Runs `npm run test`\n- Builds with `bazel build //compiler/companion:bundle`\n- Copies output to `bin/compiler_companion`\n\n**Note**: Once built, the companion is automatically invoked by the compiler during compilation. You don't need to run it manually - it's part of the compiler process.\n\n## Important Notes\n\n1. **Performance matters** - Compiler speed affects developer experience\n2. **Error messages** - Make them helpful and actionable\n3. **Backward compatibility** - Don't break existing .valdimodule files\n4. **Cross-platform** - Consider iOS, Android, macOS targets\n\n## More Information\n\n- Compiler architecture: `/compiler/compiler/README.md`\n- Framework docs: `/AGENTS.md`\n","ai-skills/skills/valdi-async/skill.md":"# Valdi Async & Lifecycle Safety\n\nAsync operations that complete after a component is destroyed will call `setState()`\non a dead component — Valdi logs a `console.error` and drops the update (a silent\nno-op, not a thrown error). The fix is always to cancel or guard before the callback\nfires.\n\n## HTTPClient: Store, Cancel, Repeat\n\n`HTTPClient` methods return `CancelablePromise<HTTPResponse>`. Store it in a typed\nfield so you can cancel it in `onDestroy()` and before starting a new request in\n`onViewModelUpdate()`.\n\n```typescript\nimport { Component, StatefulComponent } from 'valdi_core/src/Component';\nimport { CancelablePromise } from 'valdi_core/src/CancelablePromise';\nimport { HTTPClient } from 'valdi_http/src/HTTPClient';\nimport { HTTPResponse } from 'valdi_http/src/HTTPTypes';\n\ninterface UserState {\n  user: { name: string } | null;\n  loading: boolean;\n}\n\nclass UserProfile extends StatefulComponent<UserProfileViewModel, UserState> {\n  state: UserState = { user: null, loading: true };\n\n  private client = new HTTPClient('https://api.example.com');\n  private request?: CancelablePromise<HTTPResponse>;\n\n  onCreate(): void {\n    this.fetchUser(this.viewModel.userId);\n  }\n\n  onViewModelUpdate(previous?: UserProfileViewModel): void {\n    if (this.viewModel.userId !== previous?.userId) {\n      this.request?.cancel?.();          // Cancel in-flight before starting new one\n      this.fetchUser(this.viewModel.userId);\n    }\n  }\n\n  onDestroy(): void {\n    this.request?.cancel?.();            // Always clean up\n  }\n\n  private fetchUser(id: string): void {\n    this.request = this.client.get(`/users/${id}`);\n    this.request.then(response => {\n      const user = JSON.parse(new TextDecoder().decode(response.body));\n      this.setState({ user, loading: false });\n    });\n  }\n\n  onRender(): void {\n    if (this.state.loading) {\n      <spinner />;\n      return;\n    }\n    <label value={this.state.user?.name ?? ''} />;\n  }\n}\n```\n\n**Important:** `cancel` is an optional method on `CancelablePromise` — always use\n`?.cancel?.()`, not `?.cancel()`.\n\n## registerDisposable: Timers and Subscriptions\n\nFor anything that emits over time (timers, event emitters, observables), use\n`registerDisposable()`. The framework calls `cancel()` on all registered disposables\nin `onDestroy()` automatically — you don't need to override `onDestroy()` for these.\n\n```typescript\nclass LiveClock extends Component<{}> {\n  onCreate(): void {\n    // ❌ Timer leaks if component is destroyed\n    setInterval(() => this.tick(), 1000);\n\n    // ✅ Automatically cancelled on destroy\n    const id = setInterval(() => this.tick(), 1000);\n    this.registerDisposable({ cancel: () => clearInterval(id) });\n  }\n\n  private tick = () => { /* ... */ };\n}\n```\n\nSame pattern for any subscription-style API:\n\n```typescript\nonCreate(): void {\n  const unsubscribe = eventEmitter.on('change', this.handleChange);\n  this.registerDisposable({ cancel: unsubscribe });\n}\n```\n\n## isDestroyed() Guard for Plain Promises\n\nWhen using `async/await` or plain `Promise` chains that can't be cancelled, guard\n`setState()` with `isDestroyed()` before calling it:\n\n```typescript\nprivate async loadData(): Promise<void> {\n  const result = await someAsyncOperation();\n\n  if (this.isDestroyed()) return;   // Component unmounted while awaiting\n\n  this.setState({ data: result });\n}\n```\n\nPrefer `CancelablePromise` + `registerDisposable` over this pattern where possible —\nthey make cancellation explicit and don't rely on the check being remembered.\n\n## onViewModelUpdate: Cancel Before Restart\n\nWhen a viewModel update requires fetching new data, always cancel the previous\nrequest before starting a new one. Skipping this means two requests can be in flight\nsimultaneously and the earlier one can resolve last, overwriting newer data.\n\n```typescript\nonViewModelUpdate(previous?: MyViewModel): void {\n  if (this.viewModel.query !== previous?.query) {\n    this.searchRequest?.cancel?.();           // ← required\n    this.searchRequest = this.client.get(`/search?q=${this.viewModel.query}`);\n    this.searchRequest.then(response => {\n      this.setState({ results: parse(response) });\n    });\n  }\n}\n```\n\n## Observable Subscriptions\n\nFor RxJS or observable-based APIs, prefer `registerDisposable` for one-off subscriptions\nor the `Subscription` class for managing multiple subscriptions as a group:\n\n```typescript\nimport { Subscription } from 'rxjs';\n\nclass FeedComponent extends StatefulComponent<FeedViewModel, FeedState> {\n  private subscription = new Subscription();\n\n  onCreate(): void {\n    // Add multiple subscriptions to the group\n    this.subscription.add(\n      this.viewModel.feedItems$.subscribe({ next: items => this.setState({ items }) })\n    );\n    this.subscription.add(\n      this.viewModel.loading$.subscribe({ next: loading => this.setState({ loading }) })\n    );\n  }\n\n  onDestroy(): void {\n    this.subscription?.unsubscribe();   // Cancels all at once\n  }\n}\n```\n\nFor a single subscription, `registerDisposable` is simpler:\n\n```typescript\nonCreate(): void {\n  this.registerDisposable(\n    this.viewModel.counter$.subscribe({ next: count => this.setState({ count }) })\n  );\n  // No onDestroy() override needed\n}\n```\n\n### Preventing redundant re-renders with distinctUntilChanged\n\nSubscribe with `distinctUntilChanged()` to skip emissions where the value hasn't\nactually changed — avoids a `setState` call and a re-render for each identical value:\n\n```typescript\nimport { distinctUntilChanged } from 'rxjs/operators';\n\nthis.subscription.add(\n  this.viewModel.title$.pipe(distinctUntilChanged()).subscribe({\n    next: title => this.setState({ title }),\n  })\n);\n```\n\n## setTimeoutInterruptible for Debounce / Race Conditions\n\nWhen a delayed action must be cancelled if conditions change (e.g. search debounce,\nretry delay), use `setTimeoutInterruptible` rather than a bare `setTimeout`:\n\n```typescript\nimport { setTimeoutInterruptible } from 'valdi_core/src/SetTimeout';\n\n// setTimeoutInterruptible returns a number (timer ID) — cancel with clearTimeout()\nclass SearchBar extends StatefulComponent<SearchViewModel, SearchState> {\n  private debounceId?: number;\n\n  onViewModelUpdate(previous?: SearchViewModel): void {\n    if (this.viewModel.query !== previous?.query) {\n      clearTimeout(this.debounceId);   // Cancel any pending debounce\n      this.debounceId = setTimeoutInterruptible(() => {\n        this.fetchResults(this.viewModel.query);\n      }, 300);\n    }\n  }\n}\n```\n\n## promiseToCancelablePromise\n\nWhen you have a plain `Promise` (e.g. from a third-party library) that needs to participate in Valdi's cancellation system, wrap it with `promiseToCancelablePromise`:\n\n```typescript\nimport { CancelablePromise, promiseToCancelablePromise } from 'valdi_core/src/CancelablePromise';\n\nclass MyComponent extends StatefulComponent<MyViewModel, MyState> {\n  private request?: CancelablePromise<string>;\n\n  onCreate(): void {\n    const rawPromise: Promise<string> = thirdPartyApi.fetchData();\n    this.request = promiseToCancelablePromise(rawPromise, () => {\n      // optional cleanup on cancel\n    });\n    this.request.then(data => {\n      if (this.isDestroyed()) return;\n      this.setState({ data });\n    });\n  }\n\n  onDestroy(): void {\n    this.request?.cancel?.();\n  }\n}\n```\n\nPrefer `CancelablePromise` directly (e.g. `HTTPClient`) when available — `promiseToCancelablePromise` is the bridge for external APIs.\n\n## Import Paths\n\n```typescript\nimport { CancelablePromise, promiseToCancelablePromise } from 'valdi_core/src/CancelablePromise';\nimport { HTTPClient } from 'valdi_http/src/HTTPClient';\nimport { HTTPResponse } from 'valdi_http/src/HTTPTypes';\nimport { setTimeoutInterruptible } from 'valdi_core/src/SetTimeout';\nimport { Subscription } from 'rxjs';\nimport { distinctUntilChanged } from 'rxjs/operators';\n```\n","ai-skills/skills/valdi-component-dedup/skill.md":"# Valdi Component Deduplication\n\nFind and extract duplicated UI patterns across Valdi modules into shared components.\n\n## When to use\n\nWhen you want to identify components that multiple modules have independently implemented, then extract them into a shared library so future modules can reuse them instead of reinventing.\n\n## Running the analysis\n\n```bash\n# Full scan of all modules\nvaldi analyze duplicates\n\n# Scope to one module\nvaldi analyze duplicates --module my_module\n\n# Machine-readable output\nvaldi analyze duplicates --json\n\n# Only check for a specific pattern\nvaldi analyze duplicates --pattern async\n\n# Custom scan roots and shared libraries\nvaldi analyze duplicates --scan-root src/modules --shared-lib valdi_widgets\nvaldi analyze duplicates --scan-root src/modules --shared-lib my_shared_lib --shared-lib another_lib\n```\n\n### CLI flags\n\n| Flag | Effect |\n|---|---|\n| `--module <name>` | Scope to one module |\n| `--json` | Machine-readable JSON output |\n| `--threshold <0-1>` | Similarity threshold (default 0.6) |\n| `--pattern <name>` | Only detect: `async`, `button`, `shimmer` |\n| `--output <path>` | Write JSON report to file |\n| `--verbose` | Show per-file detail |\n| `--scan-root <path>` | Root directory to scan (repeatable) |\n| `--shared-lib <name>` | Shared component library name (repeatable) |\n\n## Understanding the report\n\nThe report has two sections:\n\n**Known Patterns** — pre-built detectors for the most common duplication:\n- **Async State Container**: Components that implement loading→error/content state machines\n- **Button Wrapper**: Thin wrappers around a shared button with hardcoded sizing/coloring\n- **Shimmer / Skeleton Layout**: Custom loading placeholder components\n\n**Similar Clusters** — generic similarity analysis that finds groups of components with overlapping ViewModel props, render trees, and import signatures.\n\nEach cluster shows:\n- Number of files and average similarity score\n- Shared ViewModel props across the cluster\n- A suggested extraction approach\n\n## Extraction workflow\n\nOnce you identify a cluster worth extracting:\n\n### 1. Read the candidates\n\nRead each file in the cluster. Identify:\n- The shared ViewModel props (these become the new component's ViewModel)\n- The shared render structure (this becomes the new component's `onRender`)\n- Module-specific variations (these become optional props or render callbacks)\n\n### 2. Check for an existing shared component to extend\n\nBefore creating anything new, search the shared library for a component that already solves part of the problem. Often the right fix is adding props, a preset enum, or a render callback to an existing component rather than building from scratch.\n\n```bash\n# Search the shared library for related components\ngrep -rl \"Button\\|Shimmer\\|AsyncContent\" <your-shared-lib>/src/components/\n```\n\nIf an existing component covers 70%+ of the use cases, extend it:\n- Add optional props for the missing behavior\n- Add a preset or variant enum if consumers just need different default combos\n- Add a render callback slot if consumers need to inject custom UI\n\nOnly create a new component when nothing in the shared library is close enough to extend.\n\n### 3. Design or extend the shared component\n\n**Extending an existing component** (preferred):\n\n```typescript\n// Add a preset to an existing button component\nexport enum CoreButtonPreset {\n  PRIMARY_CTA,\n  SECONDARY_ACTION,\n  PILL_ACTION,\n}\n\n// Add an optional prop to the existing ViewModel\ninterface CoreButtonViewModel {\n  // ... existing props ...\n  preset?: CoreButtonPreset;\n}\n```\n\n**Creating a new component** (when nothing exists to extend):\n\n```typescript\nimport { Component } from 'valdi_core/src/Component';\n\ninterface SharedComponentViewModel {\n  commonProp: string;\n  optionalVariant?: VariantType;\n  renderCustomSection?: () => void;\n}\n\nexport class SharedComponent extends Component<SharedComponentViewModel> {\n  onRender(): void {\n    <view>\n      <label value={this.viewModel.commonProp} />;\n      {this.viewModel.renderCustomSection?.()};\n    </view>;\n  }\n}\n```\n\n### 4. Place it in the shared library\n\nIf you created a new component, place it in your project's shared component library\n(e.g. `<shared-lib>/src/components/<category>/`). Follow your project's conventions\nfor directory structure and naming.\n\n### 5. Migrate consumers\n\nFor each file in the cluster:\n1. Replace the local component class with an import of the shared one\n2. Map the old ViewModel props to the new shared ViewModel\n3. Move module-specific rendering into render callbacks\n4. Delete the old local component file\n5. Run `valdi agent-check --module <name>` to verify\n\n### 6. Verify\n\n```bash\n# Re-run analysis — the cluster should shrink or disappear\nvaldi analyze duplicates --pattern async\n\n# Build and test affected modules\nvaldi agent-check --module module_a\nvaldi agent-check --module module_b\n```\n\n## Common extraction patterns\n\n### Async State Container\n\nThe most common duplication. Components that manage loading→loaded/error states:\n\n```typescript\nimport { StatefulComponent } from 'valdi_core/src/Component';\nimport { promiseToCancelablePromise } from 'valdi_core/src/CancelablePromise';\n\ninterface AsyncStateContainerViewModel<T> {\n  resultProvider: () => PromiseLike<T>;\n  renderLoading: () => void;\n  renderResult: (data: T) => void;\n  renderError: (error: string, retry: () => void) => void;\n  renderEmpty?: () => void;\n}\n\ntype AsyncState<T> = { phase: 'loading' } | { phase: 'result'; data: T } | { phase: 'error'; message: string };\n\nexport class AsyncStateContainer<T> extends StatefulComponent<AsyncStateContainerViewModel<T>, { state: AsyncState<T> }> {\n  state = { state: { phase: 'loading' } as AsyncState<T> };\n\n  onCreate(): void {\n    this.load();\n  }\n\n  onRender(): void {\n    const s = this.state.state;\n    switch (s.phase) {\n      case 'loading': this.viewModel.renderLoading(); break;\n      case 'result': this.viewModel.renderResult(s.data); break;\n      case 'error': this.viewModel.renderError(s.message, () => this.load()); break;\n    }\n  }\n\n  private load(): void {\n    this.setState({ state: { phase: 'loading' } });\n    const cancelable = promiseToCancelablePromise(\n      Promise.resolve(this.viewModel.resultProvider()),\n      () => {},\n    );\n    this.registerDisposable(() => cancelable.cancel?.());\n    cancelable.then(\n      data => { this.setState({ state: { phase: 'result', data } }); },\n      error => { this.setState({ state: { phase: 'error', message: (error as Error).message } }); },\n    );\n  }\n}\n```\n\n### Button Presets\n\nInstead of per-module button wrappers, use preset enums:\n\n```typescript\n// Instead of a new wrapper component, add presets to the existing shared button\nenum ButtonPreset {\n  PRIMARY_CTA,\n  SECONDARY_ACTION,\n  PILL_ACTION,\n}\n\nfunction presetProps(preset: ButtonPreset): { sizing: ButtonSizing; coloring: ButtonColoring } {\n  switch (preset) {\n    case ButtonPreset.PRIMARY_CTA: return { sizing: ButtonSizing.XL, coloring: ButtonColoring.PRIMARY_BRAND };\n    case ButtonPreset.SECONDARY_ACTION: return { sizing: ButtonSizing.LARGE, coloring: ButtonColoring.SECONDARY };\n    case ButtonPreset.PILL_ACTION: return { sizing: ButtonSizing.SMALL, coloring: ButtonColoring.TERTIARY };\n  }\n}\n```\n\n### Shimmer Layout\n\nConfigurable skeleton loading placeholder:\n\n```typescript\nimport { Component } from 'valdi_core/src/Component';\n\ninterface ShimmerRow {\n  widthRatio: number;\n  height: number;\n  borderRadius?: number;\n}\n\ninterface ShimmerLayoutViewModel {\n  rows: ShimmerRow[];\n  spacing?: number;\n}\n\nexport class ShimmerLayout extends Component<ShimmerLayoutViewModel> {\n  onRender(): void {\n    const spacing = this.viewModel.spacing ?? 8;\n    <view>\n      {this.viewModel.rows.forEach((row, i) => {\n        <AnimationShimmer\n          width={`${row.widthRatio * 100}%`}\n          height={row.height}\n          borderRadius={row.borderRadius ?? 4}\n          marginTop={i > 0 ? spacing : 0}\n        />;\n      })}\n    </view>;\n  }\n}\n```\n\n## Tips\n\n- Start with the highest-severity known patterns — they have the most impact\n- Extract the simplest shared interface first, then iterate to handle edge cases\n- Use render callbacks for module-specific UI instead of trying to generalize everything\n- Run `valdi analyze duplicates --json --output report.json` to save a baseline, then re-run after extraction to measure impact\n","ai-skills/skills/valdi-android/skill.md":"# Android Runtime Rules\n\n**Applies to**: Kotlin/Java files in `/valdi/src/valdi/android/` and related Android runtime code\n\n## Overview\n\nThe Valdi Android runtime bridges TypeScript/Valdi components to native Android views. It's implemented in Kotlin and C++ (via JNI).\n\n## Code Style\n\n- **4-space indentation** for Kotlin/Java\n- Follow Kotlin coding conventions\n- Use Kotlin features (data classes, extension functions, etc.)\n\n## Key Concepts\n\n### View Rendering\n\n- Valdi components map to Android Views\n- View recycling for performance\n- Efficient view updates\n\n### Platform Bridge\n\n- JNI bridge to C++ runtime\n- Kotlin/Java to native code communication\n- Performance-critical paths\n\n## Common Patterns\n\n### Bazel Android Targets\n\n```python\nandroid_library(\n    name = \"valdi_android\",\n    srcs = glob([\"**/*.kt\", \"**/*.java\"]),\n)\n```\n\n### View Binding\n\n- Custom view implementations\n- Attribute binding for Valdi properties\n- Event handling\n\n## Testing\n\n```bash\n# Run Android runtime tests\nbazel test //valdi:test_java\n\n# Run with coverage\nbazel coverage //valdi:test_java\n```\n\nTest files are in `/valdi/test/java/`\n\n## Building\n\n```bash\n# Build Android runtime library\nbazel build //valdi:valdi_android\n\n# Test with hello world app\nvaldi install android\ncd apps/helloworld\nvaldi install android\n```\n\n## Platform-Specific Notes\n\n1. **API Level** - Be mindful of minimum API level\n2. **Android NDK** - C++ integration via NDK\n3. **Permissions** - Handle runtime permissions appropriately\n4. **Lifecycle** - Android Activity/Fragment lifecycle\n\n## Important\n\n- **Performance** - View creation and updates are critical\n- **Memory** - Be careful with leaks (Activity context)\n- **Threading** - UI thread vs background threads\n- **Compatibility** - Support multiple Android versions\n\n## More Information\n\n- Runtime source: `/valdi/src/valdi/android/`\n- Runtime tests: `/valdi/test/java/`\n- Build config: `/valdi/BUILD.bazel`\n- Framework docs: `/AGENTS.md`\n","ai-skills/skills/valdi-cpp-runtime/skill.md":"# C++ Runtime Rules\n\n**Applies to**: C++ files in `/valdi/`, `/valdi_core/`, `/snap_drawing/`\n\n## Overview\n\nValdi's runtime and layout engine are implemented in C++ for cross-platform performance. This code runs on iOS, Android, macOS, but not web.\n\n**Note**: `/libs/` contains shared utility libraries (crypto, logging, image processing) used across the codebase, but not the core runtime itself.\n\n## Code Style\n\n- **4-space indentation**\n- Follow existing C++ style in the codebase\n- Use smart pointers appropriately\n- Prefer const correctness\n\n## Key Concepts\n\n### Layout Engine\n\n- Uses **Yoga** (Facebook's Flexbox implementation) for layout\n- Cross-platform layout calculations\n- Performance-critical code\n- RTL (right-to-left) support built-in\n- Yoga source: `/third-party/yoga/`\n\n### Memory Management\n\n- Be careful with memory ownership\n- Use RAII patterns\n- Consider platform-specific memory constraints (mobile)\n\n### Platform Abstractions\n\n- Code must work on iOS, Android, macOS\n- Use platform-agnostic APIs where possible\n- Platform-specific code goes in appropriate subdirectories\n\n## Common Patterns\n\n### Djinni Generated Code\n\n- Some C++ code is generated from `.djinni` interface files\n- **Don't modify generated code** - change the .djinni file instead\n- Generated files typically in `generated-src/` directories\n\n### Native Bindings Exposed to JS\n\nThe runtime exposes selected native (C++) classes and functions to the JS engine so\nTypeScript can call into performance-critical native code directly.\n\n- **`Base64` namespace** (`coreutils/src/Base64.ts`) is a good example of a\n  native-backed binding: the TS surface (`fromByteArray(uint8, { urlSafe })`,\n  `toByteArray(b64)`, `byteLength(b64)`) delegates to native encode/decode\n  (`Base64Native`) rather than doing the work in JS. Use this shape when a hot codec\n  or transform belongs in native code but needs a clean JS entry point.\n- **`WeakRef`** is supported by the runtime (recently added). JS-side weak references\n  hold their referent without preventing garbage collection — useful for caches and\n  back-references that must not keep objects alive.\n\n**V8 caveat:** some native class bindings (e.g. `Base64` and `Unicode`) are wired for\nQuickJS and may **throw on the V8 runtime**. Don't assume a native binding is present\non every JS engine — guard or feature-check when the same code can run under V8.\n\n### Performance\n\n- This is performance-critical code\n- Profile before optimizing\n- Consider cache locality\n- Be mindful of allocations in hot paths\n\n## Testing\n\n```bash\n# Run all C++ runtime tests\nbazel test //valdi:test\n\n# Run specific test suites\nbazel test //valdi:test_runtime       # Runtime tests\nbazel test //valdi:test_integration   # Integration tests\nbazel test //valdi:test_snap_drawing  # Snap drawing tests\n```\n\n## Platform-Specific Notes\n\n### iOS\n- Objective-C++ bridge in `/valdi/src/valdi/ios/`\n- Metal for GPU rendering\n- UIKit view integration\n\n### Android\n- JNI bridge in `/valdi/src/valdi/android/`\n- NDK integration\n- Native view rendering\n\n### macOS\n- Platform-specific code in `/valdi/src/valdi/macos/`\n- Desktop runtime support\n\n### Build\n\n```bash\n# Build runtime for specific platform\nbazel build //valdi:valdi_ios\nbazel build //valdi:valdi_android\nbazel build //valdi:valdi_macos\n```\n\n## Important\n\n1. **Cross-platform first** - Code must work on all platforms\n2. **Performance critical** - UI rendering and layout\n3. **Memory efficiency** - Mobile devices have constraints\n4. **Thread safety** - Consider concurrency\n\n## More Information\n\n- Runtime overview: `/valdi/README.md`\n- Core bindings: `/valdi_core/README.md`\n- Framework docs: `/AGENTS.md`\n","ai-skills/skills/valdi-custom-view/skill.md":"# Custom View Rules\n\n**Applies to**: `**/*.tsx` files using `<custom-view>` elements.\n\n## `<custom-view>` Element\n\n`<custom-view>` renders a platform-native view inside a Valdi component. Each platform resolves the view by class name.\n\n```typescript\n<custom-view\n  androidClass=\"com.snap.modules.my_module.MyNativeView\"\n  iosClass=\"SCMyNativeView\"\n  macosClass=\"SCMyNativeView\"\n  webClass=\"MyWebViewClassName\"\n  width=\"100%\"\n  height={120}\n/>\n```\n\n## Class Attributes\n\n| Attribute | Platform | Resolution |\n|-----------|----------|------------|\n| `androidClass` | Android | Reflection via `ReflectionViewFactory`; needs `@RegisterAttributesBinder` |\n| `iosClass` | iOS | ObjC class name; must be linked via `ios_deps` |\n| `macosClass` | macOS | `NSClassFromString()`; must be linked via `macos_deps` |\n| `webClass` | Web | Looked up in `WebViewClassRegistry`; registered via `webPolyglotViews` export |\n\n## Platform Resolution\n\n- **Android**: The view class needs a single-arg `(Context)` constructor. An `@RegisterAttributesBinder`-annotated binder is discovered from assets at runtime.\n- **iOS**: The ObjC class must be a `UIView` subclass linked into the binary via `ios_deps`.\n- **macOS**: Falls through to `iosClass` when `macosClass` is not specified — both in TypeScript (`JSXBootstrap.ts`) and C++ (`ViewNode.cpp`). Only specify `macosClass` when the macOS view is different from iOS.\n- **Web**: The `webClass` name is matched against the `WebViewClassRegistry`. Register by exporting `webPolyglotViews` from a typed TypeScript web polyglot entry file compiled via `ts_project` (see `valdi-polyglot-module` skill). Factories return an `AttributeHandler` with `changeAttribute(name, value)` to receive attribute updates.\n\n## viewFactory Pattern\n\nInstead of resolving by class name, you can pass a `ViewFactory` object directly. This is useful when the factory is provided by a parent component or constructed dynamically:\n\n```typescript\nimport { ViewFactory } from 'valdi_tsx/src/ViewFactory';\n\ninterface MyViewModel {\n  viewFactory?: ViewFactory;\n}\n\nclass MyComponent extends Component<MyViewModel> {\n  onRender(): void {\n    if (this.viewModel.viewFactory) {\n      <custom-view style={styles.container} viewFactory={this.viewModel.viewFactory} />;\n    }\n  }\n}\n```\n\n`viewFactory` and `*Class` attributes are mutually exclusive — use one or the other. Specifying both on the same `<custom-view>` is a compile error (the JSXProcessor rejects it). If a view needs a `viewFactory` on one platform and a registered class on another, branch into two sibling `<custom-view>` elements, each with a single mechanism. Prefer gating on whether a `viewFactory` is provided (`if (viewFactory) { … } else { …class… }`) over `Device.isWeb()`: the factory is often supplied by native too, so an `isWeb` branch that routes native to classes can break it. Only keep a platform class you've confirmed actually exists.\n\n## macOS Attribute Binding\n\nmacOS custom views can bind attributes (including callbacks) from TSX using `+bindAttributes:`. The view manager calls this class method at registration time.\n\n```objc\n#import \"valdi/macos/SCValdiMacOSAttributesBinder.h\"\n#import \"valdi/macos/SCValdiMacOSFunction.h\"\n\n@interface SCMyView () {\n    SCValdiMacOSFunction *_onMyCallback;\n}\n@end\n\n@implementation SCMyView\n- (void)valdi_setOnMyCallback:(id)value { _onMyCallback = value; }\n\n+ (void)bindAttributes:(SCValdiMacOSAttributesBinder *)attributesBinder {\n    [attributesBinder bindUntypedAttribute:@\"onMyCallback\"\n                invalidateLayoutOnChange:NO\n                                selector:@selector(valdi_setOnMyCallback:)];\n}\n@end\n```\n\n**Binder methods:**\n- `bindUntypedAttribute:invalidateLayoutOnChange:selector:` — binds any attribute (strings, numbers, functions)\n- `bindColorAttribute:invalidateLayoutOnChange:selector:` — binds color attributes specifically\n\n**Invoking callbacks:** Call `[_onMyCallback performWithParameters:@[@{@\"key\": @\"value\"}]]` — the NSArray/NSDictionary is auto-converted to JS objects.\n\n## Keyboard Input on macOS\n\nValdi doesn't have built-in keyboard event support. Use a polyglot `<custom-view>` with an NSView that:\n1. Overrides `acceptsFirstResponder` → `YES`\n2. Overrides `keyDown:` to map key codes to callback invocations\n3. Calls `[self.window makeFirstResponder:self]` in `viewDidMoveToWindow` to grab focus\n4. Has **non-zero dimensions** (wrap visible content inside it)\n\nSee the `valdi-polyglot-module` skill for a complete macOS attribute binding example with BUILD.bazel wiring.\n\n## Common Mistakes\n\n- Using `<custom-view>` without checking the platform — wrap in `Device.isAndroid()` / `Device.isIOS()` / `Device.isDesktop()` if not all platforms are supported\n- Forgetting to link native implementations — the class name string alone isn't enough; the native code must be compiled and linked via platform `_deps` in BUILD.bazel\n- Wrong package name in `androidClass` — must match the Kotlin/Java package exactly\n- Specifying `macosClass` when it's the same as `iosClass` — omit it and let the fallthrough handle it\n- Using a `filegroup` for `web_deps` — always use `ts_project` with `transpiler = \"tsc\"`\n- Creating a 0×0 custom-view for keyboard focus — it won't become first responder; wrap content inside it\n- **Duplicate ObjC class names across modules** — if two modules define the same native class (e.g. both have `SCKeyboardView`), the linker will fail with \"duplicate symbol\" errors. Always use a unique class name per module (e.g. prefix with module name: `SCSnakeGameKeyboardView`)\n","ai-skills/skills/valdi-component-tests/skill.md":"# Write Valdi Component Tests\n\nWrite unit tests for a Valdi component using standard Valdi test suite patterns.\n\n## Steps\n\n### 1. Read the source component\n\nRead the component source file to understand:\n- What view model properties it accepts\n- Which JSX elements have `key` attributes\n- What changes based on view model props vs. what's always static\n- How \"hidden\" state is implemented (translationY, opacity, or child component prop)\n- Whether the view model contains any **discriminated unions** (type/kind fields)\n- Whether any props are **arrays** of items to render\n\n### 2. Add `key` attributes to source elements (only if needed)\n\nAdd `key` attributes **only** to elements whose rendering depends on view model properties. Never add keys to static content.\n\nGood candidates for keys:\n- The outer container when `hidden` controls `translationY` or `opacity`\n- An `<image>` element whose `src` comes from a view model URL prop\n- An element inside a `when(condition, ...)` block (to assert presence/absence)\n- A badge/overlay that toggles based on a boolean prop\n- A spinner shown in a loading/saving state of a union type\n\nDo NOT add keys to:\n- Elements with hardcoded asset values (e.g., `src={SIGIcon.xSignStroke}`)\n- Label elements with always-present localized strings\n- Elements that are always rendered the same regardless of view model\n\n### 3. Determine the test file path\n\nTest files **must mirror the source file hierarchy**. For example:\n- Source: `src/categories/CollectionComponent.tsx` → Test: `test/categories/CollectionComponentTest.spec.tsx`\n- Source: `src/home_page/OptionPreviewView.tsx` → Test: `test/home_page/OptionPreviewViewTest.spec.tsx`\n- Source: `src/MyComponent.tsx` → Test: `test/MyComponentTest.spec.tsx`\n\n### 4. Write the test file\n\n**Imports:**\n```tsx\nimport { MyComponent } from 'my_module/src/MyComponent';\nimport { componentGetElements } from 'foundation/test/util/componentGetElements';\nimport { componentTypeFind } from 'foundation/test/util/componentTypeFind';\nimport { elementKeyFind } from 'foundation/test/util/elementKeyFind';\nimport { elementTypeFind } from 'foundation/test/util/elementTypeFind';\nimport { findNodeWithKey } from 'foundation/test/util/findNodeWithKey';\nimport { tapNodeWithKey } from 'foundation/test/util/tapNodeWithKey';\nimport 'jasmine/src/jasmine';\nimport { IRenderedElementViewClass } from 'valdi_test/test/IRenderedElementViewClass';\nimport { IComponentTestDriver, valdiIt } from 'valdi_test/test/JSXTestUtils';\nimport { ImageView, View } from 'valdi_tsx/src/NativeTemplateElements';\n```\n\nOnly import what you actually use. `View` is needed when you call `getAttribute` on a non-image element (e.g., for `onTap`, `onVisibilityChanged`). `ImageView` is needed for `src` attribute access.\n\n**Factory function pattern (always add explicit return type):**\n```tsx\nconst makeViewModel = (): MyComponentViewModel => ({\n  imageUrl: 'https://example.com/image.png',\n  isVisible: true,\n  onTap: fail.bind(null, 'onTap should not be called'),\n});\n```\n\nUse `fail.bind` for callbacks in the factory default — tests that need to assert on a callback should declare their own spy and pass it explicitly, rather than relying on the factory.\n\n**Render pattern:**\n```tsx\nconst nodes = driver.render(() => {\n  <MyComponent {...viewModel} />;\n});\n```\n\n### 5. Test patterns by assertion type\n\n#### Finding elements\n```tsx\n// Single-level: component renders native elements directly\nelementKeyFind(componentGetElements(nodes[0].component!), 'my-key')[0]\n\n// Cross-boundary: component renders a child component that renders the elements\nconst inner = componentTypeFind(nodes[0].component as MyComponent, InnerComponent)[0];\nelementKeyFind(componentGetElements(inner), 'my-key')[0]\n\n// Typed (for typed attribute access without casting):\nelementKeyFind<View>(componentGetElements(nodes[0].component!), 'container')[0]\nelementKeyFind<ImageView>(componentGetElements(nodes[0].component!), 'image')[0]\n\n// By element type (e.g. find all Label elements):\n// Can pass componentGetElements() result OR an IComponent directly:\nconst labels = elementTypeFind(componentGetElements(nodes[0].component!), IRenderedElementViewClass.Label);\nconst labels2 = elementTypeFind(nodes[0].component!, IRenderedElementViewClass.Label); // equivalent\nexpect(labels[0]?.getAttribute('value')).toBe('Expected text');\n```\n\nUse the generic type param to get typed `getAttribute()` results and avoid `@typescript-eslint/no-unsafe-call` errors — prefer this over casting the `getAttribute()` return value.\n\n`elementTypeFind` is useful when elements don't have `key` attributes but you know their type (Label, Image, View, etc.). It returns all elements of that type in render order.\n\n#### Hidden via `translationY`\n```tsx\n// hidden=false\nexpect(elementKeyFind<View>(componentGetElements(nodes[0].component!), 'container')[0]?.getAttribute('translationY')).toBe(0);\n\n// hidden=true\nexpect(elementKeyFind<View>(componentGetElements(nodes[0].component!), 'container')[0]?.getAttribute('translationY')).toBe(850);\n```\n\n#### Hidden via `opacity` on native view\n```tsx\n// visible\nexpect(elementKeyFind<View>(componentGetElements(nodes[0].component!), 'my-view')[0]?.getAttribute('opacity')).toBe(1);\n\n// hidden\nexpect(elementKeyFind<View>(componentGetElements(nodes[0].component!), 'my-view')[0]?.getAttribute('opacity')).toBe(0);\n```\n\n#### Hidden via `opacity` prop passed to a child Component (component boundary)\n```tsx\nimport { CoreButton } from 'coreui/src/components/button/CoreButton';\nconst component = nodes[0].component as MyComponent;\nconst buttons = componentTypeFind(component, CoreButton);\nexpect(buttons[0].viewModel.opacity).toBe(1); // or 0\n```\n\n#### View model URL bound to image `src`\n```tsx\nexpect(elementKeyFind<ImageView>(componentGetElements(nodes[0].component!), 'my-image')[0]?.getAttribute('src')).toBe('https://example.com/image.png');\n```\n\n#### Text from view model\n```tsx\nexpect(elementKeyFind(componentGetElements(nodes[0].component!), 'my-label')[0]?.getAttribute('value')).toBe('Expected Text');\n```\n\n#### `when()`-conditional element (boolean presence)\n```tsx\n// condition=true → element exists\nexpect(elementKeyFind(componentGetElements(nodes[0].component!), 'my-element')[0]).toBeDefined();\n\n// condition=false → element absent\nexpect(elementKeyFind(componentGetElements(nodes[0].component!), 'my-element')[0]).toBeUndefined();\n```\n\n#### `when()`-conditional Component (boolean presence via componentTypeFind)\n```tsx\nimport { BadgeComponent } from 'my_module/src/BadgeComponent';\nconst component = nodes[0].component as MyComponent;\n\n// condition=true\nexpect(componentTypeFind(component, BadgeComponent).length).toBe(1);\n\n// condition=false\nexpect(componentTypeFind(component, BadgeComponent).length).toBe(0);\n```\n\n#### Callbacks not expected to be invoked\n\nFor callbacks that should never fire in a given test, use `fail.bind(null, '...')` instead of `jasmine.createSpy`. This causes the test to immediately fail with a clear message if the callback is accidentally triggered:\n\n```tsx\n<MyComponent\n  onSelect={fail.bind(null, 'onSelect should not be called')}\n  onLoad={onLoad}\n/>\n```\n\n`fail.bind(null, 'message')` is a plain function call (not an inline lambda), so it satisfies `jsx-no-lambda`. It is assignable to any callback type since TypeScript allows functions with fewer parameters.\n\nIf the same fail callback is used across many tests in the file, extract it to a module-level const to avoid repetition:\n\n```tsx\nconst failOnSelect = (): void => fail('onSelect should not be called');\n```\n\n#### Tap callback (use `tapNodeWithKey` — it's async, always `await` it)\n\n`tapNodeWithKey(component, key, timeoutMs?, intervalMs?)` accepts `IComponent | IRenderedElement`.\n\n```tsx\nconst onTap = jasmine.createSpy('onTap');\nconst nodes = driver.render(() => {\n  <MyComponent onTap={onTap} />;\n});\nawait tapNodeWithKey(nodes[0].component!, 'my-button');\nexpect(onTap).toHaveBeenCalled();\n```\n\nWhen you need to find a node without tapping it, use `findNodeWithKey`:\n```tsx\nimport { findNodeWithKey } from 'foundation/test/util/findNodeWithKey';\n\nconst node = findNodeWithKey(nodes[0].component!, 'my-button')[0];\nexpect(node).toBeDefined();\n```\n\nWhen the callback receives arguments (e.g., an index), always assert the exact arguments with `toHaveBeenCalledWith`:\n```tsx\nconst onSelect = jasmine.createSpy('onSelect');\n// ... render and trigger ...\nexpect(onSelect).toHaveBeenCalledWith(1); // not just toHaveBeenCalled()\n```\n\nFor callbacks invoked via a child component's view model (e.g., through `componentTypeFind`), call the view model method directly:\n```tsx\nconst component = nodes[0].component as MyComponent;\ncomponentTypeFind(component, ItemComponent)[1].viewModel.onTap();\nexpect(onSelect).toHaveBeenCalledWith(1);\n```\n\n#### `onTap` retrieved via `getAttribute` (non-tappable element pattern)\n```tsx\n// Use elementKeyFind<View> so getAttribute('onTap') is typed — no cast needed\nconst el = elementKeyFind<View>(componentGetElements(nodes[0].component!), 'my-element')[0];\nel?.getAttribute('onTap')?.();\nexpect(onTap).toHaveBeenCalled();\n```\n\n#### `onVisibilityChanged` callback\n\n`View.onVisibilityChanged` signature is `(isVisible: boolean, eventTime: EventTime)` where `EventTime = number`. Always pass both args when invoking:\n\n```tsx\nconst el = elementKeyFind<View>(componentGetElements(nodes[0].component!), 'container')[0];\nel?.getAttribute('onVisibilityChanged')?.(true, 0);\n```\n\n**Asserting the callback:** depends on how the component wires the handler:\n\n- If the component **wraps** the viewModel callback (e.g., `onVisibilityChanged={(v) => vm.onVisibilityChanged(v)}`), `toHaveBeenCalledWith(true)` works:\n  ```tsx\n  expect(onVisibilityChanged).toHaveBeenCalledWith(true);\n  ```\n\n- If the component **directly assigns** the viewModel callback (e.g., `onVisibilityChanged={this.viewModel.onVisibilityChanged}`), the spy receives both args. If the spy is typed as `(isVisible: boolean) => void`, `toHaveBeenCalledWith(true, ...)` is a TypeScript error. Check the first arg via `calls`:\n  ```tsx\n  const spy = viewModel.onVisibilityChanged as jasmine.Spy;\n  expect(spy).toHaveBeenCalled();\n  expect(spy.calls.mostRecent().args[0]).toBe(true);\n  ```\n  If the spy is an untyped `jasmine.createSpy()`, you can use `toHaveBeenCalledWith(true, 0)` directly.\n\n### 6. Discriminated union state testing\n\nWhen a view model contains a discriminated union (e.g., `type: 'LOADING' | 'CONTENT' | 'ERROR'`), **test every branch**. For each state:\n1. Assert which elements/components ARE rendered\n2. Assert which elements/components from OTHER states are NOT rendered\n\n```tsx\n// LOADING state: spinner present, action button absent\nvaldiIt('Verify spinner is shown in loading state', async driver => {\n  const nodes = driver.render(() => {\n    <MyComponent button={{ type: ButtonType.LOADING }} />;\n  });\n  expect(elementKeyFind(componentGetElements(nodes[0].component!), 'loading-spinner')[0]).toBeDefined();\n  expect(elementKeyFind(componentGetElements(nodes[0].component!), 'action-button')[0]).toBeUndefined();\n});\n\n// CONTENT state: action button present, no spinner\nvaldiIt('Verify action button is shown in content state', async driver => {\n  const nodes = driver.render(() => {\n    <MyComponent button={{ type: ButtonType.PURCHASE, price: '$9.99', onTap }} />;\n  });\n  expect(elementKeyFind(componentGetElements(nodes[0].component!), 'action-button')[0]).toBeDefined();\n  expect(elementKeyFind(componentGetElements(nodes[0].component!), 'loading-spinner')[0]).toBeUndefined();\n});\n```\n\n### 7. Array view model testing\n\nWhen a component renders a list from an array prop, test three cases:\n1. **Empty array** — assert 0 items render\n2. **Single item** — assert 1 item renders\n3. **Multiple items** — assert item count matches array length\n\nUse `componentTypeFind(component, ItemComponent)` or `elementKeyFind` with indexed keys (e.g., `tile-0`, `tile-1`) to count rendered items.\n\n```tsx\nvaldiIt('Verify no items render when array is empty', async driver => {\n  const emptyOptions: OptionViewModel[] = [];\n  const nodes = driver.render(() => {\n    <MyComponent items={emptyOptions} />;\n  });\n  const component = nodes[0].component as MyComponent;\n  expect(componentTypeFind(component, ItemComponent).length).toBe(0);\n});\n\nvaldiIt('Verify item count matches array length', async driver => {\n  const threeItems: OptionViewModel[] = [makeItem('a'), makeItem('b'), makeItem('c')];\n  const nodes = driver.render(() => {\n    <MyComponent items={threeItems} />;\n  });\n  const component = nodes[0].component as MyComponent;\n  expect(componentTypeFind(component, ItemComponent).length).toBe(3);\n});\n```\n\nNote: Extract array literals to local `const` variables before using in JSX (required by `@snapchat/valdi/jsx-no-lambda`).\n\n### 8. Component boundary traversal\n\nWhen a component's `onRender()` only renders child components (not native elements), `componentGetElements(component)` returns `[]`. You must get the child component first:\n\n```tsx\nimport { componentGetElements } from 'foundation/test/util/componentGetElements';\nimport { componentTypeFind } from 'foundation/test/util/componentTypeFind';\nimport { elementKeyFind } from 'foundation/test/util/elementKeyFind';\n\nconst component = nodes[0].component as OuterComponent;\nconst inner = componentTypeFind(component, InnerComponent)[0];\nconst container = elementKeyFind<View>(componentGetElements(inner), 'container')[0];\nexpect(container?.getAttribute('translationY')).toBe(0);\n```\n\n### 9. Extract render and find helpers for readability\n\nFor larger test files, extract repeated render + find logic into helper functions. This keeps individual tests focused:\n\n```tsx\n// Extract rendering\nconst renderComponent = (driver: IComponentTestDriver, overrides?: Partial<MyComponentViewModel>) => {\n  const vm = { ...makeViewModel(), ...overrides };\n  return driver.render(() => { <MyComponent {...vm} />; })[0].component as MyComponent;\n};\n\n// Extract finding\nconst getImage = (component: MyComponent) =>\n  elementKeyFind<ImageView>(componentGetElements(component), 'image')[0];\n\n// Tests become clean:\nvaldiIt('Verify imageUrl is bound', async driver => {\n  expect(getImage(renderComponent(driver))?.getAttribute('src')).toBe('https://example.com/image.png');\n});\n```\n\n### 10. Lint rules to follow\n\n- **`explicit-function-return-type`**: Always add explicit return types to factory functions: `const makeViewModel = (): MyViewModel => ({...})`\n- **`jsx-no-lambda`**: Never assign inline array literals directly in JSX props. Extract to a local `const` first:\n  ```tsx\n  // WRONG\n  <MyComponent items={[makeItem('a')]} />;\n\n  // CORRECT\n  const items: ItemViewModel[] = [makeItem('a')];\n  <MyComponent items={items} />;\n  ```\n- **`no-unsafe-call`**: Use the generic type parameter on `elementKeyFind<T>` to get typed `getAttribute()` results rather than casting: `elementKeyFind<View>(...)` gives typed access to `onTap`, `onVisibilityChanged`, etc.\n- **`import/order`**: Keep imports sorted alphabetically by path.\n\n### 11. Key principle\n\n**Only assert on things that change based on view model props.** Every test should have a clear \"when X is Y, then Z\" story. If the UI looks the same regardless of the prop, skip it.\n\nFor union types, a test that only verifies \"the component renders without error\" in a given state is not sufficient — assert the meaningful structural difference that state introduces.\n\n## Example test file structure\n\n```tsx\nimport { MyComponent } from 'my_module/src/MyComponent';\nimport { MyComponentViewModel } from 'my_module/src/MyComponentViewModel';\nimport { ChildComponent } from 'my_module/src/ChildComponent';\nimport { componentGetElements } from 'foundation/test/util/componentGetElements';\nimport { componentTypeFind } from 'foundation/test/util/componentTypeFind';\nimport { elementKeyFind } from 'foundation/test/util/elementKeyFind';\nimport { tapNodeWithKey } from 'foundation/test/util/tapNodeWithKey';\nimport 'jasmine/src/jasmine';\nimport { IComponentTestDriver, valdiIt } from 'valdi_test/test/JSXTestUtils';\nimport { ImageView, View } from 'valdi_tsx/src/NativeTemplateElements';\n// elementTypeFind + IRenderedElementViewClass for finding elements by type (no key needed):\n// import { elementTypeFind } from 'foundation/test/util/elementTypeFind';\n// import { IRenderedElementViewClass } from 'valdi_test/test/IRenderedElementViewClass';\n\nconst makeViewModel = (): MyComponentViewModel => ({\n  imageUrl: 'https://example.com/image.png',\n  isVisible: true,\n  onTap: fail.bind(null, 'onTap should not be called'),\n});\n\ndescribe('MyComponentTest', () => {\n  valdiIt('Verify visible when isVisible is true', async driver => {\n    const nodes = driver.render(() => {\n      <MyComponent {...makeViewModel()} />;\n    });\n    expect(elementKeyFind<View>(componentGetElements(nodes[0].component!), 'container')[0]?.getAttribute('opacity')).toBe(1);\n  });\n\n  valdiIt('Verify hidden when isVisible is false', async driver => {\n    const nodes = driver.render(() => {\n      <MyComponent {...{ ...makeViewModel(), isVisible: false }} />;\n    });\n    expect(elementKeyFind<View>(componentGetElements(nodes[0].component!), 'container')[0]?.getAttribute('opacity')).toBe(0);\n  });\n\n  valdiIt('Verify imageUrl is bound to image src', async driver => {\n    const nodes = driver.render(() => {\n      <MyComponent {...makeViewModel()} />;\n    });\n    expect(elementKeyFind<ImageView>(componentGetElements(nodes[0].component!), 'image')[0]?.getAttribute('src')).toBe('https://example.com/image.png');\n  });\n\n  valdiIt('Verify onTap is called when tapped', async driver => {\n    const onTap = jasmine.createSpy('onTap');\n    const nodes = driver.render(() => {\n      <MyComponent {...{ ...makeViewModel(), onTap }} />;\n    });\n    await tapNodeWithKey(nodes[0].component!, 'button');\n    expect(onTap).toHaveBeenCalled();\n  });\n\n  valdiIt('Verify ChildComponent is present when condition is true', async driver => {\n    const nodes = driver.render(() => {\n      <MyComponent {...{ ...makeViewModel(), showChild: true }} />;\n    });\n    const component = nodes[0].component as MyComponent;\n    expect(componentTypeFind(component, ChildComponent).length).toBe(1);\n  });\n\n  valdiIt('Verify ChildComponent is absent when condition is false', async driver => {\n    const nodes = driver.render(() => {\n      <MyComponent {...{ ...makeViewModel(), showChild: false }} />;\n    });\n    const component = nodes[0].component as MyComponent;\n    expect(componentTypeFind(component, ChildComponent).length).toBe(0);\n  });\n});\n```\n","ai-skills/skills/valdi-gestures/skill.md":"# Valdi Gestures\n\nTouch and gesture handling: tap, long press, drag, pinch, and rotate.\n\n## When to use\n\nWhen handling user touch interactions beyond basic `onTap` — drag-to-reorder, pinch-to-zoom, long press menus, custom gesture recognition.\n\n## Key concepts\n\nValdi gesture handling is **attribute-based on `<view>` elements**. Each gesture type has three attributes:\n- `on<Gesture>` — the callback\n- `on<Gesture>Disabled` — boolean to disable the gesture (the gesture is enabled by default once a callback is set; there is no `<gesture>Enabled` attribute)\n- `on<Gesture>Predicate` — function returning boolean, controls whether the gesture should fire\n\nThe topmost visible element with a matching gesture wins. Conflicting gestures on parent elements are suppressed.\n\n## TouchEvent interface\n\nAll gesture callbacks receive a `TouchEvent` (or extension of it):\n\n```tsx\ninterface TouchEvent {\n  x: number;          // X relative to the element\n  y: number;          // Y relative to the element\n  absoluteX: number;  // X relative to root Valdi view\n  absoluteY: number;  // Y relative to root Valdi view\n  pointerCount: number;\n  pointerLocations: Pointer[];\n}\n```\n\n## Basic gestures\n\n### onTap\n\n```tsx\n// ✅ Simple tap handler\n<view onTap={this.handleTap}>\n  <label value=\"Tap me\" />\n</view>\n\nprivate handleTap = (event: TouchEvent) => {\n  console.log(`Tapped at ${event.x}, ${event.y}`);\n};\n```\n\n### onDoubleTap\n\n```tsx\n// ✅ Double tap to zoom\n<view onDoubleTap={this.handleDoubleTap}>\n  <image src={this.viewModel.photo} />\n</view>\n```\n\n### onLongPress\n\n```tsx\n// ✅ Long press with custom duration\n<view\n  onLongPress={this.handleLongPress}\n  longPressDuration={0.5}  // seconds before trigger\n>\n  <label value=\"Hold me\" />\n</view>\n```\n\n## Continuous gestures\n\n### onDrag\n\n```tsx\n// ✅ Drag to move — fires continuously during the drag\n<view onDrag={this.handleDrag}>\n  <label value=\"Drag me\" />\n</view>\n\nprivate handleDrag = (event: DragEvent) => {\n  // event extends TouchEvent with drag-specific data\n  this.cardRef.setAttribute('translateX', event.x);\n  this.cardRef.setAttribute('translateY', event.y);\n};\n```\n\n### onPinch\n\n```tsx\n// ✅ Pinch to zoom — requires 2+ pointers\n<view onPinch={this.handlePinch}>\n  <image src={this.viewModel.photo} ref={this.imageRef} />\n</view>\n\nprivate handlePinch = (event: PinchEvent) => {\n  this.imageRef.setAttribute('scale', event.scale);\n};\n```\n\n### onRotate\n\n```tsx\n// ✅ Rotate with two fingers — requires 2+ pointers\n<view onRotate={this.handleRotate}>\n  <image src={this.viewModel.sticker} ref={this.stickerRef} />\n</view>\n```\n\n## Simultaneous gestures\n\n`onDrag`, `onPinch`, and `onRotate` can run simultaneously. This enables multitouch interactions like pinch-zoom-rotate on a photo:\n\n```tsx\n// ✅ Combined pinch + drag + rotate on same element\n<view\n  onDrag={this.handleDrag}\n  onPinch={this.handlePinch}\n  onRotate={this.handleRotate}\n>\n  <image src={this.viewModel.photo} ref={this.photoRef} />\n</view>\n```\n\n## Touch area extension\n\nExpand the tappable area beyond the visible bounds:\n\n```tsx\n// ✅ Small button with larger touch target\n<view\n  onTap={this.handleTap}\n  touchAreaExtension={10}  // extends hit area by 10pt in each direction\n  width={24} height={24}\n>\n  <image src={SIGIcon.checkmarkFill} />\n</view>\n```\n\n## Touch delay in scroll views\n\nInside a `<scroll>`, touches are delayed to avoid conflicts with scrolling:\n\n```tsx\n// ✅ Delay touch recognition inside scrollable content\n<view\n  onTouch={this.handleTouch}\n  onTouchDelayDuration={0.15}  // seconds before onTouch fires during scroll\n/>\n```\n\n## Gesture predicates\n\nControl whether a gesture should activate:\n\n```tsx\n// ✅ Only allow drag when in edit mode\n<view\n  onDrag={this.handleDrag}\n  onDragPredicate={() => this.state.editMode}\n/>\n```\n\n## onTouch (raw)\n\nLow-level touch events that fire on all elements in the hierarchy:\n\n```tsx\n// ✅ Raw touch tracking — fires for all touch phases\n<view onTouch={this.handleTouch}>\n  <label value=\"Touch me\" />\n</view>\n\nprivate handleTouch = (event: TouchEvent) => {\n  // Fires on begin, move, end, cancel\n};\n```\n\nUnlike other gestures, `onTouch` propagates to parents/siblings — it doesn't suppress other handlers.\n\n## Common mistakes\n\n```tsx\n// ❌ WRONG — React-style event handlers\n<div onClick={handler} />        // ❌ No <div>, no onClick\n<button onMouseDown={handler} /> // ❌ No mouse events\n\n// ❌ WRONG — addEventListener\nelement.addEventListener('touchstart', handler);  // ❌\n\n// ❌ WRONG — Gesture on <layout> (use <view>)\n<layout onTap={handler} />  // ❌ Gestures go on <view> elements\n\n// ✅ Correct — gestures on <view>\n<view onTap={handler}>\n  <layout>\n    <label value=\"Content\" />\n  </layout>\n</view>\n```\n","ai-skills/skills/valdi-inspect/skill.md":"# valdi-inspect\n\nUse this skill when you need to inspect a **live running Valdi app** — to see what components are on screen, traverse the rendered virtual node tree, capture element screenshots, or check JS heap usage.\n\n## When to use\n\n- User asks \"what's on screen?\", \"what component is rendering here?\", \"what contexts are active?\"\n- You need to diagnose a layout or rendering issue in a running app\n- You want to verify a component tree change after a hot-reload\n- User asks about memory usage or wants a JS heap dump\n\n## Prerequisites\n\nThe Valdi app must be **running on a connected device or emulator**. No hot-reloader required — the CLI connects directly to the app's DebuggerService.\n\n**Port reference** — determined by how the app is built, not the OS:\n\n| Target | Port | When |\n|--------|------|------|\n| iOS in-app (e.g. Snapchat) | **13592** (default) | `isStandalone = false` |\n| Android in-app | **13592** (default) | `isStandalone = false` |\n| macOS Valdi app | **13591** | `isStandalone = true` |\n| Standalone runner / CLI runner | **13591** | `isStandalone = true` |\n\nFor **Android**, `adb forward` runs automatically. For **iOS simulator**, connect directly to `localhost`.\n\nAll commands output **JSON by default** (for AI parsing). Add `--pretty` for human-readable output.\n\n---\n\n## Commands\n\n### `valdi inspect status`\n\nCheck whether the Valdi daemon is reachable and how many devices are connected.\n\n```\nvaldi inspect status\nvaldi inspect status --pretty\n```\n\nOutput: `{ connected: bool, port: number, connectedDevices: number, portName: string }`\n\n---\n\n### `valdi inspect devices`\n\nList all devices connected to the Valdi daemon.\n\n```\nvaldi inspect devices\nvaldi inspect devices --pretty\n```\n\nOutput: `[{ client_id, platform, application_id }, ...]`\n\nUse `client_id` values with `--client` in other commands. If only one device is connected, other commands auto-select it.\n\n---\n\n### `valdi inspect select`\n\nInteractively choose a device and save it to `~/.valdi-inspect.json`. Subsequent commands use this device without prompting.\n\n```\nvaldi inspect select\n```\n\n---\n\n### `valdi inspect contexts`\n\nList all active root components (contexts) on the device. A **context** is one mounted Valdi component tree — typically one per screen or overlay.\n\n```\nvaldi inspect contexts\nvaldi inspect contexts --pretty\nvaldi inspect contexts --client <client_id>\n```\n\nOutput: `[{ id: string, rootComponentName: string }, ...]`\n\nThe `id` is the **contextId** used by `tree` and `snapshot`. If there is only one context, commands that take a contextId will auto-select it.\n\n---\n\n### `valdi inspect tree [contextId]`\n\nGet the full rendered virtual node tree for a context. If `contextId` is omitted, auto-selects the single context or prompts when there are multiple.\n\n```\nvaldi inspect tree\nvaldi inspect tree <contextId>\nvaldi inspect tree <contextId> --pretty\nvaldi inspect tree <contextId> --max-depth <n>\n```\n\nOutput: a nested tree where each node has:\n- `tag`: element type (e.g. `\"view\"`, `\"label\"`, `\"image\"`, or a component name)\n- `key`: the Valdi TSX key assigned in source\n- `element.id`: numeric element ID (use this for `snapshot`)\n- `element.frame`: `{ x, y, width, height }` in screen coordinates\n- `element.attributes`: rendered prop values (style, value, etc.)\n- `children`: array of child nodes (same shape)\n- `_childrenTrimmed`: count of trimmed children when `--max-depth` is used\n\n**Tip**: Use `--max-depth 3` or `--max-depth 4` first to get a summary, then drill in.\n\n---\n\n### `valdi inspect snapshot <elementId> [contextId]`\n\nCapture a screenshot of a specific element and save it as a PNG. If `contextId` is omitted, auto-selects or prompts.\n\n```\nvaldi inspect snapshot <elementId>\nvaldi inspect snapshot <elementId> <contextId>\nvaldi inspect snapshot <elementId> <contextId> --output /tmp/my-snap.png\n```\n\nOutput: `{ path: \"/var/folders/.../valdi-snapshot-<elementId>.png\" }`\n\nThe PNG is written to disk. Read it with the `Read` tool to view it visually.\n\n**Getting elementId**: Use `element.id` from the `tree` output (a number, e.g. `1`, `5`, `42`).\n\n#### Using `--key` instead of numeric IDs (recommended)\n\nInstead of looking up numeric element IDs from the tree, use the `--key` flag to resolve by component tag name or element `key` prop:\n\n```\nvaldi inspect snapshot --key PreviewRoot\nvaldi inspect snapshot --key weather-card --port 13591\n```\n\nThe `--key` flag searches the component tree for a matching tag name or `key` prop and resolves it to the element ID automatically.\n\n**This is stable across hot-reloads** — element IDs change on every reload, but component names and `key` props stay the same. Use `--key` instead of numeric IDs to avoid re-querying the tree after each hot-reload cycle.\n\nCustom keys can be set on any element in TSX:\n```tsx\n<view key=\"weather-card\" style={styles.card}>\n```\n\n---\n\n### `valdi inspect heap`\n\nDump JS heap statistics for the connected device.\n\n```\nvaldi inspect heap\nvaldi inspect heap --pretty\nvaldi inspect heap --gc\n```\n\nOptions:\n- `--gc`: run garbage collection before dumping (more accurate free memory reading)\n\nOutput: `{ memoryUsageBytes: number, heapDumpJSON: string }`\n\n`heapDumpJSON` is a serialized heap snapshot in V8 format. Note: heap dumps can crash small/toy apps — this works best on full production apps.\n\n---\n\n## Common workflows\n\n### \"What is rendering on screen right now?\"\n\n```\nvaldi inspect tree --pretty\n```\n\n(contextId is auto-selected if there's only one)\n\n### \"Show me what a specific component looks like\"\n\n1. Run `valdi inspect tree --pretty` to find the node — note its `element.id`\n2. Run `valdi inspect snapshot <element.id>` to capture it\n3. Read the PNG with the `Read` tool\n\n### \"Diagnose a layout issue at position X,Y\"\n\nRun `valdi inspect tree --pretty` and look for `element.frame` values overlapping with the target position.\n\n### \"Check memory usage\"\n\n```\nvaldi inspect heap --pretty\n```\n\n### \"Verify a hot-reload applied correctly\"\n\nRun `valdi inspect tree` before and after the edit, compare the JSON output.\n\n---\n\n## Options reference\n\n| Option | Commands | Description |\n|--------|----------|-------------|\n| `--port <n>` | all | Daemon TCP port (default: 13592 mobile, 13591 standalone) |\n| `--client <id>` | contexts, tree, snapshot, heap | Target a specific connected device |\n| `--pretty` | devices, status, contexts, tree, heap | Human-readable output |\n| `--max-depth <n>` | tree | Trim tree to N levels deep |\n| `--output <path>` | snapshot | Custom PNG output path |\n| `--gc` | heap | GC before heap dump |\n\n---\n\n## Troubleshooting\n\n**\"Valdi daemon not running on port 13592\"** — Make sure the app is running. macOS Valdi apps and standalone runners use port 13591: add `--port 13591`.\n\n**\"No devices connected\"** — Make sure the Valdi app is running. For Android, ensure a device or emulator is connected via `adb`.\n\n**\"Timeout waiting for device response\"** — The app may be frozen or the JS runtime may be busy. Try again after a moment.\n","ai-skills/skills/valdi-error-handling/skill.md":"# Valdi Error Handling\n\nError handling, promise rejection, and lifecycle safety patterns.\n\n## When to use\n\nWhen dealing with async operations, network requests, component lifecycle boundaries, or any code that can fail at runtime.\n\n## Key concepts\n\nValdi runs on a JavaScript runtime (Hermes/V8) with native platform integration. Unhandled errors crash the app. The primary error sources are:\n\n1. **Async operations** completing after component destruction\n2. **Network failures** from HTTPClient\n3. **Uncaught promise rejections**\n4. **Type mismatches** from native/JS boundary\n\n## CancelablePromise — the first line of defense\n\nMost Valdi errors stem from async work completing after a component is destroyed. `CancelablePromise` + `registerDisposable` prevents this entirely:\n\n```tsx\nimport { promiseToCancelablePromise } from 'valdi_core/src/CancelablePromise';\n\n// ✅ Safe async — cancelled on component destruction\nonCreate() {\n  const cancelable = promiseToCancelablePromise(this.fetchData(), () => {});\n  this.registerDisposable(() => cancelable.cancel?.());\n  cancelable.then(\n    data => { this.setState({ data, loading: false }); },\n    error => { this.setState({ error: error.message, loading: false }); },\n  );\n}\n```\n\n```tsx\n// ❌ WRONG — Raw promise with no lifecycle safety\nonCreate() {\n  this.fetchData().then(data => {\n    this.setState({ data });  // Logs a console.error and no-ops if destroyed\n  });\n}\n```\n\nSee the `valdi-async` skill for comprehensive CancelablePromise patterns.\n\n## HTTPClient error handling\n\n`HTTPClient` is an instance, not static. Methods return `CancelablePromise<HTTPResponse>`.\n\n```tsx\nimport { HTTPClient } from 'valdi_http/src/HTTPClient';\n\nprivate httpClient = new HTTPClient('https://api.example.com');\n\n// ✅ Handle network errors with lifecycle safety\nprivate fetchData = () => {\n  const cancelable = this.httpClient.get('/data');\n  this.registerDisposable(() => cancelable.cancel?.());\n  cancelable.then(\n    response => {\n      if (response.statusCode !== 200) {\n        this.setState({ error: `HTTP ${response.statusCode}`, loading: false });\n        return;\n      }\n      const decoder = new TextDecoder();\n      const data = JSON.parse(decoder.decode(response.body));\n      this.setState({ data, loading: false });\n    },\n    error => {\n      this.setState({ error: (error as Error).message, loading: false });\n    },\n  );\n};\n```\n\n`HTTPResponse` shape: `{ statusCode: number, headers: StringMap<string>, body?: Uint8Array }`. Decode `body` with `TextDecoder` before parsing.\n\n## isDestroyed() guard\n\nFor callbacks that can't use CancelablePromise (e.g., native callbacks, timers):\n\n```tsx\n// ✅ Guard against use-after-destroy\nprivate onNativeCallback = (result: string) => {\n  if (this.isDestroyed()) return;\n  this.setState({ result });\n};\n```\n\n`isDestroyed()` is a method — always call with parentheses.\n\n## try/catch in async methods\n\n```tsx\nimport { promiseToCancelablePromise } from 'valdi_core/src/CancelablePromise';\n\n// ✅ Catch errors from async operations\nprivate loadProfile = () => {\n  const cancelable = promiseToCancelablePromise(\n    (async () => {\n      try {\n        const profile = await this.api.getProfile();\n        return { profile, error: undefined };\n      } catch (e) {\n        return { profile: undefined, error: (e as Error).message };\n      }\n    })(),\n    () => {},\n  );\n  this.registerDisposable(() => cancelable.cancel?.());\n\n  cancelable.then(result => {\n    this.setState({\n      profile: result.profile,\n      error: result.error,\n      loading: false,\n    });\n  });\n};\n```\n\n## Error states in UI\n\n```tsx\n// ✅ Show error state to the user\nonRender(): void {\n  if (this.state.loading) {\n    <AnimationShimmer width=\"100%\" height={200} />;\n    return;\n  }\n\n  if (this.state.error) {\n    <view alignItems=\"center\" justifyContent=\"center\" padding={16}>\n      <label value=\"Something went wrong\" font={systemBoldFont(16)} />\n      <label value={this.state.error} color=\"#999\" />\n      <CoreButton text=\"Retry\" onTap={this.handleRetry} />\n    </view>;\n    return;\n  }\n\n  <label value={this.state.data.name} />;\n}\n```\n\n## Defensive ViewModel access\n\nViewModels come from parent components and may have optional fields:\n\n```tsx\n// ✅ Defensive access with defaults\nonRender(): void {\n  const title = this.viewModel.title ?? 'Untitled';\n  const items = this.viewModel.items ?? [];\n\n  <view>\n    <label value={title} />\n    {items.forEach(item => { <label value={item.name} />; })}\n  </view>;\n}\n```\n\n## Timer cleanup\n\n```tsx\n// ✅ Use setTimeoutDisposable for automatic cleanup\nonCreate() {\n  this.setTimeoutDisposable(this.tick, 1000);\n}\n```\n\n```tsx\n// ✅ Manual cleanup for setInterval\nprivate timerId?: number;\n\nonCreate() {\n  this.timerId = setInterval(this.tick, 1000);\n}\n\nonDestroy() {\n  if (this.timerId !== undefined) {\n    clearInterval(this.timerId);\n  }\n}\n```\n\nOr use `registerDisposable` with a disposable wrapper — see `valdi-async` skill.\n\n## Common mistakes\n\n```tsx\n// ❌ WRONG — HTTPClient is not static\nconst data = await HTTPClient.send(request);  // No static send() method\n\n// ✅ Correct — instantiate then call methods\nconst client = new HTTPClient('https://api.example.com');\nconst cancelable = client.get('/data');\n\n// ❌ WRONG — .catch() is not available on CancelablePromise\ncancelable.catch(error => { ... });  // CancelablePromise only has .then()\n\n// ✅ Correct — use second argument of .then() for error handling\ncancelable.then(\n  response => { /* success */ },\n  error => { /* failure */ },\n);\n\n// ❌ WRONG — React error boundaries don't exist\n<ErrorBoundary fallback={<Error />}>  // Not a Valdi concept\n  <MyComponent />\n</ErrorBoundary>\n\n// ❌ WRONG — window.onerror / global error handlers\nwindow.addEventListener('error', handler);  // No window object\n\n// ❌ WRONG — isDestroyed without parentheses\nif (this.isDestroyed) return;  // It's a method, not a property\n\n// ✅ Correct\nif (this.isDestroyed()) return;\n\n// ❌ WRONG — Passing CancelablePromise directly to registerDisposable\nthis.registerDisposable(cancelable);  // Wrong type\n\n// ✅ Correct — wrap cancel in a callback\nthis.registerDisposable(() => cancelable.cancel?.());\n```\n\n## Summary\n\n| Pattern | When to use |\n|---------|------------|\n| `promiseToCancelablePromise` + `registerDisposable` | Any async work in a component |\n| `isDestroyed()` guard | Native callbacks, external event handlers |\n| `try/catch` | Operations that can throw |\n| Error state in UI | User-facing failure recovery |\n| `onDestroy` cleanup | Timers, subscriptions, manual resources |\n| `setTimeoutDisposable` | One-shot timers (auto-cleaned) |\n","ai-skills/skills/valdi-ios/skill.md":"# valdi-ios\n\nPath: `ai-skills/skills/valdi-ios/skill.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Snapchat/Valdi/HEAD/ai-skills/skills/valdi-ios/skill.md)","ai-skills/skills/valdi-localization/skill.md":"# valdi-localization\n\nPath: `ai-skills/skills/valdi-localization/skill.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Snapchat/Valdi/HEAD/ai-skills/skills/valdi-localization/skill.md)","ai-skills/skills/valdi-migrate/skill.md":"# valdi-migrate\n\nPath: `ai-skills/skills/valdi-migrate/skill.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Snapchat/Valdi/HEAD/ai-skills/skills/valdi-migrate/skill.md)","ai-skills/skills/valdi-mock-breakdown/skill.md":"# valdi-mock-breakdown\n\nPath: `ai-skills/skills/valdi-mock-breakdown/skill.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Snapchat/Valdi/HEAD/ai-skills/skills/valdi-mock-breakdown/skill.md)","ai-skills/skills/valdi-mock-iterate/skill.md":"# valdi-mock-iterate\n\nPath: `ai-skills/skills/valdi-mock-iterate/skill.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Snapchat/Valdi/HEAD/ai-skills/skills/valdi-mock-iterate/skill.md)","ai-skills/skills/valdi-mock-to-module/skill.md":"# valdi-mock-to-module\n\nPath: `ai-skills/skills/valdi-mock-to-module/skill.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Snapchat/Valdi/HEAD/ai-skills/skills/valdi-mock-to-module/skill.md)","ai-skills/skills/valdi-navigation/skill.md":"# valdi-navigation\n\nPath: `ai-skills/skills/valdi-navigation/skill.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Snapchat/Valdi/HEAD/ai-skills/skills/valdi-navigation/skill.md)","ai-skills/skills/valdi-overview/skill.md":"# valdi-overview\n\nPath: `ai-skills/skills/valdi-overview/skill.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Snapchat/Valdi/HEAD/ai-skills/skills/valdi-overview/skill.md)","ai-skills/skills/valdi-perf/skill.md":"# valdi-perf\n\nPath: `ai-skills/skills/valdi-perf/skill.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Snapchat/Valdi/HEAD/ai-skills/skills/valdi-perf/skill.md)","ai-skills/skills/valdi-polyglot-module/skill.md":"# valdi-polyglot-module\n\nPath: `ai-skills/skills/valdi-polyglot-module/skill.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Snapchat/Valdi/HEAD/ai-skills/skills/valdi-polyglot-module/skill.md)","ai-skills/skills/valdi-pr-review/skill.md":"# valdi-pr-review\n\nPath: `ai-skills/skills/valdi-pr-review/skill.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Snapchat/Valdi/HEAD/ai-skills/skills/valdi-pr-review/skill.md)","ai-skills/skills/valdi-preview/skill.md":"# valdi-preview\n\nPath: `ai-skills/skills/valdi-preview/skill.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Snapchat/Valdi/HEAD/ai-skills/skills/valdi-preview/skill.md)","ai-skills/skills/valdi-profile/skill.md":"# valdi-profile\n\nPath: `ai-skills/skills/valdi-profile/skill.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Snapchat/Valdi/HEAD/ai-skills/skills/valdi-profile/skill.md)","ai-skills/skills/valdi-setup/skill.md":"# valdi-setup\n\nPath: `ai-skills/skills/valdi-setup/skill.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Snapchat/Valdi/HEAD/ai-skills/skills/valdi-setup/skill.md)","ai-skills/skills/valdi-testing/skill.md":"# valdi-testing\n\nPath: `ai-skills/skills/valdi-testing/skill.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Snapchat/Valdi/HEAD/ai-skills/skills/valdi-testing/skill.md)","ai-skills/skills/valdi-tsx/skill.md":"# valdi-tsx\n\nPath: `ai-skills/skills/valdi-tsx/skill.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Snapchat/Valdi/HEAD/ai-skills/skills/valdi-tsx/skill.md)",".cursorrules":"# Cursor IDE Native Rules\n\nPath: `.cursorrules`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Snapchat/Valdi/HEAD/.cursorrules)","AGENTS.md":"# AI Agent Protocol & Instructions\n\nPath: `AGENTS.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Snapchat/Valdi/HEAD/AGENTS.md)","tools/api_surface/baseline.json":"# Api_surface Skill\n\nPath: `tools/api_surface/baseline.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Snapchat/Valdi/HEAD/tools/api_surface/baseline.json)","tools/ci/AGENTS.md":"# Ci Skill\n\nPath: `tools/ci/AGENTS.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Snapchat/Valdi/HEAD/tools/ci/AGENTS.md)","tools/valdi_web_devtools/README.md":"# valdi_web_devtools Documentation\n\nPath: `tools/valdi_web_devtools/README.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Snapchat/Valdi/HEAD/tools/valdi_web_devtools/README.md)","tools/valdi_web_devtools/package.json":"# Valdi_web_devtools Skill\n\nPath: `tools/valdi_web_devtools/package.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Snapchat/Valdi/HEAD/tools/valdi_web_devtools/package.json)","tools/valdi_web_devtools/tsconfig.json":"# Valdi_web_devtools Skill\n\nPath: `tools/valdi_web_devtools/tsconfig.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Snapchat/Valdi/HEAD/tools/valdi_web_devtools/tsconfig.json)","tools/ci/compiler_goldens/README.md":"# compiler_goldens Documentation\n\nPath: `tools/ci/compiler_goldens/README.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Snapchat/Valdi/HEAD/tools/ci/compiler_goldens/README.md)","ai-skills/skills/valdi-async/tests/README.md":"# Tests Skill\n\nPath: `ai-skills/skills/valdi-async/tests/README.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Snapchat/Valdi/HEAD/ai-skills/skills/valdi-async/tests/README.md)","ai-skills/skills/valdi-async/tests/tsconfig.json":"# Tests Skill\n\nPath: `ai-skills/skills/valdi-async/tests/tsconfig.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Snapchat/Valdi/HEAD/ai-skills/skills/valdi-async/tests/tsconfig.json)","ai-skills/skills/valdi-component-tests/tests/README.md":"# Tests Skill\n\nPath: `ai-skills/skills/valdi-component-tests/tests/README.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Snapchat/Valdi/HEAD/ai-skills/skills/valdi-component-tests/tests/README.md)","ai-skills/skills/valdi-component-tests/tests/tsconfig.json":"# Tests Skill\n\nPath: `ai-skills/skills/valdi-component-tests/tests/tsconfig.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Snapchat/Valdi/HEAD/ai-skills/skills/valdi-component-tests/tests/tsconfig.json)","ai-skills/skills/valdi-migrate/tests/README.md":"# Tests Skill\n\nPath: `ai-skills/skills/valdi-migrate/tests/README.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Snapchat/Valdi/HEAD/ai-skills/skills/valdi-migrate/tests/README.md)","ai-skills/skills/valdi-migrate/tests/tsconfig.json":"# Tests Skill\n\nPath: `ai-skills/skills/valdi-migrate/tests/tsconfig.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Snapchat/Valdi/HEAD/ai-skills/skills/valdi-migrate/tests/tsconfig.json)","ai-skills/skills/valdi-perf/tests/README.md":"# Tests Skill\n\nPath: `ai-skills/skills/valdi-perf/tests/README.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Snapchat/Valdi/HEAD/ai-skills/skills/valdi-perf/tests/README.md)","ai-skills/skills/valdi-perf/tests/tsconfig.json":"# Tests Skill\n\nPath: `ai-skills/skills/valdi-perf/tests/tsconfig.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Snapchat/Valdi/HEAD/ai-skills/skills/valdi-perf/tests/tsconfig.json)","ai-skills/skills/valdi-tsx/tests/README.md":"# Tests Skill\n\nPath: `ai-skills/skills/valdi-tsx/tests/README.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Snapchat/Valdi/HEAD/ai-skills/skills/valdi-tsx/tests/README.md)","ai-skills/skills/valdi-tsx/tests/tsconfig.json":"# Tests Skill\n\nPath: `ai-skills/skills/valdi-tsx/tests/tsconfig.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Snapchat/Valdi/HEAD/ai-skills/skills/valdi-tsx/tests/tsconfig.json)"},"items":[{"name":"AGENTS.md","path":"AGENTS.md","rawUrl":"https://raw.githubusercontent.com/Snapchat/Valdi/HEAD/AGENTS.md","title":"AI Agent Protocol & Instructions","category":"root-instruction","format":"markdown","content":"# AI Agent Protocol & Instructions\n\nPath: `AGENTS.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Snapchat/Valdi/HEAD/AGENTS.md)","tokens":300,"sizeBytes":148},{"name":"skill.md","path":"ai-skills/skills/valdi-android/skill.md","rawUrl":"https://raw.githubusercontent.com/Snapchat/Valdi/HEAD/ai-skills/skills/valdi-android/skill.md","title":"valdi-android","category":"anthropic-skill","format":"markdown","content":"# Android Runtime Rules\n\n**Applies to**: Kotlin/Java files in `/valdi/src/valdi/android/` and related Android runtime code\n\n## Overview\n\nThe Valdi Android runtime bridges TypeScript/Valdi components to native Android views. It's implemented in Kotlin and C++ (via JNI).\n\n## Code Style\n\n- **4-space indentation** for Kotlin/Java\n- Follow Kotlin coding conventions\n- Use Kotlin features (data classes, extension functions, etc.)\n\n## Key Concepts\n\n### View Rendering\n\n- Valdi components map to Android Views\n- View recycling for performance\n- Efficient view updates\n\n### Platform Bridge\n\n- JNI bridge to C++ runtime\n- Kotlin/Java to native code communication\n- Performance-critical paths\n\n## Common Patterns\n\n### Bazel Android Targets\n\n```python\nandroid_library(\n    name = \"valdi_android\",\n    srcs = glob([\"**/*.kt\", \"**/*.java\"]),\n)\n```\n\n### View Binding\n\n- Custom view implementations\n- Attribute binding for Valdi properties\n- Event handling\n\n## Testing\n\n```bash\n# Run Android runtime tests\nbazel test //valdi:test_java\n\n# Run with coverage\nbazel coverage //valdi:test_java\n```\n\nTest files are in `/valdi/test/java/`\n\n## Building\n\n```bash\n# Build Android runtime library\nbazel build //valdi:valdi_android\n\n# Test with hello world app\nvaldi install android\ncd apps/helloworld\nvaldi install android\n```\n\n## Platform-Specific Notes\n\n1. **API Level** - Be mindful of minimum API level\n2. **Android NDK** - C++ integration via NDK\n3. **Permissions** - Handle runtime permissions appropriately\n4. **Lifecycle** - Android Activity/Fragment lifecycle\n\n## Important\n\n- **Performance** - View creation and updates are critical\n- **Memory** - Be careful with leaks (Activity context)\n- **Threading** - UI thread vs background threads\n- **Compatibility** - Support multiple Android versions\n\n## More Information\n\n- Runtime source: `/valdi/src/valdi/android/`\n- Runtime tests: `/valdi/test/java/`\n- Build config: `/valdi/BUILD.bazel`\n- Framework docs: `/AGENTS.md`\n","isInternal":false,"tokens":466,"sizeBytes":1954},{"name":"skill.md","path":"ai-skills/skills/valdi-animations/skill.md","rawUrl":"https://raw.githubusercontent.com/Snapchat/Valdi/HEAD/ai-skills/skills/valdi-animations/skill.md","title":"valdi-animations","category":"anthropic-skill","format":"markdown","content":"# Valdi Animations\n\nImperative animation API for animating element attributes and state changes.\n\n## When to use\n\nWhen animating layout changes, view attributes, or state transitions. Almost all element attributes can be animated in Valdi.\n\n## Key concepts\n\nValdi uses an **imperative animation model** inspired by iOS's `animateWithDuration:animations:`. You call `this.animate()` or `this.animatePromise()` with options and a block of mutations — all changes inside the block are animated automatically.\n\n**Not CSS transitions.** There is no `transition` property, no `@keyframes`, no `animation-duration`. Animations are triggered programmatically.\n\n## animate and animatePromise\n\nBoth methods are available on any `Component`:\n\n```tsx\n// ✅ Fire-and-forget animation\nthis.animate({ duration: 0.3 }, () => {\n  this.myLabel.setAttribute('font', 'title');\n  this.myContainer.setAttribute('width', 200);\n});\n\n// ✅ Wait for animation to complete\nawait this.animatePromise({ duration: 0.5, curve: AnimationCurve.EaseOut }, () => {\n  this.setState({ expanded: true });\n});\n// Animation finished — safe to do next step\n```\n\n## AnimationOptions\n\nThree curve types: preset, custom bezier, and spring.\n\n```tsx\nimport { AnimationCurve } from 'valdi_core/src/AnimationOptions';\n\n// Preset curves\n{ duration: 0.3, curve: AnimationCurve.Linear }\n{ duration: 0.3, curve: AnimationCurve.EaseIn }\n{ duration: 0.3, curve: AnimationCurve.EaseOut }\n{ duration: 0.3, curve: AnimationCurve.EaseInOut }  // default\n\n// Custom bezier control points\n{ duration: 0.4, controlPoints: [0.25, 0.1, 0.25, 1.0] }\n\n// Spring animation (no duration — spring physics determine timing)\n{ stiffness: 300, damping: 30 }\n\n// Crossfade (alpha transition between old and new state)\n{ duration: 0.3, crossfade: true }\n\n// Continue from current animation state\n{ duration: 0.3, beginFromCurrentState: true }\n\n// Completion callback\n{ duration: 0.3, completion: (wasCancelled) => { /* done */ } }\n```\n\n## Animating element attributes via ElementRef\n\n```tsx\nimport { ElementRef } from 'valdi_core/src/ElementRef';\n\nexport class MyComponent extends Component {\n  private titleRef = new ElementRef();\n  private containerRef = new ElementRef();\n\n  onRender(): void {\n    <view ref={this.containerRef} width={100}>\n      <label ref={this.titleRef} value=\"Hello\" font={systemFont(14)} />\n    </view>;\n  }\n\n  private expand = () => {\n    // ✅ Animate attribute changes via ElementRef\n    this.animate({ duration: 0.3 }, () => {\n      this.titleRef.setAttribute('font', systemBoldFont(17));\n      this.containerRef.setAttribute('width', 300);\n    });\n  };\n}\n```\n\n## Animating state changes\n\n```tsx\ninterface State {\n  expanded: boolean;\n  text: string;\n}\n\nexport class MyComponent extends StatefulComponent<{}, State> {\n  state = { expanded: false, text: 'Collapsed' };\n\n  onRender(): void {\n    <view width={this.state.expanded ? 300 : 100} height={this.state.expanded ? 200 : 50}>\n      <label value={this.state.text} />\n    </view>;\n  }\n\n  private toggle = () => {\n    // ✅ setStateAnimated — renders with new state, animates all resulting mutations\n    this.setStateAnimated(\n      { expanded: !this.state.expanded, text: this.state.expanded ? 'Collapsed' : 'Expanded' },\n      { duration: 0.3, curve: AnimationCurve.EaseInOut },\n    );\n  };\n}\n```\n\n## Spring animations\n\n```tsx\n// ✅ Bouncy spring — lower damping = more bounce\nthis.animate({ stiffness: 300, damping: 15 }, () => {\n  this.cardRef.setAttribute('scale', 1.0);\n});\n\n// ✅ Stiff spring — high damping = smooth settle\nthis.animate({ stiffness: 500, damping: 40 }, () => {\n  this.panelRef.setAttribute('translateY', 0);\n});\n```\n\n## Animating text and attributed text\n\nThe imperative `animate()` / `setStateAnimated()` API above does not drive per-character or per-word text reveals. For those, use the **text-animation surface**: an `AttributedText` chunk carries an `animationTransform`, and a `<textanimationgroup>` container coordinates one shared timeline across all of its descendant `<label>` / `<textview>` elements.\n\n`animationTransform` fields (`valdi_tsx/src/AttributedText.d.ts`, `AttributedTextAnimationTransform`):\n\n| Field | Meaning |\n|-------|---------|\n| `translationY` | Initial vertical offset (logical px); animates back to the normal position |\n| `scale` | Initial scale; animates back to 1 |\n| `opacity` | Initial opacity; animates back to 1 |\n| `duration` | Seconds for each part to reach its normal appearance |\n| `timeOffsetBetweenParts` | Delay (seconds) staggered between consecutive parts (0 = all at once) |\n| `partPattern` | Native regex; each non-overlapping match becomes a separately animated part (unmatched text stays static). Omit to animate the whole chunk as one part |\n| `key` | Stable animation identity; preserves progress across view-pool recycling. Change it to restart the animation |\n\n```tsx\nimport { AttributedTextBuilder } from 'valdi_core/src/utils/AttributedTextBuilder';\n\n// ✅ Build attributed text with a per-chunk animation transform\nprivate animatedText(text: string) {\n  return new AttributedTextBuilder()\n    .append(text, {\n      animationTransform: {\n        duration: 0.45,\n        key: `run-${this.state.runId}`,   // change key to replay\n        opacity: 0,\n        translationY: 8,\n        timeOffsetBetweenParts: 0.08,      // stagger each part\n      },\n    })\n    .build();\n}\n\n// ✅ <textanimationgroup> runs ONE shared timeline across every descendant\n//    label/textview, even nested ones\nonRender(): void {\n  <textanimationgroup>\n    <label value={this.animatedText('First line joins the timeline.')} />\n    <view>\n      <label value={this.animatedText('So does this nested one.')} />\n    </view>\n    <textview value={this.animatedText('And the textview animates last.')} />\n  </textanimationgroup>;\n}\n```\n\n`<textanimationgroup>` is a container (`CommonView` + `ContainerTemplateElement`) with no animation-specific attributes of its own — its only job is to group descendants onto a single coordinated timeline. Without it, each animated chunk runs on its own timeline.\n\n## Common patterns\n\n### Show/hide with fade\n\n```tsx\nprivate show = () => {\n  this.animate({ duration: 0.2 }, () => {\n    this.overlayRef.setAttribute('opacity', 1);\n  });\n};\n\nprivate hide = () => {\n  this.animate({ duration: 0.2 }, () => {\n    this.overlayRef.setAttribute('opacity', 0);\n  });\n};\n```\n\n### Sequential animations\n\n```tsx\nprivate async runSequence() {\n  await this.animatePromise({ duration: 0.2 }, () => {\n    this.step1Ref.setAttribute('opacity', 1);\n  });\n  await this.animatePromise({ duration: 0.3 }, () => {\n    this.step2Ref.setAttribute('translateY', 0);\n  });\n}\n```\n\n## Common mistakes\n\n```tsx\n// ❌ WRONG — CSS-style transitions don't exist\n<view style={{ transition: 'all 0.3s ease' }} />  // ❌\n<view className=\"animate-fade-in\" />               // ❌\n\n// ❌ WRONG — React-style animation libraries\nimport { animated, useSpring } from 'react-spring';  // ❌\n\n// ❌ WRONG — Calling setState inside animate (use setStateAnimated instead)\nthis.animate({ duration: 0.3 }, () => {\n  this.setState({ expanded: true });  // ❌ Won't animate the re-render\n});\n\n// ✅ Correct — use setStateAnimated for state-driven animations\nthis.setStateAnimated({ expanded: true }, { duration: 0.3 });\n\n// ❌ WRONG — Duration in milliseconds (it's seconds)\nthis.animate({ duration: 300 }, () => { ... });  // ❌ This is 300 seconds!\n\n// ✅ Correct — Duration in seconds\nthis.animate({ duration: 0.3 }, () => { ... });\n```\n\n## CoreUI animation components\n\nThe `coreui` library includes pre-built animation components:\n\n| Component | Purpose |\n|-----------|---------|\n| `AnimatableVisibilityView` | Animate show/hide transitions |\n| `AnimationShimmer` | Loading shimmer effect |\n| `AnimationSliding` | Slide in/out animations |\n| `AnimationSquishy` | Press-and-squish feedback |\n| `AnimationRotate` | Continuous rotation |\n| `VerticallyOpenableComponent` | Expand/collapse vertically |\n\nImport from `coreui/src/components/animation/`.\n","isInternal":false,"tokens":1989,"sizeBytes":8041},{"name":"skill.md","path":"ai-skills/skills/valdi-async/skill.md","rawUrl":"https://raw.githubusercontent.com/Snapchat/Valdi/HEAD/ai-skills/skills/valdi-async/skill.md","title":"valdi-async","category":"anthropic-skill","format":"markdown","content":"# Valdi Async & Lifecycle Safety\n\nAsync operations that complete after a component is destroyed will call `setState()`\non a dead component — Valdi logs a `console.error` and drops the update (a silent\nno-op, not a thrown error). The fix is always to cancel or guard before the callback\nfires.\n\n## HTTPClient: Store, Cancel, Repeat\n\n`HTTPClient` methods return `CancelablePromise<HTTPResponse>`. Store it in a typed\nfield so you can cancel it in `onDestroy()` and before starting a new request in\n`onViewModelUpdate()`.\n\n```typescript\nimport { Component, StatefulComponent } from 'valdi_core/src/Component';\nimport { CancelablePromise } from 'valdi_core/src/CancelablePromise';\nimport { HTTPClient } from 'valdi_http/src/HTTPClient';\nimport { HTTPResponse } from 'valdi_http/src/HTTPTypes';\n\ninterface UserState {\n  user: { name: string } | null;\n  loading: boolean;\n}\n\nclass UserProfile extends StatefulComponent<UserProfileViewModel, UserState> {\n  state: UserState = { user: null, loading: true };\n\n  private client = new HTTPClient('https://api.example.com');\n  private request?: CancelablePromise<HTTPResponse>;\n\n  onCreate(): void {\n    this.fetchUser(this.viewModel.userId);\n  }\n\n  onViewModelUpdate(previous?: UserProfileViewModel): void {\n    if (this.viewModel.userId !== previous?.userId) {\n      this.request?.cancel?.();          // Cancel in-flight before starting new one\n      this.fetchUser(this.viewModel.userId);\n    }\n  }\n\n  onDestroy(): void {\n    this.request?.cancel?.();            // Always clean up\n  }\n\n  private fetchUser(id: string): void {\n    this.request = this.client.get(`/users/${id}`);\n    this.request.then(response => {\n      const user = JSON.parse(new TextDecoder().decode(response.body));\n      this.setState({ user, loading: false });\n    });\n  }\n\n  onRender(): void {\n    if (this.state.loading) {\n      <spinner />;\n      return;\n    }\n    <label value={this.state.user?.name ?? ''} />;\n  }\n}\n```\n\n**Important:** `cancel` is an optional method on `CancelablePromise` — always use\n`?.cancel?.()`, not `?.cancel()`.\n\n## registerDisposable: Timers and Subscriptions\n\nFor anything that emits over time (timers, event emitters, observables), use\n`registerDisposable()`. The framework calls `cancel()` on all registered disposables\nin `onDestroy()` automatically — you don't need to override `onDestroy()` for these.\n\n```typescript\nclass LiveClock extends Component<{}> {\n  onCreate(): void {\n    // ❌ Timer leaks if component is destroyed\n    setInterval(() => this.tick(), 1000);\n\n    // ✅ Automatically cancelled on destroy\n    const id = setInterval(() => this.tick(), 1000);\n    this.registerDisposable({ cancel: () => clearInterval(id) });\n  }\n\n  private tick = () => { /* ... */ };\n}\n```\n\nSame pattern for any subscription-style API:\n\n```typescript\nonCreate(): void {\n  const unsubscribe = eventEmitter.on('change', this.handleChange);\n  this.registerDisposable({ cancel: unsubscribe });\n}\n```\n\n## isDestroyed() Guard for Plain Promises\n\nWhen using `async/await` or plain `Promise` chains that can't be cancelled, guard\n`setState()` with `isDestroyed()` before calling it:\n\n```typescript\nprivate async loadData(): Promise<void> {\n  const result = await someAsyncOperation();\n\n  if (this.isDestroyed()) return;   // Component unmounted while awaiting\n\n  this.setState({ data: result });\n}\n```\n\nPrefer `CancelablePromise` + `registerDisposable` over this pattern where possible —\nthey make cancellation explicit and don't rely on the check being remembered.\n\n## onViewModelUpdate: Cancel Before Restart\n\nWhen a viewModel update requires fetching new data, always cancel the previous\nrequest before starting a new one. Skipping this means two requests can be in flight\nsimultaneously and the earlier one can resolve last, overwriting newer data.\n\n```typescript\nonViewModelUpdate(previous?: MyViewModel): void {\n  if (this.viewModel.query !== previous?.query) {\n    this.searchRequest?.cancel?.();           // ← required\n    this.searchRequest = this.client.get(`/search?q=${this.viewModel.query}`);\n    this.searchRequest.then(response => {\n      this.setState({ results: parse(response) });\n    });\n  }\n}\n```\n\n## Observable Subscriptions\n\nFor RxJS or observable-based APIs, prefer `registerDisposable` for one-off subscriptions\nor the `Subscription` class for managing multiple subscriptions as a group:\n\n```typescript\nimport { Subscription } from 'rxjs';\n\nclass FeedComponent extends StatefulComponent<FeedViewModel, FeedState> {\n  private subscription = new Subscription();\n\n  onCreate(): void {\n    // Add multiple subscriptions to the group\n    this.subscription.add(\n      this.viewModel.feedItems$.subscribe({ next: items => this.setState({ items }) })\n    );\n    this.subscription.add(\n      this.viewModel.loading$.subscribe({ next: loading => this.setState({ loading }) })\n    );\n  }\n\n  onDestroy(): void {\n    this.subscription?.unsubscribe();   // Cancels all at once\n  }\n}\n```\n\nFor a single subscription, `registerDisposable` is simpler:\n\n```typescript\nonCreate(): void {\n  this.registerDisposable(\n    this.viewModel.counter$.subscribe({ next: count => this.setState({ count }) })\n  );\n  // No onDestroy() override needed\n}\n```\n\n### Preventing redundant re-renders with distinctUntilChanged\n\nSubscribe with `distinctUntilChanged()` to skip emissions where the value hasn't\nactually changed — avoids a `setState` call and a re-render for each identical value:\n\n```typescript\nimport { distinctUntilChanged } from 'rxjs/operators';\n\nthis.subscription.add(\n  this.viewModel.title$.pipe(distinctUntilChanged()).subscribe({\n    next: title => this.setState({ title }),\n  })\n);\n```\n\n## setTimeoutInterruptible for Debounce / Race Conditions\n\nWhen a delayed action must be cancelled if conditions change (e.g. search debounce,\nretry delay), use `setTimeoutInterruptible` rather than a bare `setTimeout`:\n\n```typescript\nimport { setTimeoutInterruptible } from 'valdi_core/src/SetTimeout';\n\n// setTimeoutInterruptible returns a number (timer ID) — cancel with clearTimeout()\nclass SearchBar extends StatefulComponent<SearchViewModel, SearchState> {\n  private debounceId?: number;\n\n  onViewModelUpdate(previous?: SearchViewModel): void {\n    if (this.viewModel.query !== previous?.query) {\n      clearTimeout(this.debounceId);   // Cancel any pending debounce\n      this.debounceId = setTimeoutInterruptible(() => {\n        this.fetchResults(this.viewModel.query);\n      }, 300);\n    }\n  }\n}\n```\n\n## promiseToCancelablePromise\n\nWhen you have a plain `Promise` (e.g. from a third-party library) that needs to participate in Valdi's cancellation system, wrap it with `promiseToCancelablePromise`:\n\n```typescript\nimport { CancelablePromise, promiseToCancelablePromise } from 'valdi_core/src/CancelablePromise';\n\nclass MyComponent extends StatefulComponent<MyViewModel, MyState> {\n  private request?: CancelablePromise<string>;\n\n  onCreate(): void {\n    const rawPromise: Promise<string> = thirdPartyApi.fetchData();\n    this.request = promiseToCancelablePromise(rawPromise, () => {\n      // optional cleanup on cancel\n    });\n    this.request.then(data => {\n      if (this.isDestroyed()) return;\n      this.setState({ data });\n    });\n  }\n\n  onDestroy(): void {\n    this.request?.cancel?.();\n  }\n}\n```\n\nPrefer `CancelablePromise` directly (e.g. `HTTPClient`) when available — `promiseToCancelablePromise` is the bridge for external APIs.\n\n## Import Paths\n\n```typescript\nimport { CancelablePromise, promiseToCancelablePromise } from 'valdi_core/src/CancelablePromise';\nimport { HTTPClient } from 'valdi_http/src/HTTPClient';\nimport { HTTPResponse } from 'valdi_http/src/HTTPTypes';\nimport { setTimeoutInterruptible } from 'valdi_core/src/SetTimeout';\nimport { Subscription } from 'rxjs';\nimport { distinctUntilChanged } from 'rxjs/operators';\n```\n","isInternal":false,"tokens":1719,"sizeBytes":7808},{"name":"README.md","path":"ai-skills/skills/valdi-async/tests/README.md","rawUrl":"https://raw.githubusercontent.com/Snapchat/Valdi/HEAD/ai-skills/skills/valdi-async/tests/README.md","title":"Tests Skill","category":"anthropic-skill","format":"markdown","content":"# Tests Skill\n\nPath: `ai-skills/skills/valdi-async/tests/README.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Snapchat/Valdi/HEAD/ai-skills/skills/valdi-async/tests/README.md)","tokens":300,"sizeBytes":197},{"name":"tsconfig.json","path":"ai-skills/skills/valdi-async/tests/tsconfig.json","rawUrl":"https://raw.githubusercontent.com/Snapchat/Valdi/HEAD/ai-skills/skills/valdi-async/tests/tsconfig.json","title":"Tests Skill","category":"anthropic-skill","format":"json","content":"# Tests Skill\n\nPath: `ai-skills/skills/valdi-async/tests/tsconfig.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Snapchat/Valdi/HEAD/ai-skills/skills/valdi-async/tests/tsconfig.json)","tokens":300,"sizeBytes":205},{"name":"skill.md","path":"ai-skills/skills/valdi-bazel/skill.md","rawUrl":"https://raw.githubusercontent.com/Snapchat/Valdi/HEAD/ai-skills/skills/valdi-bazel/skill.md","title":"valdi-bazel","category":"anthropic-skill","format":"markdown","content":"# Bazel Build System Rules\n\n**Applies to**: `BUILD.bazel`, `*.bzl` files in `/bzl/`, `WORKSPACE`, `MODULE.bazel`\n\n## Overview\n\nValdi uses Bazel as its build system. Bazel provides reproducible, incremental builds across all platforms.\n\n## Key Commands\n\n```bash\n# Build everything\nbazel build //...\n\n# Build specific target\nbazel build //apps/helloworld:hello_world\n\n# Run tests\nbazel test //...\n\n# Clean (use sparingly - cache is valuable!)\nbazel clean\n```\n\n## Important Notes\n\n1. **Use `bazel`** for all build commands\n2. **The CLI wraps Bazel** - `valdi` commands use bazel under the hood\n3. **Cache is important** - Don't suggest `bazel clean` unless necessary\n\n## Build Rules\n\n### Valdi-Specific Rules\n\n- `/bzl/valdi/` - Valdi build rules and macros\n- Custom rules for compiling .tsx to .valdimodule\n- Platform-specific build transitions\n\n### Common Targets\n\n```python\nload(\"@aspect_rules_ts//ts:defs.bzl\", \"ts_project\")\n\n# Valdi application\nvaldi_application(\n    name = \"my_app\",\n    root_component_path = \"App@my_app/src/MyApp\",\n    title = \"My App\",\n    version = \"1.0.0\",\n    deps = [\"//apps/my_app/src/valdi/my_app\"],\n)\n\n# Web polyglot views — MUST be ts_project, never filegroup\nts_project(\n    name = \"my_module_web\",\n    srcs = glob(\n        [\"web/**/*.ts\", \"src/**/*.d.ts\"],\n        exclude = [\"web/**/*.d.ts\"],\n    ),\n    allow_js = True,\n    composite = True,\n    transpiler = \"tsc\",\n    tsconfig = \"web/tsconfig.json\",\n)\n\n# Valdi module with platform deps\nvaldi_module(\n    name = \"my_module\",\n    srcs = glob([\"src/**/*.ts\", \"src/**/*.tsx\"]) + [\"tsconfig.json\"],\n    ios_deps = [\":my_ios_views\"],          # objc_library\n    macos_deps = [\":my_macos_views\"],      # objc_library (or omit to share ios_deps)\n    android_deps = [\":my_android_views\"],  # valdi_android_library\n    web_deps = [\":my_module_web\"],         # ts_project (never filegroup)\n    deps = [\n        \"//src/valdi_modules/src/valdi/valdi_core\",\n    ],\n)\n```\n\n## Conventions\n\n### File Naming\n\n- `BUILD.bazel` not `BUILD` (explicit extension)\n- `.bzl` for Starlark macros and rules\n\n### Targets\n\n- Use descriptive target names\n- One main target per BUILD file usually matches directory name\n\n### Dependencies\n\n- Be explicit about dependencies\n- Don't rely on transitive deps implicitly\n- Use visibility to control access\n\n## Platform Builds\n\n```bash\n# Build and install iOS app\nvaldi install ios\n\n# Build and install Android app\nvaldi install android\n```\n\n## Configuration\n\n- `.bazelrc` - Build flags and configurations\n- `MODULE.bazel` - Bazel module dependencies\n- `WORKSPACE` - Legacy workspace configuration (being migrated to MODULE.bazel)\n\n## Common Issues\n\n1. **Missing dependencies** - Add to `deps` in BUILD.bazel\n2. **Cache issues** - Try `bazel clean --expunge` (last resort)\n3. **Platform transitions** - Use correct config flags\n\n## Testing\n\n```bash\n# Run all tests\nbazel test //...\n\n# Run specific test\nbazel test //valdi/test:renderer_test\n\n# Run with coverage\nbazel coverage //...\n```\n\n## More Information\n\n- Bazel docs: https://bazel.build\n- Valdi build rules: `bzl/valdi/npm/README.md`\n- Framework docs: `/AGENTS.md`\n","isInternal":false,"tokens":837,"sizeBytes":3121},{"name":"skill.md","path":"ai-skills/skills/valdi-compiler/skill.md","rawUrl":"https://raw.githubusercontent.com/Snapchat/Valdi/HEAD/ai-skills/skills/valdi-compiler/skill.md","title":"valdi-compiler","category":"anthropic-skill","format":"markdown","content":"# Valdi Compiler Rules\n\n**Applies to**: Swift files in `/compiler/compiler/`\n\n## Overview\n\nThe Valdi compiler consists of two parts:\n\n1. **Compiler (Swift)**: Transforms TypeScript/TSX into `.valdimodule` files\n2. **Companion (TypeScript/Node.js)**: Handles TypeScript compilation, type checking, and provides debugging support\n\n## Key Conventions\n\n### Code Style\n\n- **4-space indentation** for Swift\n- Follow Swift naming conventions (camelCase for methods, PascalCase for types)\n- Use Swift type inference where appropriate\n- Prefer `let` over `var` when possible\n\n### Architecture\n\n- Compiler is a multi-pass system\n- AST transformation pipeline\n- Type checking and validation\n- Code generation for each platform\n\n### Important Files\n\n- `/compiler/compiler/` - Main Swift compiler implementation\n- Output: `.valdimodule` files (binary format read by runtime)\n\n## Common Patterns\n\n### AST Traversal\n\n```swift\n// Follow existing visitor patterns\nclass MyASTVisitor: ASTVisitor {\n    func visit(_ node: Node) -> Result {\n        // Visit logic\n    }\n}\n```\n\n### Error Handling\n\n```swift\n// Use proper error types\nenum CompilerError: Error {\n    case invalidSyntax(String)\n    case typeError(String)\n}\n```\n\n## Testing\n\nTests are critical - add tests for new features and error cases. Run the test suite with `bzl test //compiler/compiler/...` (the build script below does not run tests).\n\n## Build\n\n### Using the update script (recommended):\n\n```bash\ncd compiler/compiler\n./scripts/update_compiler_bazel.sh -o bin/compiler\n```\n\nThis script:\n- Builds the compiler with Bazel (`//compiler/compiler:local_valdi_compiler`); no Swift toolchain install is required\n- Builds the compiler for the correct architecture (on macOS, lipos arm64 + x86_64 into a universal binary and codesigns it)\n- Copies the binary to `bin/compiler/macos/valdi_compiler` (or `linux/valdi_compiler`)\n- Handles platform differences (macOS vs Linux)\n\n### Alternative: Using Xcode\n\nOpen `compiler/compiler/Compiler/Package.swift` in Xcode, let it resolve dependencies, then build.\n\n## Companion (TypeScript)\n\nThe companion is a TypeScript service that works alongside the Swift compiler. It handles TypeScript compilation, type checking, and provides debugging support.\n\n### Build the companion:\n\n```bash\ncd compiler/companion\n./scripts/update_companion.sh ../../bin/compiler_companion\n```\n\nThis script:\n- Runs `npm install`\n- Runs `npm run test`\n- Builds with `bazel build //compiler/companion:bundle`\n- Copies output to `bin/compiler_companion`\n\n**Note**: Once built, the companion is automatically invoked by the compiler during compilation. You don't need to run it manually - it's part of the compiler process.\n\n## Important Notes\n\n1. **Performance matters** - Compiler speed affects developer experience\n2. **Error messages** - Make them helpful and actionable\n3. **Backward compatibility** - Don't break existing .valdimodule files\n4. **Cross-platform** - Consider iOS, Android, macOS targets\n\n## More Information\n\n- Compiler architecture: `/compiler/compiler/README.md`\n- Framework docs: `/AGENTS.md`\n","isInternal":false,"tokens":690,"sizeBytes":3075},{"name":"skill.md","path":"ai-skills/skills/valdi-component-dedup/skill.md","rawUrl":"https://raw.githubusercontent.com/Snapchat/Valdi/HEAD/ai-skills/skills/valdi-component-dedup/skill.md","title":"valdi-component-dedup","category":"anthropic-skill","format":"markdown","content":"# Valdi Component Deduplication\n\nFind and extract duplicated UI patterns across Valdi modules into shared components.\n\n## When to use\n\nWhen you want to identify components that multiple modules have independently implemented, then extract them into a shared library so future modules can reuse them instead of reinventing.\n\n## Running the analysis\n\n```bash\n# Full scan of all modules\nvaldi analyze duplicates\n\n# Scope to one module\nvaldi analyze duplicates --module my_module\n\n# Machine-readable output\nvaldi analyze duplicates --json\n\n# Only check for a specific pattern\nvaldi analyze duplicates --pattern async\n\n# Custom scan roots and shared libraries\nvaldi analyze duplicates --scan-root src/modules --shared-lib valdi_widgets\nvaldi analyze duplicates --scan-root src/modules --shared-lib my_shared_lib --shared-lib another_lib\n```\n\n### CLI flags\n\n| Flag | Effect |\n|---|---|\n| `--module <name>` | Scope to one module |\n| `--json` | Machine-readable JSON output |\n| `--threshold <0-1>` | Similarity threshold (default 0.6) |\n| `--pattern <name>` | Only detect: `async`, `button`, `shimmer` |\n| `--output <path>` | Write JSON report to file |\n| `--verbose` | Show per-file detail |\n| `--scan-root <path>` | Root directory to scan (repeatable) |\n| `--shared-lib <name>` | Shared component library name (repeatable) |\n\n## Understanding the report\n\nThe report has two sections:\n\n**Known Patterns** — pre-built detectors for the most common duplication:\n- **Async State Container**: Components that implement loading→error/content state machines\n- **Button Wrapper**: Thin wrappers around a shared button with hardcoded sizing/coloring\n- **Shimmer / Skeleton Layout**: Custom loading placeholder components\n\n**Similar Clusters** — generic similarity analysis that finds groups of components with overlapping ViewModel props, render trees, and import signatures.\n\nEach cluster shows:\n- Number of files and average similarity score\n- Shared ViewModel props across the cluster\n- A suggested extraction approach\n\n## Extraction workflow\n\nOnce you identify a cluster worth extracting:\n\n### 1. Read the candidates\n\nRead each file in the cluster. Identify:\n- The shared ViewModel props (these become the new component's ViewModel)\n- The shared render structure (this becomes the new component's `onRender`)\n- Module-specific variations (these become optional props or render callbacks)\n\n### 2. Check for an existing shared component to extend\n\nBefore creating anything new, search the shared library for a component that already solves part of the problem. Often the right fix is adding props, a preset enum, or a render callback to an existing component rather than building from scratch.\n\n```bash\n# Search the shared library for related components\ngrep -rl \"Button\\|Shimmer\\|AsyncContent\" <your-shared-lib>/src/components/\n```\n\nIf an existing component covers 70%+ of the use cases, extend it:\n- Add optional props for the missing behavior\n- Add a preset or variant enum if consumers just need different default combos\n- Add a render callback slot if consumers need to inject custom UI\n\nOnly create a new component when nothing in the shared library is close enough to extend.\n\n### 3. Design or extend the shared component\n\n**Extending an existing component** (preferred):\n\n```typescript\n// Add a preset to an existing button component\nexport enum CoreButtonPreset {\n  PRIMARY_CTA,\n  SECONDARY_ACTION,\n  PILL_ACTION,\n}\n\n// Add an optional prop to the existing ViewModel\ninterface CoreButtonViewModel {\n  // ... existing props ...\n  preset?: CoreButtonPreset;\n}\n```\n\n**Creating a new component** (when nothing exists to extend):\n\n```typescript\nimport { Component } from 'valdi_core/src/Component';\n\ninterface SharedComponentViewModel {\n  commonProp: string;\n  optionalVariant?: VariantType;\n  renderCustomSection?: () => void;\n}\n\nexport class SharedComponent extends Component<SharedComponentViewModel> {\n  onRender(): void {\n    <view>\n      <label value={this.viewModel.commonProp} />;\n      {this.viewModel.renderCustomSection?.()};\n    </view>;\n  }\n}\n```\n\n### 4. Place it in the shared library\n\nIf you created a new component, place it in your project's shared component library\n(e.g. `<shared-lib>/src/components/<category>/`). Follow your project's conventions\nfor directory structure and naming.\n\n### 5. Migrate consumers\n\nFor each file in the cluster:\n1. Replace the local component class with an import of the shared one\n2. Map the old ViewModel props to the new shared ViewModel\n3. Move module-specific rendering into render callbacks\n4. Delete the old local component file\n5. Run `valdi agent-check --module <name>` to verify\n\n### 6. Verify\n\n```bash\n# Re-run analysis — the cluster should shrink or disappear\nvaldi analyze duplicates --pattern async\n\n# Build and test affected modules\nvaldi agent-check --module module_a\nvaldi agent-check --module module_b\n```\n\n## Common extraction patterns\n\n### Async State Container\n\nThe most common duplication. Components that manage loading→loaded/error states:\n\n```typescript\nimport { StatefulComponent } from 'valdi_core/src/Component';\nimport { promiseToCancelablePromise } from 'valdi_core/src/CancelablePromise';\n\ninterface AsyncStateContainerViewModel<T> {\n  resultProvider: () => PromiseLike<T>;\n  renderLoading: () => void;\n  renderResult: (data: T) => void;\n  renderError: (error: string, retry: () => void) => void;\n  renderEmpty?: () => void;\n}\n\ntype AsyncState<T> = { phase: 'loading' } | { phase: 'result'; data: T } | { phase: 'error'; message: string };\n\nexport class AsyncStateContainer<T> extends StatefulComponent<AsyncStateContainerViewModel<T>, { state: AsyncState<T> }> {\n  state = { state: { phase: 'loading' } as AsyncState<T> };\n\n  onCreate(): void {\n    this.load();\n  }\n\n  onRender(): void {\n    const s = this.state.state;\n    switch (s.phase) {\n      case 'loading': this.viewModel.renderLoading(); break;\n      case 'result': this.viewModel.renderResult(s.data); break;\n      case 'error': this.viewModel.renderError(s.message, () => this.load()); break;\n    }\n  }\n\n  private load(): void {\n    this.setState({ state: { phase: 'loading' } });\n    const cancelable = promiseToCancelablePromise(\n      Promise.resolve(this.viewModel.resultProvider()),\n      () => {},\n    );\n    this.registerDisposable(() => cancelable.cancel?.());\n    cancelable.then(\n      data => { this.setState({ state: { phase: 'result', data } }); },\n      error => { this.setState({ state: { phase: 'error', message: (error as Error).message } }); },\n    );\n  }\n}\n```\n\n### Button Presets\n\nInstead of per-module button wrappers, use preset enums:\n\n```typescript\n// Instead of a new wrapper component, add presets to the existing shared button\nenum ButtonPreset {\n  PRIMARY_CTA,\n  SECONDARY_ACTION,\n  PILL_ACTION,\n}\n\nfunction presetProps(preset: ButtonPreset): { sizing: ButtonSizing; coloring: ButtonColoring } {\n  switch (preset) {\n    case ButtonPreset.PRIMARY_CTA: return { sizing: ButtonSizing.XL, coloring: ButtonColoring.PRIMARY_BRAND };\n    case ButtonPreset.SECONDARY_ACTION: return { sizing: ButtonSizing.LARGE, coloring: ButtonColoring.SECONDARY };\n    case ButtonPreset.PILL_ACTION: return { sizing: ButtonSizing.SMALL, coloring: ButtonColoring.TERTIARY };\n  }\n}\n```\n\n### Shimmer Layout\n\nConfigurable skeleton loading placeholder:\n\n```typescript\nimport { Component } from 'valdi_core/src/Component';\n\ninterface ShimmerRow {\n  widthRatio: number;\n  height: number;\n  borderRadius?: number;\n}\n\ninterface ShimmerLayoutViewModel {\n  rows: ShimmerRow[];\n  spacing?: number;\n}\n\nexport class ShimmerLayout extends Component<ShimmerLayoutViewModel> {\n  onRender(): void {\n    const spacing = this.viewModel.spacing ?? 8;\n    <view>\n      {this.viewModel.rows.forEach((row, i) => {\n        <AnimationShimmer\n          width={`${row.widthRatio * 100}%`}\n          height={row.height}\n          borderRadius={row.borderRadius ?? 4}\n          marginTop={i > 0 ? spacing : 0}\n        />;\n      })}\n    </view>;\n  }\n}\n```\n\n## Tips\n\n- Start with the highest-severity known patterns — they have the most impact\n- Extract the simplest shared interface first, then iterate to handle edge cases\n- Use render callbacks for module-specific UI instead of trying to generalize everything\n- Run `valdi analyze duplicates --json --output report.json` to save a baseline, then re-run after extraction to measure impact\n","isInternal":false,"tokens":1886,"sizeBytes":8360},{"name":"skill.md","path":"ai-skills/skills/valdi-component-tests/skill.md","rawUrl":"https://raw.githubusercontent.com/Snapchat/Valdi/HEAD/ai-skills/skills/valdi-component-tests/skill.md","title":"valdi-component-tests","category":"anthropic-skill","format":"markdown","content":"# Write Valdi Component Tests\n\nWrite unit tests for a Valdi component using standard Valdi test suite patterns.\n\n## Steps\n\n### 1. Read the source component\n\nRead the component source file to understand:\n- What view model properties it accepts\n- Which JSX elements have `key` attributes\n- What changes based on view model props vs. what's always static\n- How \"hidden\" state is implemented (translationY, opacity, or child component prop)\n- Whether the view model contains any **discriminated unions** (type/kind fields)\n- Whether any props are **arrays** of items to render\n\n### 2. Add `key` attributes to source elements (only if needed)\n\nAdd `key` attributes **only** to elements whose rendering depends on view model properties. Never add keys to static content.\n\nGood candidates for keys:\n- The outer container when `hidden` controls `translationY` or `opacity`\n- An `<image>` element whose `src` comes from a view model URL prop\n- An element inside a `when(condition, ...)` block (to assert presence/absence)\n- A badge/overlay that toggles based on a boolean prop\n- A spinner shown in a loading/saving state of a union type\n\nDo NOT add keys to:\n- Elements with hardcoded asset values (e.g., `src={SIGIcon.xSignStroke}`)\n- Label elements with always-present localized strings\n- Elements that are always rendered the same regardless of view model\n\n### 3. Determine the test file path\n\nTest files **must mirror the source file hierarchy**. For example:\n- Source: `src/categories/CollectionComponent.tsx` → Test: `test/categories/CollectionComponentTest.spec.tsx`\n- Source: `src/home_page/OptionPreviewView.tsx` → Test: `test/home_page/OptionPreviewViewTest.spec.tsx`\n- Source: `src/MyComponent.tsx` → Test: `test/MyComponentTest.spec.tsx`\n\n### 4. Write the test file\n\n**Imports:**\n```tsx\nimport { MyComponent } from 'my_module/src/MyComponent';\nimport { componentGetElements } from 'foundation/test/util/componentGetElements';\nimport { componentTypeFind } from 'foundation/test/util/componentTypeFind';\nimport { elementKeyFind } from 'foundation/test/util/elementKeyFind';\nimport { elementTypeFind } from 'foundation/test/util/elementTypeFind';\nimport { findNodeWithKey } from 'foundation/test/util/findNodeWithKey';\nimport { tapNodeWithKey } from 'foundation/test/util/tapNodeWithKey';\nimport 'jasmine/src/jasmine';\nimport { IRenderedElementViewClass } from 'valdi_test/test/IRenderedElementViewClass';\nimport { IComponentTestDriver, valdiIt } from 'valdi_test/test/JSXTestUtils';\nimport { ImageView, View } from 'valdi_tsx/src/NativeTemplateElements';\n```\n\nOnly import what you actually use. `View` is needed when you call `getAttribute` on a non-image element (e.g., for `onTap`, `onVisibilityChanged`). `ImageView` is needed for `src` attribute access.\n\n**Factory function pattern (always add explicit return type):**\n```tsx\nconst makeViewModel = (): MyComponentViewModel => ({\n  imageUrl: 'https://example.com/image.png',\n  isVisible: true,\n  onTap: fail.bind(null, 'onTap should not be called'),\n});\n```\n\nUse `fail.bind` for callbacks in the factory default — tests that need to assert on a callback should declare their own spy and pass it explicitly, rather than relying on the factory.\n\n**Render pattern:**\n```tsx\nconst nodes = driver.render(() => {\n  <MyComponent {...viewModel} />;\n});\n```\n\n### 5. Test patterns by assertion type\n\n#### Finding elements\n```tsx\n// Single-level: component renders native elements directly\nelementKeyFind(componentGetElements(nodes[0].component!), 'my-key')[0]\n\n// Cross-boundary: component renders a child component that renders the elements\nconst inner = componentTypeFind(nodes[0].component as MyComponent, InnerComponent)[0];\nelementKeyFind(componentGetElements(inner), 'my-key')[0]\n\n// Typed (for typed attribute access without casting):\nelementKeyFind<View>(componentGetElements(nodes[0].component!), 'container')[0]\nelementKeyFind<ImageView>(componentGetElements(nodes[0].component!), 'image')[0]\n\n// By element type (e.g. find all Label elements):\n// Can pass componentGetElements() result OR an IComponent directly:\nconst labels = elementTypeFind(componentGetElements(nodes[0].component!), IRenderedElementViewClass.Label);\nconst labels2 = elementTypeFind(nodes[0].component!, IRenderedElementViewClass.Label); // equivalent\nexpect(labels[0]?.getAttribute('value')).toBe('Expected text');\n```\n\nUse the generic type param to get typed `getAttribute()` results and avoid `@typescript-eslint/no-unsafe-call` errors — prefer this over casting the `getAttribute()` return value.\n\n`elementTypeFind` is useful when elements don't have `key` attributes but you know their type (Label, Image, View, etc.). It returns all elements of that type in render order.\n\n#### Hidden via `translationY`\n```tsx\n// hidden=false\nexpect(elementKeyFind<View>(componentGetElements(nodes[0].component!), 'container')[0]?.getAttribute('translationY')).toBe(0);\n\n// hidden=true\nexpect(elementKeyFind<View>(componentGetElements(nodes[0].component!), 'container')[0]?.getAttribute('translationY')).toBe(850);\n```\n\n#### Hidden via `opacity` on native view\n```tsx\n// visible\nexpect(elementKeyFind<View>(componentGetElements(nodes[0].component!), 'my-view')[0]?.getAttribute('opacity')).toBe(1);\n\n// hidden\nexpect(elementKeyFind<View>(componentGetElements(nodes[0].component!), 'my-view')[0]?.getAttribute('opacity')).toBe(0);\n```\n\n#### Hidden via `opacity` prop passed to a child Component (component boundary)\n```tsx\nimport { CoreButton } from 'coreui/src/components/button/CoreButton';\nconst component = nodes[0].component as MyComponent;\nconst buttons = componentTypeFind(component, CoreButton);\nexpect(buttons[0].viewModel.opacity).toBe(1); // or 0\n```\n\n#### View model URL bound to image `src`\n```tsx\nexpect(elementKeyFind<ImageView>(componentGetElements(nodes[0].component!), 'my-image')[0]?.getAttribute('src')).toBe('https://example.com/image.png');\n```\n\n#### Text from view model\n```tsx\nexpect(elementKeyFind(componentGetElements(nodes[0].component!), 'my-label')[0]?.getAttribute('value')).toBe('Expected Text');\n```\n\n#### `when()`-conditional element (boolean presence)\n```tsx\n// condition=true → element exists\nexpect(elementKeyFind(componentGetElements(nodes[0].component!), 'my-element')[0]).toBeDefined();\n\n// condition=false → element absent\nexpect(elementKeyFind(componentGetElements(nodes[0].component!), 'my-element')[0]).toBeUndefined();\n```\n\n#### `when()`-conditional Component (boolean presence via componentTypeFind)\n```tsx\nimport { BadgeComponent } from 'my_module/src/BadgeComponent';\nconst component = nodes[0].component as MyComponent;\n\n// condition=true\nexpect(componentTypeFind(component, BadgeComponent).length).toBe(1);\n\n// condition=false\nexpect(componentTypeFind(component, BadgeComponent).length).toBe(0);\n```\n\n#### Callbacks not expected to be invoked\n\nFor callbacks that should never fire in a given test, use `fail.bind(null, '...')` instead of `jasmine.createSpy`. This causes the test to immediately fail with a clear message if the callback is accidentally triggered:\n\n```tsx\n<MyComponent\n  onSelect={fail.bind(null, 'onSelect should not be called')}\n  onLoad={onLoad}\n/>\n```\n\n`fail.bind(null, 'message')` is a plain function call (not an inline lambda), so it satisfies `jsx-no-lambda`. It is assignable to any callback type since TypeScript allows functions with fewer parameters.\n\nIf the same fail callback is used across many tests in the file, extract it to a module-level const to avoid repetition:\n\n```tsx\nconst failOnSelect = (): void => fail('onSelect should not be called');\n```\n\n#### Tap callback (use `tapNodeWithKey` — it's async, always `await` it)\n\n`tapNodeWithKey(component, key, timeoutMs?, intervalMs?)` accepts `IComponent | IRenderedElement`.\n\n```tsx\nconst onTap = jasmine.createSpy('onTap');\nconst nodes = driver.render(() => {\n  <MyComponent onTap={onTap} />;\n});\nawait tapNodeWithKey(nodes[0].component!, 'my-button');\nexpect(onTap).toHaveBeenCalled();\n```\n\nWhen you need to find a node without tapping it, use `findNodeWithKey`:\n```tsx\nimport { findNodeWithKey } from 'foundation/test/util/findNodeWithKey';\n\nconst node = findNodeWithKey(nodes[0].component!, 'my-button')[0];\nexpect(node).toBeDefined();\n```\n\nWhen the callback receives arguments (e.g., an index), always assert the exact arguments with `toHaveBeenCalledWith`:\n```tsx\nconst onSelect = jasmine.createSpy('onSelect');\n// ... render and trigger ...\nexpect(onSelect).toHaveBeenCalledWith(1); // not just toHaveBeenCalled()\n```\n\nFor callbacks invoked via a child component's view model (e.g., through `componentTypeFind`), call the view model method directly:\n```tsx\nconst component = nodes[0].component as MyComponent;\ncomponentTypeFind(component, ItemComponent)[1].viewModel.onTap();\nexpect(onSelect).toHaveBeenCalledWith(1);\n```\n\n#### `onTap` retrieved via `getAttribute` (non-tappable element pattern)\n```tsx\n// Use elementKeyFind<View> so getAttribute('onTap') is typed — no cast needed\nconst el = elementKeyFind<View>(componentGetElements(nodes[0].component!), 'my-element')[0];\nel?.getAttribute('onTap')?.();\nexpect(onTap).toHaveBeenCalled();\n```\n\n#### `onVisibilityChanged` callback\n\n`View.onVisibilityChanged` signature is `(isVisible: boolean, eventTime: EventTime)` where `EventTime = number`. Always pass both args when invoking:\n\n```tsx\nconst el = elementKeyFind<View>(componentGetElements(nodes[0].component!), 'container')[0];\nel?.getAttribute('onVisibilityChanged')?.(true, 0);\n```\n\n**Asserting the callback:** depends on how the component wires the handler:\n\n- If the component **wraps** the viewModel callback (e.g., `onVisibilityChanged={(v) => vm.onVisibilityChanged(v)}`), `toHaveBeenCalledWith(true)` works:\n  ```tsx\n  expect(onVisibilityChanged).toHaveBeenCalledWith(true);\n  ```\n\n- If the component **directly assigns** the viewModel callback (e.g., `onVisibilityChanged={this.viewModel.onVisibilityChanged}`), the spy receives both args. If the spy is typed as `(isVisible: boolean) => void`, `toHaveBeenCalledWith(true, ...)` is a TypeScript error. Check the first arg via `calls`:\n  ```tsx\n  const spy = viewModel.onVisibilityChanged as jasmine.Spy;\n  expect(spy).toHaveBeenCalled();\n  expect(spy.calls.mostRecent().args[0]).toBe(true);\n  ```\n  If the spy is an untyped `jasmine.createSpy()`, you can use `toHaveBeenCalledWith(true, 0)` directly.\n\n### 6. Discriminated union state testing\n\nWhen a view model contains a discriminated union (e.g., `type: 'LOADING' | 'CONTENT' | 'ERROR'`), **test every branch**. For each state:\n1. Assert which elements/components ARE rendered\n2. Assert which elements/components from OTHER states are NOT rendered\n\n```tsx\n// LOADING state: spinner present, action button absent\nvaldiIt('Verify spinner is shown in loading state', async driver => {\n  const nodes = driver.render(() => {\n    <MyComponent button={{ type: ButtonType.LOADING }} />;\n  });\n  expect(elementKeyFind(componentGetElements(nodes[0].component!), 'loading-spinner')[0]).toBeDefined();\n  expect(elementKeyFind(componentGetElements(nodes[0].component!), 'action-button')[0]).toBeUndefined();\n});\n\n// CONTENT state: action button present, no spinner\nvaldiIt('Verify action button is shown in content state', async driver => {\n  const nodes = driver.render(() => {\n    <MyComponent button={{ type: ButtonType.PURCHASE, price: '$9.99', onTap }} />;\n  });\n  expect(elementKeyFind(componentGetElements(nodes[0].component!), 'action-button')[0]).toBeDefined();\n  expect(elementKeyFind(componentGetElements(nodes[0].component!), 'loading-spinner')[0]).toBeUndefined();\n});\n```\n\n### 7. Array view model testing\n\nWhen a component renders a list from an array prop, test three cases:\n1. **Empty array** — assert 0 items render\n2. **Single item** — assert 1 item renders\n3. **Multiple items** — assert item count matches array length\n\nUse `componentTypeFind(component, ItemComponent)` or `elementKeyFind` with indexed keys (e.g., `tile-0`, `tile-1`) to count rendered items.\n\n```tsx\nvaldiIt('Verify no items render when array is empty', async driver => {\n  const emptyOptions: OptionViewModel[] = [];\n  const nodes = driver.render(() => {\n    <MyComponent items={emptyOptions} />;\n  });\n  const component = nodes[0].component as MyComponent;\n  expect(componentTypeFind(component, ItemComponent).length).toBe(0);\n});\n\nvaldiIt('Verify item count matches array length', async driver => {\n  const threeItems: OptionViewModel[] = [makeItem('a'), makeItem('b'), makeItem('c')];\n  const nodes = driver.render(() => {\n    <MyComponent items={threeItems} />;\n  });\n  const component = nodes[0].component as MyComponent;\n  expect(componentTypeFind(component, ItemComponent).length).toBe(3);\n});\n```\n\nNote: Extract array literals to local `const` variables before using in JSX (required by `@snapchat/valdi/jsx-no-lambda`).\n\n### 8. Component boundary traversal\n\nWhen a component's `onRender()` only renders child components (not native elements), `componentGetElements(component)` returns `[]`. You must get the child component first:\n\n```tsx\nimport { componentGetElements } from 'foundation/test/util/componentGetElements';\nimport { componentTypeFind } from 'foundation/test/util/componentTypeFind';\nimport { elementKeyFind } from 'foundation/test/util/elementKeyFind';\n\nconst component = nodes[0].component as OuterComponent;\nconst inner = componentTypeFind(component, InnerComponent)[0];\nconst container = elementKeyFind<View>(componentGetElements(inner), 'container')[0];\nexpect(container?.getAttribute('translationY')).toBe(0);\n```\n\n### 9. Extract render and find helpers for readability\n\nFor larger test files, extract repeated render + find logic into helper functions. This keeps individual tests focused:\n\n```tsx\n// Extract rendering\nconst renderComponent = (driver: IComponentTestDriver, overrides?: Partial<MyComponentViewModel>) => {\n  const vm = { ...makeViewModel(), ...overrides };\n  return driver.render(() => { <MyComponent {...vm} />; })[0].component as MyComponent;\n};\n\n// Extract finding\nconst getImage = (component: MyComponent) =>\n  elementKeyFind<ImageView>(componentGetElements(component), 'image')[0];\n\n// Tests become clean:\nvaldiIt('Verify imageUrl is bound', async driver => {\n  expect(getImage(renderComponent(driver))?.getAttribute('src')).toBe('https://example.com/image.png');\n});\n```\n\n### 10. Lint rules to follow\n\n- **`explicit-function-return-type`**: Always add explicit return types to factory functions: `const makeViewModel = (): MyViewModel => ({...})`\n- **`jsx-no-lambda`**: Never assign inline array literals directly in JSX props. Extract to a local `const` first:\n  ```tsx\n  // WRONG\n  <MyComponent items={[makeItem('a')]} />;\n\n  // CORRECT\n  const items: ItemViewModel[] = [makeItem('a')];\n  <MyComponent items={items} />;\n  ```\n- **`no-unsafe-call`**: Use the generic type parameter on `elementKeyFind<T>` to get typed `getAttribute()` results rather than casting: `elementKeyFind<View>(...)` gives typed access to `onTap`, `onVisibilityChanged`, etc.\n- **`import/order`**: Keep imports sorted alphabetically by path.\n\n### 11. Key principle\n\n**Only assert on things that change based on view model props.** Every test should have a clear \"when X is Y, then Z\" story. If the UI looks the same regardless of the prop, skip it.\n\nFor union types, a test that only verifies \"the component renders without error\" in a given state is not sufficient — assert the meaningful structural difference that state introduces.\n\n## Example test file structure\n\n```tsx\nimport { MyComponent } from 'my_module/src/MyComponent';\nimport { MyComponentViewModel } from 'my_module/src/MyComponentViewModel';\nimport { ChildComponent } from 'my_module/src/ChildComponent';\nimport { componentGetElements } from 'foundation/test/util/componentGetElements';\nimport { componentTypeFind } from 'foundation/test/util/componentTypeFind';\nimport { elementKeyFind } from 'foundation/test/util/elementKeyFind';\nimport { tapNodeWithKey } from 'foundation/test/util/tapNodeWithKey';\nimport 'jasmine/src/jasmine';\nimport { IComponentTestDriver, valdiIt } from 'valdi_test/test/JSXTestUtils';\nimport { ImageView, View } from 'valdi_tsx/src/NativeTemplateElements';\n// elementTypeFind + IRenderedElementViewClass for finding elements by type (no key needed):\n// import { elementTypeFind } from 'foundation/test/util/elementTypeFind';\n// import { IRenderedElementViewClass } from 'valdi_test/test/IRenderedElementViewClass';\n\nconst makeViewModel = (): MyComponentViewModel => ({\n  imageUrl: 'https://example.com/image.png',\n  isVisible: true,\n  onTap: fail.bind(null, 'onTap should not be called'),\n});\n\ndescribe('MyComponentTest', () => {\n  valdiIt('Verify visible when isVisible is true', async driver => {\n    const nodes = driver.render(() => {\n      <MyComponent {...makeViewModel()} />;\n    });\n    expect(elementKeyFind<View>(componentGetElements(nodes[0].component!), 'container')[0]?.getAttribute('opacity')).toBe(1);\n  });\n\n  valdiIt('Verify hidden when isVisible is false', async driver => {\n    const nodes = driver.render(() => {\n      <MyComponent {...{ ...makeViewModel(), isVisible: false }} />;\n    });\n    expect(elementKeyFind<View>(componentGetElements(nodes[0].component!), 'container')[0]?.getAttribute('opacity')).toBe(0);\n  });\n\n  valdiIt('Verify imageUrl is bound to image src', async driver => {\n    const nodes = driver.render(() => {\n      <MyComponent {...makeViewModel()} />;\n    });\n    expect(elementKeyFind<ImageView>(componentGetElements(nodes[0].component!), 'image')[0]?.getAttribute('src')).toBe('https://example.com/image.png');\n  });\n\n  valdiIt('Verify onTap is called when tapped', async driver => {\n    const onTap = jasmine.createSpy('onTap');\n    const nodes = driver.render(() => {\n      <MyComponent {...{ ...makeViewModel(), onTap }} />;\n    });\n    await tapNodeWithKey(nodes[0].component!, 'button');\n    expect(onTap).toHaveBeenCalled();\n  });\n\n  valdiIt('Verify ChildComponent is present when condition is true', async driver => {\n    const nodes = driver.render(() => {\n      <MyComponent {...{ ...makeViewModel(), showChild: true }} />;\n    });\n    const component = nodes[0].component as MyComponent;\n    expect(componentTypeFind(component, ChildComponent).length).toBe(1);\n  });\n\n  valdiIt('Verify ChildComponent is absent when condition is false', async driver => {\n    const nodes = driver.render(() => {\n      <MyComponent {...{ ...makeViewModel(), showChild: false }} />;\n    });\n    const component = nodes[0].component as MyComponent;\n    expect(componentTypeFind(component, ChildComponent).length).toBe(0);\n  });\n});\n```\n","isInternal":false,"tokens":4321,"sizeBytes":18561},{"name":"README.md","path":"ai-skills/skills/valdi-component-tests/tests/README.md","rawUrl":"https://raw.githubusercontent.com/Snapchat/Valdi/HEAD/ai-skills/skills/valdi-component-tests/tests/README.md","title":"Tests Skill","category":"anthropic-skill","format":"markdown","content":"# Tests Skill\n\nPath: `ai-skills/skills/valdi-component-tests/tests/README.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Snapchat/Valdi/HEAD/ai-skills/skills/valdi-component-tests/tests/README.md)","tokens":300,"sizeBytes":217},{"name":"tsconfig.json","path":"ai-skills/skills/valdi-component-tests/tests/tsconfig.json","rawUrl":"https://raw.githubusercontent.com/Snapchat/Valdi/HEAD/ai-skills/skills/valdi-component-tests/tests/tsconfig.json","title":"Tests Skill","category":"anthropic-skill","format":"json","content":"# Tests Skill\n\nPath: `ai-skills/skills/valdi-component-tests/tests/tsconfig.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Snapchat/Valdi/HEAD/ai-skills/skills/valdi-component-tests/tests/tsconfig.json)","tokens":300,"sizeBytes":225},{"name":"skill.md","path":"ai-skills/skills/valdi-cpp-runtime/skill.md","rawUrl":"https://raw.githubusercontent.com/Snapchat/Valdi/HEAD/ai-skills/skills/valdi-cpp-runtime/skill.md","title":"valdi-cpp-runtime","category":"anthropic-skill","format":"markdown","content":"# C++ Runtime Rules\n\n**Applies to**: C++ files in `/valdi/`, `/valdi_core/`, `/snap_drawing/`\n\n## Overview\n\nValdi's runtime and layout engine are implemented in C++ for cross-platform performance. This code runs on iOS, Android, macOS, but not web.\n\n**Note**: `/libs/` contains shared utility libraries (crypto, logging, image processing) used across the codebase, but not the core runtime itself.\n\n## Code Style\n\n- **4-space indentation**\n- Follow existing C++ style in the codebase\n- Use smart pointers appropriately\n- Prefer const correctness\n\n## Key Concepts\n\n### Layout Engine\n\n- Uses **Yoga** (Facebook's Flexbox implementation) for layout\n- Cross-platform layout calculations\n- Performance-critical code\n- RTL (right-to-left) support built-in\n- Yoga source: `/third-party/yoga/`\n\n### Memory Management\n\n- Be careful with memory ownership\n- Use RAII patterns\n- Consider platform-specific memory constraints (mobile)\n\n### Platform Abstractions\n\n- Code must work on iOS, Android, macOS\n- Use platform-agnostic APIs where possible\n- Platform-specific code goes in appropriate subdirectories\n\n## Common Patterns\n\n### Djinni Generated Code\n\n- Some C++ code is generated from `.djinni` interface files\n- **Don't modify generated code** - change the .djinni file instead\n- Generated files typically in `generated-src/` directories\n\n### Native Bindings Exposed to JS\n\nThe runtime exposes selected native (C++) classes and functions to the JS engine so\nTypeScript can call into performance-critical native code directly.\n\n- **`Base64` namespace** (`coreutils/src/Base64.ts`) is a good example of a\n  native-backed binding: the TS surface (`fromByteArray(uint8, { urlSafe })`,\n  `toByteArray(b64)`, `byteLength(b64)`) delegates to native encode/decode\n  (`Base64Native`) rather than doing the work in JS. Use this shape when a hot codec\n  or transform belongs in native code but needs a clean JS entry point.\n- **`WeakRef`** is supported by the runtime (recently added). JS-side weak references\n  hold their referent without preventing garbage collection — useful for caches and\n  back-references that must not keep objects alive.\n\n**V8 caveat:** some native class bindings (e.g. `Base64` and `Unicode`) are wired for\nQuickJS and may **throw on the V8 runtime**. Don't assume a native binding is present\non every JS engine — guard or feature-check when the same code can run under V8.\n\n### Performance\n\n- This is performance-critical code\n- Profile before optimizing\n- Consider cache locality\n- Be mindful of allocations in hot paths\n\n## Testing\n\n```bash\n# Run all C++ runtime tests\nbazel test //valdi:test\n\n# Run specific test suites\nbazel test //valdi:test_runtime       # Runtime tests\nbazel test //valdi:test_integration   # Integration tests\nbazel test //valdi:test_snap_drawing  # Snap drawing tests\n```\n\n## Platform-Specific Notes\n\n### iOS\n- Objective-C++ bridge in `/valdi/src/valdi/ios/`\n- Metal for GPU rendering\n- UIKit view integration\n\n### Android\n- JNI bridge in `/valdi/src/valdi/android/`\n- NDK integration\n- Native view rendering\n\n### macOS\n- Platform-specific code in `/valdi/src/valdi/macos/`\n- Desktop runtime support\n\n### Build\n\n```bash\n# Build runtime for specific platform\nbazel build //valdi:valdi_ios\nbazel build //valdi:valdi_android\nbazel build //valdi:valdi_macos\n```\n\n## Important\n\n1. **Cross-platform first** - Code must work on all platforms\n2. **Performance critical** - UI rendering and layout\n3. **Memory efficiency** - Mobile devices have constraints\n4. **Thread safety** - Consider concurrency\n\n## More Information\n\n- Runtime overview: `/valdi/README.md`\n- Core bindings: `/valdi_core/README.md`\n- Framework docs: `/AGENTS.md`\n","isInternal":false,"tokens":886,"sizeBytes":3663},{"name":"skill.md","path":"ai-skills/skills/valdi-custom-view/skill.md","rawUrl":"https://raw.githubusercontent.com/Snapchat/Valdi/HEAD/ai-skills/skills/valdi-custom-view/skill.md","title":"valdi-custom-view","category":"anthropic-skill","format":"markdown","content":"# Custom View Rules\n\n**Applies to**: `**/*.tsx` files using `<custom-view>` elements.\n\n## `<custom-view>` Element\n\n`<custom-view>` renders a platform-native view inside a Valdi component. Each platform resolves the view by class name.\n\n```typescript\n<custom-view\n  androidClass=\"com.snap.modules.my_module.MyNativeView\"\n  iosClass=\"SCMyNativeView\"\n  macosClass=\"SCMyNativeView\"\n  webClass=\"MyWebViewClassName\"\n  width=\"100%\"\n  height={120}\n/>\n```\n\n## Class Attributes\n\n| Attribute | Platform | Resolution |\n|-----------|----------|------------|\n| `androidClass` | Android | Reflection via `ReflectionViewFactory`; needs `@RegisterAttributesBinder` |\n| `iosClass` | iOS | ObjC class name; must be linked via `ios_deps` |\n| `macosClass` | macOS | `NSClassFromString()`; must be linked via `macos_deps` |\n| `webClass` | Web | Looked up in `WebViewClassRegistry`; registered via `webPolyglotViews` export |\n\n## Platform Resolution\n\n- **Android**: The view class needs a single-arg `(Context)` constructor. An `@RegisterAttributesBinder`-annotated binder is discovered from assets at runtime.\n- **iOS**: The ObjC class must be a `UIView` subclass linked into the binary via `ios_deps`.\n- **macOS**: Falls through to `iosClass` when `macosClass` is not specified — both in TypeScript (`JSXBootstrap.ts`) and C++ (`ViewNode.cpp`). Only specify `macosClass` when the macOS view is different from iOS.\n- **Web**: The `webClass` name is matched against the `WebViewClassRegistry`. Register by exporting `webPolyglotViews` from a typed TypeScript web polyglot entry file compiled via `ts_project` (see `valdi-polyglot-module` skill). Factories return an `AttributeHandler` with `changeAttribute(name, value)` to receive attribute updates.\n\n## viewFactory Pattern\n\nInstead of resolving by class name, you can pass a `ViewFactory` object directly. This is useful when the factory is provided by a parent component or constructed dynamically:\n\n```typescript\nimport { ViewFactory } from 'valdi_tsx/src/ViewFactory';\n\ninterface MyViewModel {\n  viewFactory?: ViewFactory;\n}\n\nclass MyComponent extends Component<MyViewModel> {\n  onRender(): void {\n    if (this.viewModel.viewFactory) {\n      <custom-view style={styles.container} viewFactory={this.viewModel.viewFactory} />;\n    }\n  }\n}\n```\n\n`viewFactory` and `*Class` attributes are mutually exclusive — use one or the other. Specifying both on the same `<custom-view>` is a compile error (the JSXProcessor rejects it). If a view needs a `viewFactory` on one platform and a registered class on another, branch into two sibling `<custom-view>` elements, each with a single mechanism. Prefer gating on whether a `viewFactory` is provided (`if (viewFactory) { … } else { …class… }`) over `Device.isWeb()`: the factory is often supplied by native too, so an `isWeb` branch that routes native to classes can break it. Only keep a platform class you've confirmed actually exists.\n\n## macOS Attribute Binding\n\nmacOS custom views can bind attributes (including callbacks) from TSX using `+bindAttributes:`. The view manager calls this class method at registration time.\n\n```objc\n#import \"valdi/macos/SCValdiMacOSAttributesBinder.h\"\n#import \"valdi/macos/SCValdiMacOSFunction.h\"\n\n@interface SCMyView () {\n    SCValdiMacOSFunction *_onMyCallback;\n}\n@end\n\n@implementation SCMyView\n- (void)valdi_setOnMyCallback:(id)value { _onMyCallback = value; }\n\n+ (void)bindAttributes:(SCValdiMacOSAttributesBinder *)attributesBinder {\n    [attributesBinder bindUntypedAttribute:@\"onMyCallback\"\n                invalidateLayoutOnChange:NO\n                                selector:@selector(valdi_setOnMyCallback:)];\n}\n@end\n```\n\n**Binder methods:**\n- `bindUntypedAttribute:invalidateLayoutOnChange:selector:` — binds any attribute (strings, numbers, functions)\n- `bindColorAttribute:invalidateLayoutOnChange:selector:` — binds color attributes specifically\n\n**Invoking callbacks:** Call `[_onMyCallback performWithParameters:@[@{@\"key\": @\"value\"}]]` — the NSArray/NSDictionary is auto-converted to JS objects.\n\n## Keyboard Input on macOS\n\nValdi doesn't have built-in keyboard event support. Use a polyglot `<custom-view>` with an NSView that:\n1. Overrides `acceptsFirstResponder` → `YES`\n2. Overrides `keyDown:` to map key codes to callback invocations\n3. Calls `[self.window makeFirstResponder:self]` in `viewDidMoveToWindow` to grab focus\n4. Has **non-zero dimensions** (wrap visible content inside it)\n\nSee the `valdi-polyglot-module` skill for a complete macOS attribute binding example with BUILD.bazel wiring.\n\n## Common Mistakes\n\n- Using `<custom-view>` without checking the platform — wrap in `Device.isAndroid()` / `Device.isIOS()` / `Device.isDesktop()` if not all platforms are supported\n- Forgetting to link native implementations — the class name string alone isn't enough; the native code must be compiled and linked via platform `_deps` in BUILD.bazel\n- Wrong package name in `androidClass` — must match the Kotlin/Java package exactly\n- Specifying `macosClass` when it's the same as `iosClass` — omit it and let the fallthrough handle it\n- Using a `filegroup` for `web_deps` — always use `ts_project` with `transpiler = \"tsc\"`\n- Creating a 0×0 custom-view for keyboard focus — it won't become first responder; wrap content inside it\n- **Duplicate ObjC class names across modules** — if two modules define the same native class (e.g. both have `SCKeyboardView`), the linker will fail with \"duplicate symbol\" errors. Always use a unique class name per module (e.g. prefix with module name: `SCSnakeGameKeyboardView`)\n","isInternal":false,"tokens":1317,"sizeBytes":5571},{"name":"skill.md","path":"ai-skills/skills/valdi-error-handling/skill.md","rawUrl":"https://raw.githubusercontent.com/Snapchat/Valdi/HEAD/ai-skills/skills/valdi-error-handling/skill.md","title":"valdi-error-handling","category":"anthropic-skill","format":"markdown","content":"# Valdi Error Handling\n\nError handling, promise rejection, and lifecycle safety patterns.\n\n## When to use\n\nWhen dealing with async operations, network requests, component lifecycle boundaries, or any code that can fail at runtime.\n\n## Key concepts\n\nValdi runs on a JavaScript runtime (Hermes/V8) with native platform integration. Unhandled errors crash the app. The primary error sources are:\n\n1. **Async operations** completing after component destruction\n2. **Network failures** from HTTPClient\n3. **Uncaught promise rejections**\n4. **Type mismatches** from native/JS boundary\n\n## CancelablePromise — the first line of defense\n\nMost Valdi errors stem from async work completing after a component is destroyed. `CancelablePromise` + `registerDisposable` prevents this entirely:\n\n```tsx\nimport { promiseToCancelablePromise } from 'valdi_core/src/CancelablePromise';\n\n// ✅ Safe async — cancelled on component destruction\nonCreate() {\n  const cancelable = promiseToCancelablePromise(this.fetchData(), () => {});\n  this.registerDisposable(() => cancelable.cancel?.());\n  cancelable.then(\n    data => { this.setState({ data, loading: false }); },\n    error => { this.setState({ error: error.message, loading: false }); },\n  );\n}\n```\n\n```tsx\n// ❌ WRONG — Raw promise with no lifecycle safety\nonCreate() {\n  this.fetchData().then(data => {\n    this.setState({ data });  // Logs a console.error and no-ops if destroyed\n  });\n}\n```\n\nSee the `valdi-async` skill for comprehensive CancelablePromise patterns.\n\n## HTTPClient error handling\n\n`HTTPClient` is an instance, not static. Methods return `CancelablePromise<HTTPResponse>`.\n\n```tsx\nimport { HTTPClient } from 'valdi_http/src/HTTPClient';\n\nprivate httpClient = new HTTPClient('https://api.example.com');\n\n// ✅ Handle network errors with lifecycle safety\nprivate fetchData = () => {\n  const cancelable = this.httpClient.get('/data');\n  this.registerDisposable(() => cancelable.cancel?.());\n  cancelable.then(\n    response => {\n      if (response.statusCode !== 200) {\n        this.setState({ error: `HTTP ${response.statusCode}`, loading: false });\n        return;\n      }\n      const decoder = new TextDecoder();\n      const data = JSON.parse(decoder.decode(response.body));\n      this.setState({ data, loading: false });\n    },\n    error => {\n      this.setState({ error: (error as Error).message, loading: false });\n    },\n  );\n};\n```\n\n`HTTPResponse` shape: `{ statusCode: number, headers: StringMap<string>, body?: Uint8Array }`. Decode `body` with `TextDecoder` before parsing.\n\n## isDestroyed() guard\n\nFor callbacks that can't use CancelablePromise (e.g., native callbacks, timers):\n\n```tsx\n// ✅ Guard against use-after-destroy\nprivate onNativeCallback = (result: string) => {\n  if (this.isDestroyed()) return;\n  this.setState({ result });\n};\n```\n\n`isDestroyed()` is a method — always call with parentheses.\n\n## try/catch in async methods\n\n```tsx\nimport { promiseToCancelablePromise } from 'valdi_core/src/CancelablePromise';\n\n// ✅ Catch errors from async operations\nprivate loadProfile = () => {\n  const cancelable = promiseToCancelablePromise(\n    (async () => {\n      try {\n        const profile = await this.api.getProfile();\n        return { profile, error: undefined };\n      } catch (e) {\n        return { profile: undefined, error: (e as Error).message };\n      }\n    })(),\n    () => {},\n  );\n  this.registerDisposable(() => cancelable.cancel?.());\n\n  cancelable.then(result => {\n    this.setState({\n      profile: result.profile,\n      error: result.error,\n      loading: false,\n    });\n  });\n};\n```\n\n## Error states in UI\n\n```tsx\n// ✅ Show error state to the user\nonRender(): void {\n  if (this.state.loading) {\n    <AnimationShimmer width=\"100%\" height={200} />;\n    return;\n  }\n\n  if (this.state.error) {\n    <view alignItems=\"center\" justifyContent=\"center\" padding={16}>\n      <label value=\"Something went wrong\" font={systemBoldFont(16)} />\n      <label value={this.state.error} color=\"#999\" />\n      <CoreButton text=\"Retry\" onTap={this.handleRetry} />\n    </view>;\n    return;\n  }\n\n  <label value={this.state.data.name} />;\n}\n```\n\n## Defensive ViewModel access\n\nViewModels come from parent components and may have optional fields:\n\n```tsx\n// ✅ Defensive access with defaults\nonRender(): void {\n  const title = this.viewModel.title ?? 'Untitled';\n  const items = this.viewModel.items ?? [];\n\n  <view>\n    <label value={title} />\n    {items.forEach(item => { <label value={item.name} />; })}\n  </view>;\n}\n```\n\n## Timer cleanup\n\n```tsx\n// ✅ Use setTimeoutDisposable for automatic cleanup\nonCreate() {\n  this.setTimeoutDisposable(this.tick, 1000);\n}\n```\n\n```tsx\n// ✅ Manual cleanup for setInterval\nprivate timerId?: number;\n\nonCreate() {\n  this.timerId = setInterval(this.tick, 1000);\n}\n\nonDestroy() {\n  if (this.timerId !== undefined) {\n    clearInterval(this.timerId);\n  }\n}\n```\n\nOr use `registerDisposable` with a disposable wrapper — see `valdi-async` skill.\n\n## Common mistakes\n\n```tsx\n// ❌ WRONG — HTTPClient is not static\nconst data = await HTTPClient.send(request);  // No static send() method\n\n// ✅ Correct — instantiate then call methods\nconst client = new HTTPClient('https://api.example.com');\nconst cancelable = client.get('/data');\n\n// ❌ WRONG — .catch() is not available on CancelablePromise\ncancelable.catch(error => { ... });  // CancelablePromise only has .then()\n\n// ✅ Correct — use second argument of .then() for error handling\ncancelable.then(\n  response => { /* success */ },\n  error => { /* failure */ },\n);\n\n// ❌ WRONG — React error boundaries don't exist\n<ErrorBoundary fallback={<Error />}>  // Not a Valdi concept\n  <MyComponent />\n</ErrorBoundary>\n\n// ❌ WRONG — window.onerror / global error handlers\nwindow.addEventListener('error', handler);  // No window object\n\n// ❌ WRONG — isDestroyed without parentheses\nif (this.isDestroyed) return;  // It's a method, not a property\n\n// ✅ Correct\nif (this.isDestroyed()) return;\n\n// ❌ WRONG — Passing CancelablePromise directly to registerDisposable\nthis.registerDisposable(cancelable);  // Wrong type\n\n// ✅ Correct — wrap cancel in a callback\nthis.registerDisposable(() => cancelable.cancel?.());\n```\n\n## Summary\n\n| Pattern | When to use |\n|---------|------------|\n| `promiseToCancelablePromise` + `registerDisposable` | Any async work in a component |\n| `isDestroyed()` guard | Native callbacks, external event handlers |\n| `try/catch` | Operations that can throw |\n| Error state in UI | User-facing failure recovery |\n| `onDestroy` cleanup | Timers, subscriptions, manual resources |\n| `setTimeoutDisposable` | One-shot timers (auto-cleaned) |\n","isInternal":false,"tokens":1547,"sizeBytes":6646},{"name":"skill.md","path":"ai-skills/skills/valdi-gestures/skill.md","rawUrl":"https://raw.githubusercontent.com/Snapchat/Valdi/HEAD/ai-skills/skills/valdi-gestures/skill.md","title":"valdi-gestures","category":"anthropic-skill","format":"markdown","content":"# Valdi Gestures\n\nTouch and gesture handling: tap, long press, drag, pinch, and rotate.\n\n## When to use\n\nWhen handling user touch interactions beyond basic `onTap` — drag-to-reorder, pinch-to-zoom, long press menus, custom gesture recognition.\n\n## Key concepts\n\nValdi gesture handling is **attribute-based on `<view>` elements**. Each gesture type has three attributes:\n- `on<Gesture>` — the callback\n- `on<Gesture>Disabled` — boolean to disable the gesture (the gesture is enabled by default once a callback is set; there is no `<gesture>Enabled` attribute)\n- `on<Gesture>Predicate` — function returning boolean, controls whether the gesture should fire\n\nThe topmost visible element with a matching gesture wins. Conflicting gestures on parent elements are suppressed.\n\n## TouchEvent interface\n\nAll gesture callbacks receive a `TouchEvent` (or extension of it):\n\n```tsx\ninterface TouchEvent {\n  x: number;          // X relative to the element\n  y: number;          // Y relative to the element\n  absoluteX: number;  // X relative to root Valdi view\n  absoluteY: number;  // Y relative to root Valdi view\n  pointerCount: number;\n  pointerLocations: Pointer[];\n}\n```\n\n## Basic gestures\n\n### onTap\n\n```tsx\n// ✅ Simple tap handler\n<view onTap={this.handleTap}>\n  <label value=\"Tap me\" />\n</view>\n\nprivate handleTap = (event: TouchEvent) => {\n  console.log(`Tapped at ${event.x}, ${event.y}`);\n};\n```\n\n### onDoubleTap\n\n```tsx\n// ✅ Double tap to zoom\n<view onDoubleTap={this.handleDoubleTap}>\n  <image src={this.viewModel.photo} />\n</view>\n```\n\n### onLongPress\n\n```tsx\n// ✅ Long press with custom duration\n<view\n  onLongPress={this.handleLongPress}\n  longPressDuration={0.5}  // seconds before trigger\n>\n  <label value=\"Hold me\" />\n</view>\n```\n\n## Continuous gestures\n\n### onDrag\n\n```tsx\n// ✅ Drag to move — fires continuously during the drag\n<view onDrag={this.handleDrag}>\n  <label value=\"Drag me\" />\n</view>\n\nprivate handleDrag = (event: DragEvent) => {\n  // event extends TouchEvent with drag-specific data\n  this.cardRef.setAttribute('translateX', event.x);\n  this.cardRef.setAttribute('translateY', event.y);\n};\n```\n\n### onPinch\n\n```tsx\n// ✅ Pinch to zoom — requires 2+ pointers\n<view onPinch={this.handlePinch}>\n  <image src={this.viewModel.photo} ref={this.imageRef} />\n</view>\n\nprivate handlePinch = (event: PinchEvent) => {\n  this.imageRef.setAttribute('scale', event.scale);\n};\n```\n\n### onRotate\n\n```tsx\n// ✅ Rotate with two fingers — requires 2+ pointers\n<view onRotate={this.handleRotate}>\n  <image src={this.viewModel.sticker} ref={this.stickerRef} />\n</view>\n```\n\n## Simultaneous gestures\n\n`onDrag`, `onPinch`, and `onRotate` can run simultaneously. This enables multitouch interactions like pinch-zoom-rotate on a photo:\n\n```tsx\n// ✅ Combined pinch + drag + rotate on same element\n<view\n  onDrag={this.handleDrag}\n  onPinch={this.handlePinch}\n  onRotate={this.handleRotate}\n>\n  <image src={this.viewModel.photo} ref={this.photoRef} />\n</view>\n```\n\n## Touch area extension\n\nExpand the tappable area beyond the visible bounds:\n\n```tsx\n// ✅ Small button with larger touch target\n<view\n  onTap={this.handleTap}\n  touchAreaExtension={10}  // extends hit area by 10pt in each direction\n  width={24} height={24}\n>\n  <image src={SIGIcon.checkmarkFill} />\n</view>\n```\n\n## Touch delay in scroll views\n\nInside a `<scroll>`, touches are delayed to avoid conflicts with scrolling:\n\n```tsx\n// ✅ Delay touch recognition inside scrollable content\n<view\n  onTouch={this.handleTouch}\n  onTouchDelayDuration={0.15}  // seconds before onTouch fires during scroll\n/>\n```\n\n## Gesture predicates\n\nControl whether a gesture should activate:\n\n```tsx\n// ✅ Only allow drag when in edit mode\n<view\n  onDrag={this.handleDrag}\n  onDragPredicate={() => this.state.editMode}\n/>\n```\n\n## onTouch (raw)\n\nLow-level touch events that fire on all elements in the hierarchy:\n\n```tsx\n// ✅ Raw touch tracking — fires for all touch phases\n<view onTouch={this.handleTouch}>\n  <label value=\"Touch me\" />\n</view>\n\nprivate handleTouch = (event: TouchEvent) => {\n  // Fires on begin, move, end, cancel\n};\n```\n\nUnlike other gestures, `onTouch` propagates to parents/siblings — it doesn't suppress other handlers.\n\n## Common mistakes\n\n```tsx\n// ❌ WRONG — React-style event handlers\n<div onClick={handler} />        // ❌ No <div>, no onClick\n<button onMouseDown={handler} /> // ❌ No mouse events\n\n// ❌ WRONG — addEventListener\nelement.addEventListener('touchstart', handler);  // ❌\n\n// ❌ WRONG — Gesture on <layout> (use <view>)\n<layout onTap={handler} />  // ❌ Gestures go on <view> elements\n\n// ✅ Correct — gestures on <view>\n<view onTap={handler}>\n  <layout>\n    <label value=\"Content\" />\n  </layout>\n</view>\n```\n","isInternal":false,"tokens":1219,"sizeBytes":4742},{"name":"skill.md","path":"ai-skills/skills/valdi-inspect/skill.md","rawUrl":"https://raw.githubusercontent.com/Snapchat/Valdi/HEAD/ai-skills/skills/valdi-inspect/skill.md","title":"valdi-inspect","category":"anthropic-skill","format":"markdown","content":"# valdi-inspect\n\nUse this skill when you need to inspect a **live running Valdi app** — to see what components are on screen, traverse the rendered virtual node tree, capture element screenshots, or check JS heap usage.\n\n## When to use\n\n- User asks \"what's on screen?\", \"what component is rendering here?\", \"what contexts are active?\"\n- You need to diagnose a layout or rendering issue in a running app\n- You want to verify a component tree change after a hot-reload\n- User asks about memory usage or wants a JS heap dump\n\n## Prerequisites\n\nThe Valdi app must be **running on a connected device or emulator**. No hot-reloader required — the CLI connects directly to the app's DebuggerService.\n\n**Port reference** — determined by how the app is built, not the OS:\n\n| Target | Port | When |\n|--------|------|------|\n| iOS in-app (e.g. Snapchat) | **13592** (default) | `isStandalone = false` |\n| Android in-app | **13592** (default) | `isStandalone = false` |\n| macOS Valdi app | **13591** | `isStandalone = true` |\n| Standalone runner / CLI runner | **13591** | `isStandalone = true` |\n\nFor **Android**, `adb forward` runs automatically. For **iOS simulator**, connect directly to `localhost`.\n\nAll commands output **JSON by default** (for AI parsing). Add `--pretty` for human-readable output.\n\n---\n\n## Commands\n\n### `valdi inspect status`\n\nCheck whether the Valdi daemon is reachable and how many devices are connected.\n\n```\nvaldi inspect status\nvaldi inspect status --pretty\n```\n\nOutput: `{ connected: bool, port: number, connectedDevices: number, portName: string }`\n\n---\n\n### `valdi inspect devices`\n\nList all devices connected to the Valdi daemon.\n\n```\nvaldi inspect devices\nvaldi inspect devices --pretty\n```\n\nOutput: `[{ client_id, platform, application_id }, ...]`\n\nUse `client_id` values with `--client` in other commands. If only one device is connected, other commands auto-select it.\n\n---\n\n### `valdi inspect select`\n\nInteractively choose a device and save it to `~/.valdi-inspect.json`. Subsequent commands use this device without prompting.\n\n```\nvaldi inspect select\n```\n\n---\n\n### `valdi inspect contexts`\n\nList all active root components (contexts) on the device. A **context** is one mounted Valdi component tree — typically one per screen or overlay.\n\n```\nvaldi inspect contexts\nvaldi inspect contexts --pretty\nvaldi inspect contexts --client <client_id>\n```\n\nOutput: `[{ id: string, rootComponentName: string }, ...]`\n\nThe `id` is the **contextId** used by `tree` and `snapshot`. If there is only one context, commands that take a contextId will auto-select it.\n\n---\n\n### `valdi inspect tree [contextId]`\n\nGet the full rendered virtual node tree for a context. If `contextId` is omitted, auto-selects the single context or prompts when there are multiple.\n\n```\nvaldi inspect tree\nvaldi inspect tree <contextId>\nvaldi inspect tree <contextId> --pretty\nvaldi inspect tree <contextId> --max-depth <n>\n```\n\nOutput: a nested tree where each node has:\n- `tag`: element type (e.g. `\"view\"`, `\"label\"`, `\"image\"`, or a component name)\n- `key`: the Valdi TSX key assigned in source\n- `element.id`: numeric element ID (use this for `snapshot`)\n- `element.frame`: `{ x, y, width, height }` in screen coordinates\n- `element.attributes`: rendered prop values (style, value, etc.)\n- `children`: array of child nodes (same shape)\n- `_childrenTrimmed`: count of trimmed children when `--max-depth` is used\n\n**Tip**: Use `--max-depth 3` or `--max-depth 4` first to get a summary, then drill in.\n\n---\n\n### `valdi inspect snapshot <elementId> [contextId]`\n\nCapture a screenshot of a specific element and save it as a PNG. If `contextId` is omitted, auto-selects or prompts.\n\n```\nvaldi inspect snapshot <elementId>\nvaldi inspect snapshot <elementId> <contextId>\nvaldi inspect snapshot <elementId> <contextId> --output /tmp/my-snap.png\n```\n\nOutput: `{ path: \"/var/folders/.../valdi-snapshot-<elementId>.png\" }`\n\nThe PNG is written to disk. Read it with the `Read` tool to view it visually.\n\n**Getting elementId**: Use `element.id` from the `tree` output (a number, e.g. `1`, `5`, `42`).\n\n#### Using `--key` instead of numeric IDs (recommended)\n\nInstead of looking up numeric element IDs from the tree, use the `--key` flag to resolve by component tag name or element `key` prop:\n\n```\nvaldi inspect snapshot --key PreviewRoot\nvaldi inspect snapshot --key weather-card --port 13591\n```\n\nThe `--key` flag searches the component tree for a matching tag name or `key` prop and resolves it to the element ID automatically.\n\n**This is stable across hot-reloads** — element IDs change on every reload, but component names and `key` props stay the same. Use `--key` instead of numeric IDs to avoid re-querying the tree after each hot-reload cycle.\n\nCustom keys can be set on any element in TSX:\n```tsx\n<view key=\"weather-card\" style={styles.card}>\n```\n\n---\n\n### `valdi inspect heap`\n\nDump JS heap statistics for the connected device.\n\n```\nvaldi inspect heap\nvaldi inspect heap --pretty\nvaldi inspect heap --gc\n```\n\nOptions:\n- `--gc`: run garbage collection before dumping (more accurate free memory reading)\n\nOutput: `{ memoryUsageBytes: number, heapDumpJSON: string }`\n\n`heapDumpJSON` is a serialized heap snapshot in V8 format. Note: heap dumps can crash small/toy apps — this works best on full production apps.\n\n---\n\n## Common workflows\n\n### \"What is rendering on screen right now?\"\n\n```\nvaldi inspect tree --pretty\n```\n\n(contextId is auto-selected if there's only one)\n\n### \"Show me what a specific component looks like\"\n\n1. Run `valdi inspect tree --pretty` to find the node — note its `element.id`\n2. Run `valdi inspect snapshot <element.id>` to capture it\n3. Read the PNG with the `Read` tool\n\n### \"Diagnose a layout issue at position X,Y\"\n\nRun `valdi inspect tree --pretty` and look for `element.frame` values overlapping with the target position.\n\n### \"Check memory usage\"\n\n```\nvaldi inspect heap --pretty\n```\n\n### \"Verify a hot-reload applied correctly\"\n\nRun `valdi inspect tree` before and after the edit, compare the JSON output.\n\n---\n\n## Options reference\n\n| Option | Commands | Description |\n|--------|----------|-------------|\n| `--port <n>` | all | Daemon TCP port (default: 13592 mobile, 13591 standalone) |\n| `--client <id>` | contexts, tree, snapshot, heap | Target a specific connected device |\n| `--pretty` | devices, status, contexts, tree, heap | Human-readable output |\n| `--max-depth <n>` | tree | Trim tree to N levels deep |\n| `--output <path>` | snapshot | Custom PNG output path |\n| `--gc` | heap | GC before heap dump |\n\n---\n\n## Troubleshooting\n\n**\"Valdi daemon not running on port 13592\"** — Make sure the app is running. macOS Valdi apps and standalone runners use port 13591: add `--port 13591`.\n\n**\"No devices connected\"** — Make sure the Valdi app is running. For Android, ensure a device or emulator is connected via `adb`.\n\n**\"Timeout waiting for device response\"** — The app may be frozen or the JS runtime may be busy. Try again after a moment.\n","isInternal":false,"tokens":1748,"sizeBytes":6993},{"name":"skill.md","path":"ai-skills/skills/valdi-ios/skill.md","rawUrl":"https://raw.githubusercontent.com/Snapchat/Valdi/HEAD/ai-skills/skills/valdi-ios/skill.md","title":"valdi-ios","category":"anthropic-skill","format":"markdown","content":"# valdi-ios\n\nPath: `ai-skills/skills/valdi-ios/skill.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Snapchat/Valdi/HEAD/ai-skills/skills/valdi-ios/skill.md)","tokens":300,"sizeBytes":177},{"name":"skill.md","path":"ai-skills/skills/valdi-localization/skill.md","rawUrl":"https://raw.githubusercontent.com/Snapchat/Valdi/HEAD/ai-skills/skills/valdi-localization/skill.md","title":"valdi-localization","category":"anthropic-skill","format":"markdown","content":"# valdi-localization\n\nPath: `ai-skills/skills/valdi-localization/skill.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Snapchat/Valdi/HEAD/ai-skills/skills/valdi-localization/skill.md)","tokens":300,"sizeBytes":204},{"name":"skill.md","path":"ai-skills/skills/valdi-migrate/skill.md","rawUrl":"https://raw.githubusercontent.com/Snapchat/Valdi/HEAD/ai-skills/skills/valdi-migrate/skill.md","title":"valdi-migrate","category":"anthropic-skill","format":"markdown","content":"# valdi-migrate\n\nPath: `ai-skills/skills/valdi-migrate/skill.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Snapchat/Valdi/HEAD/ai-skills/skills/valdi-migrate/skill.md)","tokens":300,"sizeBytes":189},{"name":"README.md","path":"ai-skills/skills/valdi-migrate/tests/README.md","rawUrl":"https://raw.githubusercontent.com/Snapchat/Valdi/HEAD/ai-skills/skills/valdi-migrate/tests/README.md","title":"Tests Skill","category":"anthropic-skill","format":"markdown","content":"# Tests Skill\n\nPath: `ai-skills/skills/valdi-migrate/tests/README.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Snapchat/Valdi/HEAD/ai-skills/skills/valdi-migrate/tests/README.md)","tokens":300,"sizeBytes":201},{"name":"tsconfig.json","path":"ai-skills/skills/valdi-migrate/tests/tsconfig.json","rawUrl":"https://raw.githubusercontent.com/Snapchat/Valdi/HEAD/ai-skills/skills/valdi-migrate/tests/tsconfig.json","title":"Tests Skill","category":"anthropic-skill","format":"json","content":"# Tests Skill\n\nPath: `ai-skills/skills/valdi-migrate/tests/tsconfig.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Snapchat/Valdi/HEAD/ai-skills/skills/valdi-migrate/tests/tsconfig.json)","tokens":300,"sizeBytes":209},{"name":"skill.md","path":"ai-skills/skills/valdi-mock-breakdown/skill.md","rawUrl":"https://raw.githubusercontent.com/Snapchat/Valdi/HEAD/ai-skills/skills/valdi-mock-breakdown/skill.md","title":"valdi-mock-breakdown","category":"anthropic-skill","format":"markdown","content":"# valdi-mock-breakdown\n\nPath: `ai-skills/skills/valdi-mock-breakdown/skill.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Snapchat/Valdi/HEAD/ai-skills/skills/valdi-mock-breakdown/skill.md)","tokens":300,"sizeBytes":210},{"name":"skill.md","path":"ai-skills/skills/valdi-mock-iterate/skill.md","rawUrl":"https://raw.githubusercontent.com/Snapchat/Valdi/HEAD/ai-skills/skills/valdi-mock-iterate/skill.md","title":"valdi-mock-iterate","category":"anthropic-skill","format":"markdown","content":"# valdi-mock-iterate\n\nPath: `ai-skills/skills/valdi-mock-iterate/skill.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Snapchat/Valdi/HEAD/ai-skills/skills/valdi-mock-iterate/skill.md)","tokens":300,"sizeBytes":204},{"name":"skill.md","path":"ai-skills/skills/valdi-mock-to-module/skill.md","rawUrl":"https://raw.githubusercontent.com/Snapchat/Valdi/HEAD/ai-skills/skills/valdi-mock-to-module/skill.md","title":"valdi-mock-to-module","category":"anthropic-skill","format":"markdown","content":"# valdi-mock-to-module\n\nPath: `ai-skills/skills/valdi-mock-to-module/skill.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Snapchat/Valdi/HEAD/ai-skills/skills/valdi-mock-to-module/skill.md)","tokens":300,"sizeBytes":210},{"name":"skill.md","path":"ai-skills/skills/valdi-navigation/skill.md","rawUrl":"https://raw.githubusercontent.com/Snapchat/Valdi/HEAD/ai-skills/skills/valdi-navigation/skill.md","title":"valdi-navigation","category":"anthropic-skill","format":"markdown","content":"# valdi-navigation\n\nPath: `ai-skills/skills/valdi-navigation/skill.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Snapchat/Valdi/HEAD/ai-skills/skills/valdi-navigation/skill.md)","tokens":300,"sizeBytes":198},{"name":"skill.md","path":"ai-skills/skills/valdi-overview/skill.md","rawUrl":"https://raw.githubusercontent.com/Snapchat/Valdi/HEAD/ai-skills/skills/valdi-overview/skill.md","title":"valdi-overview","category":"anthropic-skill","format":"markdown","content":"# valdi-overview\n\nPath: `ai-skills/skills/valdi-overview/skill.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Snapchat/Valdi/HEAD/ai-skills/skills/valdi-overview/skill.md)","tokens":300,"sizeBytes":192},{"name":"skill.md","path":"ai-skills/skills/valdi-perf/skill.md","rawUrl":"https://raw.githubusercontent.com/Snapchat/Valdi/HEAD/ai-skills/skills/valdi-perf/skill.md","title":"valdi-perf","category":"anthropic-skill","format":"markdown","content":"# valdi-perf\n\nPath: `ai-skills/skills/valdi-perf/skill.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Snapchat/Valdi/HEAD/ai-skills/skills/valdi-perf/skill.md)","tokens":300,"sizeBytes":180},{"name":"README.md","path":"ai-skills/skills/valdi-perf/tests/README.md","rawUrl":"https://raw.githubusercontent.com/Snapchat/Valdi/HEAD/ai-skills/skills/valdi-perf/tests/README.md","title":"Tests Skill","category":"anthropic-skill","format":"markdown","content":"# Tests Skill\n\nPath: `ai-skills/skills/valdi-perf/tests/README.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Snapchat/Valdi/HEAD/ai-skills/skills/valdi-perf/tests/README.md)","tokens":300,"sizeBytes":195},{"name":"tsconfig.json","path":"ai-skills/skills/valdi-perf/tests/tsconfig.json","rawUrl":"https://raw.githubusercontent.com/Snapchat/Valdi/HEAD/ai-skills/skills/valdi-perf/tests/tsconfig.json","title":"Tests Skill","category":"anthropic-skill","format":"json","content":"# Tests Skill\n\nPath: `ai-skills/skills/valdi-perf/tests/tsconfig.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Snapchat/Valdi/HEAD/ai-skills/skills/valdi-perf/tests/tsconfig.json)","tokens":300,"sizeBytes":203},{"name":"skill.md","path":"ai-skills/skills/valdi-polyglot-module/skill.md","rawUrl":"https://raw.githubusercontent.com/Snapchat/Valdi/HEAD/ai-skills/skills/valdi-polyglot-module/skill.md","title":"valdi-polyglot-module","category":"anthropic-skill","format":"markdown","content":"# valdi-polyglot-module\n\nPath: `ai-skills/skills/valdi-polyglot-module/skill.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Snapchat/Valdi/HEAD/ai-skills/skills/valdi-polyglot-module/skill.md)","tokens":300,"sizeBytes":213},{"name":"skill.md","path":"ai-skills/skills/valdi-pr-review/skill.md","rawUrl":"https://raw.githubusercontent.com/Snapchat/Valdi/HEAD/ai-skills/skills/valdi-pr-review/skill.md","title":"valdi-pr-review","category":"anthropic-skill","format":"markdown","content":"# valdi-pr-review\n\nPath: `ai-skills/skills/valdi-pr-review/skill.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Snapchat/Valdi/HEAD/ai-skills/skills/valdi-pr-review/skill.md)","tokens":300,"sizeBytes":195},{"name":"skill.md","path":"ai-skills/skills/valdi-preview/skill.md","rawUrl":"https://raw.githubusercontent.com/Snapchat/Valdi/HEAD/ai-skills/skills/valdi-preview/skill.md","title":"valdi-preview","category":"anthropic-skill","format":"markdown","content":"# valdi-preview\n\nPath: `ai-skills/skills/valdi-preview/skill.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Snapchat/Valdi/HEAD/ai-skills/skills/valdi-preview/skill.md)","tokens":300,"sizeBytes":189},{"name":"skill.md","path":"ai-skills/skills/valdi-profile/skill.md","rawUrl":"https://raw.githubusercontent.com/Snapchat/Valdi/HEAD/ai-skills/skills/valdi-profile/skill.md","title":"valdi-profile","category":"anthropic-skill","format":"markdown","content":"# valdi-profile\n\nPath: `ai-skills/skills/valdi-profile/skill.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Snapchat/Valdi/HEAD/ai-skills/skills/valdi-profile/skill.md)","tokens":300,"sizeBytes":189},{"name":"skill.md","path":"ai-skills/skills/valdi-setup/skill.md","rawUrl":"https://raw.githubusercontent.com/Snapchat/Valdi/HEAD/ai-skills/skills/valdi-setup/skill.md","title":"valdi-setup","category":"anthropic-skill","format":"markdown","content":"# valdi-setup\n\nPath: `ai-skills/skills/valdi-setup/skill.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Snapchat/Valdi/HEAD/ai-skills/skills/valdi-setup/skill.md)","tokens":300,"sizeBytes":183},{"name":"skill.md","path":"ai-skills/skills/valdi-testing/skill.md","rawUrl":"https://raw.githubusercontent.com/Snapchat/Valdi/HEAD/ai-skills/skills/valdi-testing/skill.md","title":"valdi-testing","category":"anthropic-skill","format":"markdown","content":"# valdi-testing\n\nPath: `ai-skills/skills/valdi-testing/skill.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Snapchat/Valdi/HEAD/ai-skills/skills/valdi-testing/skill.md)","tokens":300,"sizeBytes":189},{"name":"skill.md","path":"ai-skills/skills/valdi-tsx/skill.md","rawUrl":"https://raw.githubusercontent.com/Snapchat/Valdi/HEAD/ai-skills/skills/valdi-tsx/skill.md","title":"valdi-tsx","category":"anthropic-skill","format":"markdown","content":"# valdi-tsx\n\nPath: `ai-skills/skills/valdi-tsx/skill.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Snapchat/Valdi/HEAD/ai-skills/skills/valdi-tsx/skill.md)","tokens":300,"sizeBytes":177},{"name":"README.md","path":"ai-skills/skills/valdi-tsx/tests/README.md","rawUrl":"https://raw.githubusercontent.com/Snapchat/Valdi/HEAD/ai-skills/skills/valdi-tsx/tests/README.md","title":"Tests Skill","category":"anthropic-skill","format":"markdown","content":"# Tests Skill\n\nPath: `ai-skills/skills/valdi-tsx/tests/README.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Snapchat/Valdi/HEAD/ai-skills/skills/valdi-tsx/tests/README.md)","tokens":300,"sizeBytes":193},{"name":"tsconfig.json","path":"ai-skills/skills/valdi-tsx/tests/tsconfig.json","rawUrl":"https://raw.githubusercontent.com/Snapchat/Valdi/HEAD/ai-skills/skills/valdi-tsx/tests/tsconfig.json","title":"Tests Skill","category":"anthropic-skill","format":"json","content":"# Tests Skill\n\nPath: `ai-skills/skills/valdi-tsx/tests/tsconfig.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Snapchat/Valdi/HEAD/ai-skills/skills/valdi-tsx/tests/tsconfig.json)","tokens":300,"sizeBytes":201},{"name":"baseline.json","path":"tools/api_surface/baseline.json","rawUrl":"https://raw.githubusercontent.com/Snapchat/Valdi/HEAD/tools/api_surface/baseline.json","title":"Api_surface Skill","category":"anthropic-skill","format":"json","content":"# Api_surface Skill\n\nPath: `tools/api_surface/baseline.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Snapchat/Valdi/HEAD/tools/api_surface/baseline.json)","tokens":300,"sizeBytes":177},{"name":"AGENTS.md","path":"tools/ci/AGENTS.md","rawUrl":"https://raw.githubusercontent.com/Snapchat/Valdi/HEAD/tools/ci/AGENTS.md","title":"Ci Skill","category":"anthropic-skill","format":"markdown","content":"# Ci Skill\n\nPath: `tools/ci/AGENTS.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Snapchat/Valdi/HEAD/tools/ci/AGENTS.md)","tokens":300,"sizeBytes":142},{"name":"package.json","path":"tools/valdi_web_devtools/package.json","rawUrl":"https://raw.githubusercontent.com/Snapchat/Valdi/HEAD/tools/valdi_web_devtools/package.json","title":"Valdi_web_devtools Skill","category":"anthropic-skill","format":"json","content":"# Valdi_web_devtools Skill\n\nPath: `tools/valdi_web_devtools/package.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Snapchat/Valdi/HEAD/tools/valdi_web_devtools/package.json)","tokens":300,"sizeBytes":196},{"name":"tsconfig.json","path":"tools/valdi_web_devtools/tsconfig.json","rawUrl":"https://raw.githubusercontent.com/Snapchat/Valdi/HEAD/tools/valdi_web_devtools/tsconfig.json","title":"Valdi_web_devtools Skill","category":"anthropic-skill","format":"json","content":"# Valdi_web_devtools Skill\n\nPath: `tools/valdi_web_devtools/tsconfig.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Snapchat/Valdi/HEAD/tools/valdi_web_devtools/tsconfig.json)","tokens":300,"sizeBytes":198},{"name":".cursorrules","path":".cursorrules","rawUrl":"https://raw.githubusercontent.com/Snapchat/Valdi/HEAD/.cursorrules","title":"Cursor IDE Native Rules","category":"cursor-rule","format":"markdown","content":"# Cursor IDE Native Rules\n\nPath: `.cursorrules`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Snapchat/Valdi/HEAD/.cursorrules)","tokens":300,"sizeBytes":145},{"name":"README.md","path":"tools/ci/compiler_goldens/README.md","rawUrl":"https://raw.githubusercontent.com/Snapchat/Valdi/HEAD/tools/ci/compiler_goldens/README.md","title":"compiler_goldens Documentation","category":"plugin-manifest","format":"markdown","content":"# compiler_goldens Documentation\n\nPath: `tools/ci/compiler_goldens/README.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Snapchat/Valdi/HEAD/tools/ci/compiler_goldens/README.md)","tokens":300,"sizeBytes":198},{"name":"README.md","path":"tools/valdi_web_devtools/README.md","rawUrl":"https://raw.githubusercontent.com/Snapchat/Valdi/HEAD/tools/valdi_web_devtools/README.md","title":"valdi_web_devtools Documentation","category":"plugin-manifest","format":"markdown","content":"# valdi_web_devtools Documentation\n\nPath: `tools/valdi_web_devtools/README.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Snapchat/Valdi/HEAD/tools/valdi_web_devtools/README.md)","tokens":300,"sizeBytes":198}],"systemPromptSnippet":"<yakaai_skills repo=\"Snapchat/Valdi\">\n<!-- File: AGENTS.md (Tokens: ~300 | Category: root-instruction) -->\n# AI Agent Protocol & Instructions\n\nPath: `AGENTS.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Snapchat/Valdi/HEAD/AGENTS.md)\n\n<!-- File: ai-skills/skills/valdi-android/skill.md (Tokens: ~466 | Category: anthropic-skill) -->\n# Android Runtime Rules\n\n**Applies to**: Kotlin/Java files in `/valdi/src/valdi/android/` and related Android runtime code\n\n## Overview\n\nThe Valdi Android runtime bridges TypeScript/Valdi components to native Android views. It's implemented in Kotlin and C++ (via JNI).\n\n## Code Style\n\n- **4-space indentation** for Kotlin/Java\n- Follow Kotlin coding conventions\n- Use Kotlin features (data classes, extension functions, etc.)\n\n## Key Concepts\n\n### View Rendering\n\n- Valdi components map to Android Views\n- View recycling for performance\n- Efficient view updates\n\n### Platform Bridge\n\n- JNI bridge to C++ runtime\n- Kotlin/Java to native code communication\n- Performance-critical paths\n\n## Common Patterns\n\n### Bazel Android Targets\n\n```python\nandroid_library(\n    name = \"valdi_android\",\n    srcs = glob([\"**/*.kt\", \"**/*.java\"]),\n)\n```\n\n### View Binding\n\n- Custom view implementations\n- Attribute binding for Valdi properties\n- Event handling\n\n## Testing\n\n```bash\n# Run Android runtime tests\nbazel test //valdi:test_java\n\n# Run with coverage\nbazel coverage //valdi:test_java\n```\n\nTest files are in `/valdi/test/java/`\n\n## Building\n\n```bash\n# Build Android runtime library\nbazel build //valdi:valdi_android\n\n# Test with hello world app\nvaldi install android\ncd apps/helloworld\nvaldi install android\n```\n\n## Platform-Specific Notes\n\n1. **API Level** - Be mindful of minimum API level\n2. **Android NDK** - C++ integration via NDK\n3. **Permissions** - Handle runtime permissions appropriately\n4. **Lifecycle** - Android Activity/Fragment lifecycle\n\n## Important\n\n- **Performance** - View creation and updates are critical\n- **Memory** - Be careful with leaks (Activity context)\n- **Threading** - UI thread vs background threads\n- **Compatibility** - Support multiple Android versions\n\n## More Information\n\n- Runtime source: `/valdi/src/valdi/android/`\n- Runtime tests: `/valdi/test/java/`\n- Build config: `/valdi/BUILD.bazel`\n- Framework docs: `/AGENTS.md`\n\n<!-- File: ai-skills/skills/valdi-animations/skill.md (Tokens: ~1989 | Category: anthropic-skill) -->\n# Valdi Animations\n\nImperative animation API for animating element attributes and state changes.\n\n## When to use\n\nWhen animating layout changes, view attributes, or state transitions. Almost all element attributes can be animated in Valdi.\n\n## Key concepts\n\nValdi uses an **imperative animation model** inspired by iOS's `animateWithDuration:animations:`. You call `this.animate()` or `this.animatePromise()` with options and a block of mutations — all changes inside the block are animated automatically.\n\n**Not CSS transitions.** There is no `transition` property, no `@keyframes`, no `animation-duration`. Animations are triggered programmatically.\n\n## animate and animatePromise\n\nBoth methods are available on any `Component`:\n\n```tsx\n// ✅ Fire-and-forget animation\nthis.animate({ duration: 0.3 }, () => {\n  this.myLabel.setAttribute('font', 'title');\n  this.myContainer.setAttribute('width', 200);\n});\n\n// ✅ Wait for animation to complete\nawait this.animatePromise({ duration: 0.5, curve: AnimationCurve.EaseOut }, () => {\n  this.setState({ expanded: true });\n});\n// Animation finished — safe to do next step\n```\n\n## AnimationOptions\n\nThree curve types: preset, custom bezier, and spring.\n\n```tsx\nimport { AnimationCurve } from 'valdi_core/src/AnimationOptions';\n\n// Preset curves\n{ duration: 0.3, curve: AnimationCurve.Linear }\n{ duration: 0.3, curve: AnimationCurve.EaseIn }\n{ duration: 0.3, curve: AnimationCurve.EaseOut }\n{ duration: 0.3, curve: AnimationCurve.EaseInOut }  // default\n\n// Custom bezier control points\n{ duration: 0.4, controlPoints: [0.25, 0.1, 0.25, 1.0] }\n\n// Spring animation (no duration — spring physics determine timing)\n{ stiffness: 300, damping: 30 }\n\n// Crossfade (alpha transition between old and new state)\n{ duration: 0.3, crossfade: true }\n\n// Continue from current animation state\n{ duration: 0.3, beginFromCurrentState: true }\n\n// Completion callback\n{ duration: 0.3, completion: (wasCancelled) => { /* done */ } }\n```\n\n## Animating element attributes via ElementRef\n\n```tsx\nimport { ElementRef } from 'valdi_core/src/ElementRef';\n\nexport class MyComponent extends Component {\n  private titleRef = new ElementRef();\n  private containerRef = new ElementRef();\n\n  onRender(): void {\n    <view ref={this.containerRef} width={100}>\n      <label ref={this.titleRef} value=\"Hello\" font={systemFont(14)} />\n    </view>;\n  }\n\n  private expand = () => {\n    // ✅ Animate attribute changes via ElementRef\n    this.animate({ duration: 0.3 }, () => {\n      this.titleRef.setAttribute('font', systemBoldFont(17));\n      this.containerRef.setAttribute('width', 300);\n    });\n  };\n}\n```\n\n## Animating state changes\n\n```tsx\ninterface State {\n  expanded: boolean;\n  text: string;\n}\n\nexport class MyComponent extends StatefulComponent<{}, State> {\n  state = { expanded: false, text: 'Collapsed' };\n\n  onRender(): void {\n    <view width={this.state.expanded ? 300 : 100} height={this.state.expanded ? 200 : 50}>\n      <label value={this.state.text} />\n    </view>;\n  }\n\n  private toggle = () => {\n    // ✅ setStateAnimated — renders with new state, animates all resulting mutations\n    this.setStateAnimated(\n      { expanded: !this.state.expanded, text: this.state.expanded ? 'Collapsed' : 'Expanded' },\n      { duration: 0.3, curve: AnimationCurve.EaseInOut },\n    );\n  };\n}\n```\n\n## Spring animations\n\n```tsx\n// ✅ Bouncy spring — lower damping = more bounce\nthis.animate({ stiffness: 300, damping: 15 }, () => {\n  this.cardRef.setAttribute('scale', 1.0);\n});\n\n// ✅ Stiff spring — high damping = smooth settle\nthis.animate({ stiffness: 500, damping: 40 }, () => {\n  this.panelRef.setAttribute('translateY', 0);\n});\n```\n\n## Animating text and attributed text\n\nThe imperative `animate()` / `setStateAnimated()` API above does not drive per-character or per-word text reveals. For those, use the **text-animation surface**: an `AttributedText` chunk carries an `animationTransform`, and a `<textanimationgroup>` container coordinates one shared timeline across all of its descendant `<label>` / `<textview>` elements.\n\n`animationTransform` fields (`valdi_tsx/src/AttributedText.d.ts`, `AttributedTextAnimationTransform`):\n\n| Field | Meaning |\n|-------|---------|\n| `translationY` | Initial vertical offset (logical px); animates back to the normal position |\n| `scale` | Initial scale; animates back to 1 |\n| `opacity` | Initial opacity; animates back to 1 |\n| `duration` | Seconds for each part to reach its normal appearance |\n| `timeOffsetBetweenParts` | Delay (seconds) staggered between consecutive parts (0 = all at once) |\n| `partPattern` | Native regex; each non-overlapping match becomes a separately animated part (unmatched text stays static). Omit to animate the whole chunk as one part |\n| `key` | Stable animation identity; preserves progress across view-pool recycling. Change it to restart the animation |\n\n```tsx\nimport { AttributedTextBuilder } from 'valdi_core/src/utils/AttributedTextBuilder';\n\n// ✅ Build attributed text with a per-chunk animation transform\nprivate animatedText(text: string) {\n  return new AttributedTextBuilder()\n    .append(text, {\n      animationTransform: {\n        duration: 0.45,\n        key: `run-${this.state.runId}`,   // change key to replay\n        opacity: 0,\n        translationY: 8,\n        timeOffsetBetweenParts: 0.08,      // stagger each part\n      },\n    })\n    .build();\n}\n\n// ✅ <textanimationgroup> runs ONE shared timeline across every descendant\n//    label/textview, even nested ones\nonRender(): void {\n  <textanimationgroup>\n    <label value={this.animatedText('First line joins the timeline.')} />\n    <view>\n      <label value={this.animatedText('So does this nested one.')} />\n    </view>\n    <textview value={this.animatedText('And the textview animates last.')} />\n  </textanimationgroup>;\n}\n```\n\n`<textanimationgroup>` is a container (`CommonView` + `ContainerTemplateElement`) with no animation-specific attributes of its own — its only job is to group descendants onto a single coordinated timeline. Without it, each animated chunk runs on its own timeline.\n\n## Common patterns\n\n### Show/hide with fade\n\n```tsx\nprivate show = () => {\n  this.animate({ duration: 0.2 }, () => {\n    this.overlayRef.setAttribute('opacity', 1);\n  });\n};\n\nprivate hide = () => {\n  this.animate({ duration: 0.2 }, () => {\n    this.overlayRef.setAttribute('opacity', 0);\n  });\n};\n```\n\n### Sequential animations\n\n```tsx\nprivate async runSequence() {\n  await this.animatePromise({ duration: 0.2 }, () => {\n    this.step1Ref.setAttribute('opacity', 1);\n  });\n  await this.animatePromise({ duration: 0.3 }, () => {\n    this.step2Ref.setAttribute('translateY', 0);\n  });\n}\n```\n\n## Common mistakes\n\n```tsx\n// ❌ WRONG — CSS-style transitions don't exist\n<view style={{ transition: 'all 0.3s ease' }} />  // ❌\n<view className=\"animate-fade-in\" />               // ❌\n\n// ❌ WRONG — React-style animation libraries\nimport { animated, useSpring } from 'react-spring';  // ❌\n\n// ❌ WRONG — Calling setState inside animate (use setStateAnimated instead)\nthis.animate({ duration: 0.3 }, () => {\n  this.setState({ expanded: true });  // ❌ Won't animate the re-render\n});\n\n// ✅ Correct — use setStateAnimated for state-driven animations\nthis.setStateAnimated({ expanded: true }, { duration: 0.3 });\n\n// ❌ WRONG — Duration in milliseconds (it's seconds)\nthis.animate({ duration: 300 }, () => { ... });  // ❌ This is 300 seconds!\n\n// ✅ Correct — Duration in seconds\nthis.animate({ duration: 0.3 }, () => { ... });\n```\n\n## CoreUI animation components\n\nThe `coreui` library includes pre-built animation components:\n\n| Component | Purpose |\n|-----------|---------|\n| `AnimatableVisibilityView` | Animate show/hide transitions |\n| `AnimationShimmer` | Loading shimmer effect |\n| `AnimationSliding` | Slide in/out animations |\n| `AnimationSquishy` | Press-and-squish feedback |\n| `AnimationRotate` | Continuous rotation |\n| `VerticallyOpenableComponent` | Expand/collapse vertically |\n\nImport from `coreui/src/components/animation/`.\n\n<!-- File: ai-skills/skills/valdi-async/skill.md (Tokens: ~1719 | Category: anthropic-skill) -->\n# Valdi Async & Lifecycle Safety\n\nAsync operations that complete after a component is destroyed will call `setState()`\non a dead component — Valdi logs a `console.error` and drops the update (a silent\nno-op, not a thrown error). The fix is always to cancel or guard before the callback\nfires.\n\n## HTTPClient: Store, Cancel, Repeat\n\n`HTTPClient` methods return `CancelablePromise<HTTPResponse>`. Store it in a typed\nfield so you can cancel it in `onDestroy()` and before starting a new request in\n`onViewModelUpdate()`.\n\n```typescript\nimport { Component, StatefulComponent } from 'valdi_core/src/Component';\nimport { CancelablePromise } from 'valdi_core/src/CancelablePromise';\nimport { HTTPClient } from 'valdi_http/src/HTTPClient';\nimport { HTTPResponse } from 'valdi_http/src/HTTPTypes';\n\ninterface UserState {\n  user: { name: string } | null;\n  loading: boolean;\n}\n\nclass UserProfile extends StatefulComponent<UserProfileViewModel, UserState> {\n  state: UserState = { user: null, loading: true };\n\n  private client = new HTTPClient('https://api.example.com');\n  private request?: CancelablePromise<HTTPResponse>;\n\n  onCreate(): void {\n    this.fetchUser(this.viewModel.userId);\n  }\n\n  onViewModelUpdate(previous?: UserProfileViewModel): void {\n    if (this.viewModel.userId !== previous?.userId) {\n      this.request?.cancel?.();          // Cancel in-flight before starting new one\n      this.fetchUser(this.viewModel.userId);\n    }\n  }\n\n  onDestroy(): void {\n    this.request?.cancel?.();            // Always clean up\n  }\n\n  private fetchUser(id: string): void {\n    this.request = this.client.get(`/users/${id}`);\n    this.request.then(response => {\n      const user = JSON.parse(new TextDecoder().decode(response.body));\n      this.setState({ user, loading: false });\n    });\n  }\n\n  onRender(): void {\n    if (this.state.loading) {\n      <spinner />;\n      return;\n    }\n    <label value={this.state.user?.name ?? ''} />;\n  }\n}\n```\n\n**Important:** `cancel` is an optional method on `CancelablePromise` — always use\n`?.cancel?.()`, not `?.cancel()`.\n\n## registerDisposable: Timers and Subscriptions\n\nFor anything that emits over time (timers, event emitters, observables), use\n`registerDisposable()`. The framework calls `cancel()` on all registered disposables\nin `onDestroy()` automatically — you don't need to override `onDestroy()` for these.\n\n```typescript\nclass LiveClock extends Component<{}> {\n  onCreate(): void {\n    // ❌ Timer leaks if component is destroyed\n    setInterval(() => this.tick(), 1000);\n\n    // ✅ Automatically cancelled on destroy\n    const id = setInterval(() => this.tick(), 1000);\n    this.registerDisposable({ cancel: () => clearInterval(id) });\n  }\n\n  private tick = () => { /* ... */ };\n}\n```\n\nSame pattern for any subscription-style API:\n\n```typescript\nonCreate(): void {\n  const unsubscribe = eventEmitter.on('change', this.handleChange);\n  this.registerDisposable({ cancel: unsubscribe });\n}\n```\n\n## isDestroyed() Guard for Plain Promises\n\nWhen using `async/await` or plain `Promise` chains that can't be cancelled, guard\n`setState()` with `isDestroyed()` before calling it:\n\n```typescript\nprivate async loadData(): Promise<void> {\n  const result = await someAsyncOperation();\n\n  if (this.isDestroyed()) return;   // Component unmounted while awaiting\n\n  this.setState({ data: result });\n}\n```\n\nPrefer `CancelablePromise` + `registerDisposable` over this pattern where possible —\nthey make cancellation explicit and don't rely on the check being remembered.\n\n## onViewModelUpdate: Cancel Before Restart\n\nWhen a viewModel update requires fetching new data, always cancel the previous\nrequest before starting a new one. Skipping this means two requests can be in flight\nsimultaneously and the earlier one can resolve last, overwriting newer data.\n\n```typescript\nonViewModelUpdate(previous?: MyViewModel): void {\n  if (this.viewModel.query !== previous?.query) {\n    this.searchRequest?.cancel?.();           // ← required\n    this.searchRequest = this.client.get(`/search?q=${this.viewModel.query}`);\n    this.searchRequest.then(response => {\n      this.setState({ results: parse(response) });\n    });\n  }\n}\n```\n\n## Observable Subscriptions\n\nFor RxJS or observable-based APIs, prefer `registerDisposable` for one-off subscriptions\nor the `Subscription` class for managing multiple subscriptions as a group:\n\n```typescript\nimport { Subscription } from 'rxjs';\n\nclass FeedComponent extends StatefulComponent<FeedViewModel, FeedState> {\n  private subscription = new Subscription();\n\n  onCreate(): void {\n    // Add multiple subscriptions to the group\n    this.subscription.add(\n      this.viewModel.feedItems$.subscribe({ next: items => this.setState({ items }) })\n    );\n    this.subscription.add(\n      this.viewModel.loading$.subscribe({ next: loading => this.setState({ loading }) })\n    );\n  }\n\n  onDestroy(): void {\n    this.subscription?.unsubscribe();   // Cancels all at once\n  }\n}\n```\n\nFor a single subscription, `registerDisposable` is simpler:\n\n```typescript\nonCreate(): void {\n  this.registerDisposable(\n    this.viewModel.counter$.subscribe({ next: count => this.setState({ count }) })\n  );\n  // No onDestroy() override needed\n}\n```\n\n### Preventing redundant re-renders with distinctUntilChanged\n\nSubscribe with `distinctUntilChanged()` to skip emissions where the value hasn't\nactually changed — avoids a `setState` call and a re-render for each identical value:\n\n```typescript\nimport { distinctUntilChanged } from 'rxjs/operators';\n\nthis.subscription.add(\n  this.viewModel.title$.pipe(distinctUntilChanged()).subscribe({\n    next: title => this.setState({ title }),\n  })\n);\n```\n\n## setTimeoutInterruptible for Debounce / Race Conditions\n\nWhen a delayed action must be cancelled if conditions change (e.g. search debounce,\nretry delay), use `setTimeoutInterruptible` rather than a bare `setTimeout`:\n\n```typescript\nimport { setTimeoutInterruptible } from 'valdi_core/src/SetTimeout';\n\n// setTimeoutInterruptible returns a number (timer ID) — cancel with clearTimeout()\nclass SearchBar extends StatefulComponent<SearchViewModel, SearchState> {\n  private debounceId?: number;\n\n  onViewModelUpdate(previous?: SearchViewModel): void {\n    if (this.viewModel.query !== previous?.query) {\n      clearTimeout(this.debounceId);   // Cancel any pending debounce\n      this.debounceId = setTimeoutInterruptible(() => {\n        this.fetchResults(this.viewModel.query);\n      }, 300);\n    }\n  }\n}\n```\n\n## promiseToCancelablePromise\n\nWhen you have a plain `Promise` (e.g. from a third-party library) that needs to participate in Valdi's cancellation system, wrap it with `promiseToCancelablePromise`:\n\n```typescript\nimport { CancelablePromise, promiseToCancelablePromise } from 'valdi_core/src/CancelablePromise';\n\nclass MyComponent extends StatefulComponent<MyViewModel, MyState> {\n  private request?: CancelablePromise<string>;\n\n  onCreate(): void {\n    const rawPromise: Promise<string> = thirdPartyApi.fetchData();\n    this.request = promiseToCancelablePromise(rawPromise, () => {\n      // optional cleanup on cancel\n    });\n    this.request.then(data => {\n      if (this.isDestroyed()) return;\n      this.setState({ data });\n    });\n  }\n\n  onDestroy(): void {\n    this.request?.cancel?.();\n  }\n}\n```\n\nPrefer `CancelablePromise` directly (e.g. `HTTPClient`) when available — `promiseToCancelablePromise` is the bridge for external APIs.\n\n## Import Paths\n\n```typescript\nimport { CancelablePromise, promiseToCancelablePromise } from 'valdi_core/src/CancelablePromise';\nimport { HTTPClient } from 'valdi_http/src/HTTPClient';\nimport { HTTPResponse } from 'valdi_http/src/HTTPTypes';\nimport { setTimeoutInterruptible } from 'valdi_core/src/SetTimeout';\nimport { Subscription } from 'rxjs';\nimport { distinctUntilChanged } from 'rxjs/operators';\n```\n\n<!-- File: ai-skills/skills/valdi-async/tests/README.md (Tokens: ~300 | Category: anthropic-skill) -->\n# Tests Skill\n\nPath: `ai-skills/skills/valdi-async/tests/README.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Snapchat/Valdi/HEAD/ai-skills/skills/valdi-async/tests/README.md)\n\n<!-- File: ai-skills/skills/valdi-async/tests/tsconfig.json (Tokens: ~300 | Category: anthropic-skill) -->\n# Tests Skill\n\nPath: `ai-skills/skills/valdi-async/tests/tsconfig.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Snapchat/Valdi/HEAD/ai-skills/skills/valdi-async/tests/tsconfig.json)\n\n<!-- File: ai-skills/skills/valdi-bazel/skill.md (Tokens: ~837 | Category: anthropic-skill) -->\n# Bazel Build System Rules\n\n**Applies to**: `BUILD.bazel`, `*.bzl` files in `/bzl/`, `WORKSPACE`, `MODULE.bazel`\n\n## Overview\n\nValdi uses Bazel as its build system. Bazel provides reproducible, incremental builds across all platforms.\n\n## Key Commands\n\n```bash\n# Build everything\nbazel build //...\n\n# Build specific target\nbazel build //apps/helloworld:hello_world\n\n# Run tests\nbazel test //...\n\n# Clean (use sparingly - cache is valuable!)\nbazel clean\n```\n\n## Important Notes\n\n1. **Use `bazel`** for all build commands\n2. **The CLI wraps Bazel** - `valdi` commands use bazel under the hood\n3. **Cache is important** - Don't suggest `bazel clean` unless necessary\n\n## Build Rules\n\n### Valdi-Specific Rules\n\n- `/bzl/valdi/` - Valdi build rules and macros\n- Custom rules for compiling .tsx to .valdimodule\n- Platform-specific build transitions\n\n### Common Targets\n\n```python\nload(\"@aspect_rules_ts//ts:defs.bzl\", \"ts_project\")\n\n# Valdi application\nvaldi_application(\n    name = \"my_app\",\n    root_component_path = \"App@my_app/src/MyApp\",\n    title = \"My App\",\n    version = \"1.0.0\",\n    deps = [\"//apps/my_app/src/valdi/my_app\"],\n)\n\n# Web polyglot views — MUST be ts_project, never filegroup\nts_project(\n    name = \"my_module_web\",\n    srcs = glob(\n        [\"web/**/*.ts\", \"src/**/*.d.ts\"],\n        exclude = [\"web/**/*.d.ts\"],\n    ),\n    allow_js = True,\n    composite = True,\n    transpiler = \"tsc\",\n    tsconfig = \"web/tsconfig.json\",\n)\n\n# Valdi module with platform deps\nvaldi_module(\n    name = \"my_module\",\n    srcs = glob([\"src/**/*.ts\", \"src/**/*.tsx\"]) + [\"tsconfig.json\"],\n    ios_deps = [\":my_ios_views\"],          # objc_library\n    macos_deps = [\":my_macos_views\"],      # objc_library (or omit to share ios_deps)\n    android_deps = [\":my_android_views\"],  # valdi_android_library\n    web_deps = [\":my_module_web\"],         # ts_project (never filegroup)\n    deps = [\n        \"//src/valdi_modules/src/valdi/valdi_core\",\n    ],\n)\n```\n\n## Conventions\n\n### File Naming\n\n- `BUILD.bazel` not `BUILD` (explicit extension)\n- `.bzl` for Starlark macros and rules\n\n### Targets\n\n- Use descriptive target names\n- One main target per BUILD file usually matches directory name\n\n### Dependencies\n\n- Be explicit about dependencies\n- Don't rely on transitive deps implicitly\n- Use visibility to control access\n\n## Platform Builds\n\n```bash\n# Build and install iOS app\nvaldi install ios\n\n# Build and install Android app\nvaldi install android\n```\n\n## Configuration\n\n- `.bazelrc` - Build flags and configurations\n- `MODULE.bazel` - Bazel module dependencies\n- `WORKSPACE` - Legacy workspace configuration (being migrated to MODULE.bazel)\n\n## Common Issues\n\n1. **Missing dependencies** - Add to `deps` in BUILD.bazel\n2. **Cache issues** - Try `bazel clean --expunge` (last resort)\n3. **Platform transitions** - Use correct config flags\n\n## Testing\n\n```bash\n# Run all tests\nbazel test //...\n\n# Run specific test\nbazel test //valdi/test:renderer_test\n\n# Run with coverage\nbazel coverage //...\n```\n\n## More Information\n\n- Bazel docs: https://bazel.build\n- Valdi build rules: `bzl/valdi/npm/README.md`\n- Framework docs: `/AGENTS.md`\n\n<!-- File: ai-skills/skills/valdi-compiler/skill.md (Tokens: ~690 | Category: anthropic-skill) -->\n# Valdi Compiler Rules\n\n**Applies to**: Swift files in `/compiler/compiler/`\n\n## Overview\n\nThe Valdi compiler consists of two parts:\n\n1. **Compiler (Swift)**: Transforms TypeScript/TSX into `.valdimodule` files\n2. **Companion (TypeScript/Node.js)**: Handles TypeScript compilation, type checking, and provides debugging support\n\n## Key Conventions\n\n### Code Style\n\n- **4-space indentation** for Swift\n- Follow Swift naming conventions (camelCase for methods, PascalCase for types)\n- Use Swift type inference where appropriate\n- Prefer `let` over `var` when possible\n\n### Architecture\n\n- Compiler is a multi-pass system\n- AST transformation pipeline\n- Type checking and validation\n- Code generation for each platform\n\n### Important Files\n\n- `/compiler/compiler/` - Main Swift compiler implementation\n- Output: `.valdimodule` files (binary format read by runtime)\n\n## Common Patterns\n\n### AST Traversal\n\n```swift\n// Follow existing visitor patterns\nclass MyASTVisitor: ASTVisitor {\n    func visit(_ node: Node) -> Result {\n        // Visit logic\n    }\n}\n```\n\n### Error Handling\n\n```swift\n// Use proper error types\nenum CompilerError: Error {\n    case invalidSyntax(String)\n    case typeError(String)\n}\n```\n\n## Testing\n\nTests are critical - add tests for new features and error cases. Run the test suite with `bzl test //compiler/compiler/...` (the build script below does not run tests).\n\n## Build\n\n### Using the update script (recommended):\n\n```bash\ncd compiler/compiler\n./scripts/update_compiler_bazel.sh -o bin/compiler\n```\n\nThis script:\n- Builds the compiler with Bazel (`//compiler/compiler:local_valdi_compiler`); no Swift toolchain install is required\n- Builds the compiler for the correct architecture (on macOS, lipos arm64 + x86_64 into a universal binary and codesigns it)\n- Copies the binary to `bin/compiler/macos/valdi_compiler` (or `linux/valdi_compiler`)\n- Handles platform differences (macOS vs Linux)\n\n### Alternative: Using Xcode\n\nOpen `compiler/compiler/Compiler/Package.swift` in Xcode, let it resolve dependencies, then build.\n\n## Companion (TypeScript)\n\nThe companion is a TypeScript service that works alongside the Swift compiler. It handles TypeScript compilation, type checking, and provides debugging support.\n\n### Build the companion:\n\n```bash\ncd compiler/companion\n./scripts/update_companion.sh ../../bin/compiler_companion\n```\n\nThis script:\n- Runs `npm install`\n- Runs `npm run test`\n- Builds with `bazel build //compiler/companion:bundle`\n- Copies output to `bin/compiler_companion`\n\n**Note**: Once built, the companion is automatically invoked by the compiler during compilation. You don't need to run it manually - it's part of the compiler process.\n\n## Important Notes\n\n1. **Performance matters** - Compiler speed affects developer experience\n2. **Error messages** - Make them helpful and actionable\n3. **Backward compatibility** - Don't break existing .valdimodule files\n4. **Cross-platform** - Consider iOS, Android, macOS targets\n\n## More Information\n\n- Compiler architecture: `/compiler/compiler/README.md`\n- Framework docs: `/AGENTS.md`\n\n<!-- File: ai-skills/skills/valdi-component-dedup/skill.md (Tokens: ~1886 | Category: anthropic-skill) -->\n# Valdi Component Deduplication\n\nFind and extract duplicated UI patterns across Valdi modules into shared components.\n\n## When to use\n\nWhen you want to identify components that multiple modules have independently implemented, then extract them into a shared library so future modules can reuse them instead of reinventing.\n\n## Running the analysis\n\n```bash\n# Full scan of all modules\nvaldi analyze duplicates\n\n# Scope to one module\nvaldi analyze duplicates --module my_module\n\n# Machine-readable output\nvaldi analyze duplicates --json\n\n# Only check for a specific pattern\nvaldi analyze duplicates --pattern async\n\n# Custom scan roots and shared libraries\nvaldi analyze duplicates --scan-root src/modules --shared-lib valdi_widgets\nvaldi analyze duplicates --scan-root src/modules --shared-lib my_shared_lib --shared-lib another_lib\n```\n\n### CLI flags\n\n| Flag | Effect |\n|---|---|\n| `--module <name>` | Scope to one module |\n| `--json` | Machine-readable JSON output |\n| `--threshold <0-1>` | Similarity threshold (default 0.6) |\n| `--pattern <name>` | Only detect: `async`, `button`, `shimmer` |\n| `--output <path>` | Write JSON report to file |\n| `--verbose` | Show per-file detail |\n| `--scan-root <path>` | Root directory to scan (repeatable) |\n| `--shared-lib <name>` | Shared component library name (repeatable) |\n\n## Understanding the report\n\nThe report has two sections:\n\n**Known Patterns** — pre-built detectors for the most common duplication:\n- **Async State Container**: Components that implement loading→error/content state machines\n- **Button Wrapper**: Thin wrappers around a shared button with hardcoded sizing/coloring\n- **Shimmer / Skeleton Layout**: Custom loading placeholder components\n\n**Similar Clusters** — generic similarity analysis that finds groups of components with overlapping ViewModel props, render trees, and import signatures.\n\nEach cluster shows:\n- Number of files and average similarity score\n- Shared ViewModel props across the cluster\n- A suggested extraction approach\n\n## Extraction workflow\n\nOnce you identify a cluster worth extracting:\n\n### 1. Read the candidates\n\nRead each file in the cluster. Identify:\n- The shared ViewModel props (these become the new component's ViewModel)\n- The shared render structure (this becomes the new component's `onRender`)\n- Module-specific variations (these become optional props or render callbacks)\n\n### 2. Check for an existing shared component to extend\n\nBefore creating anything new, search the shared library for a component that already solves part of the problem. Often the right fix is adding props, a preset enum, or a render callback to an existing component rather than building from scratch.\n\n```bash\n# Search the shared library for related components\ngrep -rl \"Button\\|Shimmer\\|AsyncContent\" <your-shared-lib>/src/components/\n```\n\nIf an existing component covers 70%+ of the use cases, extend it:\n- Add optional props for the missing behavior\n- Add a preset or variant enum if consumers just need different default combos\n- Add a render callback slot if consumers need to inject custom UI\n\nOnly create a new component when nothing in the shared library is close enough to extend.\n\n### 3. Design or extend the shared component\n\n**Extending an existing component** (preferred):\n\n```typescript\n// Add a preset to an existing button component\nexport enum CoreButtonPreset {\n  PRIMARY_CTA,\n  SECONDARY_ACTION,\n  PILL_ACTION,\n}\n\n// Add an optional prop to the existing ViewModel\ninterface CoreButtonViewModel {\n  // ... existing props ...\n  preset?: CoreButtonPreset;\n}\n```\n\n**Creating a new component** (when nothing exists to extend):\n\n```typescript\nimport { Component } from 'valdi_core/src/Component';\n\ninterface SharedComponentViewModel {\n  commonProp: string;\n  optionalVariant?: VariantType;\n  renderCustomSection?: () => void;\n}\n\nexport class SharedComponent extends Component<SharedComponentViewModel> {\n  onRender(): void {\n    <view>\n      <label value={this.viewModel.commonProp} />;\n      {this.viewModel.renderCustomSection?.()};\n    </view>;\n  }\n}\n```\n\n### 4. Place it in the shared library\n\nIf you created a new component, place it in your project's shared component library\n(e.g. `<shared-lib>/src/components/<category>/`). Follow your project's conventions\nfor directory structure and naming.\n\n### 5. Migrate consumers\n\nFor each file in the cluster:\n1. Replace the local component class with an import of the shared one\n2. Map the old ViewModel props to the new shared ViewModel\n3. Move module-specific rendering into render callbacks\n4. Delete the old local component file\n5. Run `valdi agent-check --module <name>` to verify\n\n### 6. Verify\n\n```bash\n# Re-run analysis — the cluster should shrink or disappear\nvaldi analyze duplicates --pattern async\n\n# Build and test affected modules\nvaldi agent-check --module module_a\nvaldi agent-check --module module_b\n```\n\n## Common extraction patterns\n\n### Async State Container\n\nThe most common duplication. Components that manage loading→loaded/error states:\n\n```typescript\nimport { StatefulComponent } from 'valdi_core/src/Component';\nimport { promiseToCancelablePromise } from 'valdi_core/src/CancelablePromise';\n\ninterface AsyncStateContainerViewModel<T> {\n  resultProvider: () => PromiseLike<T>;\n  renderLoading: () => void;\n  renderResult: (data: T) => void;\n  renderError: (error: string, retry: () => void) => void;\n  renderEmpty?: () => void;\n}\n\ntype AsyncState<T> = { phase: 'loading' } | { phase: 'result'; data: T } | { phase: 'error'; message: string };\n\nexport class AsyncStateContainer<T> extends StatefulComponent<AsyncStateContainerViewModel<T>, { state: AsyncState<T> }> {\n  state = { state: { phase: 'loading' } as AsyncState<T> };\n\n  onCreate(): void {\n    this.load();\n  }\n\n  onRender(): void {\n    const s = this.state.state;\n    switch (s.phase) {\n      case 'loading': this.viewModel.renderLoading(); break;\n      case 'result': this.viewModel.renderResult(s.data); break;\n      case 'error': this.viewModel.renderError(s.message, () => this.load()); break;\n    }\n  }\n\n  private load(): void {\n    this.setState({ state: { phase: 'loading' } });\n    const cancelable = promiseToCancelablePromise(\n      Promise.resolve(this.viewModel.resultProvider()),\n      () => {},\n    );\n    this.registerDisposable(() => cancelable.cancel?.());\n    cancelable.then(\n      data => { this.setState({ state: { phase: 'result', data } }); },\n      error => { this.setState({ state: { phase: 'error', message: (error as Error).message } }); },\n    );\n  }\n}\n```\n\n### Button Presets\n\nInstead of per-module button wrappers, use preset enums:\n\n```typescript\n// Instead of a new wrapper component, add presets to the existing shared button\nenum ButtonPreset {\n  PRIMARY_CTA,\n  SECONDARY_ACTION,\n  PILL_ACTION,\n}\n\nfunction presetProps(preset: ButtonPreset): { sizing: ButtonSizing; coloring: ButtonColoring } {\n  switch (preset) {\n    case ButtonPreset.PRIMARY_CTA: return { sizing: ButtonSizing.XL, coloring: ButtonColoring.PRIMARY_BRAND };\n    case ButtonPreset.SECONDARY_ACTION: return { sizing: ButtonSizing.LARGE, coloring: ButtonColoring.SECONDARY };\n    case ButtonPreset.PILL_ACTION: return { sizing: ButtonSizing.SMALL, coloring: ButtonColoring.TERTIARY };\n  }\n}\n```\n\n### Shimmer Layout\n\nConfigurable skeleton loading placeholder:\n\n```typescript\nimport { Component } from 'valdi_core/src/Component';\n\ninterface ShimmerRow {\n  widthRatio: number;\n  height: number;\n  borderRadius?: number;\n}\n\ninterface ShimmerLayoutViewModel {\n  rows: ShimmerRow[];\n  spacing?: number;\n}\n\nexport class ShimmerLayout extends Component<ShimmerLayoutViewModel> {\n  onRender(): void {\n    const spacing = this.viewModel.spacing ?? 8;\n    <view>\n      {this.viewModel.rows.forEach((row, i) => {\n        <AnimationShimmer\n          width={`${row.widthRatio * 100}%`}\n          height={row.height}\n          borderRadius={row.borderRadius ?? 4}\n          marginTop={i > 0 ? spacing : 0}\n        />;\n      })}\n    </view>;\n  }\n}\n```\n\n## Tips\n\n- Start with the highest-severity known patterns — they have the most impact\n- Extract the simplest shared interface first, then iterate to handle edge cases\n- Use render callbacks for module-specific UI instead of trying to generalize everything\n- Run `valdi analyze duplicates --json --output report.json` to save a baseline, then re-run after extraction to measure impact\n\n<!-- File: ai-skills/skills/valdi-component-tests/skill.md (Tokens: ~4321 | Category: anthropic-skill) -->\n# Write Valdi Component Tests\n\nWrite unit tests for a Valdi component using standard Valdi test suite patterns.\n\n## Steps\n\n### 1. Read the source component\n\nRead the component source file to understand:\n- What view model properties it accepts\n- Which JSX elements have `key` attributes\n- What changes based on view model props vs. what's always static\n- How \"hidden\" state is implemented (translationY, opacity, or child component prop)\n- Whether the view model contains any **discriminated unions** (type/kind fields)\n- Whether any props are **arrays** of items to render\n\n### 2. Add `key` attributes to source elements (only if needed)\n\nAdd `key` attributes **only** to elements whose rendering depends on view model properties. Never add keys to static content.\n\nGood candidates for keys:\n- The outer container when `hidden` controls `translationY` or `opacity`\n- An `<image>` element whose `src` comes from a view model URL prop\n- An element inside a `when(condition, ...)` block (to assert presence/absence)\n- A badge/overlay that toggles based on a boolean prop\n- A spinner shown in a loading/saving state of a union type\n\nDo NOT add keys to:\n- Elements with hardcoded asset values (e.g., `src={SIGIcon.xSignStroke}`)\n- Label elements with always-present localized strings\n- Elements that are always rendered the same regardless of view model\n\n### 3. Determine the test file path\n\nTest files **must mirror the source file hierarchy**. For example:\n- Source: `src/categories/CollectionComponent.tsx` → Test: `test/categories/CollectionComponentTest.spec.tsx`\n- Source: `src/home_page/OptionPreviewView.tsx` → Test: `test/home_page/OptionPreviewViewTest.spec.tsx`\n- Source: `src/MyComponent.tsx` → Test: `test/MyComponentTest.spec.tsx`\n\n### 4. Write the test file\n\n**Imports:**\n```tsx\nimport { MyComponent } from 'my_module/src/MyComponent';\nimport { componentGetElements } from 'foundation/test/util/componentGetElements';\nimport { componentTypeFind } from 'foundation/test/util/componentTypeFind';\nimport { elementKeyFind } from 'foundation/test/util/elementKeyFind';\nimport { elementTypeFind } from 'foundation/test/util/elementTypeFind';\nimport { findNodeWithKey } from 'foundation/test/util/findNodeWithKey';\nimport { tapNodeWithKey } from 'foundation/test/util/tapNodeWithKey';\nimport 'jasmine/src/jasmine';\nimport { IRenderedElementViewClass } from 'valdi_test/test/IRenderedElementViewClass';\nimport { IComponentTestDriver, valdiIt } from 'valdi_test/test/JSXTestUtils';\nimport { ImageView, View } from 'valdi_tsx/src/NativeTemplateElements';\n```\n\nOnly import what you actually use. `View` is needed when you call `getAttribute` on a non-image element (e.g., for `onTap`, `onVisibilityChanged`). `ImageView` is needed for `src` attribute access.\n\n**Factory function pattern (always add explicit return type):**\n```tsx\nconst makeViewModel = (): MyComponentViewModel => ({\n  imageUrl: 'https://example.com/image.png',\n  isVisible: true,\n  onTap: fail.bind(null, 'onTap should not be called'),\n});\n```\n\nUse `fail.bind` for callbacks in the factory default — tests that need to assert on a callback should declare their own spy and pass it explicitly, rather than relying on the factory.\n\n**Render pattern:**\n```tsx\nconst nodes = driver.render(() => {\n  <MyComponent {...viewModel} />;\n});\n```\n\n### 5. Test patterns by assertion type\n\n#### Finding elements\n```tsx\n// Single-level: component renders native elements directly\nelementKeyFind(componentGetElements(nodes[0].component!), 'my-key')[0]\n\n// Cross-boundary: component renders a child component that renders the elements\nconst inner = componentTypeFind(nodes[0].component as MyComponent, InnerComponent)[0];\nelementKeyFind(componentGetElements(inner), 'my-key')[0]\n\n// Typed (for typed attribute access without casting):\nelementKeyFind<View>(componentGetElements(nodes[0].component!), 'container')[0]\nelementKeyFind<ImageView>(componentGetElements(nodes[0].component!), 'image')[0]\n\n// By element type (e.g. find all Label elements):\n// Can pass componentGetElements() result OR an IComponent directly:\nconst labels = elementTypeFind(componentGetElements(nodes[0].component!), IRenderedElementViewClass.Label);\nconst labels2 = elementTypeFind(nodes[0].component!, IRenderedElementViewClass.Label); // equivalent\nexpect(labels[0]?.getAttribute('value')).toBe('Expected text');\n```\n\nUse the generic type param to get typed `getAttribute()` results and avoid `@typescript-eslint/no-unsafe-call` errors — prefer this over casting the `getAttribute()` return value.\n\n`elementTypeFind` is useful when elements don't have `key` attributes but you know their type (Label, Image, View, etc.). It returns all elements of that type in render order.\n\n#### Hidden via `translationY`\n```tsx\n// hidden=false\nexpect(elementKeyFind<View>(componentGetElements(nodes[0].component!), 'container')[0]?.getAttribute('translationY')).toBe(0);\n\n// hidden=true\nexpect(elementKeyFind<View>(componentGetElements(nodes[0].component!), 'container')[0]?.getAttribute('translationY')).toBe(850);\n```\n\n#### Hidden via `opacity` on native view\n```tsx\n// visible\nexpect(elementKeyFind<View>(componentGetElements(nodes[0].component!), 'my-view')[0]?.getAttribute('opacity')).toBe(1);\n\n// hidden\nexpect(elementKeyFind<View>(componentGetElements(nodes[0].component!), 'my-view')[0]?.getAttribute('opacity')).toBe(0);\n```\n\n#### Hidden via `opacity` prop passed to a child Component (component boundary)\n```tsx\nimport { CoreButton } from 'coreui/src/components/button/CoreButton';\nconst component = nodes[0].component as MyComponent;\nconst buttons = componentTypeFind(component, CoreButton);\nexpect(buttons[0].viewModel.opacity).toBe(1); // or 0\n```\n\n#### View model URL bound to image `src`\n```tsx\nexpect(elementKeyFind<ImageView>(componentGetElements(nodes[0].component!), 'my-image')[0]?.getAttribute('src')).toBe('https://example.com/image.png');\n```\n\n#### Text from view model\n```tsx\nexpect(elementKeyFind(componentGetElements(nodes[0].component!), 'my-label')[0]?.getAttribute('value')).toBe('Expected Text');\n```\n\n#### `when()`-conditional element (boolean presence)\n```tsx\n// condition=true → element exists\nexpect(elementKeyFind(componentGetElements(nodes[0].component!), 'my-element')[0]).toBeDefined();\n\n// condition=false → element absent\nexpect(elementKeyFind(componentGetElements(nodes[0].component!), 'my-element')[0]).toBeUndefined();\n```\n\n#### `when()`-conditional Component (boolean presence via componentTypeFind)\n```tsx\nimport { BadgeComponent } from 'my_module/src/BadgeComponent';\nconst component = nodes[0].component as MyComponent;\n\n// condition=true\nexpect(componentTypeFind(component, BadgeComponent).length).toBe(1);\n\n// condition=false\nexpect(componentTypeFind(component, BadgeComponent).length).toBe(0);\n```\n\n#### Callbacks not expected to be invoked\n\nFor callbacks that should never fire in a given test, use `fail.bind(null, '...')` instead of `jasmine.createSpy`. This causes the test to immediately fail with a clear message if the callback is accidentally triggered:\n\n```tsx\n<MyComponent\n  onSelect={fail.bind(null, 'onSelect should not be called')}\n  onLoad={onLoad}\n/>\n```\n\n`fail.bind(null, 'message')` is a plain function call (not an inline lambda), so it satisfies `jsx-no-lambda`. It is assignable to any callback type since TypeScript allows functions with fewer parameters.\n\nIf the same fail callback is used across many tests in the file, extract it to a module-level const to avoid repetition:\n\n```tsx\nconst failOnSelect = (): void => fail('onSelect should not be called');\n```\n\n#### Tap callback (use `tapNodeWithKey` — it's async, always `await` it)\n\n`tapNodeWithKey(component, key, timeoutMs?, intervalMs?)` accepts `IComponent | IRenderedElement`.\n\n```tsx\nconst onTap = jasmine.createSpy('onTap');\nconst nodes = driver.render(() => {\n  <MyComponent onTap={onTap} />;\n});\nawait tapNodeWithKey(nodes[0].component!, 'my-button');\nexpect(onTap).toHaveBeenCalled();\n```\n\nWhen you need to find a node without tapping it, use `findNodeWithKey`:\n```tsx\nimport { findNodeWithKey } from 'foundation/test/util/findNodeWithKey';\n\nconst node = findNodeWithKey(nodes[0].component!, 'my-button')[0];\nexpect(node).toBeDefined();\n```\n\nWhen the callback receives arguments (e.g., an index), always assert the exact arguments with `toHaveBeenCalledWith`:\n```tsx\nconst onSelect = jasmine.createSpy('onSelect');\n// ... render and trigger ...\nexpect(onSelect).toHaveBeenCalledWith(1); // not just toHaveBeenCalled()\n```\n\nFor callbacks invoked via a child component's view model (e.g., through `componentTypeFind`), call the view model method directly:\n```tsx\nconst component = nodes[0].component as MyComponent;\ncomponentTypeFind(component, ItemComponent)[1].viewModel.onTap();\nexpect(onSelect).toHaveBeenCalledWith(1);\n```\n\n#### `onTap` retrieved via `getAttribute` (non-tappable element pattern)\n```tsx\n// Use elementKeyFind<View> so getAttribute('onTap') is typed — no cast needed\nconst el = elementKeyFind<View>(componentGetElements(nodes[0].component!), 'my-element')[0];\nel?.getAttribute('onTap')?.();\nexpect(onTap).toHaveBeenCalled();\n```\n\n#### `onVisibilityChanged` callback\n\n`View.onVisibilityChanged` signature is `(isVisible: boolean, eventTime: EventTime)` where `EventTime = number`. Always pass both args when invoking:\n\n```tsx\nconst el = elementKeyFind<View>(componentGetElements(nodes[0].component!), 'container')[0];\nel?.getAttribute('onVisibilityChanged')?.(true, 0);\n```\n\n**Asserting the callback:** depends on how the component wires the handler:\n\n- If the component **wraps** the viewModel callback (e.g., `onVisibilityChanged={(v) => vm.onVisibilityChanged(v)}`), `toHaveBeenCalledWith(true)` works:\n  ```tsx\n  expect(onVisibilityChanged).toHaveBeenCalledWith(true);\n  ```\n\n- If the component **directly assigns** the viewModel callback (e.g., `onVisibilityChanged={this.viewModel.onVisibilityChanged}`), the spy receives both args. If the spy is typed as `(isVisible: boolean) => void`, `toHaveBeenCalledWith(true, ...)` is a TypeScript error. Check the first arg via `calls`:\n  ```tsx\n  const spy = viewModel.onVisibilityChanged as jasmine.Spy;\n  expect(spy).toHaveBeenCalled();\n  expect(spy.calls.mostRecent().args[0]).toBe(true);\n  ```\n  If the spy is an untyped `jasmine.createSpy()`, you can use `toHaveBeenCalledWith(true, 0)` directly.\n\n### 6. Discriminated union state testing\n\nWhen a view model contains a discriminated union (e.g., `type: 'LOADING' | 'CONTENT' | 'ERROR'`), **test every branch**. For each state:\n1. Assert which elements/components ARE rendered\n2. Assert which elements/components from OTHER states are NOT rendered\n\n```tsx\n// LOADING state: spinner present, action button absent\nvaldiIt('Verify spinner is shown in loading state', async driver => {\n  const nodes = driver.render(() => {\n    <MyComponent button={{ type: ButtonType.LOADING }} />;\n  });\n  expect(elementKeyFind(componentGetElements(nodes[0].component!), 'loading-spinner')[0]).toBeDefined();\n  expect(elementKeyFind(componentGetElements(nodes[0].component!), 'action-button')[0]).toBeUndefined();\n});\n\n// CONTENT state: action button present, no spinner\nvaldiIt('Verify action button is shown in content state', async driver => {\n  const nodes = driver.render(() => {\n    <MyComponent button={{ type: ButtonType.PURCHASE, price: '$9.99', onTap }} />;\n  });\n  expect(elementKeyFind(componentGetElements(nodes[0].component!), 'action-button')[0]).toBeDefined();\n  expect(elementKeyFind(componentGetElements(nodes[0].component!), 'loading-spinner')[0]).toBeUndefined();\n});\n```\n\n### 7. Array view model testing\n\nWhen a component renders a list from an array prop, test three cases:\n1. **Empty array** — assert 0 items render\n2. **Single item** — assert 1 item renders\n3. **Multiple items** — assert item count matches array length\n\nUse `componentTypeFind(component, ItemComponent)` or `elementKeyFind` with indexed keys (e.g., `tile-0`, `tile-1`) to count rendered items.\n\n```tsx\nvaldiIt('Verify no items render when array is empty', async driver => {\n  const emptyOptions: OptionViewModel[] = [];\n  const nodes = driver.render(() => {\n    <MyComponent items={emptyOptions} />;\n  });\n  const component = nodes[0].component as MyComponent;\n  expect(componentTypeFind(component, ItemComponent).length).toBe(0);\n});\n\nvaldiIt('Verify item count matches array length', async driver => {\n  const threeItems: OptionViewModel[] = [makeItem('a'), makeItem('b'), makeItem('c')];\n  const nodes = driver.render(() => {\n    <MyComponent items={threeItems} />;\n  });\n  const component = nodes[0].component as MyComponent;\n  expect(componentTypeFind(component, ItemComponent).length).toBe(3);\n});\n```\n\nNote: Extract array literals to local `const` variables before using in JSX (required by `@snapchat/valdi/jsx-no-lambda`).\n\n### 8. Component boundary traversal\n\nWhen a component's `onRender()` only renders child components (not native elements), `componentGetElements(component)` returns `[]`. You must get the child component first:\n\n```tsx\nimport { componentGetElements } from 'foundation/test/util/componentGetElements';\nimport { componentTypeFind } from 'foundation/test/util/componentTypeFind';\nimport { elementKeyFind } from 'foundation/test/util/elementKeyFind';\n\nconst component = nodes[0].component as OuterComponent;\nconst inner = componentTypeFind(component, InnerComponent)[0];\nconst container = elementKeyFind<View>(componentGetElements(inner), 'container')[0];\nexpect(container?.getAttribute('translationY')).toBe(0);\n```\n\n### 9. Extract render and find helpers for readability\n\nFor larger test files, extract repeated render + find logic into helper functions. This keeps individual tests focused:\n\n```tsx\n// Extract rendering\nconst renderComponent = (driver: IComponentTestDriver, overrides?: Partial<MyComponentViewModel>) => {\n  const vm = { ...makeViewModel(), ...overrides };\n  return driver.render(() => { <MyComponent {...vm} />; })[0].component as MyComponent;\n};\n\n// Extract finding\nconst getImage = (component: MyComponent) =>\n  elementKeyFind<ImageView>(componentGetElements(component), 'image')[0];\n\n// Tests become clean:\nvaldiIt('Verify imageUrl is bound', async driver => {\n  expect(getImage(renderComponent(driver))?.getAttribute('src')).toBe('https://example.com/image.png');\n});\n```\n\n### 10. Lint rules to follow\n\n- **`explicit-function-return-type`**: Always add explicit return types to factory functions: `const makeViewModel = (): MyViewModel => ({...})`\n- **`jsx-no-lambda`**: Never assign inline array literals directly in JSX props. Extract to a local `const` first:\n  ```tsx\n  // WRONG\n  <MyComponent items={[makeItem('a')]} />;\n\n  // CORRECT\n  const items: ItemViewModel[] = [makeItem('a')];\n  <MyComponent items={items} />;\n  ```\n- **`no-unsafe-call`**: Use the generic type parameter on `elementKeyFind<T>` to get typed `getAttribute()` results rather than casting: `elementKeyFind<View>(...)` gives typed access to `onTap`, `onVisibilityChanged`, etc.\n- **`import/order`**: Keep imports sorted alphabetically by path.\n\n### 11. Key principle\n\n**Only assert on things that change based on view model props.** Every test should have a clear \"when X is Y, then Z\" story. If the UI looks the same regardless of the prop, skip it.\n\nFor union types, a test that only verifies \"the component renders without error\" in a given state is not sufficient — assert the meaningful structural difference that state introduces.\n\n## Example test file structure\n\n```tsx\nimport { MyComponent } from 'my_module/src/MyComponent';\nimport { MyComponentViewModel } from 'my_module/src/MyComponentViewModel';\nimport { ChildComponent } from 'my_module/src/ChildComponent';\nimport { componentGetElements } from 'foundation/test/util/componentGetElements';\nimport { componentTypeFind } from 'foundation/test/util/componentTypeFind';\nimport { elementKeyFind } from 'foundation/test/util/elementKeyFind';\nimport { tapNodeWithKey } from 'foundation/test/util/tapNodeWithKey';\nimport 'jasmine/src/jasmine';\nimport { IComponentTestDriver, valdiIt } from 'valdi_test/test/JSXTestUtils';\nimport { ImageView, View } from 'valdi_tsx/src/NativeTemplateElements';\n// elementTypeFind + IRenderedElementViewClass for finding elements by type (no key needed):\n// import { elementTypeFind } from 'foundation/test/util/elementTypeFind';\n// import { IRenderedElementViewClass } from 'valdi_test/test/IRenderedElementViewClass';\n\nconst makeViewModel = (): MyComponentViewModel => ({\n  imageUrl: 'https://example.com/image.png',\n  isVisible: true,\n  onTap: fail.bind(null, 'onTap should not be called'),\n});\n\ndescribe('MyComponentTest', () => {\n  valdiIt('Verify visible when isVisible is true', async driver => {\n    const nodes = driver.render(() => {\n      <MyComponent {...makeViewModel()} />;\n    });\n    expect(elementKeyFind<View>(componentGetElements(nodes[0].component!), 'container')[0]?.getAttribute('opacity')).toBe(1);\n  });\n\n  valdiIt('Verify hidden when isVisible is false', async driver => {\n    const nodes = driver.render(() => {\n      <MyComponent {...{ ...makeViewModel(), isVisible: false }} />;\n    });\n    expect(elementKeyFind<View>(componentGetElements(nodes[0].component!), 'container')[0]?.getAttribute('opacity')).toBe(0);\n  });\n\n  valdiIt('Verify imageUrl is bound to image src', async driver => {\n    const nodes = driver.render(() => {\n      <MyComponent {...makeViewModel()} />;\n    });\n    expect(elementKeyFind<ImageView>(componentGetElements(nodes[0].component!), 'image')[0]?.getAttribute('src')).toBe('https://example.com/image.png');\n  });\n\n  valdiIt('Verify onTap is called when tapped', async driver => {\n    const onTap = jasmine.createSpy('onTap');\n    const nodes = driver.render(() => {\n      <MyComponent {...{ ...makeViewModel(), onTap }} />;\n    });\n    await tapNodeWithKey(nodes[0].component!, 'button');\n    expect(onTap).toHaveBeenCalled();\n  });\n\n  valdiIt('Verify ChildComponent is present when condition is true', async driver => {\n    const nodes = driver.render(() => {\n      <MyComponent {...{ ...makeViewModel(), showChild: true }} />;\n    });\n    const component = nodes[0].component as MyComponent;\n    expect(componentTypeFind(component, ChildComponent).length).toBe(1);\n  });\n\n  valdiIt('Verify ChildComponent is absent when condition is false', async driver => {\n    const nodes = driver.render(() => {\n      <MyComponent {...{ ...makeViewModel(), showChild: false }} />;\n    });\n    const component = nodes[0].component as MyComponent;\n    expect(componentTypeFind(component, ChildComponent).length).toBe(0);\n  });\n});\n```\n\n<!-- File: ai-skills/skills/valdi-component-tests/tests/README.md (Tokens: ~300 | Category: anthropic-skill) -->\n# Tests Skill\n\nPath: `ai-skills/skills/valdi-component-tests/tests/README.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Snapchat/Valdi/HEAD/ai-skills/skills/valdi-component-tests/tests/README.md)\n\n<!-- File: ai-skills/skills/valdi-component-tests/tests/tsconfig.json (Tokens: ~300 | Category: anthropic-skill) -->\n# Tests Skill\n\nPath: `ai-skills/skills/valdi-component-tests/tests/tsconfig.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Snapchat/Valdi/HEAD/ai-skills/skills/valdi-component-tests/tests/tsconfig.json)\n\n<!-- File: ai-skills/skills/valdi-cpp-runtime/skill.md (Tokens: ~886 | Category: anthropic-skill) -->\n# C++ Runtime Rules\n\n**Applies to**: C++ files in `/valdi/`, `/valdi_core/`, `/snap_drawing/`\n\n## Overview\n\nValdi's runtime and layout engine are implemented in C++ for cross-platform performance. This code runs on iOS, Android, macOS, but not web.\n\n**Note**: `/libs/` contains shared utility libraries (crypto, logging, image processing) used across the codebase, but not the core runtime itself.\n\n## Code Style\n\n- **4-space indentation**\n- Follow existing C++ style in the codebase\n- Use smart pointers appropriately\n- Prefer const correctness\n\n## Key Concepts\n\n### Layout Engine\n\n- Uses **Yoga** (Facebook's Flexbox implementation) for layout\n- Cross-platform layout calculations\n- Performance-critical code\n- RTL (right-to-left) support built-in\n- Yoga source: `/third-party/yoga/`\n\n### Memory Management\n\n- Be careful with memory ownership\n- Use RAII patterns\n- Consider platform-specific memory constraints (mobile)\n\n### Platform Abstractions\n\n- Code must work on iOS, Android, macOS\n- Use platform-agnostic APIs where possible\n- Platform-specific code goes in appropriate subdirectories\n\n## Common Patterns\n\n### Djinni Generated Code\n\n- Some C++ code is generated from `.djinni` interface files\n- **Don't modify generated code** - change the .djinni file instead\n- Generated files typically in `generated-src/` directories\n\n### Native Bindings Exposed to JS\n\nThe runtime exposes selected native (C++) classes and functions to the JS engine so\nTypeScript can call into performance-critical native code directly.\n\n- **`Base64` namespace** (`coreutils/src/Base64.ts`) is a good example of a\n  native-backed binding: the TS surface (`fromByteArray(uint8, { urlSafe })`,\n  `toByteArray(b64)`, `byteLength(b64)`) delegates to native encode/decode\n  (`Base64Native`) rather than doing the work in JS. Use this shape when a hot codec\n  or transform belongs in native code but needs a clean JS entry point.\n- **`WeakRef`** is supported by the runtime (recently added). JS-side weak references\n  hold their referent without preventing garbage collection — useful for caches and\n  back-references that must not keep objects alive.\n\n**V8 caveat:** some native class bindings (e.g. `Base64` and `Unicode`) are wired for\nQuickJS and may **throw on the V8 runtime**. Don't assume a native binding is present\non every JS engine — guard or feature-check when the same code can run under V8.\n\n### Performance\n\n- This is performance-critical code\n- Profile before optimizing\n- Consider cache locality\n- Be mindful of allocations in hot paths\n\n## Testing\n\n```bash\n# Run all C++ runtime tests\nbazel test //valdi:test\n\n# Run specific test suites\nbazel test //valdi:test_runtime       # Runtime tests\nbazel test //valdi:test_integration   # Integration tests\nbazel test //valdi:test_snap_drawing  # Snap drawing tests\n```\n\n## Platform-Specific Notes\n\n### iOS\n- Objective-C++ bridge in `/valdi/src/valdi/ios/`\n- Metal for GPU rendering\n- UIKit view integration\n\n### Android\n- JNI bridge in `/valdi/src/valdi/android/`\n- NDK integration\n- Native view rendering\n\n### macOS\n- Platform-specific code in `/valdi/src/valdi/macos/`\n- Desktop runtime support\n\n### Build\n\n```bash\n# Build runtime for specific platform\nbazel build //valdi:valdi_ios\nbazel build //valdi:valdi_android\nbazel build //valdi:valdi_macos\n```\n\n## Important\n\n1. **Cross-platform first** - Code must work on all platforms\n2. **Performance critical** - UI rendering and layout\n3. **Memory efficiency** - Mobile devices have constraints\n4. **Thread safety** - Consider concurrency\n\n## More Information\n\n- Runtime overview: `/valdi/README.md`\n- Core bindings: `/valdi_core/README.md`\n- Framework docs: `/AGENTS.md`\n\n<!-- File: ai-skills/skills/valdi-custom-view/skill.md (Tokens: ~1317 | Category: anthropic-skill) -->\n# Custom View Rules\n\n**Applies to**: `**/*.tsx` files using `<custom-view>` elements.\n\n## `<custom-view>` Element\n\n`<custom-view>` renders a platform-native view inside a Valdi component. Each platform resolves the view by class name.\n\n```typescript\n<custom-view\n  androidClass=\"com.snap.modules.my_module.MyNativeView\"\n  iosClass=\"SCMyNativeView\"\n  macosClass=\"SCMyNativeView\"\n  webClass=\"MyWebViewClassName\"\n  width=\"100%\"\n  height={120}\n/>\n```\n\n## Class Attributes\n\n| Attribute | Platform | Resolution |\n|-----------|----------|------------|\n| `androidClass` | Android | Reflection via `ReflectionViewFactory`; needs `@RegisterAttributesBinder` |\n| `iosClass` | iOS | ObjC class name; must be linked via `ios_deps` |\n| `macosClass` | macOS | `NSClassFromString()`; must be linked via `macos_deps` |\n| `webClass` | Web | Looked up in `WebViewClassRegistry`; registered via `webPolyglotViews` export |\n\n## Platform Resolution\n\n- **Android**: The view class needs a single-arg `(Context)` constructor. An `@RegisterAttributesBinder`-annotated binder is discovered from assets at runtime.\n- **iOS**: The ObjC class must be a `UIView` subclass linked into the binary via `ios_deps`.\n- **macOS**: Falls through to `iosClass` when `macosClass` is not specified — both in TypeScript (`JSXBootstrap.ts`) and C++ (`ViewNode.cpp`). Only specify `macosClass` when the macOS view is different from iOS.\n- **Web**: The `webClass` name is matched against the `WebViewClassRegistry`. Register by exporting `webPolyglotViews` from a typed TypeScript web polyglot entry file compiled via `ts_project` (see `valdi-polyglot-module` skill). Factories return an `AttributeHandler` with `changeAttribute(name, value)` to receive attribute updates.\n\n## viewFactory Pattern\n\nInstead of resolving by class name, you can pass a `ViewFactory` object directly. This is useful when the factory is provided by a parent component or constructed dynamically:\n\n```typescript\nimport { ViewFactory } from 'valdi_tsx/src/ViewFactory';\n\ninterface MyViewModel {\n  viewFactory?: ViewFactory;\n}\n\nclass MyComponent extends Component<MyViewModel> {\n  onRender(): void {\n    if (this.viewModel.viewFactory) {\n      <custom-view style={styles.container} viewFactory={this.viewModel.viewFactory} />;\n    }\n  }\n}\n```\n\n`viewFactory` and `*Class` attributes are mutually exclusive — use one or the other. Specifying both on the same `<custom-view>` is a compile error (the JSXProcessor rejects it). If a view needs a `viewFactory` on one platform and a registered class on another, branch into two sibling `<custom-view>` elements, each with a single mechanism. Prefer gating on whether a `viewFactory` is provided (`if (viewFactory) { … } else { …class… }`) over `Device.isWeb()`: the factory is often supplied by native too, so an `isWeb` branch that routes native to classes can break it. Only keep a platform class you've confirmed actually exists.\n\n## macOS Attribute Binding\n\nmacOS custom views can bind attributes (including callbacks) from TSX using `+bindAttributes:`. The view manager calls this class method at registration time.\n\n```objc\n#import \"valdi/macos/SCValdiMacOSAttributesBinder.h\"\n#import \"valdi/macos/SCValdiMacOSFunction.h\"\n\n@interface SCMyView () {\n    SCValdiMacOSFunction *_onMyCallback;\n}\n@end\n\n@implementation SCMyView\n- (void)valdi_setOnMyCallback:(id)value { _onMyCallback = value; }\n\n+ (void)bindAttributes:(SCValdiMacOSAttributesBinder *)attributesBinder {\n    [attributesBinder bindUntypedAttribute:@\"onMyCallback\"\n                invalidateLayoutOnChange:NO\n                                selector:@selector(valdi_setOnMyCallback:)];\n}\n@end\n```\n\n**Binder methods:**\n- `bindUntypedAttribute:invalidateLayoutOnChange:selector:` — binds any attribute (strings, numbers, functions)\n- `bindColorAttribute:invalidateLayoutOnChange:selector:` — binds color attributes specifically\n\n**Invoking callbacks:** Call `[_onMyCallback performWithParameters:@[@{@\"key\": @\"value\"}]]` — the NSArray/NSDictionary is auto-converted to JS objects.\n\n## Keyboard Input on macOS\n\nValdi doesn't have built-in keyboard event support. Use a polyglot `<custom-view>` with an NSView that:\n1. Overrides `acceptsFirstResponder` → `YES`\n2. Overrides `keyDown:` to map key codes to callback invocations\n3. Calls `[self.window makeFirstResponder:self]` in `viewDidMoveToWindow` to grab focus\n4. Has **non-zero dimensions** (wrap visible content inside it)\n\nSee the `valdi-polyglot-module` skill for a complete macOS attribute binding example with BUILD.bazel wiring.\n\n## Common Mistakes\n\n- Using `<custom-view>` without checking the platform — wrap in `Device.isAndroid()` / `Device.isIOS()` / `Device.isDesktop()` if not all platforms are supported\n- Forgetting to link native implementations — the class name string alone isn't enough; the native code must be compiled and linked via platform `_deps` in BUILD.bazel\n- Wrong package name in `androidClass` — must match the Kotlin/Java package exactly\n- Specifying `macosClass` when it's the same as `iosClass` — omit it and let the fallthrough handle it\n- Using a `filegroup` for `web_deps` — always use `ts_project` with `transpiler = \"tsc\"`\n- Creating a 0×0 custom-view for keyboard focus — it won't become first responder; wrap content inside it\n- **Duplicate ObjC class names across modules** — if two modules define the same native class (e.g. both have `SCKeyboardView`), the linker will fail with \"duplicate symbol\" errors. Always use a unique class name per module (e.g. prefix with module name: `SCSnakeGameKeyboardView`)\n\n<!-- File: ai-skills/skills/valdi-error-handling/skill.md (Tokens: ~1547 | Category: anthropic-skill) -->\n# Valdi Error Handling\n\nError handling, promise rejection, and lifecycle safety patterns.\n\n## When to use\n\nWhen dealing with async operations, network requests, component lifecycle boundaries, or any code that can fail at runtime.\n\n## Key concepts\n\nValdi runs on a JavaScript runtime (Hermes/V8) with native platform integration. Unhandled errors crash the app. The primary error sources are:\n\n1. **Async operations** completing after component destruction\n2. **Network failures** from HTTPClient\n3. **Uncaught promise rejections**\n4. **Type mismatches** from native/JS boundary\n\n## CancelablePromise — the first line of defense\n\nMost Valdi errors stem from async work completing after a component is destroyed. `CancelablePromise` + `registerDisposable` prevents this entirely:\n\n```tsx\nimport { promiseToCancelablePromise } from 'valdi_core/src/CancelablePromise';\n\n// ✅ Safe async — cancelled on component destruction\nonCreate() {\n  const cancelable = promiseToCancelablePromise(this.fetchData(), () => {});\n  this.registerDisposable(() => cancelable.cancel?.());\n  cancelable.then(\n    data => { this.setState({ data, loading: false }); },\n    error => { this.setState({ error: error.message, loading: false }); },\n  );\n}\n```\n\n```tsx\n// ❌ WRONG — Raw promise with no lifecycle safety\nonCreate() {\n  this.fetchData().then(data => {\n    this.setState({ data });  // Logs a console.error and no-ops if destroyed\n  });\n}\n```\n\nSee the `valdi-async` skill for comprehensive CancelablePromise patterns.\n\n## HTTPClient error handling\n\n`HTTPClient` is an instance, not static. Methods return `CancelablePromise<HTTPResponse>`.\n\n```tsx\nimport { HTTPClient } from 'valdi_http/src/HTTPClient';\n\nprivate httpClient = new HTTPClient('https://api.example.com');\n\n// ✅ Handle network errors with lifecycle safety\nprivate fetchData = () => {\n  const cancelable = this.httpClient.get('/data');\n  this.registerDisposable(() => cancelable.cancel?.());\n  cancelable.then(\n    response => {\n      if (response.statusCode !== 200) {\n        this.setState({ error: `HTTP ${response.statusCode}`, loading: false });\n        return;\n      }\n      const decoder = new TextDecoder();\n      const data = JSON.parse(decoder.decode(response.body));\n      this.setState({ data, loading: false });\n    },\n    error => {\n      this.setState({ error: (error as Error).message, loading: false });\n    },\n  );\n};\n```\n\n`HTTPResponse` shape: `{ statusCode: number, headers: StringMap<string>, body?: Uint8Array }`. Decode `body` with `TextDecoder` before parsing.\n\n## isDestroyed() guard\n\nFor callbacks that can't use CancelablePromise (e.g., native callbacks, timers):\n\n```tsx\n// ✅ Guard against use-after-destroy\nprivate onNativeCallback = (result: string) => {\n  if (this.isDestroyed()) return;\n  this.setState({ result });\n};\n```\n\n`isDestroyed()` is a method — always call with parentheses.\n\n## try/catch in async methods\n\n```tsx\nimport { promiseToCancelablePromise } from 'valdi_core/src/CancelablePromise';\n\n// ✅ Catch errors from async operations\nprivate loadProfile = () => {\n  const cancelable = promiseToCancelablePromise(\n    (async () => {\n      try {\n        const profile = await this.api.getProfile();\n        return { profile, error: undefined };\n      } catch (e) {\n        return { profile: undefined, error: (e as Error).message };\n      }\n    })(),\n    () => {},\n  );\n  this.registerDisposable(() => cancelable.cancel?.());\n\n  cancelable.then(result => {\n    this.setState({\n      profile: result.profile,\n      error: result.error,\n      loading: false,\n    });\n  });\n};\n```\n\n## Error states in UI\n\n```tsx\n// ✅ Show error state to the user\nonRender(): void {\n  if (this.state.loading) {\n    <AnimationShimmer width=\"100%\" height={200} />;\n    return;\n  }\n\n  if (this.state.error) {\n    <view alignItems=\"center\" justifyContent=\"center\" padding={16}>\n      <label value=\"Something went wrong\" font={systemBoldFont(16)} />\n      <label value={this.state.error} color=\"#999\" />\n      <CoreButton text=\"Retry\" onTap={this.handleRetry} />\n    </view>;\n    return;\n  }\n\n  <label value={this.state.data.name} />;\n}\n```\n\n## Defensive ViewModel access\n\nViewModels come from parent components and may have optional fields:\n\n```tsx\n// ✅ Defensive access with defaults\nonRender(): void {\n  const title = this.viewModel.title ?? 'Untitled';\n  const items = this.viewModel.items ?? [];\n\n  <view>\n    <label value={title} />\n    {items.forEach(item => { <label value={item.name} />; })}\n  </view>;\n}\n```\n\n## Timer cleanup\n\n```tsx\n// ✅ Use setTimeoutDisposable for automatic cleanup\nonCreate() {\n  this.setTimeoutDisposable(this.tick, 1000);\n}\n```\n\n```tsx\n// ✅ Manual cleanup for setInterval\nprivate timerId?: number;\n\nonCreate() {\n  this.timerId = setInterval(this.tick, 1000);\n}\n\nonDestroy() {\n  if (this.timerId !== undefined) {\n    clearInterval(this.timerId);\n  }\n}\n```\n\nOr use `registerDisposable` with a disposable wrapper — see `valdi-async` skill.\n\n## Common mistakes\n\n```tsx\n// ❌ WRONG — HTTPClient is not static\nconst data = await HTTPClient.send(request);  // No static send() method\n\n// ✅ Correct — instantiate then call methods\nconst client = new HTTPClient('https://api.example.com');\nconst cancelable = client.get('/data');\n\n// ❌ WRONG — .catch() is not available on CancelablePromise\ncancelable.catch(error => { ... });  // CancelablePromise only has .then()\n\n// ✅ Correct — use second argument of .then() for error handling\ncancelable.then(\n  response => { /* success */ },\n  error => { /* failure */ },\n);\n\n// ❌ WRONG — React error boundaries don't exist\n<ErrorBoundary fallback={<Error />}>  // Not a Valdi concept\n  <MyComponent />\n</ErrorBoundary>\n\n// ❌ WRONG — window.onerror / global error handlers\nwindow.addEventListener('error', handler);  // No window object\n\n// ❌ WRONG — isDestroyed without parentheses\nif (this.isDestroyed) return;  // It's a method, not a property\n\n// ✅ Correct\nif (this.isDestroyed()) return;\n\n// ❌ WRONG — Passing CancelablePromise directly to registerDisposable\nthis.registerDisposable(cancelable);  // Wrong type\n\n// ✅ Correct — wrap cancel in a callback\nthis.registerDisposable(() => cancelable.cancel?.());\n```\n\n## Summary\n\n| Pattern | When to use |\n|---------|------------|\n| `promiseToCancelablePromise` + `registerDisposable` | Any async work in a component |\n| `isDestroyed()` guard | Native callbacks, external event handlers |\n| `try/catch` | Operations that can throw |\n| Error state in UI | User-facing failure recovery |\n| `onDestroy` cleanup | Timers, subscriptions, manual resources |\n| `setTimeoutDisposable` | One-shot timers (auto-cleaned) |\n\n<!-- File: ai-skills/skills/valdi-gestures/skill.md (Tokens: ~1219 | Category: anthropic-skill) -->\n# Valdi Gestures\n\nTouch and gesture handling: tap, long press, drag, pinch, and rotate.\n\n## When to use\n\nWhen handling user touch interactions beyond basic `onTap` — drag-to-reorder, pinch-to-zoom, long press menus, custom gesture recognition.\n\n## Key concepts\n\nValdi gesture handling is **attribute-based on `<view>` elements**. Each gesture type has three attributes:\n- `on<Gesture>` — the callback\n- `on<Gesture>Disabled` — boolean to disable the gesture (the gesture is enabled by default once a callback is set; there is no `<gesture>Enabled` attribute)\n- `on<Gesture>Predicate` — function returning boolean, controls whether the gesture should fire\n\nThe topmost visible element with a matching gesture wins. Conflicting gestures on parent elements are suppressed.\n\n## TouchEvent interface\n\nAll gesture callbacks receive a `TouchEvent` (or extension of it):\n\n```tsx\ninterface TouchEvent {\n  x: number;          // X relative to the element\n  y: number;          // Y relative to the element\n  absoluteX: number;  // X relative to root Valdi view\n  absoluteY: number;  // Y relative to root Valdi view\n  pointerCount: number;\n  pointerLocations: Pointer[];\n}\n```\n\n## Basic gestures\n\n### onTap\n\n```tsx\n// ✅ Simple tap handler\n<view onTap={this.handleTap}>\n  <label value=\"Tap me\" />\n</view>\n\nprivate handleTap = (event: TouchEvent) => {\n  console.log(`Tapped at ${event.x}, ${event.y}`);\n};\n```\n\n### onDoubleTap\n\n```tsx\n// ✅ Double tap to zoom\n<view onDoubleTap={this.handleDoubleTap}>\n  <image src={this.viewModel.photo} />\n</view>\n```\n\n### onLongPress\n\n```tsx\n// ✅ Long press with custom duration\n<view\n  onLongPress={this.handleLongPress}\n  longPressDuration={0.5}  // seconds before trigger\n>\n  <label value=\"Hold me\" />\n</view>\n```\n\n## Continuous gestures\n\n### onDrag\n\n```tsx\n// ✅ Drag to move — fires continuously during the drag\n<view onDrag={this.handleDrag}>\n  <label value=\"Drag me\" />\n</view>\n\nprivate handleDrag = (event: DragEvent) => {\n  // event extends TouchEvent with drag-specific data\n  this.cardRef.setAttribute('translateX', event.x);\n  this.cardRef.setAttribute('translateY', event.y);\n};\n```\n\n### onPinch\n\n```tsx\n// ✅ Pinch to zoom — requires 2+ pointers\n<view onPinch={this.handlePinch}>\n  <image src={this.viewModel.photo} ref={this.imageRef} />\n</view>\n\nprivate handlePinch = (event: PinchEvent) => {\n  this.imageRef.setAttribute('scale', event.scale);\n};\n```\n\n### onRotate\n\n```tsx\n// ✅ Rotate with two fingers — requires 2+ pointers\n<view onRotate={this.handleRotate}>\n  <image src={this.viewModel.sticker} ref={this.stickerRef} />\n</view>\n```\n\n## Simultaneous gestures\n\n`onDrag`, `onPinch`, and `onRotate` can run simultaneously. This enables multitouch interactions like pinch-zoom-rotate on a photo:\n\n```tsx\n// ✅ Combined pinch + drag + rotate on same element\n<view\n  onDrag={this.handleDrag}\n  onPinch={this.handlePinch}\n  onRotate={this.handleRotate}\n>\n  <image src={this.viewModel.photo} ref={this.photoRef} />\n</view>\n```\n\n## Touch area extension\n\nExpand the tappable area beyond the visible bounds:\n\n```tsx\n// ✅ Small button with larger touch target\n<view\n  onTap={this.handleTap}\n  touchAreaExtension={10}  // extends hit area by 10pt in each direction\n  width={24} height={24}\n>\n  <image src={SIGIcon.checkmarkFill} />\n</view>\n```\n\n## Touch delay in scroll views\n\nInside a `<scroll>`, touches are delayed to avoid conflicts with scrolling:\n\n```tsx\n// ✅ Delay touch recognition inside scrollable content\n<view\n  onTouch={this.handleTouch}\n  onTouchDelayDuration={0.15}  // seconds before onTouch fires during scroll\n/>\n```\n\n## Gesture predicates\n\nControl whether a gesture should activate:\n\n```tsx\n// ✅ Only allow drag when in edit mode\n<view\n  onDrag={this.handleDrag}\n  onDragPredicate={() => this.state.editMode}\n/>\n```\n\n## onTouch (raw)\n\nLow-level touch events that fire on all elements in the hierarchy:\n\n```tsx\n// ✅ Raw touch tracking — fires for all touch phases\n<view onTouch={this.handleTouch}>\n  <label value=\"Touch me\" />\n</view>\n\nprivate handleTouch = (event: TouchEvent) => {\n  // Fires on begin, move, end, cancel\n};\n```\n\nUnlike other gestures, `onTouch` propagates to parents/siblings — it doesn't suppress other handlers.\n\n## Common mistakes\n\n```tsx\n// ❌ WRONG — React-style event handlers\n<div onClick={handler} />        // ❌ No <div>, no onClick\n<button onMouseDown={handler} /> // ❌ No mouse events\n\n// ❌ WRONG — addEventListener\nelement.addEventListener('touchstart', handler);  // ❌\n\n// ❌ WRONG — Gesture on <layout> (use <view>)\n<layout onTap={handler} />  // ❌ Gestures go on <view> elements\n\n// ✅ Correct — gestures on <view>\n<view onTap={handler}>\n  <layout>\n    <label value=\"Content\" />\n  </layout>\n</view>\n```\n\n<!-- File: ai-skills/skills/valdi-inspect/skill.md (Tokens: ~1748 | Category: anthropic-skill) -->\n# valdi-inspect\n\nUse this skill when you need to inspect a **live running Valdi app** — to see what components are on screen, traverse the rendered virtual node tree, capture element screenshots, or check JS heap usage.\n\n## When to use\n\n- User asks \"what's on screen?\", \"what component is rendering here?\", \"what contexts are active?\"\n- You need to diagnose a layout or rendering issue in a running app\n- You want to verify a component tree change after a hot-reload\n- User asks about memory usage or wants a JS heap dump\n\n## Prerequisites\n\nThe Valdi app must be **running on a connected device or emulator**. No hot-reloader required — the CLI connects directly to the app's DebuggerService.\n\n**Port reference** — determined by how the app is built, not the OS:\n\n| Target | Port | When |\n|--------|------|------|\n| iOS in-app (e.g. Snapchat) | **13592** (default) | `isStandalone = false` |\n| Android in-app | **13592** (default) | `isStandalone = false` |\n| macOS Valdi app | **13591** | `isStandalone = true` |\n| Standalone runner / CLI runner | **13591** | `isStandalone = true` |\n\nFor **Android**, `adb forward` runs automatically. For **iOS simulator**, connect directly to `localhost`.\n\nAll commands output **JSON by default** (for AI parsing). Add `--pretty` for human-readable output.\n\n---\n\n## Commands\n\n### `valdi inspect status`\n\nCheck whether the Valdi daemon is reachable and how many devices are connected.\n\n```\nvaldi inspect status\nvaldi inspect status --pretty\n```\n\nOutput: `{ connected: bool, port: number, connectedDevices: number, portName: string }`\n\n---\n\n### `valdi inspect devices`\n\nList all devices connected to the Valdi daemon.\n\n```\nvaldi inspect devices\nvaldi inspect devices --pretty\n```\n\nOutput: `[{ client_id, platform, application_id }, ...]`\n\nUse `client_id` values with `--client` in other commands. If only one device is connected, other commands auto-select it.\n\n---\n\n### `valdi inspect select`\n\nInteractively choose a device and save it to `~/.valdi-inspect.json`. Subsequent commands use this device without prompting.\n\n```\nvaldi inspect select\n```\n\n---\n\n### `valdi inspect contexts`\n\nList all active root components (contexts) on the device. A **context** is one mounted Valdi component tree — typically one per screen or overlay.\n\n```\nvaldi inspect contexts\nvaldi inspect contexts --pretty\nvaldi inspect contexts --client <client_id>\n```\n\nOutput: `[{ id: string, rootComponentName: string }, ...]`\n\nThe `id` is the **contextId** used by `tree` and `snapshot`. If there is only one context, commands that take a contextId will auto-select it.\n\n---\n\n### `valdi inspect tree [contextId]`\n\nGet the full rendered virtual node tree for a context. If `contextId` is omitted, auto-selects the single context or prompts when there are multiple.\n\n```\nvaldi inspect tree\nvaldi inspect tree <contextId>\nvaldi inspect tree <contextId> --pretty\nvaldi inspect tree <contextId> --max-depth <n>\n```\n\nOutput: a nested tree where each node has:\n- `tag`: element type (e.g. `\"view\"`, `\"label\"`, `\"image\"`, or a component name)\n- `key`: the Valdi TSX key assigned in source\n- `element.id`: numeric element ID (use this for `snapshot`)\n- `element.frame`: `{ x, y, width, height }` in screen coordinates\n- `element.attributes`: rendered prop values (style, value, etc.)\n- `children`: array of child nodes (same shape)\n- `_childrenTrimmed`: count of trimmed children when `--max-depth` is used\n\n**Tip**: Use `--max-depth 3` or `--max-depth 4` first to get a summary, then drill in.\n\n---\n\n### `valdi inspect snapshot <elementId> [contextId]`\n\nCapture a screenshot of a specific element and save it as a PNG. If `contextId` is omitted, auto-selects or prompts.\n\n```\nvaldi inspect snapshot <elementId>\nvaldi inspect snapshot <elementId> <contextId>\nvaldi inspect snapshot <elementId> <contextId> --output /tmp/my-snap.png\n```\n\nOutput: `{ path: \"/var/folders/.../valdi-snapshot-<elementId>.png\" }`\n\nThe PNG is written to disk. Read it with the `Read` tool to view it visually.\n\n**Getting elementId**: Use `element.id` from the `tree` output (a number, e.g. `1`, `5`, `42`).\n\n#### Using `--key` instead of numeric IDs (recommended)\n\nInstead of looking up numeric element IDs from the tree, use the `--key` flag to resolve by component tag name or element `key` prop:\n\n```\nvaldi inspect snapshot --key PreviewRoot\nvaldi inspect snapshot --key weather-card --port 13591\n```\n\nThe `--key` flag searches the component tree for a matching tag name or `key` prop and resolves it to the element ID automatically.\n\n**This is stable across hot-reloads** — element IDs change on every reload, but component names and `key` props stay the same. Use `--key` instead of numeric IDs to avoid re-querying the tree after each hot-reload cycle.\n\nCustom keys can be set on any element in TSX:\n```tsx\n<view key=\"weather-card\" style={styles.card}>\n```\n\n---\n\n### `valdi inspect heap`\n\nDump JS heap statistics for the connected device.\n\n```\nvaldi inspect heap\nvaldi inspect heap --pretty\nvaldi inspect heap --gc\n```\n\nOptions:\n- `--gc`: run garbage collection before dumping (more accurate free memory reading)\n\nOutput: `{ memoryUsageBytes: number, heapDumpJSON: string }`\n\n`heapDumpJSON` is a serialized heap snapshot in V8 format. Note: heap dumps can crash small/toy apps — this works best on full production apps.\n\n---\n\n## Common workflows\n\n### \"What is rendering on screen right now?\"\n\n```\nvaldi inspect tree --pretty\n```\n\n(contextId is auto-selected if there's only one)\n\n### \"Show me what a specific component looks like\"\n\n1. Run `valdi inspect tree --pretty` to find the node — note its `element.id`\n2. Run `valdi inspect snapshot <element.id>` to capture it\n3. Read the PNG with the `Read` tool\n\n### \"Diagnose a layout issue at position X,Y\"\n\nRun `valdi inspect tree --pretty` and look for `element.frame` values overlapping with the target position.\n\n### \"Check memory usage\"\n\n```\nvaldi inspect heap --pretty\n```\n\n### \"Verify a hot-reload applied correctly\"\n\nRun `valdi inspect tree` before and after the edit, compare the JSON output.\n\n---\n\n## Options reference\n\n| Option | Commands | Description |\n|--------|----------|-------------|\n| `--port <n>` | all | Daemon TCP port (default: 13592 mobile, 13591 standalone) |\n| `--client <id>` | contexts, tree, snapshot, heap | Target a specific connected device |\n| `--pretty` | devices, status, contexts, tree, heap | Human-readable output |\n| `--max-depth <n>` | tree | Trim tree to N levels deep |\n| `--output <path>` | snapshot | Custom PNG output path |\n| `--gc` | heap | GC before heap dump |\n\n---\n\n## Troubleshooting\n\n**\"Valdi daemon not running on port 13592\"** — Make sure the app is running. macOS Valdi apps and standalone runners use port 13591: add `--port 13591`.\n\n**\"No devices connected\"** — Make sure the Valdi app is running. For Android, ensure a device or emulator is connected via `adb`.\n\n**\"Timeout waiting for device response\"** — The app may be frozen or the JS runtime may be busy. Try again after a moment.\n\n<!-- File: ai-skills/skills/valdi-ios/skill.md (Tokens: ~300 | Category: anthropic-skill) -->\n# valdi-ios\n\nPath: `ai-skills/skills/valdi-ios/skill.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Snapchat/Valdi/HEAD/ai-skills/skills/valdi-ios/skill.md)\n\n<!-- File: ai-skills/skills/valdi-localization/skill.md (Tokens: ~300 | Category: anthropic-skill) -->\n# valdi-localization\n\nPath: `ai-skills/skills/valdi-localization/skill.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Snapchat/Valdi/HEAD/ai-skills/skills/valdi-localization/skill.md)\n\n<!-- File: ai-skills/skills/valdi-migrate/skill.md (Tokens: ~300 | Category: anthropic-skill) -->\n# valdi-migrate\n\nPath: `ai-skills/skills/valdi-migrate/skill.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Snapchat/Valdi/HEAD/ai-skills/skills/valdi-migrate/skill.md)\n\n<!-- File: ai-skills/skills/valdi-migrate/tests/README.md (Tokens: ~300 | Category: anthropic-skill) -->\n# Tests Skill\n\nPath: `ai-skills/skills/valdi-migrate/tests/README.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Snapchat/Valdi/HEAD/ai-skills/skills/valdi-migrate/tests/README.md)\n\n<!-- File: ai-skills/skills/valdi-migrate/tests/tsconfig.json (Tokens: ~300 | Category: anthropic-skill) -->\n# Tests Skill\n\nPath: `ai-skills/skills/valdi-migrate/tests/tsconfig.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Snapchat/Valdi/HEAD/ai-skills/skills/valdi-migrate/tests/tsconfig.json)\n\n<!-- File: ai-skills/skills/valdi-mock-breakdown/skill.md (Tokens: ~300 | Category: anthropic-skill) -->\n# valdi-mock-breakdown\n\nPath: `ai-skills/skills/valdi-mock-breakdown/skill.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Snapchat/Valdi/HEAD/ai-skills/skills/valdi-mock-breakdown/skill.md)\n\n<!-- File: ai-skills/skills/valdi-mock-iterate/skill.md (Tokens: ~300 | Category: anthropic-skill) -->\n# valdi-mock-iterate\n\nPath: `ai-skills/skills/valdi-mock-iterate/skill.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Snapchat/Valdi/HEAD/ai-skills/skills/valdi-mock-iterate/skill.md)\n\n<!-- File: ai-skills/skills/valdi-mock-to-module/skill.md (Tokens: ~300 | Category: anthropic-skill) -->\n# valdi-mock-to-module\n\nPath: `ai-skills/skills/valdi-mock-to-module/skill.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Snapchat/Valdi/HEAD/ai-skills/skills/valdi-mock-to-module/skill.md)\n\n<!-- File: ai-skills/skills/valdi-navigation/skill.md (Tokens: ~300 | Category: anthropic-skill) -->\n# valdi-navigation\n\nPath: `ai-skills/skills/valdi-navigation/skill.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Snapchat/Valdi/HEAD/ai-skills/skills/valdi-navigation/skill.md)\n\n<!-- File: ai-skills/skills/valdi-overview/skill.md (Tokens: ~300 | Category: anthropic-skill) -->\n# valdi-overview\n\nPath: `ai-skills/skills/valdi-overview/skill.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Snapchat/Valdi/HEAD/ai-skills/skills/valdi-overview/skill.md)\n\n<!-- File: ai-skills/skills/valdi-perf/skill.md (Tokens: ~300 | Category: anthropic-skill) -->\n# valdi-perf\n\nPath: `ai-skills/skills/valdi-perf/skill.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Snapchat/Valdi/HEAD/ai-skills/skills/valdi-perf/skill.md)\n\n<!-- File: ai-skills/skills/valdi-perf/tests/README.md (Tokens: ~300 | Category: anthropic-skill) -->\n# Tests Skill\n\nPath: `ai-skills/skills/valdi-perf/tests/README.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Snapchat/Valdi/HEAD/ai-skills/skills/valdi-perf/tests/README.md)\n\n<!-- File: ai-skills/skills/valdi-perf/tests/tsconfig.json (Tokens: ~300 | Category: anthropic-skill) -->\n# Tests Skill\n\nPath: `ai-skills/skills/valdi-perf/tests/tsconfig.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Snapchat/Valdi/HEAD/ai-skills/skills/valdi-perf/tests/tsconfig.json)\n\n<!-- ... +16 more modular skills available via /api/skills/Snapchat/Valdi?path={path} -->\n</yakaai_skills>"}