Skip to content
titah

Reference

Changelog

Every release of Titah, newest first. This page mirrors `CHANGELOG.md` in the repository.

Format follows Keep a Changelog. Versioning follows SemVer.

[Unreleased]

[0.9.1] — 2026-09-27

Diperbaiki

  • Gagal menjangkau server artifact kini menyebut sebab aslinya. fetch di Node selalu melempar fetch failed; sebab sebenarnya (ENOTFOUND, ECONNREFUSED, ECONNRESET, TLS) ada di error.cause dan dulu dibuang, sehingga model mengarang penjelasannya sendiri. Pesannya sekarang memuat kode dan pesan sebabnya, dan menyatakan bahwa masalahnya ada di jaringan antara mesin user dan server.
  • Satu percobaan ulang, hanya kalau tidak bisa menggandakan halaman: untuk kode yang berarti permintaan tidak pernah terkirim (DNS, ditolak, tak terjangkau), atau untuk kode jaringan apa pun kalau slug dikirim. Kegagalan ambigu seperti ECONNRESET tanpa slug tidak lagi dilaporkan sebagai “Nothing was published” — permintaannya mungkin sudah sampai. Kegagalan TLS tidak dicoba ulang.

$schema menunjuk ke folder config, bukan ke paket

  • config.schema.json kini disalin ke ~/.config/titah/ dan disegarkan setiap kali isinya berbeda dari versi yang sedang jalan. titah init menulis $schema ke salinan itu, bukan lagi ke folder instalasi npm yang berpindah begitu Node, prefix, atau cara pasangnya berganti.
  • Config lama dimigrasi sendiri. $schema di config global yang menunjuk ke berkas lokal bernama config.schema.json di tempat lain dipindah ke salinan baru, dengan komentar dan format tetap utuh. URL, nama berkas lain, config tanpa $schema, config yang tidak bisa diurai, dan titah.json proyek tidak disentuh.
  • titah doctor menampilkan satu baris schema: di bagian Config.

Titah bisa menulis skill sendiri — skills.learn, mati secara bawaan

  • Sesudah giliran yang selesai normal dan memakai ≥ minTools tool call (bawaan 8), satu panggilan smallModel membaca digest ringkas giliran itu — bukan transkripnya — dan memutuskan apakah ada prosedur yang layak disimpan.
  • Skill ditulis ke ~/.config/titah/skills/learned/<nama>/SKILL.md dengan namespace learned:, dan langsung terlihat di giliran berikutnya.
  • Titah hanya menimpa berkas bertanda source: titah-learn. Nama, ukuran, dan bentuk rahasia yang umum divalidasi sebelum apa pun ditulis.
  • Setiap keputusan tercatat satu baris di ~/.config/titah/learn.log, beserta token yang dipakai. titah doctor menampilkan status dan jumlahnya.
  • Sumber skill tidak lagi memindai masuk ke root sumber lain, sehingga ~/.config/titah/skills di skills.paths tidak mendaftarkan skill yang dipelajari dua kali.

[0.9.0] — 2026-09-20

Artifact — agent bisa menerbitkan halaman, bukan cuma menuliskan teks

  • Tool baru artifact. Menerbitkan satu dokumen HTML utuh ke dashboard akun dan mengembalikan URL yang bisa dibuka manusia di browser. Dipakai saat jawabannya lebih baik jadi halaman daripada paragraf — laporan, bagan, tabel yang ingin diurutkan. Ada di sumbu izin network, sama seperti webfetch: ia mengirim dokumen keluar dari mesin.

  • Halaman bersifat PRIVAT, dan CLI tidak bisa mengubahnya. Menyalakan link publik hanya bisa dilakukan manusia lewat dashboard; API menolak token CLI dengan 403. Ini yang membatasi skenario agent yang disetir oleh sesuatu yang ia baca: agent semacam itu bisa menerbitkan halaman — itu fiturnya bekerja — tapi tidak bisa menyodorkannya ke orang lain.

  • Satu slug, banyak versi. Mengoper slug dari panggilan sebelumnya merevisi halaman yang sama, jadi URL yang sudah kamu kirim tetap URL yang benar meski agent beriterasi tiga puluh kali. Menerbitkan ulang byte yang identik tidak menulis versi baru — itu yang membuat percobaan ulang setelah timeout aman.

  • Batas 512 KiB per dokumen, angka yang sama dengan batas transkrip. Dokumen kebesaran ditolak sebelum meninggalkan mesin, dan penolakannya menyebut kedua angkanya supaya model mengecilkan halamannya alih-alih mengirim yang sama lagi.

  • Salinan lokal di ~/.local/share/titah/artifacts/<slug>.html. Halaman terbitan adalah hal pertama yang Titah taruh di server yang mungkin tidak ada salinannya di tempat lain — transkrip cuma salinan sesi yang masih ada di mesinmu, halaman ini bukan salinan apa pun.

  • Config artifacts.enabled, bawaannya true — berbeda dari tracking.sync yang false. sync mengunggah otomatis tanpa user di dalam putaran; penerbitan artifact selalu melewati dialog izin yang menyebut host, judul, dan jumlah byte. titah doctor menyebut sakelar mana yang mati, bukan sekadar “off”.

  • Disajikan dari origin terpisah. Halaman terbitan dibuka di origin lain, di dalam iframe ber-sandbox tanpa allow-same-origin, dengan CSP yang mematikan connect-src, form-action, dan membatasi img-src ke daftar host bernama. Akibatnya halaman yang memusuhi pembacanya bisa menggambar kotak “masukkan API key”, tapi tidak punya saluran untuk mengirimkan apa yang diketik. Isinya tidak pernah disaring atau ditulis ulang — penyaringan akan merusak fiturnya sekaligus memberi rasa aman palsu.

