Sr. Content Developer at Microsoft, working remotely in PA, TechBash conference organizer, former Microsoft MVP, Husband, Dad and Geek.
156765 stories
·
33 followers

Learning faster with Antigravity

1 Share
Dash enjoying Antigravity

Creating Flutter frontends for ADK

How can I build a Flutter frontend for an agent when that agent is built with an SDK and a language I’ve never used before?

This was the challenge I faced when approaching a Python-based agent written with Agent Development Kit (ADK). With limited experience in Python and no prior exposure to the ADK framework, building a client that integrates with the backend server presented a significant learning curve. Plus, even if I could get a coding agent to crank out something that worked, finishing the project without understanding the code was also a form of failure.

After a few false starts, though, I found an answer. Using a structured, iterative workflow with my AI coding partner, Antigravity, I created a reusable developer skill that codified what I learned with each go-round. I started from scratch, generated notes about the code, created multiple apps that connected to the deep_search agent (a multi-agent research coordinator from the official ADK samples repository), and incrementally built up the skill and my own understanding. At times, I was using multiple agents at the same time, an “author” agent to create the skill with me, and a “coder” agent to use that guidance to build frontends.

What I ended up with was an agent skill called flutter_frontend_for_adk. It includes five reference docs that guide Antigravity through a sequence of phases, each one ending in a deliverable. The first phases generated the following notes files, so that I could structure how the “coder” agent thought about the task as it analyzed the agent and prepared to generate the app:

  • AGENT_INTERFACE_NOTES.md — Notes taken during an analysis of the agent’s source code. What is it meant to accomplish and how is it constructed? What are the interfaces and APIs this agent exposes and how do they work?
  • FRONTEND_USAGE_NOTES.md — The first spec. What should the frontend do and how should users interact with it?
  • FRONTEND_ARCHITECTURE_NOTES.md — An architecture plan. What services, classes, state, and models should be created?
  • FRONTEND_DESIGN_NOTES.md — A design document for the frontend. What will it look like? What are the colors, fonts, and other details?

After that, Antigravity could generate the application’s code, run, and test it. While the skill (and I) remain a work in progress, I’ve now got a decent handle on ADK, as well as a useful way to build frontends for existing agents.

This is how it happened.

Screenshot of a generated frontend for the deep_search agent

Start with the work of others

I’m not the only one writing skills around here, so the first step was to install some skills from the Google Cloud and Flutter teams:

npx skills add google/agents-cli --skill google-agents-cli-adk-code
npx skills add flutter/skills

Those got me some basic smarts about ADK, its CLI tool, and Flutter. I also had the Dart MCP server installed, thanks to the Dart extension for Antigravity.

Establish the loop

Rather than begin with a request for the agent to blast out an app, I established a kind of “learning loop” with it. First, I asked the coder agent to execute the workflow from my skill, and then I worked with the author agent to manually evaluate the coder’s output, identify gaps in the skill’s instructions, and update the guidelines to improve future runs. After each iteration I deleted the generated notes and app, started over, and discovered the next thing I needed to learn.

Behold my loop!

The loop consists of five distinct steps driven by conversations with Antigravity:

1) Execute the current skill: In a fresh conversation, the coder agent runs the workflow defined by the current version of the skill file.

My prompt: This codebase includes an agent built with ADK, but no frontend or client. I’d like you to read the instructions in flutter-frontend-for-adk/SKILL.md and follow the workflow. Do not examine git history, do not reference the context of other conversations we’ve had, and do not change git branches.

2) Evaluate output and specifications: In a second conversation, the author agent and I inspect the generated deliverables, such as architectural notes, design specifications, and the resulting code structure. I was better at noticing structural issues, while Antigravity helped catch all the little stuff.

My prompt: While I review the notes and code, take a look at the generated artifacts and tell me how well you think they match the instructions and our goals.

3) Identify gaps and failure modes: We identify areas where the coder agent lacked specific guidance, made incorrect assumptions, or encountered integration issues.

My prompt: I noticed two issues in the last run: first, the root gitignore is ignoring the new frontend/lib/ folder due to a global ‘lib/’ ignore rule. Second, the coder agent is executing all six phases continuously without stopping. We need a way to pause and verify the deliverables after each stage.

