Skip to content

feat(skills): add kartal Agent Skills for AI coding agents - #89

Closed
burakJs wants to merge 5 commits into
VB10:masterfrom
burakJs:master
Closed

burakJs wants to merge 5 commits into
VB10:masterfrom
burakJs:master

Conversation

@burakJs

@burakJs burakJs commented Apr 19, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Adds nine kartal-* Agent Skills under skills/ (the Serverpod skills CLI layout, installable with skills get --ide cursor / --ide claude / --ide codex) so AI coding agents pick the right kartal extension instead of writing vanilla Flutter boilerplate that re-implements features kartal already provides.

Each skill maps a single area of kartal's surface (responsive UI, theming + navigation, string validation, string platform, async/future, popup loader, utilities) and ships with concrete API tables, before/after examples, gotchas, and a kartal-do-not-overuse guardrail skill so the agent stays away from kartal helpers when a pixel-exact / design-system value is intended.

Why

Without skills, agents reach for whatever Flutter API they remember — typically MediaQuery.of(context).size, hand-rolled Future.delayed + setState loaders, raw RegExp email checks, etc. — even on a project that already imports package:kartal/kartal.dart. The skills give the agent a discoverable, kartal-shaped path for the same intent (context.sized.dynamicWidth, context.popupManager.showLoader, email.ext.isValidEmail, …) and a clear guardrail for when not to apply them.

What's added

  • skills/kartal-overview/SKILL.md — load-first router that points at the right sibling skill
  • skills/kartal-responsive-ui/SKILL.md — context.padding, context.sized, context.border, context.device
  • skills/kartal-theming-navigation/SKILL.md — context.general, context.route, widget.ext
  • skills/kartal-string-validation/SKILL.md — string.ext validators, color, JSON
  • skills/kartal-string-platform/SKILL.md — string.ext launchers, share, app/device metadata
  • skills/kartal-async-future/SKILL.md — future.ext, map.ext, list.ext, iterable.exts
  • skills/kartal-popup-loader/SKILL.md — context.popupManager.showLoader / hideLoader
  • skills/kartal-utilities/SKILL.md — MapsUtility, BundleDecoder, CustomLinkPreview, CustomLogger, HttpResult, DeviceUtility, small primitive extensions
  • skills/kartal-do-not-overuse/SKILL.md — guardrail (don't replace pixel-exact values, generated files, declarative routers, security checks)

Plus install instructions in README.md and a CHANGELOG.md entry.

End-to-end test — same prompt, two fresh agent runs

A blank Flutter app with kartal: ^… in pubspec.yaml and the skills installed via skills get --ide cursor. Same login-page prompt, identical user-visible strings ('Sign in', 'Welcome back', 'Email', 'Password', 'Login'). Both runs picked the right kartal APIs end-to-end.

Run A — login_page.dart

import 'package:flutter/material.dart';
import 'package:kartal/kartal.dart';

class LoginPage extends StatefulWidget {
  const LoginPage({super.key});

  @override
  State<LoginPage> createState() => _LoginPageState();
}

class _LoginPageState extends State<LoginPage> {
  final _emailController = TextEditingController();
  final _passwordController = TextEditingController();
  String? _emailError;

  @override
  void dispose() {
    _emailController.dispose();
    _passwordController.dispose();
    super.dispose();
  }

  Future<void> _onLoginPressed() async {
    final email = _emailController.text.trim();

    if (!email.ext.isValidEmail) {
      setState(() => _emailError = 'Please enter a valid email');
      return;
    }
    setState(() => _emailError = null);

    context.popupManager.showLoader();
    await Future.delayed(const Duration(seconds: 2));
    if (!mounted) return;
    context.popupManager.hideLoader();

    context.route.pop();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Sign in')),
      body: Padding(
        padding: const EdgeInsets.symmetric(horizontal: 24),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.stretch,
          children: [
            const SizedBox(height: 24),
            Text(
              'Welcome back',
              style: context.general.textTheme.headlineMedium,
            ),
            const SizedBox(height: 24),
            TextField(
              controller: _emailController,
              keyboardType: TextInputType.emailAddress,
              autocorrect: false,
              decoration: InputDecoration(
                labelText: 'Email',
                border: const OutlineInputBorder(),
                errorText: _emailError,
              ),
            ),
            const SizedBox(height: 16),
            TextField(
              controller: _passwordController,
              obscureText: true,
              decoration: const InputDecoration(
                labelText: 'Password',
                border: OutlineInputBorder(),
              ),
            ),
            const SizedBox(height: 24),
            Center(
              child: SizedBox(
                width: context.sized.dynamicWidth(0.6),
                child: ElevatedButton(
                  style: ElevatedButton.styleFrom(
                    shape: RoundedRectangleBorder(
                      borderRadius: BorderRadius.circular(8),
                    ),
                  ),
                  onPressed: _onLoginPressed,
                  child: const Text('Login'),
                ),
              ),
            ),
          ],
        ),
      ),
    );
  }
}