[0.8.1] — 2026-09-20

Diperbaiki

  • Enter di picker extension memasang lagi. <leader>x, lalu x, lalu Enter pada sebuah baris tidak melakukan apa pun — tidak memasang, tidak melempar, tidak mengatakan apa pun.

    runSuggestion mencari baris yang tersorot di dalam extensionRows, tapi extensionRows tidak ada di dep array-nya. Callback itu menutup atas nilai render PERTAMA — array kosong — jadi pencariannya selalu gagal dan cabangnya return undefined. Ini kejadian KEEMPAT dari kelas bug yang sudah dinamai di bagian atas app.tsx.

    Yang membuatnya sulit dilihat: D dan R bekerja, dan hint di bawah picker benar. Keduanya dibaca dari useInput, yang didaftarkan ulang setiap render. Hanya Enter yang diam — tombol yang paling sering ditekan orang.

    runLeaderAction hilang dari daftar yang sama, dengan akibat yang lebih senyap: dep array-nya sendiri memuat effort, focusedSpec, dan mouseCapture, jadi aksi leader yang dipilih lewat Enter di menu bekerja atas keadaan saat aplikasi baru menyala. Ikut diperbaiki, bersama installFromPicker, openChild, cwd, dan exit.

  • F di picker extension mengambil ulang daftar dari registry. Cache registry ber-TTL dua puluh empat jam, jadi registry yang baru diperbarui tidak terlihat sampai besok — dan satu-satunya jalan keluar adalah menghapus ~/.cache/titah/registry.json dengan tangan, yang tidak disebutkan di mana pun. Gejalanya sulit dibaca: picker menyebut versi lama dan tidak ada apa pun di layar yang menyebut cache.

    loadRegistry sudah menerima force, lengkap dengan test yang membuktikan ia melewati cache yang masih segar — tapi tidak ada satu pun pemanggil yang pernah mengirimkannya, sementara komentar di berkasnya menjanjikan “tombol refresh di picker” yang tidak pernah dibuat. Sekarang tombolnya ada, dan ada test yang menekan F sungguhan lalu menuntut permintaan registry benar-benar terjadi.

    Hurufnya F dan bukan R: R sudah berarti cabut di picker itu, dan ctrl+r sudah dipegang effort_cycle. Ia berlaku di setiap baris — yang dimuat ulang adalah daftarnya, bukan barisnya — dan hasilnya dikatakan, termasuk saat jaringan mati.

[0.8.0] — 2026-09-19

Panel bisa mengirim teks ke prompt utama — EXTENSION_API naik ke 0.5.0

  • KeyVerdict bertambah prompt. onKey dan onClick sekarang boleh menjawab { prompt: { text, mode } }, dan Titah menyisipkan teksnya ke draft prompt user. "append" (bawaan) menyisip di posisi kursor, "replace" menimpa seluruh draft. Sebelumnya satu-satunya jawaban yang mungkin adalah { refresh } — panel bisa menggambar apa saja tapi tidak bisa membawa apa pun keluar dari dirinya.
  • EXTENSION_API naik dari 0.4.0 ke 0.5.0, karena bentuk sebuah tipe publik berubah. Ini memutus setiap extension yang menyatakan ^0.4.0 sampai masing-masing menaikkan engines.titah ke ^0.5.0 dan diterbitkan ulang. Yang terpengaruh: @titah/extension-git, @titah/extension-diff. Panel yang belum diperbarui ditolak saat load dengan kalimat yang menyebut sebabnya, terlihat di footer dan di titah doctor.
  • Teks dari extension dibersihkan seperti tempelan (sanitizePaste): ia input pihak ketiga yang ikut terkirim ke model, jadi karakter kontrol dibuang sebelum masuk editor.
  • Penyisipan dari onClick lewat ref, bukan closure. Penangan mouse berlangganan sekali (useEffect([mouse])) dan menutupi nilai render pertama, jadi menyisip langsung di sana akan memakai cursor yang selamanya 0 — teks selalu mendarat di awal draft, tanpa satu pun error. Ada test yang menjaganya, dengan draft yang sengaja TIDAK kosong.

Extension bisa dimatikan dan dicabut dari tempat ia dipasang

  • Picker <leader>x sekarang punya D dan R. D mematikan (enabled: false, kode tetap di disk) atau menyalakannya lagi; R mencabut — entri config dibuang dan paketnya di-npm uninstall. Sebelumnya picker hanya bisa memasang, dan orang yang memasang dari dalam TUI harus keluar dari TUI untuk membuangnya.
  • Keduanya bertanya dulu, lewat dialog merah yang menyebut apa yang akan terjadi dan berkas mana yang akan disunting. Menyalakan kembali tidak bertanya — ia tidak menghapus apa pun.
  • Keadaan keempat, ⊘ disabled, dengan penanda visual sendiri. Ia menang atas ✓: enabled: false berarti modulnya tidak pernah di-import, jadi baris yang mengaku terpasang untuk sesuatu yang tidak akan pernah dimuat adalah baris yang berbohong.
  • <leader>x saat sebuah panel fokus membuka picker dengan baris panel itu sudah tersorot. Tanpa keybind baru, dan tanpa menyentuh aturan bahwa tombol biasa milik onKey extension selama panelnya fokus.
  • Popup kini mendahului panel yang fokus. Tanpa itu, picker yang dibuka dari panel tergambar di depan mata sementara papan ketiknya masih dipegang kotak di belakangnya.
  • Panel yang sedang tampil tidak lenyap sesudah D atau R. Extension dimuat sekali per sesi dan Node tidak bisa meng-un-import modul; kalimatnya menyebut restart, cermin dari kalimat yang sudah dipakai install.