4) Update central skill and references: We update the instructions in SKILL.md or the sub-guides in the references/ directory to address the gaps.

My prompt: Let’s update Phase 6 in SKILL.md to explicitly handle the gitignore issue by adding ‘!frontend/lib’ if ‘lib/’ is ignored globally. Also, add a mandatory ‘Review’ step at the end of Phases 1 to 4 instructing the agent to pause and wait for my approval before proceeding.

5) Clear local files and rerun: I delete temporary specification files, reset the git working tree, and head back to the top of the loop.

While working with a pair of agents definitely sped things up, it didn’t make the process instantaneous. I can only absorb so much information at a time! Within each iteration, though, I read through notes and source files, reviewed the conversation with Antigravity, and (once I was generating real code) ran the app to see how it performed.

At the start, I was mostly reading and learning. By the end, though, I knew enough about what was going on to zero in on things that were failing consistently, start doing my own research, and tweak code to fix issues. After I was done modifying the notes and codebase, I’d ask Antigravity how it could update the skill to make future code generation look more like what I’d put together. As a result, the skill wasn’t just instructions for the coder agent, but a record of what I was learning.

Loop the loop

It took thirteen iterations to get me to something that I’m not embarrassed to share. Here are the first six, which were mostly about discovery and note-taking:

  1. Bootstrapping and workspace mapping — I realized that a single, monolithic SKILL.md file was too hard for the agent to navigate. I refactored the skill to be modular, creating a dedicated references/ folder and writing the first specialized guide: references/agent_discovery.md. That got Antigravity through analyzing the agent’s source and creating AGENT_INTERFACE_NOTES.md to record what it learned.
  2. Defining behavior and user experience — Next up was the usage spec for the frontend: What would it do? How would a user interact with it? I added a new phase to the skill, instructed it to read references/frontend_usage.md and interview me for platform preferences and feature requirements. This got me FRONTEND_USAGE_NOTES.md, which defined the app’s purpose and feature set before we ever touched the code.
  3. Setting the structural blueprint — Architecture. We added references/frontend_architecture.md to the skill, which focused on architectural details that the frontend would need, and I got the agent to produce FRONTEND_ARCHITECTURE_NOTES.md.
  4. Designing the visual aesthetics — We created references/frontend_design.md to instruct the agent on how to define visual aesthetics and added scaffolding instructions to SKILL.md. This guide directed the agent to interview me for theme, breakpoint, and animation preferences, and Antigravity started producing FRONTEND_DESIGN_NOTES.md.
  5. Generating and running an app — At this point, I was able to tell Antigravity to start generating the actual flutter code based on all the specifications we had gathered. The app failed pretty spectacularly, and I realized some kind of general “best practices” file was needed to get it over problems that were likely to reappear. We added references/frontend_best_practices.md to the skill files to guide the actual coding, and started fixing things one at a time.

From here on out, I continued iterating with Antigravity as I tackled one issue at a time, updating the code, updating the skill to match, and then wiping out the generated frontend to try again. In the process, the loop got tighter and the changes smaller. I did seven more iterations:

  1. The app can’t make network calls! — Fixed the entitlements and info for macOS and iOS.
  2. All this markdown isn’t getting formatted! — Took advantage of flutter_markdown to properly display text from the agent, which often contains markdown.
  3. Why doesn’t the code look right ?— Added lints, formatting rules, and other post-generation checks.
  4. Sealed classes! — Different message types should share a base type so the app can use exhaustive switch statements.
  5. Why aren’t the chats scrolling? — Added a ScrollController to automatically advance lists of messages when new ones arrive.
  6. Why did the app crash when I ran it on the web? — Removed dart:io and relied on package:http for networking.
  7. Partial events in my list! — ADK sends a stream of events, sometimes in pieces. The frontend needed to assemble and present them coherently rather than give the user a list of all the partials (with broken markdown formatting).
  8. Why is the tool window blank? — As generated, the apps weren’t dealing with tool naming conventions correctly, and the event handling wasn’t correctly identifying when tools were being invoked.