Run B — login_page2.dart

import 'package:flutter/material.dart';
import 'package:kartal/kartal.dart';

class LoginPage2 extends StatefulWidget {
  const LoginPage2({super.key});

  @override
  State<LoginPage2> createState() => _LoginPage2State();
}

class _LoginPage2State extends State<LoginPage2> {
  final _emailController = TextEditingController();
  final _passwordController = TextEditingController();
  String? _emailError;

  @override
  void dispose() {
    _emailController.dispose();
    _passwordController.dispose();
    super.dispose();
  }

  Future<void> _onLoginPressed() async {
    final email = _emailController.text.trim();

    if (!email.ext.isValidEmail) {
      setState(() => _emailError = 'Please enter a valid email');
      return;
    }
    setState(() => _emailError = null);

    context.popupManager.showLoader();
    await Future.delayed(const Duration(seconds: 2));
    if (!mounted) return;
    context.popupManager.hideLoader();

    context.route.pop();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Sign in')),
      body: Padding(
        padding: const EdgeInsets.symmetric(horizontal: 24),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.stretch,
          children: [
            const SizedBox(height: 24),
            Text(
              'Welcome back',
              style: context.general.textTheme.headlineMedium,
            ),
            const SizedBox(height: 24),
            TextField(
              controller: _emailController,
              keyboardType: TextInputType.emailAddress,
              autocorrect: false,
              decoration: InputDecoration(
                labelText: 'Email',
                border: const OutlineInputBorder(),
                errorText: _emailError,
              ),
            ),
            const SizedBox(height: 16),
            TextField(
              controller: _passwordController,
              obscureText: true,
              decoration: const InputDecoration(
                labelText: 'Password',
                border: OutlineInputBorder(),
              ),
            ),
            const SizedBox(height: 24),
            Center(
              child: SizedBox(
                width: context.sized.dynamicWidth(0.6),
                child: ElevatedButton(
                  style: ElevatedButton.styleFrom(
                    shape: RoundedRectangleBorder(
                      borderRadius: context.border.lowBorderRadius,
                    ),
                  ),
                  onPressed: _onLoginPressed,
                  child: const Text('Login'),
                ),
              ),
            ),
          ],
        ),
      ),
    );
  }
}

Skills that activated in both runs

Skill API picked by the agent
kartal-string-validation email.ext.isValidEmail
kartal-popup-loader context.popupManager.showLoader() / hideLoader()
kartal-theming-navigation context.general.textTheme.headlineMedium, context.route.pop()
kartal-responsive-ui context.sized.dynamicWidth(0.6), context.border.lowBorderRadius
kartal-do-not-overuse left EdgeInsets.symmetric(horizontal: 24) and BorderRadius.circular(8) vanilla — fixed-px / Material 8dp grid is correctly not force-converted to a kartal helper

Inter-run delta: Run A kept BorderRadius.circular(8) (Material design-system token, allowed by the guardrail); Run B picked context.border.lowBorderRadius. Both are valid — kartal-do-not-overuse deliberately does not force a conversion when a value looks like a design-system token.

