Technology · PRO

6,500 lines in one file.
The day I admitted that wasn't fine.

PC Workman reached the Microsoft Store, and a script I wrote to watch my own PC quietly became software other people run. This is the cleanup that came after.

By Marcin Firmuga·2026-07-21·9 min read·Technology

I weld plastic for a living. Not the tidy kind you see in an advert. The kind where you stand over a hot gun in a room that is already too warm and join parts that were never meant to come back apart. No respirator, before anyone writes in to scold me: in that heat a sealed mask would put me on the floor long before the fumes ever did. So I breathe carefully, keep the gun moving, and save the sitting-down half of my life for the evening.

PC Workman is the evening. It started as a small script to watch my own overheating PC, because I could not afford to keep guessing why it throttled. A while later it has an offline AI assistant, a thermal model that learns my specific hardware, and a listing on the Microsoft Store.

The Store did not change the code. It changed how I read it.

The day the Store listing went live, nothing in the repository moved. Same files, same functions, same me. And yet I opened the project that evening and saw it differently. A stranger in another country could now click Install and run this on their own machine, their own PSU, their own temperatures. It had stopped being my toy.

That is a quiet, enormous feeling, and it arrived with a bill attached. Because when you suddenly see your code as something other people depend on, you cannot keep scrolling past the one file you have been avoiding for a year.

The file I had been avoiding

It was called builder.py. One class, ResponseBuilder, 6,533 lines long. Its job is the heart of the assistant: take a parsed intent, like "what's my temperature" or "can I run this game", and turn it into a bilingual answer built from live sensor data. There are 96 of those handlers, one per intent: _resp_temperature, _resp_why_slow, _resp_game_can_run, and ninety-three more. Every single one lived in the same class, in the same file.

I want to be honest about how it got that big, because it is not a dramatic story. It grew one handler at a time. Each new thing the assistant learned to say was ten more lines, and ten more lines never feels like a problem. You do it a hundred times and one day the file makes your editor pause for a beat when it opens.

A monolith like that costs you in ways that do not show up on any screen. Every change touches a wall of unrelated code, so you stop making changes you should make. Two people (or one person and their past self) cannot work near each other without a merge fight. And bugs love it there, because nobody reads six thousand lines at once, so there is always somewhere dark to hide.

A file you are afraid to open is a file you have already stopped improving.

A facade is a polite lie about where code lives

Here is the constraint that shaped the whole fix. The rest of the app talks to this thing through exactly one method: response_builder.build(result, lang). The chat panel calls it. The proactive monitor calls it. The tests call it. I did not want to touch that surface at all. A refactor that forces every caller to change is not a cleanup, it is a second project.

So the class stayed. Only its body moved out. What is left in builder.py is a facade: a class that still answers to every old name, but keeps almost none of the code itself.

class ResponseBuilder(HardwareResponses, UpgradeResponses, ThermalResponses,
                      GamingResponses, SystemResponses, PerformanceResponses,
                      InsightsResponses, AssistantResponses):
    """Builds bilingual responses for every parsed intent."""

Each of those parents is a mixin: a small class that carries nothing but one slice of the handlers. HardwareResponses holds the CPU, GPU, RAM and disk answers. ThermalResponses holds temperatures, fans and voltage baselines. And so on down the line. Python composes them by inheritance, so from the outside ResponseBuilder still has all 96 methods hanging off it, exactly as it did when they shared one file.

The dispatch never moved a single character

This is the part I am most quietly proud of. The way a message becomes an answer did not change at all:

intent  = self._INTENT_ALIASES.get(result.intent, result.intent)
handler = getattr(self, f"_resp_{intent}", None)
if handler is None:
    return None
out = handler(result, lang)

getattr looks the method up by name, at runtime. It does not know or care which file that method is written in. Python's method resolution order walks the mixins and finds it. So "temperature" still resolves to _resp_temperature, whether that method sits in the old monolith or in the new r_thermal.py. The move was invisible to every caller and to every test that goes through build(). That was the entire goal: relocate six thousand lines and change nothing a single other file can observe.

How I actually cut it

By subject, not by size. I did not slice the file into equal chunks. I grouped handlers by what they talk about, because that is how I reach for them when I add a new one. It fell into eight modules almost on its own:

ModuleHandlersWhat it answers
r_hardware.py25CPU, GPU, RAM, disk, motherboard, full specs
r_insights.py15trends, comparisons, session digests, what changed
r_performance.py13load, throttling, why is it slow
r_system.py13health check, processes, startup, security
r_thermal.py11temperatures, fans, voltage baselines
r_assistant.py11greetings, help, about, recall earlier answers
r_gaming.py6can this game run, gaming versus work time
r_upgrade.py2offline part compatibility (new this release)