Diperbaiki

  • titah extension remove ./x benar-benar mencabut. Ia selalu menyunting config global, padahal install ./x selalu menulis ke config proyek — jadi ia mencetak “Removed” untuk entri yang masih utuh, dan panelnya muncul lagi di sesi berikutnya. Sekarang yang disunting adalah setiap berkas yang benar-benar menyebut spec itu, dan spec yang tidak disebut di mana pun dikatakan begitu alih-alih dilaporkan sebagai keberhasilan.
  • remove market:<id> menghapus kunci yang benar. Kunci config-nya market:<id>, bukan nama paketnya; yang dihapus sebelumnya adalah kunci yang tidak pernah ada.
  • titah extension list menyebut yang dimatikan. loadExtensions() melewati enabled: false sebelum sempat melaporkannya, jadi extension yang sengaja dimatikan hilang diam-diam dari daftar — dan orang mengira ia sudah terhapus.

Ditambahkan

  • titah extension disable <pkg> dan titah extension enable <pkg>. enable menghapus kunci enabled alih-alih menulis true: true adalah bawaan schema, dan baris yang tidak menyatakan apa pun tidak layak ditambahkan ke berkas yang dirawat tangan.

[0.7.0] — 2026-09-06

Extension bertahan melewati upgrade Titah

  • engines.titah sekarang diperiksa terhadap versi API, bukan versi produk. Titah mengumumkan EXTENSION_API dari titah-code/extension — versi kontrak, yang hanya bergerak kalau kontrak itu sendiri berubah. Sebelumnya setiap kenaikan minor Titah mematikan setiap extension yang terpasang, padahal src/extension.ts tidak berubah satu byte pun dari 0.4.0 sampai 0.6.1.
  • Nilai awalnya 0.4.0, dan itu disengaja: setiap extension yang sudah terbit hidup lagi tanpa diterbitkan ulang, karena semuanya menyatakan ^0.4.0.
  • Pemeriksaannya dipindahkan, bukan dilemahkan. Kontrak yang benar-benar tidak cocok tetap ditolak, dan kalimatnya menyebut kedua angka — needs extension API ^9.0.0, but Titah 0.7.0 provides 0.4.0 — karena satu angka saja terbaca seperti kerusakan.
  • satisfiesEngine mengerti rentang majemuk: ||, konjungsi berspasi (>=0.4.0 <1.0.0), dan operator < <= >. Sebelumnya satu-satunya bentuk lebar adalah >=0.4.0 yang tanpa batas atas sama sekali. Rentang yang tidak dikenali tetap ditolak, dan bagian cacat di dalam konjungsi tetap menjatuhkan kelompoknya.
  • titah extension update ikut memilih berdasarkan API, jadi ia berhenti menolak setiap versi terbit setiap kali Titah naik minor.
  • titah upgrade tidak ikut: ia membandingkan versi Titah sendiri di npm, pertanyaan yang berbeda, dan alasannya ditulis di tempatnya.

[0.6.2] — 2026-09-06

MCP dan LSP

  • Server MCP atau language server yang mati saat sedang ditulisi tidak lagi menjatuhkan seluruh Titah. #write sudah memeriksa apakah anaknya masih hidup, tapi itu potret sesaat: muatan yang lebih besar dari buffer pipa 64 KB — hasil tool MCP, didOpen yang membawa satu berkas utuh — mendarat sesudah pemeriksaan itu. EPIPE-nya datang asinkron sebagai event error di stream, dan tanpa listener Node memperlakukannya sebagai error tak tertangani.
  • Permintaannya kini ditolak dengan sebab yang sungguhan, bukan digantung sampai timeout tiga puluh detik. Kalimatnya membawa kode keluar dan stderr server, dan menyebut kalau ada permintaan yang sedang ditulis saat ia mati — itu yang menjelaskan kenapa jawabannya tidak pernah datang.
  • Kelas yang sama dengan perbaikan kait di 0.5.1, tapi penanganannya berbeda dan sengaja: di sana EPIPE diabaikan karena putusan sebuah kait datang dari kode keluarnya. Di sini permintaannya benar-benar hilang dan ada yang menunggu, jadi mengabaikannya diam-diam justru salah.

[0.6.1] — 2026-09-06

Extension yang ditolak

Tidak ada perubahan kebijakan: extension yang engines.titah-nya tidak memuat versi Titah yang berjalan tetap ditolak. Yang berubah cara Titah mengatakannya.

  • Sisi yang terbuka tapi tidak satu pun extension-nya berhasil dimuat kini berbunyi ⚠ N failed, bukan No extension. Yang kedua menyesatkan: bukan “tidak ada extension”, melainkan “ada dan ditolak” — dan bedanya menentukan ke mana orang mencari.
  • Penjelasannya bertahan. Sebelumnya hanya flash() yang hilang setelah empat detik; sesudah itu satu-satunya yang tersisa di layar justru kalimat yang menyesatkan di atas. Footer kini membawa ⚠ N side panels failed to load — run: titah extension list selama keadaan itu masih berlaku.
  • titah doctor punya bagian Extensions: manifest terbaca dan engines.titah cocok, per extension yang dikonfigurasi. Sengaja TANPA meng-import apa pun — aturan yang sama dengan bagian MCP — dan batasannya dicetak, bukan disembunyikan: kegagalan yang baru muncul saat modulnya dijalankan tetap milik titah extension list.

