Skip to main content

Ralsina.Me — Roberto Alsina's website

New project: ToCry, a ToDo/Kanban app

I have been us­ing Tasks.md for a while now and ... I like it, but I got the itch to try fix­ing some things and I did­n't re­al­ly want to do JS back­end code on week­end­s, so why not try to build a sim­i­lar ap­p?

Al­so I have been want­ing to do some "vibe cod­ing" with AI and I seem to have un­lim­it­ed Gem­i­ni 2.5 in VS Code for some rea­son, so why not try to do that too?

Well: it worked, you can see the re­sult at ToCry, get the app and use it. It's pret­ty nice!

alt text

Now, I am not say­ing it's per­fec­t, but it works and I learned a lot. Did AI help? Well, yeah. Most of the fron­tend code was AI gen­er­at­ed, most of the back­end code is mine with AI au­to­com­plet­ing stuff.

And this got writ­ten pret­ty fast, I on­ly start­ed it two days ago!

In fac­t, in those two days it was writ­ten twice... be­cause the first time it was ab­so­lute garbage.

It was so bad I not on­ly rewrote it, I re­moved the re­po his­to­ry and pushed the new code over it. I did­n't want to keep that code near, I did not want it to in­flu­ence the re­write.

What Went Wrong The First Time

The first time I went UI-­first. I just prompt­ed and prompt­ed and prompt­ed on a HTML file, try­ing to get Gem­i­ni to gen­er­ate a nice UI that worked with all the da­ta kept on the client side.

This seemed to work well, in a few hours I had a nice UI that was at least func­tion­al. How­ev­er at one point Gem­i­ni hit a wal­l, hard.

Try­ing to add a new fea­ture or fix­ing a bug was al­most im­pos­si­ble, Gem­i­ni would get in loop af­ter loop, do­ing and re­vert­ing the same changes over and over.

When I went to man­u­al­ly check the code, it was a bowl of spa­het­ti, and refac­tor­ing it was be­yond Gem­i­ni's ca­pa­bil­i­ties. Con­nect­ing it to a back­end was doable (and got done) but then state was kept in the client and the back­end, and Gem­i­ni re­fused to move it and con­sis­ten­cy was a night­mare.

It be­came in­creas­ing­ly clear that the code was of no val­ue. So then I nuked it, salt­ed the earth, and start­ed over.

What Went Right The Second Time

The sec­ond time I went back­end-­first. I wrote a sim­ple back­end in Crys­tal, with a sim­ple API that re­turned JSON da­ta for the en­ti­ties I want­ed to man­age: tasks, lanes, board­s, etc.

Af­ter the da­ta struc­tures were clear, prompts like "cre­ate a PUT /note/:id end­point that up­dates the note" worked great. The trick is, of course, that those re­quests are pret­ty much con­tex­t-free, so Gem­i­ni did­n't have to fig­ure out how to con­nect one thing to an­oth­er, it was just writ­ing al­most-boil­er­plate code.

Af­ter sev­er­al of these were cre­at­ed, I in­ter­min­gled refac­tor­ing prompt­s.

  • "Do you see any code that can be moved to a com­mon func­tion?"
  • "Please as­sume this and that are al­ways true and stop check­ing"
  • "Change the names of this and that to some­thing de­scrip­tive and short"

I still am not a huge fan of the code Gem­i­ni writes. It has a ten­den­cy to do mul­ti­ple in­ter­me­di­ate steps that are not need­ed and us­ing in­ter­me­di­ate vari­ables in some sort of over de­scrip­tive no­ta­tion all the time.

Ah, and adds stupid com­ments. Many stupid com­ments. This is an ex­am­ple of its code:

self.notes.each_with_index do |note, index| # Changed to each_with_index to get the index
        note.save                   # This saves the note to data/.notes/note_id.md

        padded_index = index.to_s.rjust(4, '0')
        sanitized_title = note.slug # Note instance method for slug

        symlink_filename = "#{padded_index}_#{sanitized_title}.md"
        source_note_path = File.join("..", ".notes", "#{note.id}.md") # Relative path for symlink
        symlink_target_path = File.join(lane_dir, symlink_filename)
        ...

Usu­al­ly, af­ter a func­tion is "fin­ished", I would go and do a pass "for taste" to make the code more read­able, re­move the un­nec­es­sary com­ments, and make it more con­cise, but the code is func­tion­al and this is CRUD, not a fash­ion show.

The same hap­pened with the fron­tend code. I prompt­ed slow, bit by bit:

  • Add a but­ton to cre­ate a new lane, so when the us­er clicks it they are asked for the lane name. Then it calls POST /lane with the right da­ta
  • Get the lane da­ta from /lanes and dis­play it as a hor­i­zon­tal list of con­tain­ers
  • Add a but­ton to the lane to delete it, which prompts the us­er for con­fir­ma­tion and then calls DELETE /lane/:id

And so on.

Some­times, I tried larg­er prompts where I asked for a whole fea­ture, but those were on­ly oc­ca­sion­al­ly suc­cess­ful. Find­ing the right gran­u­lar­i­ty is key, and I found that the best gran­u­lar­i­ty was "one func­tion" or "one com­po­nen­t".

Again, mix­ing refac­tor­ing as we went along helped a lot to keep the code clean and or­ga­nized, as well as sep­a­rat­ed in rea­son­ably func­tion­s, keep­ing spaghet­ti at bay.

What I Learned

Gem­i­ni codes like a ju­nior. It does not un­der­stand the big pic­ture, it does not un­der­stand the code it writes, It can write code that works ( I as­sume be­cause the In­ter­net is full of work­ing code) but it car­go cults AF.

On the oth­er hand, the code tends to work? And I can fix it pret­ty quick when it does­n't? It's not bad at sim­ple refac­tor­ing, and what it needs more than any­thing is a man­ag­er.

Yes, that seems to be the bad news. Cod­ing with AI felt like be­ing a man­ag­er again. A man­ag­er with a very, very, very ea­ger ju­nior dev who does­n't sleep and feels soooo clever by de­scrib­ing unini­tial­ized vari­ables as "a clas­sic ex­am­ple of a vari­able that has not been ini­tial­ized yet".

Would I Do It Again?

Yeah. I have a ton of things I want to write, and this way I can write them faster. I can al­so put ef­fort in the parts that mat­ter, like da­ta struc­tures and al­go­rithms and let Gem­i­ni do the sil­ly CSS and CRUD.

Yeah, I don't care if the CSS is re­dun­dan­t, as long as it looks ok I am hap­py telling Gem­i­ni to "make the rows tighter" and who cares how.

CRUD? It's a solved prob­lem. I will do a pass to clean up.

Da­ta struc­tures? I will do 90% of the work, be­cause that is what makes the base of the ap­p, and I want it to be sol­id.

Ethical Thoughts

I feel dirty, and like I am cheat­ing. I am prob­a­bly steal­ing code from oth­er peo­ple. Use Gem­i­ni to write a Dock­er­file and it will hap­pi­ly au­to­com­plete things with frag­ments of things in­clud­ing re­po names which ex­ist in the In­ter­net so it's not even a guess, I know it's copy past­ing oth­er peo­ple's code.

OTO­H, I al­ways copy past­ed code from oth­er peo­ple's code.


Contents © 2000-2025 Roberto Alsina