An example “thing that went wrong”

To give you an idea of the work done in each of the iterations, let’s walk through one of the changes: how the frontend handles partial events.

The ADK server streams events representing small chunks of content or partial tool execution stages. If the client application renders every incoming network event as an individual message bubble in the chat list, though, the interface becomes fragmented. A single response from the agent would appear as dozens of isolated, broken text blocks.

The old way (a naive list):

// Rendering every raw event chunk in a list view
ListView.builder(
itemCount: rawEvents.length,
itemBuilder: (context, index) {
// Each partial text chunk is rendered in a separate bubble
return ChatBubble(text: rawEvents[index].contentText);
},
)

This code leads to UI that looks like this:

Look at all those broken list items and half-bolded segments!

The fix for this isn’t particularly hard, once you know the right thing to do. I didn’t, of course, so I started asking Antigravity questions like “How does ADK use SSE to stream events?” and “What does the over-the-wire data structure for an event look like, and how do I know if it’s a partial event?” If “vibe learning” were a thing, I was doing it.

It turns out that you just need to check a flag, so I tweaked the code in the frontend’s AgentProvider class, asked Antigravity to review it in case I’d forgotten anything obvious, and ran the app to verify. It took one more request to Antigravity to update the best practices so that every new frontend would use this approach.

The new way (aggregation in AgentProvider):

await for (final event in stream) {
if (event.partial && event.contentText != null) {
// Accumulate streaming text
_activeStreamingAuthor = event.author;
_activeStreamingResponse =
(_activeStreamingResponse ?? ‘’) + event.contentText!;
} else {
// Finished chunk: Clear accumulator and add to permanent events list
_activeStreamingResponse = null;

final updatedEvents = List<Event>.from(_activeSession!.events)
..add(event);

// Merge state delta
Map<String, dynamic> mergedState = _extractSessionStateMap(
_activeSession!,
);
if (event.actions.stateDelta.isNotEmpty) {
mergedState.addAll(event.actions.stateDelta);
}

// Build a new copy of the Session with updated events & state
_activeSession = Session.fromJson({
'id': _activeSession!.id,
'app_name': _activeSession!.appName,
'user_id': _activeSession!.userId,
'events': updatedEvents.map((ev) => _serializeEvent(ev)).toList(),
'state': mergedState,
});

_processCompletedEvent(event);
}

notifyListeners();
}

Try it yourself

I’m very much a “learn by doing” person, and trying something entirely outside my comfort zone (like both a new language and a new SDK) was pretty intimidating. Having this loop, though, gave me a structure to work off of, which was really helpful, and now I have a neat new agent skill.

If you haven’t already, download Antigravity yourself and give it a try — it only took me a few hours, and I definitely picked up some new tricks.

To get started:

  1. Download Antigravity to integrate the agentic coding assistant into your development environment.
  2. Visit the AGY getting started documentation to learn how to define your own custom developer skills and reference guidelines.
  3. Draft a simple skill for a framework you want to explore, and keep looping until you get something you like!

Learning faster with Antigravity was originally published in Flutter on Medium, where people are continuing the conversation by highlighting and responding to this story.

Read the whole story
alvinashcraft
16 seconds ago
reply
Pennsylvania, USA
Share this story
Delete

AI Challenges in Software Development

1 Share

AI didn't just speed up the development of Basecamp 5, it changed how it was built. This week, Jason Fried and David Heinemeier Hansson break down how much of a role AI played in building their latest product, where it shines, and how 37signals had to adjust its process to keep their code base clean.

Key Takeaways

  • 00:11 – How much of a role AI played in building Basecamp 5
  • 09:55 – Where the intelligence technology truly excels
  • 14:42 – The new challenge of saying no when features become easier to build
  • 16:19 – Why a leaner product roadmap still matters
  • 18:32 – Staying "easy to use" while competitors focus on AI features
  • 20:44 – Why AI deserves both the hype and the skepticism
  • 25:12 – Token spend, productivity, and staying profitable

Links and Resources





