minicode

Internal & Arsitektur (untuk kontributor)

UI Render Pipeline — jalur stream ke terminal

Dokumen ini hasil audit stabilitas UI/UX terminal 2026-09-18 (baseline v0.9.27). Sumber kebenaran perilaku tetap docs/TERMINAL_CONTRACT.md (FROZEN); dokumen ini memetakan IMPLEMENTASI aktual per tahap beserta kontrak event dan temuan audit. Angka test di sini diukur di Windows + Bun 1.4.2, suite penuh hijau.

Konvensi: file:line menunjuk simbol yang dimaksud. Aturan repo tetap berlaku: cli/ boleh impor src/ui/ dan src/; src/ non-ui DILARANG impor src/ui/; src/ui/ DILARANG impor cli/, src/ non-ui, #minicore (dijaga test/ui-boundary.test.ts).

#1. Jalur pipeline (9 tahap)

Tahap A — provider stream mentah: src/providers/router.ts (fallback + provider:text/extension/tool_call/finish), src/providers/guards.ts:32 (timeout + text-cap + stream-guard tunggal), adapter anthropic.ts, responses.ts, effort.ts. Dirakit di src/app/provider-layer.ts.

Tahap B — kernel emit ke session.events: vendor/minicore/src/core/loop.ts (turn:started, provider:text per chunk, provider:extension reasoning/usage/error, context:compacted reason pressure:*/recovery, step:started/completed), executor.ts (execution:started/completed), session.ts (turn:completed HANYA di jalur sukses).

Tahap C — raw runtime/content contract: src/ui/contract.ts:33 UiEvent masih mencerminkan bentuk event kernel untuk streaming konten dan transport. Ledger, status, lifecycle, dan keputusan user-facing melalui PresentationEvent + PresentationPolicy dari cli/setup.ts; attachSimpleLogger dan attachTurnStatus tetap memasang listener runtime per turn dan melepasnya di finally agar event telat hening.

Tahap C2 — semantic presentation bridge: cli/setup.ts membuat PresentationAdapter, mengurangi event ke PresentationState lengkap (conversation, reasoning, system, plan, finding, result, diagnostic), menghitung summary dari evidence, dan memuat ulang presentation_events saat resume. Bridge memprojection seluruh DomainEventType melalui toPresentationEvent(); src/presentation/projection.ts menyediakan keputusan policy murni (deskripsi activity/turn, running pin, envelope machine) yang di-inject ke renderer sebagai PresentationPolicy. Raw ledger TUI dan rollback flag sudah dihapus (Phase 8 landed). Lihat docs/OUTPUT_ARCHITECTURE_AUDIT.md dan docs/OUTPUT_EVENT_MODEL.md.

Tahap D — logger utama src/ui/assistant/simple.ts:104: append-only scrollback, tanpa alternate screen. provider:text → sanitizeAnsi PER CHUNK → streamBuffer → flushBuf (line-buffered) → flushLine (fence-state + decorateMarkdown + formatWrapped) → stdout. Jawaban bisa minimize (buffer 1MB + marker, dibuka via /expand).

Tahap E — garis status transient src/ui/assistant/turn-status.ts: satu baris stderr untuk shell/linier, hidup hanya di fase sunyi (latch turnOn && !textOn), hilang saat teks mengalir, hidup lagi saat tool berikut. endTurn() deterministik dari driver finally (kernel tak emit completed saat gagal/abort). TUI fullscreen tidak memakai painter transient ini karena alternate screen dimiliki TuiApp.

Tahap F — arbitrator src/ui/runtime/statusline.ts: satu pemilik transient (turn vs spinner); tulis asing saat painter aktif dikomit (clear → tulis → repaint) sehingga diagnostik tak hilang; overlap = signal, bukan crash. paintWrite tak pernah melempar (fail-closed Bun Windows).

Tahap G — status bar src/ui/footer.ts dirender src/ui/tui/app.ts sebagai baris dasar frame fullscreen. TuiApp memiliki clock activity 200ms untuk menggambar ✦, timer HH.MM.SS di sebelah sparkle, dan dots reasoning ... → .. → . tanpa label Working/Thinking/Running. Timer redup saat idle dan putih saat aktif. Composer hanya menampilkan minicode › setelah Prompt tampil saat boot/idle, hilang saat busy, dan kembali otomatis setelah turn selesai; aksi edit/histori tetap dapat membuka composer. Bullet separator footer dihapus; field dipisahkan dengan spasi dan warna. Satu baris kosong memisahkan composer dari footer; layout normal dan popup memakai helper geometri yang sama. TuiApp mengaktifkan mouse tracking ?1002h + ?1006h selama TUI; wheel menggeser viewport transkrip, drag kiri membuat selection app-level, dan Ctrl+C menyalin selection melalui DI/OSC52. Popup menurunkan ke mode ?1000h; tracking dimatikan pada quit, release, dan sinyal fatal. Footer/chrome lama (src/ui/runtime/chrome.ts, DECSTBM scroll-region, MINICODE_FOOTER) DIHAPUS bersama lapisan TUI lama — alternate screen menggantikannya; nol byte di non-TTY. Footer baca session.contextTokens kernel (bukan spend kumulatif).

