How we use AI to Develop VisualGDB Features

When someone says “AI” or “Vibe Coding”, they usually imply a small hobby project, or, at best a landing page. But when it comes to larger projects, LLMs are known to get stuck on a wrong path, hallucinate, and overall not be very useful. Well, today I’ll show you how sticking to short focused prompts, and switching back and forth between insightful Anthropic Claude and a lightning-fast Llama 4 significantly reduced the coding effort for adding a small feature to a big C# project with a 13-year-old code base:

The feature

The feature I’ll be talking about today is a new VisualGDB window for synchronizing CMake targets to physical files. If you use CMake as a wrapper around existing projects or libraries, you would often come to a situation where some of the underlying sources got moved or deleted, and the project no longer loads. So, it would be nice to have a GUI where you could correlate the physical files in a folder  tree against what a particular CMake target references, and pick what should belong to that particular target with a mouse click or two.

The Plan

Adding this feature to VisualGDB involves looking up how VisualGDB internally manages the lists of CMake sources, picking the best GUI controls to display it, and wiring it all together. The thing is, once you figure out which classes to use and which patterns to apply, the rest of the work is pretty mundane:

  • Take a list of existing references from a target, represent it as a tree
  • Take a list of existing files from the file system, also represent it as a tree
  • Merge the trees into some kind of combined tree where each node tracks whether:
    • It still physically exists on the disk
    • It is still referenced by the target
  • Use some WPF controls to show it as a nice tree with filters
  • Export any changes back to the CMake target

If you ask AI to do the entire thing from the get go, it will burn through millions of tokens trying to make sense of the huge VisualGDB code base, will fill up the context window with mostly irrelevant parts, and will end up with code that won’t probably even build. On the other hand, tasks like “take this specific list of items and convert it into a tree of objects tracking x, y and z” are extremely straight-forward and don’t the knowledge of our entire code base to go ahead. So, let’s delegate them to the AI and see what happens.

Part I. Creating the Classes.

This part is way better done by hand and with custom templates. We have an in-house system for defining sheets of Visual Studio commands, and a set of WPF templates for new windows, so asking the AI to figure it out would be a waste of time. It mainly boils down to writing a handful of lines in the CMake target class:

public void SynchronizeFileList(CMakeExtendedCodeModel.Target target)
{
    var ctx = BeginEditingSourceList(ref target, out var sourceList);
    VisualGDBHost.GUIService.ShowModelBasedWindow(new CMakeSourceSynchronizationDialog.ModelImpl(sourceList, target.SourceDirectory));
}

In the newly created window XAML:

<TextControls:BasicFilterBox Text="{Binding Path=Tree.TextFilter.Text}" HorizontalAlignment="Right" Width="200"/>
<TreeControls:TreeListBox Tree="{Binding Tree}" Grid.Row="1" Margin="{StaticResource BasicMargin}">
    <DataTemplate>
        <TextBlock Text="{Binding}"/>
    </DataTemplate>
</TreeControls:TreeListBox>

And in the model for it:

public class ModelImpl : NotifyPropertyChangedImpl, IModalViewModelWithResult<bool>
{
    public PresentableTreeWithFilters<NodeBase> Tree { get; } = new PresentableTreeWithFilters<NodeBase>();

    public ModelImpl(AdvancedBuildSystemProperties.IListProperty sourceList, string baseDirectory)
    {
        _SourceList = sourceList;
        _BaseDirectory = baseDirectory;
    }

    public class NodeBase : PresentableTreeNode<NodeBase>
    {
        public NodeBase(string name)
            : base(name)
        {
        }

        protected override void LoadInitialChildren(IPresentableTreeNodeChildren<NodeBase> targetCollection)
        {
            base.LoadInitialChildren(targetCollection);
        }
    }
}

Overall, about 30 lines written by hand (a lot was generated by Visual Studio). The new window now has all the main parts, but is completely empty:

Part II. Wiring in the Data

Now the fun part begins. We need to take a list of tokens (like file.c or subdir/file.h) from the _SourceList property, and the list of files in the actual directory, and combine it into a tree of nodes, with separate nodes for folders and sources. Because PresentableTreeNode‘s child collection is not sorted and does not have a quick lookup, we would need to maintain our own dictionary of nodes while building them, and then sort and export them to the GUI. There are cleaner ways to do it (something like a ConstructedNodeCollection that would produce actual nodes after processing all inputs), but since we won’t be dealing with more than 10K nodes, a simple redundant dictionary is just fine.

WARNING: Never leave such decisions to the AI. It won’t stop, it won’t hesitate. It will make a split-second decision based on the last similar construct it encountered in the prompt, and will stick to it. You can use it to prototype and compare different approaches, but make sure you make the final call!

So, let’s ask the AI do implement it. I am using CodeVROOM that was specifically designed for huge code bases, so I just needed to click on the edit link above ModelImpl and add a fairly concise prompt:

Derive FolderNode and FileNode from NodeBase.
NodeBase should have a dict<string, NodeBase> _ChildrenByName exported into LoadInitialChildren (order by name case-insensitive)
Also add public FileNode ProvideFileNode(string[] components, int level) to NodeBase, recursively creating folders for non-last components and file for the last one.

Since the prompt was very straight-forward, most models handled it just fine. Anthropic Claude took the most time (but took 26 seconds and 1.5 cents worth of tokens), and produced the most verbose results, covering all cases:

public FileNode ProvideFileNode(string[] components, int level)
{
    if (level >= components.Length)
        return null;

    string name = components[level];

    if (level == components.Length - 1)
    {
        // This is the last component - create a file node
        if (!_ChildrenByName.TryGetValue(name, out NodeBase node))
        {
            var fileNode = new FileNode(name);
            _ChildrenByName[name] = fileNode;
            return fileNode;
        }

        if (node is FileNode fileNode)
            return fileNode;

        // Name conflict - a folder with the same name exists
        return null;
    }
    else
    {
        // This is not the last component - create a folder node
        if (!_ChildrenByName.TryGetValue(name, out NodeBase node))
        {
            var folderNode = new FolderNode(name);
            _ChildrenByName[name] = folderNode;
            return folderNode.ProvideFileNode(components, level + 1);
        }

        if (node is FolderNode folderNode)
            return folderNode.ProvideFileNode(components, level + 1);

        // Name conflict - a file with the same name exists
        return null;
    }
}

Note that the prompt was very ambiguous. E.g. it didn’t say what to do if there’s a conflict between files/folders. So using a bigger model like Claude produced noticeably better results. In either case, it was way faster to just give a brief prompt and review the results, than to manually handle each of the cases.

Now that we can create nodes, let’s go through all of the tokens in the sourceList and create a file node for each one. Again, very concise prompt:

For each token from sourceList, split path via either / or \, create file node via ProvideFileNode() and attach the token to it via ?.AttachCMakeToken().
Use var topLevelFolder = new(baseDir) as root & add it to tree.

To do this task, the model will need know the definitions of the property and token classes. These are scattrered around different files and sending them all to the model would quickly overflow the context window (remember, huge projects!), so we’ll use the CodeVROOM’s symbol-level slicing to just include what is needed:

This time, let’s use a smaller model (Llama 4 Scout) to show some mistakes it makes, and how to recover from them:

var topLevelFolder = new FolderNode(baseDirectory) as NodeBase;
Tree.Nodes.Add(topLevelFolder);

foreach (var token in sourceList.AllTokens)
{
    string path = token.Value;
    string[] components = path.Replace('\\', '/').Split('/');

    var currentNode = topLevelFolder;
    for (int i = 0; i < components.Length; i++)
    {
        var fileNode = currentNode.ProvideFileNode(components, i);
        if (fileNode != null)
        {
            fileNode.AttachCMakeToken(token);
            currentNode = fileNode;
        }
    else
    {
        break;
    }
}

Here the model assumed that the next token continues the path of the previous token. E.g. if the CMake source list contains file1.cpp and file2.cpp, it would create file1.cpp and file1.cpp/file2.cpp nodes. Bigger models like Claude or GPT would make this call better, but good luck beating the 0.5-second response time. Either way, writing “no currentNode” in the prompt window and hitting Ctrl-Enter gives you a working version in another 0.5 seconds:

foreach (var token in sourceList.AllTokens)
{
    string path = token.Value;
    string[] components = path.Replace('\\', '/').Split(new[] { '/' }, StringSplitOptions.RemoveEmptyEntries);

    var fileNode = topLevelFolder.ProvideFileNode(components, 0);
    if (fileNode != null)
    {
        fileNode.AttachCMakeToken(token);
    }
}

AI isn’t a sliver bullet. It doesn’t produce perfect code. Like it introduced a bug where adding topLevelFolder to the tree before creating its children would fill the sorted collection (without any children) giving you a nice empty tree. But I would have made the same mistake when writing it by hand, and would have found and fixed it just fine on first test run.  What the AI did is “decompressed” a lazily written concise prompt into a straight-forward code that is easy to review, debug and patch. Kind of like the compiler that turns high-level code into processor-friendly assembly so I don’t need to think how many registers my CPU has.

So, two prompts and one correction in, we have a basic tree control showing real files from a real CMake target:

Part III. Applying techniques.

The next step would be to add distinct folder and file icons to nodes. VisualGDB codebase has a reusable class for it called RuleBasedImage. You bind it to an arbitrary enum property and give rules how each enum value should get translated to an icon. So, adding icons here would involve:

  1. Creating an enum like NodeIconType
  2. Returning different types from file/folder nodes
  3. Creating a rule-based image in the XAML
  4. Binding icon values to specific icons

Does it have to be done by hand? Nope. Here’s the prompt:

add enum NodeIconType{File, Folder} next to NodeBase and public Icon{get;} to NodeBase. Initialize via base ctor. Also add bool? IsChecked to base.
Update data template to be a grid with Auto/Auto/* containing: checkbox, rule-based image per @rulebasedimg, current textblock.

Note the @rulebasedimg tag. Because we use the rule-based images all over the code, I took my time and wrote the following AI technique which is basically a block of text that gets appended to the prompt if the prompt mentions @rulebasedimg:

Rule-based images are a special WPF/Avalonia controls that bind to an enum value and shows an icon that corresponds to it. The mappings between values and icons are defined via the EnumRuleCollection, e.g.:

```
<RuleBasedControls:RuleBasedImage Value="{Binding State}" Width="16" Height="16">
<RuleBasedControls:EnumRuleCollection>
<RuleBasedControls:PresentationRule Value="Confirmed" Image="pack://application:,,,/SysprogsPortableGUI;component/Icons/16x16/Elements/checkmark.png"/>
</RuleBasedControls:EnumRuleCollection>
</RuleBasedControls:RuleBasedImage>
```

This binds to the 'State' property of some enum type and displays the checkmark icon if the state is 'Confirmed'.

If you are asked to create a new image for an existing enum, look through all enum values and create a presentation rule for each one. Don't try to guess the image path, just write Image="TODO".

CodeVROOM suggests them whenever you press ‘@’ in the prompt editor and lets you manage them hierarchically per user or per-project:

Once the technique block makes its way into the prompt as a separate markdown-highlighted section, the model usually knows what to do. So another 0.5 seconds later (+replacing TODOs with actual icon paths), we’ve got icons:

Part IV. Removing files.

The next step is removing the files from CMake targets whenever a file node is unchecked. But, it needs some attention to detail. Clicking on a file should update the check state of the parent folder. Clicking on a folder should update the containing files. Files/folders should distinguish between user clicks (propagated up and down) and programmatic updates (propagated up). Similar logic already exists in the GUI for editing the AI techinques, so can the AI just port the relevant part? Here’s the prompt:

Port logic for VisibleCheckState from SysprogsAICodingEngine.GUI.Models to IsChecked. Copy all related methods.
Folder nodes should compute visibility based on children. File nodes should return it as true when a token is attached.
Use ComputeInitialCheckState and call it after the nodes got created.

Llama 4 (Cerebras) got compilable code in less than a second, but totally missed the difference between backward and forward propagation.  If I explicitly explained it in the prompt, it would probably do it just fine, but I just stepped the session back in CodeVROOM, switched the model to Claude 3.7, and in 40 seconds it ported the logic just fine. Well, it hallucinated the Parent property in the tree node, but a brief nudge (“pass Parent via ctor”) got it working. BTW, I switched the model back to Llama for that edit, so it was instant again.

I then added a literal handful of lines by hand, and it just started working:

if (args.CommonButton == CommonBarButton.OK)
{
    foreach (var node in Tree.Nodes.GetAllNodesRecursively().OfType<FileNode>())
        node.CommitChanges();
}

//...

public void CommitChanges()
{
    if (IsChecked == false && _Token != null)
        _Token.Value = null;
}

Part V. Adding files.

So, we now need to get all files in the base directory, compute relative paths and create unchecked nodes for them. Also, should probably show a warning icon for referenced files that are physically missing. Llama can do it just fine:

for all *.c;*.cpp;*.cc files in baseDirectory (including subdirs) do:

compute relative path via @GetRelativePath , split it, provide node, do AttachPhysicalFile(string fullPath).
NodeBase should have public string WarningText, overridden by file node: if has token, but no string _PhysicalPath, return "Missing {token.Value}".
extensions should be string[] extensions; do EndsWith(insensitive) for each file

and a nudge:

no set on WarningText, compute dynamically, default null

This nicely routes the physical FS files into the tree:

Note that the AI still has no clue that the top-level node should be added to the list AFTER attaching all contents to it. But that’s won’t convince me to do the rest of the code by hand. I can move one method call, alright.

Actually adding files to the CMake statement involves gathering the names of new files from checked nodes without attached tokens, and adding them to the _SourceList:

foreach(var file in newFiles)
    _SourceList.AddToken(null, StatementPlacementDirection.After, PortablePath.GetRelativePath(_BaseDirectory, file).Replace('\\', '/'));

If I really wanted it, I could probably convince either model to do it, but writing this part by hand is just faster.

Key Takeaways

It took 5 prompts of about ~1.6KB together to gradually generate ~11KB of code. If you naively counted just the number of characters, you would get a 7x performance boost. But the reality is more nuanced. Just like it helps to not think about CPU registers and iterator structure and just write foreach(), it helps to not carve out each line of straight-forward code by hand. There is no benefit it manually changing 7 lines of code rather than writing “route parent through ctors” and waiting half of a second. AI does make mistakes, but if you keep your prompts focused, and verify the intermediate results, you can quickly get to a point where it’s just faster and less distracting than manual coding.

Also, if you haven’t done it yet, check out CodeVROOM. It’s our new cross-platform AI editor specifically designed for focused edits to huge projects. It is still early in development, but can already save you time and focus on many straight-forward tasks that you would hate doing by hand. Same AI-powered edit engine is coming to VisualGDB soon as well. Enjoy!