Download audio: https://www.buzzsprout.com/2260539/episodes/19428938-ai-challenges-in-software-development.mp3
Read the whole story
alvinashcraft
25 seconds ago
reply
Pennsylvania, USA
Share this story
Delete

Giving GitHub Copilot a Voice with Copilot Avatar

1 Share
From: Coding After Work
Duration: 9:35
Views: 6

GitHub Copilot is powerful, but most AI coding tools still feel like a prompt box. In this video, I show GitHub Copilot Avatar, a project I built to make Copilot feel more like a colleague by giving it a voice, a face, status indicators, and visual cues.

Copilot Avatar uses GitHub Copilot extensions to listen to what Copilot is doing and react when there is something useful to tell the user. Instead of reading every line of output, it focuses on the important feedback and can speak it using different text-to-speech providers like Web Speech, ElevenLabs, Deepgram, SAM, and more.

We also look at transparent window mode, badges, Copilot status, Clippy support, sub agents, Squad integration, and how the avatar can show when Copilot is idle, thinking, speaking, or done. The goal is simple: make GitHub Copilot feel less like a terminal tool and more like an AI coding assistant that is actually working with you.

Try Copilot Avatar, play around with it, and let me know what you think in the comments. What should I add next?

https://github.com/EngstromJimmy/copilot-avatar

Read the whole story
alvinashcraft
34 seconds ago
reply
Pennsylvania, USA
Share this story
Delete

AI-Accelerated Supply Chain Attacks with Mackenzie Jackson

1 Share

How are supply-chain attacks evolving? Richard chats with Mackenzie Jackson about his work helping companies protect their software supply chains from malware attacks. Mackenzie discusses the vulnerability of developers to attacks, since their accounts are often highly privileged and invariably contain access to exploitable secrets. The conversation digs into the challenges of securing various code distribution mechanisms like npm and how you can protect your organization - starting with, don't install packages as soon as they are released! There are effective tools for detecting malware in code, but they take time. Waiting 48 hours can eliminate a lot of risk!

Links

Recorded June 15, 2026





Download audio: https://cdn.simplecast.com/media/audio/transcoded/5379899c-61c5-43c3-aa3f-1128cffd9ef4/c2165e35-09c6-4ae8-b29e-2d26dad5aece/episodes/audio/group/cb7d56c1-f8a6-4f21-b795-e65e01941515/group-item/19a3ba7b-607f-4f78-abde-c3cbc775195c/128_default_tc.mp3?aid=rss_feed&feed=cRTTfxcT
Read the whole story
alvinashcraft
41 seconds ago
reply
Pennsylvania, USA
Share this story
Delete

Building a Windows Tray App by combining Microsoft.UI.Reactor and a Worker Project

1 Share

I wanted a small Windows app that lives in the tray instead of the taskbar, and minimizes/closes to the tray. This can be useful for building small service apps, while having a UI you can occasionally access to control settings etc.

Here is the full process for setting that up step by step.

1. Start with a Worker project

I started with the standard worker template. In a new project folder, create a worker project using the worker template:

dotnet new worker

I liked the Worker template for this because a tray app is really a background app first. The process needs to stay alive even when the window is hidden.

2. Retarget the project for Windows

The next step was changing the target framework in the project file - we need this to be compatible with WinUI/Reactor and gives access to the APIs needed for windowing and tray behavior. Change the target framework in the project file to:

<TargetFramework>net10.0-windows10.0.22621.0</TargetFramework>

3. Add the UI packages

After that I added the packages the app needs:

dotnet package add Microsoft.UI.Reactor --prerelease
dotnet package add WinUIEx

Microsoft.UI.Reactor gave me a clean C# way to define the window UI. WinUIEx helps with tray icon support and some of the window behavior.

4. Add the Windows app properties

Then I added a few properties in the project file needed to compile WinUI and Reactor apps:

<WindowsAppSDKSelfContained>true</WindowsAppSDKSelfContained>
<RuntimeIdentifiers>win-x64;win-arm64</RuntimeIdentifiers>
<Platforms>ARM64;X64</Platforms>

5. Add the tray icon asset

The app needs a real .ico file for the tray. I added trayicon.ico file to the project and marked it as content:

<Content Include="trayicon.ico" />