Tahap H — primitif render src/ui/render/: sanitize.ts (hanya SGR lolos), markdown.ts + highlight.ts (fence-state per baris, konten utuh), width.ts (kolom: CJK/emoji 2, potong aman-SGR), wrap.ts (lebar dibaca per baris → aman resize), theme.ts (getter warna/glyph, jangan di-const), errors.ts (pesan actionable + redact), collapse.ts (buffer /expand 200KB/entry, 500KB total), markdown-table.ts/table-grid.ts (pipe-table model), diff.ts/table.ts/money.ts/format.ts.

Tahap H2 — tabel Markdown model: src/ui/render/markdown-table.ts memvalidasi header/delimiter, escaped pipe, code span, alignment, dan fence; stream block di-buffer sampai batas tabel. table-grid.ts menghitung lebar kolom dan budget terminal, sedangkan linear/TUI me-render semantic block yang sama pada TTY dan mempertahankan source sanitized pada non-TTY. exec --json/ACP tidak melewati renderer ini. Tahap I — view murni vs controller: src/ui/screens/ (props + callback, tanpa IO) digerakkan cli/ (tui.ts, commands.ts, wizard.ts, provider/model-manager.ts, approval/prompt.ts via DI ask, setupWhenEmpty, setSubAgentSessionFactory).

#2. Kontrak Event → UI

Kolom: S = bisa di-stream, D = bisa duplikat, O = bisa out-of-order, I = bisa diinterupsi, L = bisa tiba setelah completion.

EventProducerRepresentasi UISDOIL
assistant textprovider streamstdout wrap + fence 2-spasi; pipe-table-aligned di TTY, source sanitized di non-TTYyatidak (E1)tidakya (abort)tidak (detach flush 1x, E6)
tool callmodel → kernel› nama target muted (compact: diam)tidaktidaktidakyatidak
tool resulttool → executorledger hijau/merah; minimize → + label + buffertidaktidaktidakyatidak
reasoningprovider extensionTUI composer: dots clock tanpa marker transcript; expanded transcript/linear tetap live per baris; minimized tetap buffer untuk /expandyatidaktidakyatidak
thinking effortpicker /modelbadge + level per modeltidaktidaktidaktidaktidak
usageprovider extensionverbose saja; masuk budget watcheryatidakya (tail pasca-finish)yaya (diabaikan aman)
compactionkernel loop── compacted: <reason> kuning, reason verbatimtidaktidaktidaktidaktidak
permission requestexecutor → ask DIheader stdout + askLine y/a/n, fail-closed non-TTYtidaktidaktidakya (deny)tidak
permission deniedexecutorerror tool penuh, tak pernah minimize + reasontidaktidaktidaktidaktidak
sub-agentdelegate_task → bus parentledger › biasa + usage; tanpa view khusustidaktidak (jurnal skip forward)tidakyatidak
error providerprovider extensionpendingError consume-once → satu ✗ oleh drivertidaktidak (E15)tidakyatidak
warningbudget/recovery/verify[budget]/[recovery]/[verify] kuning stderrtidaktidak (warned80 sekali)tidaktidaktidak
completionkernel loopflush + finalizeAnswer + footer idletidaktidaktidaktidaktidak
abortuser/Esc(stopped) + cleanup finallytidaktidaktidak—tidak
timeoutkernel createTimeout✗ timeout + turn dibuang transaksionaltidaktidaktidak—ya (late work ke turnStore privat, dibuang)
budget exceededwatcher/driver[budget] … stopping turn + abort ber-kindtidaktidak (fire-once)tidak—tidak
provider failurerecovery policyretry/compact/throw deterministik; cap retryAfter 30 dtktidaktidaktidaktidaktidak

Mismatch yang dicari, hasil: raw EventBus tetap ada untuk konten/streaming, tetapi keputusan user-facing kini membaca policy yang sama (raw ledger TUI dihapus di Phase 8). Buffer jawaban live TUI dibatasi 1.000.000 karakter; layout paint memakai satu proyeksi transcript dan cache render jawaban. cli/setup.ts:172-376 memproyeksikan seluruh DomainEventType secara exhaustive; TUI memakai describeActivity/matchTurn/elapsedVisible via injeksi, linear memakai deskripsi + activityFor untuk label turn-status, dan machine memakai toMachineEnvelope. bus-debug.ts hanya debug subscription. Lihat docs/OUTPUT_ARCHITECTURE_AUDIT.md OAP-001/OAP-006 dan docs/OUTPUT_IMPLEMENTATION_PLAN.md Phase 3–7. State turunan yang disengaja tetap footer memakai contextTokens (estimasi jendela), sedangkan /status memisahkan Context/Input/Output/Total/Cost/Budget di docs/CONTROL-PLANE-MAP.md.