[0.6.0] — 2026-09-06

  • Satu sisi kini menampung banyak extension. Sebelumnya yang kedua di sisi yang sama ditolak dengan “already taken” dan hanya dilaporkan lewat notice, jadi plafonnya dua extension untuk seluruh aplikasi — satu kiri, satu kanan. Kini mereka bertumpuk sebagai box terpisah, urut menurut config: satu-satunya urutan yang user bisa lihat dan ubah.
  • Box bisa dilipat jadi satu baris judul ber-▸ — lewat <leader>z pada box yang sedang fokus, klik pada baris judulnya, atau "collapsed": true di config. Satu baris dan bukan satu bingkai: box terlipat yang masih membayar bingkai hampir tidak menghemat apa pun.
  • Box terlipat tidak dirender sama sekali. Bukan sekadar disembunyikan — panel git yang terlipat berhenti menjalankan git status tiap refresh.
  • Di bawah panel.boxFloor, box melipat sendiri, dari bawah ke atas, dan dilaporkan lewat notice. Cermin lantai lebar yang sudah ada: urutan config adalah urutan prioritas, jadi yang teratas bertahan paling lama.
  • <leader>f kini berputar antar box, bukan memilih sisi — tombol yang diusulkan extension kedua di satu sisi sebelumnya tidak akan pernah sampai kepadanya.
  • Dua field config baru per extension: rows (tinggi yang diminta; permintaan, bukan jaminan) dan collapsed (nilai awal, tidak ditulis balik saat dilipat lewat tombol). Plus panel.boxFloor.

Di dalamnya

  • Identitas panel pindah dari SISI ke extension. panelHit mengembalikan spec box beserta apakah yang kena adalah baris judulnya, dan geometrinya disusun stackGeometry dari tumpukan yang sama dengan yang menggambar — bukan dihitung ulang di sisi render, yang selama ini jadi cara reservasi dan gambar menyimpang.

[0.5.1] — 2026-09-06

Kait

  • Kait yang tidak membaca stdin tidak lagi bisa menjatuhkan seluruh proses. pwd, true, dan formatter yang cuma melihat $TITAH_TOOL menutup stdin lalu keluar; tulisan yang datang sesudahnya gagal dengan EPIPE, dan kegagalan itu tiba sebagai event error di stream — bukan sebagai lemparan dari end(), jadi try/catch yang menjaganya tidak pernah melihatnya. Tanpa listener, Node memperlakukannya sebagai event error tak tertangani.
  • Bugnya tersembunyi karena muatan kecil muat seluruhnya di buffer pipa: kait selesai sebelum anaknya sempat keluar, dan tidak ada yang pernah gagal. Ia muncul ketika anaknya menang balapan — di bawah beban, atau pada muatan yang lebih besar dari buffer pipa, yang wajar karena masukan tool bisa memuat seluruh isi berkas. Test barunya memakai muatan 200 KB supaya kegagalannya pasti, bukan kadang-kadang.

[0.5.0] — 2026-09-06

Kursor prompt

  • Panah atas/bawah menelusuri baris visual, bukan hanya \n. Satu kalimat panjang yang dibungkus terminal tidak lagi dilompati sekaligus — sebelumnya satu tekanan panah di draft seperti itu menukar seluruh ketikan dengan entri histori.
  • ctrl+←/ctrl+→ melompat per kata, dengan alt+←/alt+→ dan alt+b/alt+f sebagai alias: tidak ada satu sekuens yang dikirim semua terminal.
  • ctrl+a/ctrl+e berhenti di awal/akhir baris, bukan di ujung seluruh draft — sesuai namanya, dan sesuai readline.
  • Tinggi kotak penyunting dihitung dari baris visual yang sama. Sebelumnya ia dihitung dari jumlah \n sementara Ink menggambarnya terbungkus, jadi draft satu baris yang panjang memakan baris riwayat paling bawah tanpa jejak.

Blok tool

  • Blok tool terbuka sejak awal; klik menutupnya, ctrl+x d menutup semuanya dan tekanan kedua mengembalikannya. Hasil kerja yang baru saja diminta tidak lagi bersembunyi di balik satu tekanan lagi.
  • Blok penalaran tetap terlipat: ia biasanya jauh lebih panjang daripada jawabannya. Keduanya memakai mekanisme lipat yang sama — yang berbeda hanya bawaannya, dan himpunan Expansion kini berarti “yang dibalik dari bawaan”.

Halaman transkrip sub-agent

  • Enter atau klik pada baris panel sub-agent membuka transkrip sub-agent itu sebagai halaman penuh, mengalir langsung selagi pekerjaannya berjalan.
  • ctrl+x s membuka daftar seluruh sub-agent sesi ini, termasuk giliran yang sudah selesai — panel sendiri dikosongkan setiap prompt baru.
  • Rute baru GET /session/:id/children. Sesi anak tetap disembunyikan dari GET /session: memilihnya di sana akan mengganti sesi aktif, dan prompt berikutnya ikut masuk ke sesi milik sub-agent.
  • Halamannya baca-saja. Super agent menjalankan CLI di luar Titah dan hanya mengirim jawaban akhirnya, jadi halamannya mengatakan itu alih-alih menggambar layar hampa.

[0.4.2] — 2026-08-25

Account server

  • Default login/account server diganti dari https://titah.akil.co.id ke https://titah.dev.
  • Dokumentasi default server disesuaikan agar mengikuti domain baru.

[0.4.1] — 2026-08-23

titah extension update

Sampai sekarang tidak ada jalur update sama sekali. Lockfile bekerja persis seperti yang seharusnya — install menghormatinya, jadi memasang ulang mengembalikan versi yang sama selamanya — tapi tidak ada perintah untuk menyatakan “saya mau bergerak maju”. Satu-satunya jalan adalah mengetahui nomor versinya lalu install <pkg>@<versi>, yang berarti menghafal angka yang seharusnya dicari Titah sendiri.