6. Keep the app entry point simple

Program.cs stays very small:

var builder = Host.CreateApplicationBuilder(args);
builder.Services.AddHostedService<Worker>();
var host = builder.Build();
host.Run();

7. Create the Settings Window:

Create a new SettingsWindow.cs file with the UI. We'll just use the simple Reactor sample window here as a starting point:

using Microsoft.UI.Reactor;
using Microsoft.UI.Reactor.Core;
using Microsoft.UI.Xaml.Controls;
using WinUIEx;
using static Microsoft.UI.Reactor.Factories;

namespace ReactorTrayWorker;

internal sealed class SettingsWindow : Component
{
    public override Element Render()
    {
        var (name, setName) = UseState("World");

        return 
            VStack(
            Heading($"Hello, {name}!"),
            TextBox(name, setName, placeholderText: "Your name")
                .AutomationName("NameInput")
        ).Padding(16);
    }
}

You of course want to change this to whatever content you want here.

8. Create a static UI Window creator for Reactor

Create an initialize method for WinUI/Reactor that takes an action for quitting (we'll use that later):

internal static void InitializeWinUIWindow(Action stopHost)
{
    ReactorApp.Run(_ =>
    {
        ReactorWindow? settingsWindow = null;
        settingsWindow = ReactorApp.OpenWindow(
            new WindowSpec
            {
                Title = "My Worker Settings",
                Width = 560,
                Height = 460,
                ActivateOnOpen = false,
            },
            () => new SettingsWindow(),
            configure: host =>
            {
                WindowManager manager = WindowManager.Get(host.Window);
                manager.WindowStateChanged += (_, state) =>
                {
                    // If the window is minimized, we don't want it to show in the Alt+Tab switcher + taskbar, so we set IsShownInSwitchers to false.
                    bool isVisible = state != WinUIEx.WindowState.Minimized;
                    manager.AppWindow.IsShownInSwitchers = isVisible;
                };
            });
    });
}

The app also watches WindowStateChanged through WindowManager.

When the window is minimized, the app updates IsShownInSwitchers so it does not stay visible in Alt+Tab or the taskbar. When it is restored, that visibility can come back.

This keeps the behavior consistent. Close hides it. Minimize hides it. The tray icon is the main way back in.

Inside Worker.cs, the background service starts the window setup by calling this method:

SettingsWindow.InitializeWinUIWindow(hostApplicationLifetime.StopApplication);

That shutdown callback matters. The tray menu needs a clean way to stop the whole host when the user actually wants to quit, and we'll use it in the Quit context menu further down. Also note that this call is blocking as long as the Reactor app runs, so wrap that call in a Task.Run you don't await.

9. Create the tray icon during window setup

Inside the window host configuration, let's add a TrayIcon that points to trayicon.ico:

string executablePath = new FileInfo(Assembly.GetExecutingAssembly().Location).DirectoryName!;
TrayIcon icon = new(0, Path.Combine(executablePath, @"trayicon.ico"), "My Worker") {
    IsVisible = true
};

At that point the app is available from the tray even if the window is hidden, and we just need to hook clicking it up to opening the window. The tray icon handles the Selected event. When the user clicks the tray icon, the app activates the main window, brings it to the front, and makes sure it can appear in the switcher again. Here I'm declaring a static action that I'll reuse for the context menu later as well.

var showWindow = static() => {
    ReactorApp.PrimaryWindow?.Activate(); // Activate the window
    ReactorApp.PrimaryWindow?.NativeWindow.SetForegroundWindow(); // Bring to front
    ReactorApp.PrimaryWindow?.AppWindow.IsShownInSwitchers = true; //Show in switcher
};
icon.Selected += (_, _) => showWindow();

 

10. Add the tray menu

The tray icon also handles the ContextMenu (right-click) event. We'll add two actions:

  1. Open
  2. Quit

Open brings the settings window back and does the same as left-click. Quit is the only path that fully exits the app and shuts down the worker.

icon.ContextMenu += (_, e) =>
{
    MenuFlyout flyout = new();
    flyout.Items.Add(new MenuFlyoutItem() { Text = "Open" });
    ((MenuFlyoutItem)flyout.Items[0]).Click += (_, _) => showWindow();
    flyout.Items.Add(new MenuFlyoutItem() { Text = "Quit" });
    ((MenuFlyoutItem)flyout.Items[1]).Click += (_, _) =>
    {
        isQuitting = true;
        ReactorApp.PrimaryWindow?.Close();
        icon.Dispose();
        stopHost.Invoke(); // Shuts down the worker thread
    };
    e.Flyout = flyout;
};

This is the core behavior of the whole project. The window is not supposed to control app lifetime like a normal WinUI app does. The tray icon does that. So to prevent uses from exiting the app, we'll handle the closing event as well:

host.Window.AppWindow.Closing += (_, e) =>
{
    // Prevent closing out the window and just hide to tray instead unless we're quitting
    e.Cancel = !isQuitting;
    settingsWindow?.Hide();
};

So now clicking the X button does not shut down the app. It just sends the window back to the tray, unless the quitting flag was set from the Quit context menu.

Here's what the entire initalize method now looks like:

internal static void InitializeWinUIWindow(Action stopHost)
{
    ReactorApp.Run(_ =>
    {
        bool isQuitting = false;
        ReactorWindow? settingsWindow = null;
        settingsWindow = ReactorApp.OpenWindow(
            new WindowSpec
            {
                Title = "My Worker Settings",
                Width = 560, Height = 460,
                ActivateOnOpen = false,
            },
            () => new SettingsWindow(),
            configure: host =>
            {
                // Configure Tray icon
                string executablePath = new FileInfo(System.Reflection.Assembly.GetExecutingAssembly().Location).DirectoryName!;
                TrayIcon icon = new(0, Path.Combine(executablePath, @"trayicon.ico"), "My Worker")
                {
                    IsVisible = true
                };
                var showWindow = static () => {
                    ReactorApp.PrimaryWindow?.Activate();
                    ReactorApp.PrimaryWindow?.NativeWindow.SetForegroundWindow();
                    ReactorApp.PrimaryWindow?.AppWindow.IsShownInSwitchers = true;
                };
                // Left click on the tray icon will show the window:
                icon.Selected += (_, _) => showWindow();

                // Context menu options:
                icon.ContextMenu += (_, e) =>
                {
                    MenuFlyout flyout = new();
                    flyout.Items.Add(new MenuFlyoutItem() { Text = "Open" });
                    ((MenuFlyoutItem)flyout.Items[0]).Click += (_, _) => showWindow();

                    flyout.Items.Add(new MenuFlyoutItem() { Text = "Quit" });
                    ((MenuFlyoutItem)flyout.Items[1]).Click += (_, _) =>
                    {
                        isQuitting = true;
                        ReactorApp.PrimaryWindow?.Close();
                        icon.Dispose();
                        stopHost.Invoke();
                    };
                    e.Flyout = flyout;
                };

                // Prevent closing out the window and just hide to tray instead unless we're quitting
                host.Window.AppWindow.Closing += (_, e) =>
                {
                    e.Cancel = !isQuitting;
                    settingsWindow?.Hide();
                };

                // If the window is minimized, we don't want it to show in the Alt+Tab switcher + taskbar,
                // so we set IsShownInSwitchers to false when minimized.
                WindowManager manager = WindowManager.Get(host.Window);
                manager.WindowStateChanged += (_, state) =>
                {
                    bool isVisible = state != WinUIEx.WindowState.Minimized;
                    manager.AppWindow.IsShownInSwitchers = isVisible;
                };
            });
    });
}

And that's it! We now have the framework for a tray-based WinUI/Reactor application.

You can find the full application code here: https://github.com/dotMorten/ReactorExperiments/tree/main/TrayApp

Read the whole story
alvinashcraft
49 seconds ago
reply
Pennsylvania, USA
Share this story
Delete

Visual Studio Code 1.128 (Insiders)

1 Share

Learn what's new in Visual Studio Code 1.128 (Insiders)

Read the full article

Read the whole story
alvinashcraft
1 minute ago
reply
Pennsylvania, USA
Share this story
Delete
Next Page of Stories