Nuiitivet
AI friendly Desktop UI framework for Python.
What Nuiitivet is
Nuiitivet has two themes.
Exploring what a UI framework should be in the age of AI agents
Ask a coding agent, and the first version of an app is on screen in no time. It is rarely the version you wanted, so a back-and-forth follows.
- Refine — the back-and-forth that brings what works closer to what you want.
- Debug — the back-and-forth that fixes what behaves wrong.
Both can be done through chat, and you have probably felt how hard some of it is to put into prose.
With Nuiitivet, you drag, mark and comment on the running app itself. The agent looks at the same screen, edits, drives it, and checks. The centre of your communication with the agent moves from the chat to the app. The back-and-forth stops being a matter of explaining in prose and becomes a matter of showing each other on the screen. That is far more intuitive.
A UI framework specialised in desktop apps for Python
It sits between Tkinter and PyQt / PySide. It is more modern to write than Tkinter: a declarative widget tree, reactive state, Material Design 3. It does not aim at the large applications PyQt serves. The target is small to mid-sized apps, and what there is to learn is in proportion.
The code looks like Flet's. Flet targets many platforms, mobile and web included; Nuiitivet looks at the desktop alone.
The biggest difference is state management. Observable is modelled on
ReactiveProperty, a library widely used with WPF's MVVM: Rx operators work
directly on a reactive value. And the switch from a worker thread back to the
UI thread — where things tend to go wrong — is done by Observable for you.
OS integration and shipping an executable are there too. What is still missing is listed plainly in 5. Current limitations.
1. Building with a coding agent
Everything in this chapter turns on when you launch through the dev runner.
python -m nuiitivet.dev run app.py
A saved change shows up on the screen, on the spot.
1.1 Refine
The back-and-forth that brings what works closer to what you want. There are three things you can do on the app's own screen.
Write the prompt on the app
Some instructions are hard to put into words in chat. In an app with many widgets, where is the hard part.
Ctrl+Shift+C (Cmd+Shift+C on macOS) enters comment mode. Click a widget as
it is, or drag over an area where there is nothing to click, and write your
comment right there, on what you marked. There is no need to work out how to
say where: you point with the mouse.
When you are done, run /nuiitivet-see-comments in chat. The agent reads the
comments, changes the app as they say, and drives the app itself to check.
Below, the app gets these comments:
1: too much space, match the top
2: outlined
The first marks the gap under the month title. The second marks a day cell. The agent narrows the gap and gives the day cells an outline.
Change the layout directly
A size, an alignment, the order inside a Row: some changes are not worth an
instruction. They are all layout.
Ctrl+Shift+E (Cmd+Shift+E on macOS) enters layout edit mode. Drag the
app's own screen to change the layout. The change is written into the source
code. It costs no turn and no tokens.
Jump to the source, to check it or edit it
Even when an agent writes the code, human code review does not go to zero. And a senior engineer is sometimes faster writing the code than instructing an agent.
Ctrl+Shift+Click (Cmd+Shift+Click on macOS) opens the source code that
built the widget you clicked, in your editor. The code you want to check, or
to edit, is one click away.
1.2 Debug
The back-and-forth that fixes what behaves wrong. Here there are two things the agent can do.
The agent sees the app, and drives it
- See — the widget tree, the live
Observablevalues behind it, a screenshot - Act — click, type, scroll, send keys. Targets are named by
key/labelrather than coordinates, so they survive a layout change - Wait — for async work to settle
So you can ask it to "use this app like a user would". Below, the agent gets this prompt:
Test the Events list in the running app. By spec, it lists only events from today onward.
The agent adds events and checks the list. It finds a bug at the boundary: an event for today does not show up.
The agent's last message:
Cause: the filter at readme_calendar_app.py:132 keeps only
day > self.today.day, so today's events are dropped. It should be>=.
The agent sees what you did, too
Writing down the steps that reproduce a bug is a chore. The dev bridge records the actions you took in the app. For privacy, the text you type is never recorded.
So you walk into the bug once, by hand, and that is enough. Write "this happened" on the spot in comment mode, or say it in chat. The agent reads the record, replays the same actions, sees the symptom for itself, and then fixes it.
The example below is a "works sometimes, fails sometimes" case. Every attempt is in the record, so the agent compares them and finds the one step that differed.
1.3 What makes this possible
The UI is all Python
Python is all you read and all you write. There is no new language to learn, and it is a language the agent writes well.
A widget is a Python object, and each one corresponds to one expression in your code. That is why an edit made on a displayed widget can be written straight back into the code, and why the source jump works.
Hot reload
Every save hot reloads the app: the window is rebuilt in place, no
restart. And the state your Observables hold survives — the screen you
reached with twelve clicks is not thrown away because you saved a file.
Reloads also go through with a VS Code F5 debug session attached, and your
breakpoints stay.
The dev bridge
The dev bridge is an MCP server. MCP is the standard that connects agents to tools, so no per-agent plugin is needed: Claude Code or GitHub Copilot, it works the same way.
It opens only under the dev runner and listens only on localhost. It is not part of the app you ship.
The intuitive grammar
A jump to the code is no use if the code it lands on cannot be read: no review, no touch-up. Nuiitivet takes the good parts of several frameworks, aiming at a grammar that is intuitive to read and to write.
- Flutter — the widget tree
- SwiftUI / Compose — modifiers that chain
- CSS — spacing with
paddingandgapalone, nomargin;Gridcells placed by area name - WPF —
Gridlayout,*-style weight sizing ("wt"here), and ReactiveProperty for state (that one is a desktop matter, so it waits for 2.1 ReactiveProperty-style state)
Where it departs from them, it is to keep the code readable:
- Decoration is a parameter, not a wrapper, so the nesting does not grow
- Decoration and behavior are attached as modifiers, chained with
| - Event handlers are written as procedures, not declarations
The intuitive grammar walks through it with code.
Skills
Plainly: the agent does not know Nuiitivet. There is not enough of it in the training data. The bundled skills fill that gap.
nuiitivet-app— keeps the code idiomatic. Ships with a linternuiitivet-debug— teaches the agent hot reload and the dev bridge: launch, see, act, check, down to reading the tree before spending a screenshot
Installing them is in 3.2 Installation.
2. Built for the desktop
Nuiitivet is aiming at being desktop-specialised, and most of what that takes has landed. File dialogs, the menu bar, the tray icon — the OS integration a desktop app leans on shipped piece by piece, and 2.4 keeps the checklist. What remains open is listed in 5. Current limitations.
What makes the specialisation real:
- ReactiveProperty-style state management — MVVM from WPF, as-is
- Worker threads, dispatched onto the UI thread — the answer to running heavy work locally, which is a desktop-only problem
- Shipping an executable
- OS integration — file dialogs, OS file drop, menu bar, notifications, tray icon, multiple windows
2.1 ReactiveProperty-style state
If you built desktop apps on WPF with ReactiveProperty, this is the part that makes the move easy.
Set a value on an Observable and the UI bound to it follows on its own.
You never write the code that pushes a value into a widget.
class CounterApp(nv.ComposableWidget):
def __init__(self):
super().__init__()
self.count = nv.Observable(0)
def increment(self):
self.count.value += 1
def build(self):
return nv.Column(
[
nv.Text(self.count), # bound directly
nv.Button("Increment", on_click=self.increment),
]
)
All that goes inside build() is the UI declaration. State and UI cannot drift
apart, because the state is the UI's single source of truth. With the
ViewModel pattern you separate at the class level rather than the method level.
MVVM carries over.
State derived from several values is declared as a formula — the equivalent of
WPF's ReadOnlyReactiveProperty.
# total is declared as a + b; it recalculates whenever either one changes
self.total = self.count_a.combine(self.count_b).compute(lambda a, b: a + b)
And Rx-style operators slot in, with the result bound straight to the UI.
# search 0.3 s after typing stops; if they type again, the earlier answer is dropped
self.results = self.query.debounce(0.3).switch_map(self._search, initial=[])
The function handed to switch_map runs off the UI thread, so the window
keeps painting while it searches. The build() side never learns it was async;
it binds an ordinary Observable.
map / combine / compute / debounce / throttle / filter /
switch_map, along with the async and threading details, are covered in the
State Management guide.
2.2 Heavy work runs on your machine
This is a desktop-only problem. In a web app the heavy work sits inside the server, so it never comes up. Importing a 100,000-row CSV freezes the screen if you run it on the UI thread — and if you run it on a worker, you now have to get the result back onto the UI thread.
In Nuiitivet, a write to an Observable from a worker thread is marshalled
onto the UI thread for you. You never hand-write that code.
Reporting progress, staying indeterminate until the total is known, cancelling
with a CancelToken, leaving the screen mid-run, and a worker that raises — all
of them have an answer (Background Work).
2.3 Ship an executable
There are recipes for PyInstaller and Nuitka (Packaging). One executable, onto a machine with no Python on it.
2.4 The OS is part of the app
A desktop app is more than its window. It opens the OS file dialog, puts a menu in the menu bar and an icon in the tray, raises notifications, accepts a file dragged in from Finder or Explorer. That layer is kept here as a checklist, so the distance to a complete desktop specialisation stays visible:
- File dialogs (
nv.FileDialog) — File Dialogs - File drop from the OS (
drop_targetmodifier) — Interaction modifiers - Menu bar (
nv.MenuBar) — Menu Bar - Desktop notifications (
nv.Desktop.notify) — Notifications - Tray icon (
nv.TrayIcon) — Tray Icon - Multiple windows (
nv.App/nv.Window) — Multi-Window - Window chrome customisation — Chrome
- Mouse cursor shapes
- OS accessibility — screen readers and VoiceOver cannot inspect the UI
The unchecked items are tracked in issues.
3. Getting started
3.1 Requirements
- Python 3.11 or higher
- macOS / Windows / Linux
Main libraries used for drawing and rendering: pyglet, PyOpenGL, skia-python, materialyoucolor. See LICENSES/ for third-party licenses.
3.2 Installation
pip install 'nuiitivet[dev]'
With uv, [dev] is only needed while developing, so keep it in the dev group:
uv add nuiitivet
uv add --dev 'nuiitivet[dev]'
[dev] is the extra the dev bridge's
MCP server needs. Plain nuiitivet is enough to run an app, but building with
an AI effectively requires the extra — install it up front.
Then install the bundled skills into your agent. The package bundles them, so what you install matches the nuiitivet version you have:
python -m nuiitivet.skills install
Where skills belong differs by agent: by default this writes to Claude's
project skills directory, .claude/skills/; point --dest at another
agent's. Re-run it after upgrading nuiitivet. The other channels — the
Claude Code plugin (which also wires up the dev bridge MCP server) and copying
by hand — are covered in the
install page.
Finally, register the dev bridge's MCP server with your agent, so it can see and drive the app. Add this to your MCP host's configuration:
{
"mcpServers": {
"nuiitivet-dev": {
"command": ".venv/bin/python",
"args": ["-m", "nuiitivet.dev", "mcp"]
}
}
}
The Claude Code plugin does this for you. Dev Bridge MCP has the details, Windows paths included.
3.3 Your first app
- Pull in the design system with
import nuiitivet.material as nv - Subclass
ComposableWidgetto build a UI component - Hand it to
Appand run
The counter from 2.1, complete and runnable:
import nuiitivet.material as nv
class CounterApp(nv.ComposableWidget):
def __init__(self):
super().__init__()
self.count = nv.Observable(0)
def increment(self):
self.count.value += 1
def build(self):
return nv.Column(
[
nv.Text(self.count),
nv.Button("Increment", on_click=self.increment),
],
gap=20,
padding=20,
)
def main():
# pass the class itself — it is a factory, so hot reload can rebuild it
app = nv.App(nv.Window(content=CounterApp))
app.run()
if __name__ == "__main__":
main()
3.4 Run it under the dev runner
python app.py works, but during development, use the dev runner. Everything
described above — hot reload, the dev bridge, comment mode, layout edit mode,
the source jump — turns on here.
python -m nuiitivet.dev run app.py
See AI pair-programming for the full workflow.
4. Documentation
For a deep dive into Nuiitivet's design, visit the docs site. Browse runnable examples in samples/ — the apps shown in this README live there as runnable modules under samples/readme/.
Core Concepts
| Guide | Summary |
|---|---|
| The intuitive grammar | What is borrowed from Flutter, SwiftUI / Compose, CSS and WPF, in one small card. |
| Layout | Build UIs with widgets and parameters. |
| State Management | Reactive Observable state that auto-updates the UI. |
| Modifiers | Attach decoration and behavior to widgets. |
| UI Design System | Theming and design tokens. |
Building Screens
| Guide | Summary |
|---|---|
| Overlay | Dialogs, loading, and overlays. |
| Navigation | Screens, routes, and transitions. |
| Window & Chrome | Window sizing, custom chrome, and OS integration — dialogs, menu bar, tray, notifications. |
Material Design
| Guide | Summary |
|---|---|
| Material App | App entry point and structure. |
| Material Theme | Color schemes generated from a seed. |
| Material Widgets | Catalog of built-in widgets. |
Going Further
| Guide | Summary |
|---|---|
| Concurrency | Choosing a concurrency tool, and safe UI updates from background work. |
| AI pair-programming | Refine and Debug with a coding agent: the on-screen modes, hot reload, the dev bridge, and the skills. |
| Packaging | Ship your app to users. |
5. Current limitations
There are two kinds. Constraints rooted in the design, which will not change easily, and things simply not built yet. They mean different things when you are deciding whether to adopt this, so they are kept separate.
Rooted in the design
- A display is required.
App.run()opens an OS window, so a truly headless environment — no display at all — is not supported. - A GPU is recommended, not required. By default rendering goes through an OpenGL/GPU context; on GPU-less or remote setups it falls back to CPU raster rendering, which you can also select explicitly (Renderer Selection).
Not built yet
The OS-integration checklist — what has shipped and what has not — lives in 2.4 The OS is part of the app. None of the open items is technically out of reach — they just have not been built yet, and all of them are tracked in issues.
6. License
Nuiitivet is licensed under the Apache License 2.0. See LICENSE for more info.
Release files for nuiitivet 0.23.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| nuiitivet-0.23.0.tar.gz | 17.0 MB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| nuiitivet-0.23.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 34.1 MB
Release files / nuiitivet-0.23.0.tar.gz
| Download URL | nuiitivet-0.23.0.tar.gz |
|---|---|
| Size | 17.0 MB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
322f7fb8f738e5036b766ef86b9fb08e77c1a76a4bb09ad0336efae4ba6326cd
|
|
BLAKE2b-256 checksum How to use checksums |
8e62a8a9bd8a697f50e6204ee91f5210237605856abe797ca568c40c055b6d5a
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 23, 2026.
Transparency logRelease files / nuiitivet-0.23.0-py3-none-any.whl
| Download URL | nuiitivet-0.23.0-py3-none-any.whl |
|---|---|
| Size | 17.1 MB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
0cabd01ebb761ea584bd6bf9c4b6cd57dd120d63832ad7cfb84bdcaa5b597004
|
|
BLAKE2b-256 checksum How to use checksums |
c9273d74a2aad22c04386aa59aa9b337eadc5996ed6dfe17d90e18ecce7722ec
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 23, 2026.
Transparency log