Two more files carry the shared weight. common.py holds the helpers every module reaches for: the bilingual picker _t(lang, pl, en), the follow-up hint pools, the little delta label that turns a raw number into "+23% above your norm". flows.py holds the guided, multi-step conversations. And builder.py, once 6,533 lines, is now under 600 and does exactly one job: compose the mixins and route to them.

r_upgrade.py is the youngest of the eight, and it tells the whole story in miniature. When I built the offline part-compatibility engine for this release, it got its own module from the first line, two handlers today with plenty of room to grow. A year ago I would have pasted it into whichever file was already open. That is the difference maturity makes: I gave a new feature a home instead of a corner.

Two tests so it can never grow back

Splitting a monolith is an afternoon's work. Keeping it split is the hard part, because the pressure that built the first one is still there. The next handler will always be easier to paste into an existing file than to think about. So I wrote two guards that fail the build, not a note-to-self that I will ignore.

The first caps every response module at 1,600 lines:

def test_monolith_guard(self):
    for p in glob.glob(_RESPONSES_GLOB):
        n = _read(p).count("\n") + 1
        self.assertLess(n, 1600,
            f"{os.path.basename(p)} has {n} lines - split it "
            f"instead of growing another monolith")

If any module crosses that line, the build turns red. The failure message is not "error", it is an instruction aimed at my future self: split it, do not start a new monolith.

The second guard exists because mixins have a sharp edge. If two of them define a method with the same name, one silently wins by inheritance order and the other vanishes with no warning at all. That is the kind of bug you find three weeks later, staring at an answer that changed for no reason you can see. So the second test greps every module for handler definitions and fails if any name appears twice:

all_defs = []
for p in glob.glob(_RESPONSES_GLOB):
    all_defs += re.findall(r'def (_resp_[a-z0-9_]+)\(', _read(p))
dupes = {d for d in all_defs if all_defs.count(d) > 1}
self.assertEqual(dupes, set())
The split took one afternoon. These two tests are the reason it will still be split a year from now. A refactor without a ratchet is just a delay.

It was not only the AI

The builder was the headline, but the whole release was a maturity pass, and the other pieces rhyme with it.

The My PC page opened in about two seconds. It now re-opens in 1 to 17 milliseconds. The real culprit was not the AI or the charts. It was two wmic calls, one for the CPU name and one for the disk model, sitting on the interface thread with a three-second timeout each, and on current Windows wmic is slow. They now read a hardware identity that is warmed once at startup. The page is also built a single time and kept alive, instead of being torn down and rebuilt on every visit.

The sidebar builds its sub-items the first time you open a section, not all at once at launch. Cold window start dropped to roughly 190 milliseconds.

The version number used to live in eight files, and it had already drifted. The window titled itself one version while the single-instance check searched for another, so launching the app twice quietly stopped focusing the copy already running. It now lives in exactly one file, and a test fails the build on any hardcoded duplicate.

The test suite went from 21 in June to 194.

None of that is a feature. You cannot take a screenshot of "the version now lives in one file". But this is what a project looks like when it decides to grow up. It spends weeks making itself cheaper to change, so the next real feature is easy instead of frightening.

What this kind of work actually costs

I will not pretend it is free, because a piece that hides the price is not worth reading. A refactor ships nothing the user can see. For weeks the changelog says things like "builder split into mixins", and a fair person asks what they got for it. The honest answer is: nothing today, and a faster everything after. That is a hard sell, most of all to yourself at eleven at night, after a full day over the welding gun.

It is not risk-free either. Moving six thousand lines is exactly the change that sneaks a subtle bug in, precisely because it is supposed to change nothing. That is why every step of it went behind tests that call the real public method, and why the two guards exist at all. I trust the boring, provable moves. I do not trust my own memory of a 6,533-line file.

But I would do it again without hesitating, because the alternative is worse. The alternative is a file that grows until you are scared of it, inside an app that strangers now install. Fear is not a maintenance strategy.

Back in the workshop, good welding is never the flashy part. It is the joint you cannot see coming apart later. Software is the same. The work I am proudest of this release leaves no mark on the screen: the 6,533 lines that became eight small files, each one you can open without your editor pausing, each one you are no longer afraid to change.

The Microsoft Store did not make PC Workman a real project. It just held up a mirror, and I finally cleaned up what I saw.

This is working code, not a plan. The builder split, the two guard tests and the My PC speed-up all ship in PC Workman v1.8.5, free and open-source. You can read every file. Download it or read the source. Related: what your PC has learned about itself · why thresholds fail your specific hardware.
MF

Marcin Firmuga

Solo developer · HCK_Labs · building PC Workman in public

I write about what I actually shipped, with real numbers and real code. More: my story.