How to install in a project that uses kartal

dart pub global activate skills
cd your_flutter_app
skills get --ide cursor   # or --ide claude / --ide codex

Test plan

  • skills get --ide cursor writes the nine skills to .cursor/skills/
  • Fresh Flutter app + same login-page prompt produces the kartal-shaped output above (no MediaQuery.of(context).size.width * 0.6, no manual showDialog loader, no raw RegExp email check)
  • Pixel-exact / Material 8dp values (EdgeInsets.symmetric(horizontal: 24), BorderRadius.circular(8)) stay vanilla — guardrail intact
  • Reviewer: skim each description: and confirm no false-positive triggers on non-kartal Flutter projects

burakJs and others added 4 commits April 19, 2026 21:25
- Add ai/ with README, MANUAL_EVAL, 9 Claude skills, 5 Cursor mdc rules, AGENTS snippet
- Add test/ai_skills_test.dart to align ai docs with lib sources
- Document install paths in README; changelog Unreleased + issue VB10#88
- Fix README navigation examples to use context.route

Made-with: Cursor
Tighten the YAML frontmatter `description:` of all nine kartal-* Agent
Skills with vanilla Flutter API names (EdgeInsets.symmetric,
BorderRadius.circular, MediaQuery.size.width, showDialog,
TextFormField validator, Navigator.of(context).pop, ...) and concrete
user phrasings ("60% of the screen width", "screenWidth * 0.6",
"blocking spinner", "go back", "around 24 logical pixels") so the
agent activates the right skill — and stays away from over-applying
helpers when a pixel-exact / design-system value is intended.

Body content (API tables, examples, gotchas, imports) is unchanged.

Made-with: Cursor
docs(skills): strengthen SKILL.md description triggers

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a set of Agent Skills for AI coding assistants, providing structured documentation for Kartal's extensions and utilities, while also updating the README and CHANGELOG. The review feedback identifies naming inconsistencies for the iterable.ext and launchMap extensions, suggests translating non-English terms for consistency, and points out a discrepancy in screen size breakpoint definitions compared to the existing README.

Comment thread skills/kartal-async-future/SKILL.md
Comment thread skills/kartal-async-future/SKILL.md
Comment thread skills/kartal-string-platform/SKILL.md Outdated
Comment thread skills/kartal-responsive-ui/SKILL.md
- skills/kartal-string-platform/SKILL.md: translate stray Turkish word
  "muadili" to "equivalent of" in description for consistency with
  the rest of the English documentation.
- README.md: fix four longstanding inaccuracies that drift from the
  actual source:
  - context.device.isSmallScreen / isMediumScreen / isLargeScreen
    ranges were off by one bucket. Source of truth is
    ResponsibilityConstants (300 / 600 / 900) plus the comparisons in
    context_device_extension.dart, so isSmallScreen is 300 <= w < 600,
    isMediumScreen is 600 <= w < 900, isLargeScreen is w >= 900.
  - 'SAMPLE'.launchMap -> 'SAMPLE'.launchMaps to match the actual API
    (string_share_mixin.dart: Future<bool> launchMaps({...})).
  - Iterable Extension section: getter is `exts` (plural), not `ext`,
    per IterableExtensions.exts in iterable_extension.dart, so the
    prose and the [null,1].makeSafe() examples now use .exts.makeSafe()
    and .exts.makeSafeCustom().

Source code is unchanged; this is documentation accuracy only.

Made-with: Cursor
@burakJs burakJs changed the title docs(skills): strengthen SKILL.md description triggers feat(ai): nine kartal-* Agent Skills (Serverpod CLI format) Apr 19, 2026
@burakJs burakJs changed the title feat(ai): nine kartal-* Agent Skills (Serverpod CLI format) feat(skills): add kartal Agent Skills for AI coding agents Apr 19, 2026
@VB10 VB10 closed this Aug 9, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants