I vibe coded and App and don't know how it works (yet!)

Introduction

I recently decided I needed to write an new “Launcher” app for our linux build to match what we have on windows (AppsAnywhere) this would make it easier for students who are new to Linux and make it easier to see what is actually installed on the build which is not always obvious.

As an experience developer I had a number of tools I could have used to build this, however I also want to learn some new api as part of the development so I can use them in teaching next year. To this end I decided to use PySide6 (which I already use) but instead of using QWidgets I decided to use QML / QtQuick instead.

I started by doing a few of the excellent tutorials at the Qt Academy but learning by video is not really my thing and I wanted to just dive into the code.

Using Vibes

Recently there has been lots of talk about “vibe coding” which according to wikipedia is

is an approach to producing software by using artificial intelligence (AI), where a person describes a problem in a few natural language sentences as a prompt to a large language model (LLM) tuned for coding. The LLM generates software based on the description, shifting the programmer’s role from manual coding to guiding, testing, and refining the AI-generated source code.

This also seems to be an approach used by many students over the last few year so I thought I would give it a go, after all what could go wrong?

Setup

Whilst the target for the app is our Lab Linux build (RHEL 9.5) I typically do all of my development on my Mac and remote into my Linux Machine when needed. The setup I am going to use is the excellent zed editor. I will use uv for all my python setup as this is now my core python workflow.

For the AI / LLM I have several options.

  1. ChatGPT mac desktop version.
  2. Github Co-Pilot Chat (GPT-4.1) as part of Github classroom
  3. Ollama installed models such as Devstall running locally on mac

I decided to mainly use co-pilot as it is integrated into zed well and I get very high usage as part of the educational package.

Getting Started

I already had a plan of what I wanted to do, and after reading some articles on vibe coding many people suggested creating a markdown document with the initial design which feeds into the context of the LLM chat.

I started with this basic outline of the the app

md AppLauncher/Design.md
This is a PySide6 App using QML for the core GUI elements. It uses an json to store the app information.

App Json format

App : [Path, Executable Name, App description, path to icons] The app will have pages of related items in tabs represented as icon buttons.
Each App will have a json entry to say which tab it belongs too and the user is able to re-arrange the items in the button window.

The user is also able to right click on the buttons and add to a new favourite tab as well.
Next I asked it to

Write a python json loader / writer with unit test for this format.

The results were ok but not really what I wanted (see chat log here) So I jumped straight in for a full app.

write a tabbed QML Pyside window which allows the user to re-arrange square buttons in each of the tabs. When pressed these tabs will launch an app loaded from the json file

The agent came back with suggestions of two files.

Here’s a basic structure for a PySide6 app using QML that meets your requirements:

    Tabs: Each tab represents a category (from the JSON).
    Square Buttons: Each app is a draggable square button in its tab.
    Rearrange: Drag-and-drop to rearrange buttons within a tab.
    Launch: Clicking a button launches the app (using subprocess).
    JSON Loading: Loads app data from your JSON file.

Below are the main files you’ll need:
main.qml click to expand
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts

ApplicationWindow {
    id: window
    visible: true
    width: 800
    height: 600
    title: "App Launcher"

    property var tabsModel: []

    TabView {
        id: tabView
        anchors.fill: parent

        Repeater {
            model: tabsModel
            Tab {
                title: modelData.tabName

                GridView {
                    id: gridView
                    anchors.fill: parent
                    cellWidth: 100
                    cellHeight: 100
                    model: modelData.apps
                    dragMargin: 20
                    interactive: true

                    delegate: Rectangle {
                        width: 90
                        height: 90
                        color: "#f0f0f0"
                        border.color: "#888"
                        radius: 8

                        Drag.active: dragArea.drag.active
                        Drag.hotSpot.x: 45
                        Drag.hotSpot.y: 45

                        Image {
                            source: icon
                            anchors.centerIn: parent
                            width: 48
                            height: 48
                        }
                        Text {
                            text: name
                            anchors.horizontalCenter: parent.horizontalCenter
                            anchors.bottom: parent.bottom
                            anchors.bottomMargin: 8
                            font.pixelSize: 14
                            elide: Text.ElideRight
                        }

                        MouseArea {
                            id: dragArea
                            anchors.fill: parent
                            drag.target: parent
                            onClicked: {
                                appLauncher.launchApp(path, execName)
                            }
                        }
                    }

                    onMove: {
                        appLauncher.rearrange(tabView.currentIndex, from, to)
                    }
                }
            }
        }
    }
}
main.py click to expand
import sys
import json
import subprocess
from PySide6.QtWidgets import QApplication
from PySide6.QtCore import QObject, Slot, Property, QUrl, Signal
from PySide6.QtQml import QQmlApplicationEngine

class AppLauncher(QObject):
    def __init__(self, apps_by_tab):
        super().__init__()
        self._apps_by_tab = apps_by_tab

    @Slot(str, str)
    def launchApp(self, path, execName):
        try:
            subprocess.Popen([f"{path}/{execName}"])
        except Exception as e:
            print(f"Failed to launch: {e}")

    @Slot(int, int, int)
    def rearrange(self, tabIndex, fromIndex, toIndex):
        # Implement rearrange logic and persist to JSON if needed
        tab = self._apps_by_tab[tabIndex]
        apps = tab['apps']
        app = apps.pop(fromIndex)
        apps.insert(toIndex, app)
        # Optionally: save to JSON here

def load_apps_json(json_path):
    with open(json_path, 'r', encoding='utf-8') as f:
        data = json.load(f)
    # Organize by tab
    tabs = {}
    for app_name, entry in data.items():
        path, exec_name, desc, icon, tab_name = entry
        if tab_name not in tabs:
            tabs[tab_name] = []
        tabs[tab_name].append({
            'name': app_name,
            'path': path,
            'execName': exec_name,
            'desc': desc,
            'icon': icon
        })
    # Convert to list of dicts for QML
    return [{'tabName': tab, 'apps': apps} for tab, apps in tabs.items()]

if __name__ == "__main__":
    app = QApplication(sys.argv)
    apps_by_tab = load_apps_json("apps.json")
    engine = QQmlApplicationEngine()
    appLauncher = AppLauncher(apps_by_tab)
    engine.rootContext().setContextProperty("tabsModel", apps_by_tab)
    engine.rootContext().setContextProperty("appLauncher", appLauncher)
    engine.load(QUrl("main.qml"))
    if not engine.rootObjects():
        sys.exit(-1)
    sys.exit(app.exec())

Wow amazing, not really. This didn’t work. First there was no apps.json file to load (simple fix as the agent to write one for me!).

From the chats you can see I have a number of exchanges just pasting in errors until it fixed it for me (for this is how you vibe code!). Eventually I had a working app framework.

app1

Analysis

The overall structure isn’t too bad and it works well, I like the fact it used the repeater in qml which really does make the code work well.

The python code is mainly boiler plate but does have a few quirks, for example it uses mixed styles for function name etc, do real type hints or docstrings (more on this later). It does however understand PySide Signals / Slots which is fundamental to a PySide app.

Next Steps

After this I made a bit of a mistake as I started a new Chat (I’ve done this a few times) whilst not really an issue in this having the chats in bigger sections I have discovered will make it work better see here. However I have also discoved adding things (just files) with no context can also give different / better result (as the agent is really only guessing!).

Add to Favourites

The next step was to allow right clicking on buttons to add this to the favourites tab. I used the promt

Add an extra favourites tab and allow the user to right click and add their own apps to the favourites tab

The Agent produced quite a lot of extra code

It broke down what to add and where to add it quite well. It also tried to explain what it was doing, but I’ve been ignoring all these explainations in true “student” fashion, so still not sure what is happening.

Save the favourites

This is the part where domain knowledge really comes into play. I use PySide / Qt a lot so I know it comes with a built in class called QSettings which provided platform specific saving and loading of settings.

I used (now in a new chat) the following prompt. (not the spelling mistakes!)

save the favourites using QSettings as user favourites. also allow the removal of favoureites

As i was in a new Chat / Context I started to write me code in C++ (this is where I learnt the valuable lesson about keeping things in a bigger theread).

You will notice from the chat there is a lot of back and forth about things not working but eventually all was good. It was also at this stage I got lazy and asked it to fully re-write the qml / python files as needed. zed has a system called agenic editing but I’ve not been using it on this projects, however it will be something to explore in the future as cut / paste / insert is a bit problematic at times, which leads me to the other “big thing to use when vibing” always commit before change!

zed has excellent github integrations so I have been using this, however I’ve been using my own commit messages for now. zed will actually generate them for you if you like (more Agent fun!).

Next I asked the Agent to

add the ability to search for an app and place the buttons matching in a view

The agent responded with some quite useful code

searchApps click to expand
@Slot(str, result='QVariantList')
    def searchApps(self, query):
        """Return a flat list of apps matching the query in their name."""
        query = query.strip().lower()
        if not query:
            return []
        matches = []
        for tab in self._apps_by_tab:
            for app in tab["apps"]:
                if query in app["name"].lower():
                    matches.append(app)
        return matches
And this section of the development worked very well, there was a little back and forth to correct errors and add features but in the end it worked.

Single File Hell!

The problem I was facing now was the QML file was getting big and becoming a bit of a pain to locate things. So I used my domain knowledge of QML and asked.

Can you partition the QML file into smaller more logical areas

I was quite impressed with the result

AppLauncher/
├── main.qml
├── SearchBar.qml
├── SearchResultsView.qml
├── AppGrid.qml
├── AppDelegate.qml

I know that PySide can use the Qt Resource System so I asked the Agent to

create a qt rcc file for the qml files

What was nice is that it decided to re-structure the project in a sensible way.

AppLauncher/
├── main.py
├── qml/
│   ├── main.qml
│   ├── SearchBar.qml
│   ├── SearchResultsView.qml
│   ├── AppGrid.qml
│   └── AppDelegate.qml
└── resources.qrc
resources.qrc click to expand
<RCC>
    <qresource prefix="/">
        <file>qml/main.qml</file>
        <file>qml/SearchBar.qml</file>
        <file>qml/SearchResultsView.qml</file>
        <file>qml/AppGrid.qml</file>
        <file>qml/AppDelegate.qml</file>
    </qresource>
</RCC>

It even showed me how to build the rcc file and use it.

pyside6-rcc resources.qrc -o resources_rc.py

Then, in your main.py, import the resource module before loading QML:

import resources_rc  # This registers the resources with Qt

And load your QML like this:

engine.load(QUrl("qrc:/qml/main.qml"))

Updates to the json

Next I was getting a bit annoyed with the json file as it was not really a full dictionary so I asked

I want it to be a proper dictionary

The Agent gave lots of suggestions and there was a lot of back and forth and tinkering as you can see in the linked chat.

I also made a small diversion to write a simple GUI to allow the editing and creating of the json files but gave up on that after a while as it was quicker and easier to edit the json file myself.

Wait something went wrong.

In the Chat3.md you will see I started a new thread to try and fix a bug I had introduced in the last change. I can’t fully remember why this happend (as I was vibing) but it took a while to get the search back working. I was also not particularly good at commit as yet so going back was not really helping either! A good lesson learnt and finally everything got fixed.

I should really see how this works.

Everything was now quite a mess, I decided to start a new context Chat4.md and get the Agent to re-factor the code and add comments / docstring where applicable.

Basically for each file I attached it to the agent then added the prompt

document this file and tidy it up a bit

This actually worked quite well. But the comments are very generic and not overly useful in the larger context.

click to expand
import QtQuick 2.15
import QtQuick.Controls 2.15

/**
 * AppDelegate.qml
 *
 * Represents a single application icon with its name, supporting launching and context menu actions.
 * Used within an app launcher grid or list.
 */

Rectangle {
    // The name of the current tab (e.g., "Favourites", "All Apps")
    property string tabName: ""
    // The app object, expected to have: name, icon, path, execName
    property var app: {}

    width: 90
    height: 90
    color: "#f0f0f0"
    border.color: "#888"
    radius: 8

    // App icon
    Image {
        source: app.icon
        anchors.centerIn: parent
        width: 48
        height: 48
    }

    // App name label
    Text {
        text: app.name
        anchors.horizontalCenter: parent.horizontalCenter
        anchors.bottom: parent.bottom
        anchors.bottomMargin: 8
        font.pixelSize: 14
        elide: Text.ElideRight
    }

    // Mouse interaction area for launching and context menu
    MouseArea {
        id: mouseArea
        anchors.fill: parent
        acceptedButtons: Qt.LeftButton | Qt.RightButton

        // Launch app on left click
        onClicked: function(mouse) {
            if (mouse.button === Qt.LeftButton) {
                appLauncher.launch_app(app.path, app.execName);
            }
        }
        // Show context menu on right click
        onPressed: function(mouse) {
            if (mouse.button === Qt.RightButton) {
                contextMenu.popup();
            }
        }
    }

    // Context menu for adding/removing favourites
    Menu {
        id: contextMenu

        MenuItem {
            text: "Add to Favourites"
            visible: tabName !== "Favourites"
            onTriggered: appLauncher.add_to_favourites(app.name)
        }
        MenuItem {
            text: "Remove from Favourites"
            visible: tabName === "Favourites"
            onTriggered: appLauncher.remove_from_favourites(app.name)
        }
    }
}

In retrospect, I need to add to the original context that all code should have appropriate comments and type hints if applicable. This would make this happen by default I hope.

Chat5.md we’re vibing now!

This is my finaly chat, and where I finally got what vibe coding is. At the top of the zed window I finally see my context length displayed (currently 35K/128K) this now means I’m actually getting good results as the Agent has lots more to work with.

I can now add / update code quite well and I do now have a full working app.

Deployment

I wanted to deploy the app as an executable rather than python. I already teach this so I again have domain knowledge. However I thought I would ask ChatGPT how to do it (but I gave the hint to use uv and PyInstaller).

It gave semi correct answers (for example using pip rather than uv add) but also some totally incorrect ones. Especially adding a deploy section to the pyproject.toml

[tool.pyinstaller]
entry = "main.py"

In the end I have written my own deploy script to make it easier to build and deploy the app. I also asked ChatGPT to create and show me how to make the app launch on startup which worked first time, and finally this.

This prompt was fun.

Can you create me a simple Icon for my Launcher app called AppsEre

Agent :-

Sure! I can create a simple launcher icon for your app AppsEre. First, a couple of quick questions to make it more tailored:
	1.	Style preference? (Flat, 3D, minimalist, playful, etc.)
	2.	Color scheme? (e.g., blue tones, dark background, vibrant colors?)
	3.	Symbol or motif? (e.g., letter “A”, rocket, toolbox, grid of apps, etc.)

Or would you like me to generate a default clean icon with the text A or AE inside a colored circle — a classic launcher look?

Let me know, or I’ll generate a clean default one right away.

I chose I think it should have an ear in it, make it 3D and vibrant colour

Would I do this Again?

This was a fun project, and I will be adding Agent based coding into my work and I think there is scope to use some of this in teaching, however the code produced is not always that good.

Main issues are

  1. I don’t fully know where everything is yet (as I’ve not been looking)
  2. It’s a little tangled and illogical in places
  3. Naming is hard, AI naming is terrible (my new mantra).

What Next

There are a couple of features I need to add for the final version

  1. add a popup optional flag for the json so I can display messages when using the installers
  2. add optional flags to the json so I can use things like the –debug flag.

I will vibe code this then take a snapshot, then I will re-factor the code and finally understand the structure etc, I will do this by writing it up as a project for the students to see (as we do in assignments).

Next
Previous

Related