Perintah tersendiri, bukan perilaku install: install yang diam-diam menaikkan versi membuat “kode yang sama di dua mesin” jadi harapan lagi.

  • Memilih versi terbaru yang KOMPATIBEL, bukan dist-tags.latest. Extension yang menuntut ^0.5.0 akan jadi latest di npm sementara Titah masih 0.4.0; memasangnya berarti mengganti extension yang bekerja dengan yang tidak bisa dimuat, dan pesan kegagalannya baru muncul di sesi BERIKUTNYA — jauh dari perintah yang menyebabkannya. engines ada di packument, jadi ini diketahui sebelum mengunduh apa pun.
  • Versi yang lebih baru tapi diblokir tetap dikatakan, beserta versi Titah yang dituntutnya. Tanpa itu, update yang tidak mengubah apa pun terlihat seperti tidak ada versi baru — padahal ada, dan yang menahannya adalah satu hal yang user bisa perbaiki.
  • Prerelease dan versi yang di-deprecate dilewati.
  • Tanpa argumen, seluruh extension npm di config diperiksa. Path lokal dilewati dan disebutkan: ia dimuat langsung dari disk, jadi tidak ada yang bisa di-update dari sini.

Diperbaiki

  • docs/extensions.md menjanjikan tombol U dan X di picker yang tidak pernah dibuat. Picker hari ini hanya punya Enter, dan dokumennya sekarang mengatakan itu. Penanda ✓ juga diluruskan: ia berarti “ada di disk”, BUKAN “versi terbaru” — picker tidak memanggil registry npm untuk setiap baris.

[0.4.0] — 2026-08-23

Panel bisa diklik, dan lebarnya bisa diubah tanpa keluar dari sesi

Dua pertanyaan sederhana — “bagaimana resize?” dan “apakah bisa diklik?” — menemukan bahwa jawabannya “tidak bisa” untuk keduanya, dan menemukan dua cacat yang dikirim di 0.3.0. Lihat bagian Diperbaiki.

  • onClick({ row }) di permukaan publik. row adalah indeks baris yang DIGAMBAR, bukan indeks data extension — pemetaannya milik extension, karena hanya ia yang tahu baris mana berarti apa. Klik tidak menuntut fokus (klik sudah menyebutkan sasarannya) tapi MEMINDAHKAN fokus, supaya tombol yang panelnya iklankan langsung bekerja sesudahnya.
  • + / - / = saat panel fokus: lebarkan, sempitkan, kembalikan ke lebar config. Tiga tombol itu dipesan Titah dan tidak pernah diteruskan ke extension — kebalikannya membuat artinya bergantung pada extension mana yang sedang fokus. Pelebaran berhenti di panel.floor: tanpa batas itu, satu tekanan lagi membuat panel yang sedang dilebarkan menutup sendiri.
  • Lebar hasil resize hidup di memori sesi, tidak ditulis ke config. +/- ditekan berkali-kali dalam hitungan detik.

Diperbaiki

  • onKey tidak pernah dipanggil. Ada di permukaan publik, diimplementasikan extension referensi, dan app.tsx tidak pernah memanggilnya — jadi panel git menampilkan “b branches · r refresh” dengan kedua tombol mati. Sekarang lewat <leader>f, yang memisahkan BUKA dari FOKUS: panel samping dibuka untuk dilihat sambil bekerja, dan kalau membukanya berarti memakan tombol, tidak ada yang akan membukanya.
  • Panel tidak digambar di layar pembuka. Memasang extension lalu menekan Ctrl+X ← sebelum prompt pertama tidak menghasilkan apa pun — dan itu terbaca sebagai extension yang gagal dipasang, bukan sebagai layar yang berbeda.
  • Pilihan logo layar pembuka diambil dari kolom tengah, bukan lebar terminal.

Catatan tentang kelas bug yang sama tiga kali

Ketiga cacat di atas berakar pada satu hal: callback dan listener yang dep array-nya tidak memuat extensions menutup atas render PERTAMA, saat daftarnya masih kosong. Gejalanya bukan error, melainkan tombol atau klik yang tidak melakukan apa pun. Ketiganya sekarang lewat ref yang disegarkan tiap render — pola yang sama dengan view.current, dan alasannya sama: hanya render yang tahu apa yang sedang terlihat, sedangkan penanganan input datang belakangan.

[0.3.0] — 2026-08-23

Extension — panel samping yang disumbang paket npm

Titah punya plugin dan hooks untuk mengubah perilaku. Yang belum tertutup adalah hal yang tidak berbentuk perilaku sama sekali: melihat branch git tanpa keluar dari sesi, membaca daftar worktree, menengok diff yang baru ditulis agent. Sebelum ini, semuanya berarti terminal kedua.

  • extension di config, bentuk kunci yang sama dengan plugin (npm, path, market:<id>). Disebut satu per satu; tidak ada penemuan otomatis.
  • Panel kiri dan kanan, <leader>← / <leader>→, dengan lantai: di bawah panel.floor kolom panel tertutup sendiri alih-alih membuat riwayat tidak terbaca. <leader>e menyegarkan, <leader>x membuka picker.
  • Permukaan publik titah-code/extension — hanya tipe dan pemeriksa versi. exports di package.json menolak jalur lain, jadi core/permission.js dan core/auth.js tidak bisa di-import extension sama sekali. Batasnya ditegakkan resolver Node, bukan dokumen.
  • engines.titah diperiksa saat load. Extension tanpa itu ditolak: selama API 0.x, paket yang tidak menyatakan versi targetnya tidak bisa dibedakan dari paket yang ditulis dua rilis lalu.
  • Dipasang ke ~/.local/share/titah/extension/, bukan ke node_modules proyek user — extension adalah preferensi orang, bukan dependency proyek. Lockfile di ~/.config/titah/extension-lock.json mengunci versi persis dan integrity hash, dan ia berkas yang memang ingin dibawa ke dotfiles.
  • Kegagalan tidak menjatuhkan TUI. Panel yang melempar menampilkan pesannya di tempatnya; yang menggantung dibatalkan pada dua detik. Aturan yang sama dengan plugin.
  • titah extension list|install|remove, dan titah upgrade yang MENCETAK perintah pemasangan alih-alih menjalankannya.