#3. State machine TUI (tidak eksplisit)

Tidak ada enum state; fase disimpulkan: abort !== null (busy), turnOn/textOn (painter), regionOn/detached (chrome), plus ~14 flag (mode, escTimer, pending, nullStreak, warned80, activeSection, minimize toggles, buffer, pendingError, raw-mode listener). Invarian dijaga finally/detachUI/endTurn, bukan type-state. Transisi ilegal yang terbukti dicegah: painter saat teks mengalir, endTurn ganda, error ganda, +/- di pipe, decoder bocor antar prompt, Tab membajak search. Satu lubang nyata: listener approval askLine menempel DI ATAS onBusyKey selama busy (byte Ctrl+C diterima keduanya) — aman fail-closed (raceAbort → deny) tetapi observabilitas ganda. Lihat §5 temuan O1.

#4. Temuan audit 2026-09-18 (eksperimen di /tmp, repo tak diubah)

F1 HIGH — SGR terbelah antar-chunk merusak output model. sanitizeAnsi diterapkan PER CHUNK (simple.ts:287): ["\x1b[", "32mHello"] tampil literal 32mHello. Bukti: harness E3 out="32mHellom\n". Kelas sama untuk OSC tak-terminasi (menelan sisa chunk). Perbaikan terkecil: tahan ekor escape tak-lengkap di buffer dan sambung ke chunk berikut sebelum sanitasi.

F2 MEDIUM — SGR model bocor mentah ke stdout non-TTY. "\x1b[31mRed\x1b[0m" satu chunk lolos sanitizeAnsi (by-design) lalu ditulis ke pipe apa adanya (out="\u001b[31mRed\u001b[0m\n"), melanggar kontrak non-TTY deterministik. Perbaikan terkecil: strip SGR (atau render via c.*) saat !isTTY.

F3 LOW — buffer thinking /expand membuang kepala diam-diam (thinkingBuf.slice(-200_000) tanpa marker seperti , capped 1MB pada answer). Informasi diagnostik hilang tanpa jejak.

O1 LOW — listener ganda approval vs busy-key (§3). Fail-closed, catat saja.

Status hardening (sesudah audit): F1 diperbaiki via createStreamSanitizer + splitTrailingEscape di src/ui/render/sanitize.ts (dipakai jalur teks model/reasoning/bash di simple.ts, flush deterministik saat completed/detach); F2 via kebijakan stripSgr non-TTY di wOut/wErr + rememberTurn (cerminan colorLevel → stdout.isTTY); F3 via marker … (earlier thinking truncated); O1 didokumentasikan di src/ui/tui/app.ts (busy-freeze + abort, tanpa refactor). Snap kiri/kanan: render pertama sesudah geometri berubah me-reset jangkar relatif askLine (lastGeoCols/Rows di input.ts) — geometri lebar tak dilacak footer karena erase baris-absolut tak sound pasca-reflow (terbukti model + ConPTY: reflow milik konsumen). Dijaga test/ansi-fragmentation.test.ts (termasuk properti semua-titik-belah ≡ whole-string), test/non-tty-output.test.ts, test/thinking-truncation.test.ts, test/ui-combined.test.ts (satu sistem: stream + tool + resize + footer), test/input-resize.test.ts (gagal di kode lama: CUP ke anchor basi).

Batas yang TERVERIFIKASI BENAR (bukti harness + 178 test UI hijau): token-by-token tepat-1x (E1), chunk 50KB utuh (E2), fence terbelah benar termasuk yatim saat abort (E4/E4b), abort flush tepat-1x tanpa bocor ke turn berikut (E6), urutan teks>tool>hasil (E7), permission fail-closed (E8), kontrak budget murni vs gate (E9), tiga reason kompaksi tampil beda (E10), forward sub-agent sebagai ledger biasa (E11), tool 5000-baris dipotong + ANSI jahat dibuang (E12), decoder UTF-8 per-byte + ESC-split + paste 100KB (E13), soak 50 turn tepat-1x 38ms +4.6MB tanpa ESC mentah (E14), error consume-once + format actionable (E15), resize 120→20 mid-stream aman (E5b), 10k events 151k ev/s tepat-1x (E17), CJK/emoji/wrap/truncate/fence (E5), emphasis tak-seimbang lewat literal + tabel konten-tetap (fidelitas).

#5. Residual risk (jujur, belum dibuktikan di audit ini)

Jalur TTY-live (painter denyut, footer sticky DECSTBM, overlay picker, tombol busy) hanya tercakup harness fake-TTY + 178 test; tidak ada sesi TTY nyata di audit ini. Chaos kombinasi (stream+resize+abort serentak di TTY) dan soak 100+ turn multitool belum dijalankan. Sub-agent tanpa view khusus: indukan dibedakan hanya dari nama tool (cukup untuk 1 level, belum diuji visual untuk delegasi bertingkat — yang memang dilarang: tanpa nesting).