Batas yang disengaja: extension hanya sisi TUI, jadi attach, serve, dan web tidak punya panel. titah.panel adalah satu field di antara beberapa yang mungkin, supaya sisi server nanti jadi field baru — bukan perubahan bentuk yang memutus config orang.

Rujukan implementasi: @titah/extension-git, repo terpisah, memakai hanya permukaan publik. Index picker: titah-dev/titah-extensions.

Diperbaiki

  • String menu leader yang masih Bahasa Indonesia diterjemahkan. AGENTS.md sudah mewajibkan UI berbahasa Inggris; sembilan baris LEADER_ACTIONS tertinggal.

[0.2.1] — 2026-08-22

Packaging

  • Published on npm as titah-code, not titah. npm’s typosquat filter rejects the shorter name as too similar to tiag — a create-next-app scaffold published once in March 2024 and never touched since. titah itself is unclaimed, so this is a false positive; a dispute is open with npm support.
  • The command did not change. bin is still titah, ~/.config/titah/ is still the config directory, and no migration is needed. Only the install name moved: npm i -g titah-code, or npx titah-code for a one-off run.
  • Code-identical to [email protected] on the registry, which went out before this tag existed. This release exists so that the git tag and the published version finally describe the same tree — everything below shipped in 0.2.0 on npm despite living under [Unreleased] here.

Tracking

  • tracking — the CLI finally reports to the dashboard it has had since 0.1.0. titah-web’s endpoints, models and pages were built and tested; nothing ever called them, so /dashboard/projects/ stayed empty for everyone who signed in. Metadata only: name, language, git, and the figures titah stats already prints. No file contents, no transcripts, no tool output.
  • Four ways off, widest first: not signed in, tracking.enabled: false globally, the same key in a project’s ./titah.json, or a path in tracking.exclude. The per-project one is free — config merging already did it — and travels with the repo.
  • exclude reuses permission.allowlist’s matcher. One glob dialect in the config, not two.
  • Sent after a turn, at most once per five minutes per project, and never blocking or failing a turn. The debounce lives in SQLite, not memory: titah run is one process per turn, so an in-memory window never fires exactly where a script runs it a hundred times.
  • Silent, succeeding or failing. A “heartbeat sent” line would break --output-format json for the caller. One line per attempt goes to ~/.config/titah/tracking.log, and titah doctor grew a Tracking section naming which switch is in effect.
  • Defaults on when signed in, which is a deliberate exception to “every new axis is off by default”: signing in is itself the opt-in, and demanding a second step leaves the dashboard empty for someone who already took the only step that looks like consent.

Session sync

  • tracking.sync — transcripts reach the dashboard, and it takes two switches: this one and the per-project toggle on the server. A dashboard toggle is remote policy; anyone who reaches the account can flip it and cannot edit a file on your machine.
  • Off by default, unlike tracking.enabled. Signing in is consent to be counted, not consent to be read.
  • Prompts, answers, and the order of tool names. Never tool arguments, never tool output, never reasoning. Tool output is where secrets live, and a secret filter you can rely on does not exist — one that catches AKIA… and misses an internal token is worse than none, because it manufactures confidence.
  • 32 KB per message (the number already used for tool output) and 512 KB per transcript. Over that the oldest messages go, and a marker says how many: a silently truncated transcript looks complete.
  • The server toggle is learned from the heartbeat response, which already returned it. Without that the only way to know would be attempting an upload and being refused — one wasted request per turn, for the life of a project that never turns it on. A 403 also switches the stored flag off, so it stops trying until told otherwise.
  • Only sessions that get a turn after you switch it on go up. No backfill.
  • titah doctor reports which of the switches is holding it.

Fixed

  • Session.session_id on titah-web was max_length=36 while a Titah session id is ses_ + a UUID = 40 characters. It would have failed on the first real upload; the column is 64 now. Trimming the prefix to fit would have been the wrong direction — a trimmed id matches nothing the user can look up locally.
  • The tracking request timeout went from 5s to 10s. Measured: a warm request finishes in ~200ms, but the first HTTPS connection on a cold machine (DNS plus TLS) exceeded five seconds and failed silently.
  • bus.subscribe grew client: false for observers. listenerCount decides auto-deny with no client attached (Q17), and it counted every subscriber — so a purely observing subscriber would have made titah run in CI stop auto-denying and hang waiting for an answer nobody could give.

[0.2.0] — 2026-08-19

Two days of closing the distance to the other terminal agents, measured against opencode 1.18.4 and Claude Code 2.1.233 rather than guessed. Six gaps were written down; all six are closed here.

Nothing in 0.1.0 changed shape. Every new axis is off or absent by default, except the turn budget — which is derived from a number you already declared.

Structured output

  • titah run --output-format json — one object at the end, carrying ok, agent, model, text, tools (with both status and outcome), usage, and notices. In json modes nothing human touches stdout.
  • --output-format stream-json — one Event per line as it happens, then a final {"type":"result",…}. Deliberately not a new format: it is Titah’s own event union, so a second shape cannot drift from the first.
  • --json-schema <file> — requires the answer to be JSON matching a subset of JSON Schema (type, required, properties, items, enum). Unknown keywords are skipped, not failed. Asked for in the prompt and checked locally, because most openai-compatible endpoints have no native structured output.
  • Exit codes split: 0 done, 1 the turn failed, 2 the answer was the wrong shape. The fixes are different, so the codes are too.

Cost and limits

  • titah stats [--since 7d] [--all] — tokens and cost by model and by day. Titah has recorded per-turn usage since the first version and never had a way to read it back; 30.2M tokens were sitting in one database unreadable.
  • provider.<p>.models.<m>.price — per 1M tokens, declared, never guessed. Titah ships no price table: prices change by region and contract, and a stale table produces numbers that look official and are wrong. Unpriced models still have their tokens counted and are named separately rather than counted free.
  • limits.turnTokens — a token budget for one turn, replacing step count as the real bound. Defaults to 5× the model’s contextWindow. Declaring a window also lifts the step ceiling from 40 to 200.
  • limits.sessionTokens — a budget across every turn in a session. Checked before a turn starts, never mid-turn: a turn stopped halfway leaves edited files and unrun tests.
  • limits.continueTurns — continue automatically after a limit while the plan still has unchecked - [ ] items. A loop of bounded turns, not one unbounded turn: a fresh turn starts on a clean transcript and re-reads the whole plan. Default 0.

Hooks

  • hooks."tool.before" and hooks."tool.after" — shell commands at the same hook points plugins already use, for rules that do not deserve an npm package. match is a regex over the tool name; the event arrives on stdin as JSON.
  • tool.before refuses the call on a non-zero exit, and a hook that fails to run or hangs also refuses. tool.after cancels nothing but appends its stderr to the tool output, so the model learns the formatter failed.
  • titah hooks list names invalid regexes explicitly — a hook silent because its pattern is broken looks exactly like a hook that did not match.

Background turns

  • titah run --bg detaches and returns the terminal immediately.
  • titah bg list / logs <id> [-f] / stop <id>. There is no stored status column: the state is asked of the operating system on every read. logs -f stops on its own when the turn ends. stop kills the whole process group.

Web client

  • titah web starts the server and opens a browser client. No new routes — it uses exactly the API the TUI uses, embedded as a string with no build step.
  • Permission requests and questions are answered on the page. Without that it would only be a reader, and every turn touching a file would hang.
  • / serves HTML to browsers and JSON to everything else; /health stays JSON.

Sandbox

  • sandbox.bash — runs bash commands under Seatbelt (macOS) or bubblewrap (Linux). Reads stay free; writes are confined to the project and temp. This closes the one hole the permission axes cannot: bash never passes through the delete axis.
  • Fails closed. Enabled where no sandbox exists, bash is refused rather than run unfenced. titah doctor reports which state you are in.
  • Off by default. Verified on macOS/seatbelt; the bubblewrap path is written but has not been run on Linux yet.

Agents, prompts, and defaults

  • delegation (ask | auto | always | never) plus four prompt fixes. Delegation was measured at 1 call in 5 before, 2 in 2 with always after — the tool inventory never listed task, and build said to work “directly”.
  • Sub-agents inherit the parent’s permission instead of falling back to global. build-auto used to promise no confirmations and then ask for ls through every sub-agent it spawned.
  • agent.<id>.effort (low | medium | high) — how much closing analysis an answer ends with. Unset means the model decides. Cycle it live with ctrl+r.
  • Every answer now ends with a conclusion: what changed, what is verified, what is still open. Skipped where there is nothing to conclude.
  • build and build-auto ask before decisions that shape everything after — which datastore, how auth works, REST or GraphQL — with a recommendation and the reason drawn from the code just read. Measured 3/3 on a datastore choice and 0/3 on a trivial one.
  • Turns that stop at a limit say so, to the model and to you. Measured: 13.2% of real turns were hitting the old 20-step wall silently.
  • instructions accepts a string, { "path": … }, or an array of either, and missing files are created on the first turn of a session. A starter AGENTS.md is written for projects with no instruction file at all.
  • scaffold: false turns all of that off.

Fixed

  • TITAH.md lost to AGENTS.md in the same directory. The flat result list was reversed, which also reversed the order within a directory — so the file whose whole purpose is to override was read first and therefore lost.
  • titah run dropped every session.notice: no case for it, so context-window warnings, loop detection, and limit notices were invisible outside the TUI.
  • The TUI’s step spinner and the running-step glyph now differ in kind, not just shape, so two animations on screen are told apart without being read.
  • Answers are marked with the agent that produced them, and the working indicator names the agent actually running — which is not always the one the footer shows after you press Tab mid-turn.

Notes

  • Linux and macOS. Windows is still unsupported, and the sandbox is macOS-only in practice until the bubblewrap path is exercised.
  • Requires Node.js ≥ 22.6.

[0.1.0] — 2026-08-19

First release. Titah works as a full coding agent, with delegation to other agent editors as the thing that sets it apart.

Agent & tools

  • Streaming agent loop on AI SDK v7, with OpenAI-compatible providers as first-class citizens.
  • Read tools: read, list, glob, grep. All filesystem access is confined to the session working directory.
  • Change tools: edit (exact string replace, must be unique, fails hard), write, bash (timeout + cancel).
  • Tool output above 32 KB is written to tool-output/; the context only ever receives the head plus a pointer.

Permissions & undo

  • Deny by default for edit/write/bash, with a per-session allowlist, a config allowlist, and an --auto mode.
  • With no client connected, permission is auto-denied — never hanging, never silently allowing.
  • A git snapshot is taken in a shadow repo before the first change of each turn. titah undo reverts the whole turn, including deleting files it created. Your own .git is never touched.

Context management

  • Compaction runs automatically, between turns and mid-turn, so a single long agentic turn cannot overflow the window on its own.
  • Bounded by the model’s declared contextWindow. Undeclared means compaction is off for that model, said out loud once per session and listed by titah doctor — a wrong window is more dangerous than no window, because a provider does not reject an oversized request, it truncates the oldest part and the model then answers confidently about what it can no longer see.
  • Old tool output is pruned before anything is summarised — free, and usually enough. Sub-agent (task) results are exempt from ordinary pruning: re-running one costs a whole nested turn, unlike re-reading a file.
  • The summariser’s own prompt is bounded as well. A transcript larger than the summariser’s window is summarised in chunks and the chunk summaries summarised in turn, rather than being silently truncated by the provider.
  • Tunable: compaction.auto, reserved, tailTurns, prune. reserved is capped at a quarter of the window, so the 8192 default does not swallow an 8k local model’s entire budget.
  • /compact runs the same machinery on demand, and /compact <focus> keeps the named material in full detail.

Sub-agents

  • Titah’s own model can dispatch several of its configured agents in parallel inside one turn, with the task tool or /tim <task>.
  • Dispatch depth is capped at exactly one level: a sub-agent never receives task, so nothing can spawn a tree that burns quota with no way to stop it.
  • Readers run concurrently; writers are serialised per working directory, so two sub-agents never race over the same shadow-git snapshot.
  • An agent’s delegate routes it through an external CLI instead of Titah’s own loop — the same engine @claude uses, reached from task.
  • A live panel (ctrl+x then ↓) shows each sub-agent’s state and can cancel one without killing the coordinator’s turn.

Modes

  • Three modes ship built in: Plan (refuses every change), Build Manual (confirm each step, the default), and Build Auto (no confirmations).
  • Tab switches modes, same key as opencode.
  • Permissions can be overridden per agent, not just globally — that is what separates Build Auto from Build Manual.

Interface

  • Ink TUI on the alternate screen buffer, with opencode’s default keybindings (ctrl+x leader). Esc cancels a turn without corrupting history.
  • Opening screen centres the prompt under an ASCII logo, like opening nvim. After the first prompt the layout switches to info panel, history, and a prompt pinned to the bottom.
  • Header panel shows the Titah mark beside session information, dropped automatically on short terminals.
  • /session resumes a saved session and /new starts a fresh one, both from the palette or by typing.
  • Command palette on Ctrl+P, with nested menus: picking /model lists the models from your config, and picking one switches the session model.
  • Autocomplete popups while typing: @ for agents and files, / for commands.
  • ↑/↓ scroll the history.
  • Assistant answers render as markdown: headings, bold, inline code, fenced code blocks, lists, and quotes. Your own prompts stay raw, and underscores are never treated as emphasis so snake_case survives intact.
  • A spinner with elapsed time sits right above the prompt while work is running.
  • HTTP + SSE server; the TUI is just one client, curl is another.
  • titah run for headless and scripted use.

Delegation

  • @claude and @opencode as ordinary prompts. The answer joins your conversation; the full transcript goes to tool-output/.
  • External sessions are mapped, so the next @claude resumes the same conversation.
  • External tokens and cost are shown separately from Titah’s own.
  • Config-driven registry: adding a third agent touches no code.

Consensus, agents, commands, skills

  • /consensus fans one question out to every external agent in parallel, then synthesises and flags where they disagree.
  • Custom agents (prompt + tool filter + permission override + model override, plus delegate to run one through an external CLI instead).
  • Custom commands (/review src/a.ts) with {{.Input}} placeholders.
  • Markdown skills from skills.paths, in two supported layouts — and discovered from the Claude Code and opencode registries (~/.claude, ~/.config/opencode), so skills you already have work without configuration.
  • Skills are both passive (catalogued in the system prompt, loaded by the model with the skill tool) and active (/<skill-name> inserts one yourself).
  • Per-agent steps limits, and a text answer forced when the limit is reached rather than the misleading “the model stopped without giving an answer”.

Account (optional)

  • titah login signs a machine in through the browser, using the Device Authorization Grant (RFC 8628) rather than a loopback redirect — because a coding agent is very often run over SSH or in a container, where a redirect to 127.0.0.1 reaches the loopback interface of the wrong machine.
  • The first run on a new machine asks once: sign in, or continue without an account. The answer is recorded, so it is never asked again — and “without an account” is a real answer, because nothing in the agent needs one.
  • A non-interactive run is never asked and never has a choice recorded for it, and a failed sign-in never stops the session.
  • titah whoami and titah doctor --probe verify against the server rather than trusting the file on disk, which is the only way to notice a token revoked from the dashboard. titah logout revokes remotely, then deletes locally whether or not the server could be reached.
  • /login, /logout and /account do the same from inside the TUI; Esc cancels a sign-in that is waiting.
  • The token lives in account.json at mode 0600, kept separate from auth.json: one holds your provider keys, the other your identity, and mixing them would let titah auth remove take your login with it.
  • The server is chosen from $TITAH_ACCOUNT_SERVER, then account.server, then the default — and a token records which server issued it, so changing servers asks you to sign in again instead of silently reusing one that means nothing.

Setup & maintenance

  • titah init detects keys from the environment and probes local endpoints before asking anything. --yes for a non-interactive path.
  • titah sessions prune sweeps database rows, orphaned tool-output blobs, and orphaned snapshot repos in one command.
  • titah doctor checks environment, config, credentials, and external agents.

Notes

  • Credentials are never written to the config; they live in auth.json with mode 0600, or are referenced through ${env:VAR}.
  • Linux and macOS. Windows is not supported yet.
  • Requires Node.js ≥ 22.6.