hanoman 0.9.20 → 0.9.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/build-info.json +3 -3
- package/dist/cli.js +2822 -253
- package/dist/server.js +15501 -12231
- package/docs/agent-integration.md +199 -23
- package/package.json +1 -1
- package/prisma/migrations/20261012100000_spec_reviewed_at/migration.sql +13 -0
- package/prisma/schema.prisma +6 -0
- package/web/assets/DalangHanomanScreen-Us_5qG25.js +13 -0
- package/web/assets/DalangHanomanScreen-gk7fa9dl.css +1 -0
- package/web/assets/{DocPreviewModal-Chk-jQ51.js → DocPreviewModal-CJP6d0Hx.js} +1 -1
- package/web/assets/DocsWorkspace-CSak7xFX.css +1 -0
- package/web/assets/DocsWorkspace-zGpPd7u3.js +3 -0
- package/web/assets/IdeScreen-CppUucs0.css +10 -0
- package/web/assets/IdeScreen-DTvfuR2n.js +2 -0
- package/web/assets/LeadScreen-CN53yR_e.css +1 -0
- package/web/assets/LeadScreen-DWbunLkQ.js +1 -0
- package/web/assets/ReviewScreen-Ch9UiKlx.js +1 -0
- package/web/assets/SchedulerScreen-zIUh-Vbe.js +1 -0
- package/web/assets/SettingsScreen-CLMuCJ6R.js +62 -0
- package/web/assets/SettingsScreen-DQZ3K43g.css +1 -0
- package/web/assets/TeamScreen-CV1R5Ekk.js +1 -0
- package/web/assets/TeamScreen-CtkpFpCF.css +1 -0
- package/web/assets/TerminalScreen-DrgZKlw2.css +1 -0
- package/web/assets/TerminalScreen-QVjhjrxx.js +2 -0
- package/web/assets/TriageScreen-B6_KrS6l.css +1 -0
- package/web/assets/TriageScreen-Dpj4YrBQ.js +1 -0
- package/web/assets/VpsScreen-BzXpc3le.css +1 -0
- package/web/assets/VpsScreen-DX_HEquM.js +2 -0
- package/web/assets/index-CL0_zz87.css +32 -0
- package/web/assets/index-DmnwLncG.js +1012 -0
- package/web/assets/live-B193WtZm.js +1 -0
- package/web/assets/sched-lead-shared-BjMO8Q_k.js +1 -0
- package/web/assets/scheduler-CQt-VP9-.css +1 -0
- package/web/assets/settings-rebase-BI7-Bk98.js +1 -0
- package/web/index.html +10 -3
- package/web/assets/DalangHanomanScreen-DqzYitwv.js +0 -1
- package/web/assets/DocsWorkspace-DuNVIjTg.js +0 -3
- package/web/assets/IdeScreen-BZGVyFT5.js +0 -2
- package/web/assets/IdeScreen-BfC0goYb.css +0 -10
- package/web/assets/LeadScreen-35VgddUo.js +0 -1
- package/web/assets/ReviewScreen-EkG8ywTU.js +0 -1
- package/web/assets/SchedulerScreen-ClaLLB8Y.js +0 -1
- package/web/assets/SettingsScreen-liOlVCeT.js +0 -62
- package/web/assets/TeamScreen-Dzl6Ruju.js +0 -1
- package/web/assets/TerminalScreen-CK3SzzgP.js +0 -2
- package/web/assets/TriageScreen-EtBRNg-t.js +0 -1
- package/web/assets/VpsScreen-9qeTHo1w.js +0 -2
- package/web/assets/hnm-blencong-v01-CBBfchr0.webp +0 -0
- package/web/assets/hnm-dalang-six-arms-v01-DeBacuq8.webp +0 -0
- package/web/assets/hnm-hero-rig-arm-ml-v01-Kts3pjTf.webp +0 -0
- package/web/assets/hnm-hero-rig-arm-mr-v01-DQgnE6bD.webp +0 -0
- package/web/assets/hnm-hero-rig-arm-ul-v01-CIAWAxFe.webp +0 -0
- package/web/assets/hnm-hero-rig-arm-ur-v01-C0R0CZnp.webp +0 -0
- package/web/assets/hnm-hero-rig-body-v01-DmbKJJHB.webp +0 -0
- package/web/assets/hnm-wayang-panakawan-v01-1LGv-V8N.webp +0 -0
- package/web/assets/hnm-wayang-project-v01-BG9EZqzn.webp +0 -0
- package/web/assets/hnm-wayang-putri-v01-F6p69kHq.webp +0 -0
- package/web/assets/hnm-wayang-satria-alus-v01-CpfoXKTy.webp +0 -0
- package/web/assets/hnm-wayang-satria-gagah-v01-bLfIYAsv.webp +0 -0
- package/web/assets/index-D5w059cP.js +0 -1063
- package/web/assets/index-DI8I0VA5.css +0 -32
- package/web/assets/live-Bz-jmiWI.js +0 -1
|
@@ -91,28 +91,57 @@ host-mu benar dan masalahnya ada pada token atau master switch.
|
|
|
91
91
|
## 3. Capability
|
|
92
92
|
|
|
93
93
|
Capability berformat `"<domain>:<access>"`, `access ∈ {read, write}`, dan **write meng-implikasikan
|
|
94
|
-
read** pada domain yang sama. Ada **
|
|
95
|
-
(ADR-0155) yang **tak diimplikasikan `:write`**, sehingga totalnya **
|
|
94
|
+
read** pada domain yang sama. Ada **17 domain**; empat di antaranya punya akses KETIGA `danger`
|
|
95
|
+
(ADR-0155) yang **tak diimplikasikan `:write`**, sehingga totalnya **38 capability**. Katalog resmi (dengan label &
|
|
96
|
+
read** pada domain yang sama. Ada **18 domain**; lima di antaranya punya akses KETIGA `danger`
|
|
97
|
+
(ADR-0155, ADR-0193) yang **tak diimplikasikan `:write`**, sehingga totalnya **40 capability**
|
|
98
|
+
(`portal` hanya punya `read` — lihat tabel). Katalog resmi (dengan label &
|
|
96
99
|
deskripsi) tampil di panel **Settings → Akses AI Agent** saat manusia membuat token; endpoint
|
|
97
100
|
katalognya (`GET /api/agent-tokens/capabilities`) bersifat **cookie-only** (lihat §5) — kamu tak
|
|
98
101
|
perlu mengambilnya, cukup rujuk tabel di bawah:
|
|
102
|
+
read** pada domain yang sama. Ada **23 domain** dan **48 capability**; domain yang punya akses KETIGA
|
|
103
|
+
`danger` (`sessions:spawn`, `ide:git`, `backlog:lifecycle`, `vps:exec` — ADR-0155 — dan, sejak
|
|
104
|
+
ADR-0196..0198, `access:admin`, `remote:control`, `relay:exec`) **tak diimplikasikan `:write`**.
|
|
105
|
+
Katalog resmi (dengan label & deskripsi) tampil di panel **Settings → Akses AI Agent** saat manusia
|
|
106
|
+
membuat token; endpoint katalognya (`GET /api/agent-tokens/capabilities`) kini terbaca dengan
|
|
107
|
+
`tokens:read` — selain itu cukup rujuk tabel di bawah:
|
|
99
108
|
|
|
100
109
|
| Domain | Cakupan endpoint | Catatan |
|
|
101
110
|
|---|---|---|
|
|
102
111
|
| `projects` | `/api/projects*` | project, branch, binding, Help Center |
|
|
103
112
|
| `backlog` | `/api/specs*` | spec/backlog, dokumen, review diff, integrate |
|
|
104
|
-
| `sessions` | `/api/terminal*` (+ WS terminal) | jalankan sesi agen/shell, kirim input
|
|
113
|
+
| `sessions` | `/api/terminal*` (+ WS terminal) | jalankan sesi agen/shell, kirim input — **high-risk (RCE)**. Sub-pohon `/dialog` **bukan** domain ini sejak ADR-0190: lihat `decisions`. `steer` ke sesi yang sedang menampilkan dialog juga digerbangi `sessions:decide` |
|
|
105
114
|
| `docs` | `/api/prds*`, `/api/projects/:id/{docs,prds}*` | dokumen SoT project & PRD |
|
|
106
115
|
| `ide` | `/api/projects/:id/{tree,file,file-diff,working-status,graph,commit,git,status,stashes,remotes,compare,archive,pr-url}*` | tree/file working tree, operasi git |
|
|
107
116
|
| `vps` | `/api/vps*` | kelola VPS, audit, harden, konsol — **high-risk (remote exec)** |
|
|
108
|
-
| `settings` | `/api/settings*`, `/api/config*`, `/api/scheduler*` | setelan instance & config runtime |
|
|
117
|
+
| `settings` | `/api/settings*`, `/api/config*`, `/api/scheduler*` kecuali `/api/scheduler/crons*` | setelan instance & config runtime |
|
|
109
118
|
| `support` | `/api/tickets*`, `/api/github-issues*`, `/api/projects/:id/github*` | tiket Help Center & issue GitHub (triase) |
|
|
110
119
|
| `notifications` | `/api/notifications*` | notifikasi |
|
|
111
120
|
| `lead` | `/api/lead*` | minta putusan ke hanoman-lead & baca jejaknya — **`lead:write` bisa menggerakkan sesi** (ADR-0091) |
|
|
112
121
|
| `agents` | `/api/custom-agents*` | katalog custom agent global & per project — **`agents:write` mengubah apa yang dilihat SETIAP sesi baru** (ADR-0094) |
|
|
113
122
|
| `skills` | `/api/skills*` | skill global hanoman, user, plugin (baca-saja) & per project, termasuk isi berkasnya — **`skills:write` pada skill global hanoman mengubah instruksi SETIAP sesi baru di semua project** |
|
|
114
123
|
| `telegram` | `/api/telegram*` kecuali sub-path kredensial | context/memory/reply/audit kanal operator Telegram (ADR-0096) |
|
|
124
|
+
| `qa` | `/api/projects/:id/qa*`, `/api/qa*` | laporan QA per project: test case, temuan, lampiran |
|
|
125
|
+
| `activity` | `/api/session-results*` | log aktivitas lintas device (stage, commit, PR); **`activity:write` = purge yang menyeberang ke SEMUA mesin lewat sync** (ADR-0188) |
|
|
126
|
+
| `portal-chat` | `/api/portal-chat*` | chat portal klien sisi OPERATOR: sesi, PRD draft, ekspor transkrip — berisi **email & percakapan klien**; `portal-chat:write` menulis `docs/prd/<slug>.md` di checkout (ADR-0189). Beda dari `/api/portal*` yang tetap cookie-only |
|
|
127
|
+
| `telegram` | `/api/telegram*` kecuali sub-path kredensial | context/memory/reply/audit kanal operator Telegram (ADR-0096). Sub-path **kredensial** (`/api/telegram/settings`, `/api/telegram/test`, `/api/telegram/credentials`) menuntut capability `danger` **`telegram:admin`** (ADR-0193) — tak diimplikasikan `telegram:write` maupun `settings:write`, sehingga token gateway Telegram sendiri tetap tak menjangkaunya. **Anti-eskalasi (403):** token gateway efektif tak boleh memegang capability yang tak dimiliki token pemanggil |
|
|
128
|
+
| `webhooks` | `/api/webhooks*` | webhook keluar: endpoint, putar secret, ping, riwayat & ulang pengiriman (ADR-0194). **`webhooks:write` menentukan ke mana data workspace mengalir keluar**; secret penandatanganan tampil **sekali** saat dibuat/diputar. `allowPrivate` (SSRF ke jaringan internal) tetap keputusan manusia — agent token ditolak 403 bila menyalakannya |
|
|
129
|
+
| `clients` | `/api/client-accounts*` | akun login portal klien: daftar, buat, ubah akses project/nonaktifkan, reset password, hapus (ADR-0194). `clients:write` **membuka login baru ke data project** |
|
|
130
|
+
| `portal` | `/api/portal*` (**baca** saja) | lihat portal **atas nama satu akun klien** yang disebut eksplisit lewat `?asClient=<id akun klien>` (ADR-0195); hanya `portal:read`, tak ada `portal:write`. Chat portal memuat percakapan klien |
|
|
115
131
|
| `team` | `/api/tasks*`, `/api/members*` | papan **Tim**: kartu kerja MANUSIA & direktori anggota (ADR-0157). `status` kartu milik manusia — ia **bukan** `stage` backlog. `POST /api/tasks/:id/escalate` melahirkan backlog item dan tetap `team:write` (cermin `POST /api/tickets/:id/accept`) |
|
|
132
|
+
| `qa` | `/api/projects/:id/qa/**`, `/api/qa/**` | laporan QA, test case, temuan, lampiran, ekspor/impor. Method `DELETE` menuntut `qa:delete` (akses `danger`, ADR-0185), bukan `qa:write` |
|
|
133
|
+
| `decisions` | `/api/terminal/sessions/:id/dialog*` (capability `sessions:decide`, akses `danger`) | **gerbang manusia-yang-memutuskan** (ADR-0190, ADR-0192): baca, jawab, dan rebut dialog `AskUserQuestion` sebuah sesi. **Tak diimplikasikan `sessions:write`.** Beri HANYA ke token yang tidak dipakai agen di dalam sesi hanoman — lihat §13 |
|
|
134
|
+
| `scheduler` | `/api/scheduler/crons*` | cronjob per project — **cron = sesi agen berizin penuh yang ditunda** (ADR-0191). Membuat, mengubah prompt/agen/model/effort/jadwal, mengaktifkan, dan menjalankan cron menuntut **`scheduler:write` DAN `sessions:spawn`**; server menjawab 403 `need: sessions:spawn` bila yang kedua tak dicentang |
|
|
135
|
+
| `tokens` | `/api/agent-tokens*` | cetak/ubah/cabut **agent token**. Dulu cookie-only (ADR-0065); ADR-0196 membukanya dengan **pagar tak-eskalasi-diri** (§5b). `tokens:write` **bukan** izin menaikkan hak sendiri |
|
|
136
|
+
| `devices` | `/api/device-tokens*` | **device token** (identitas mesin untuk sync & relay) atas nama akun admin yang menerbitkan agent token ini (ADR-0197). Plaintext sekali-tampil |
|
|
137
|
+
| `access` | `/api/auth/users*`, `POST /api/auth/change-password` | daftar akun (`access:read`); **`access:admin`** (danger) membuat akun admin, menghapus akun, dan menyetel ulang password akun lain — **setara akses penuh instance** |
|
|
138
|
+
| `remote` | `/api/remote-control` | grant kendali jarak jauh mesin ini. `remote:read` membaca; **`remote:control`** (danger) mengubahnya — grant yang menyala membuat hub menjalankan agen di mesin ini (ADR-0165, ADR-0197) |
|
|
139
|
+
| `relay` | `/api/devices/:id/relay/*` | **`relay:exec`** (danger): meneruskan aksi sesi ke mesin LAIN lewat relay hub, atas nama operator — eksekusi lintas mesin (ADR-0165 §4, ADR-0197) |
|
|
140
|
+
| `logs` | `/api/logs*` | log terpusat lintas device: cari, transkrip, retensi (`logs:read`); `logs:write` mengubah retensi & lajur pengiriman log (memendekkan retensi menghapus jejak audit) |
|
|
141
|
+
| `presence` | `/api/presence` | peta sesi hidup di seluruh device operator (`presence:read`, ADR-0147/0197) |
|
|
142
|
+
| `metrics` | `/api/custom-agents/{metrics,invocations}*` | metrik custom agent (`metrics:read`) dan disposisi per invokasi (`metrics:write` — vonis MANUSIA atas mutu agen; ADR-0198) |
|
|
143
|
+
| `sync` | `GET /api/sync/{conflicts,pending}`, `POST /api/sync/now`, `POST /api/sync/conflicts/:entity/:recordId/resolve` | **hanya tiga permukaan UI sync** (ADR-0199): antrean konflik & hapus-tertunda (`sync:read`), pemicu sync manual (`sync:write`), memutuskan konflik (`sync:resolve`, akses `danger` — **membuang data satu sisi tanpa jalan pulang**, tak diimplikasikan `sync:write`). Kanal mesin-ke-mesin (`/api/sync/{pull,bootstrap,push,logs,attachments,qa-attachments,ws,relay}`) tetap bukan milik agen — lihat §5 |
|
|
144
|
+
| `instance` | `POST /api/update/apply`, `POST /api/restart` | satu capability `instance:admin` (akses `danger`, ADR-0200): memasang versi hanoman terbaru dari registry npm dan me-**restart** proses server. Hanya sah bila instance punya supervisor (`canApply` di `GET /api/update`); wajib `{ "confirm": true }` dan balasan 202 dikirim **sebelum** proses keluar |
|
|
116
145
|
|
|
117
146
|
## 4. Aturan gate & kode status
|
|
118
147
|
|
|
@@ -137,6 +166,26 @@ token selalu **403**, apa pun capability-nya, dan tak ada capability yang bisa m
|
|
|
137
166
|
- `/api/auth/*` — kelola user & password
|
|
138
167
|
- `/api/agent-tokens*` — agen tak boleh mencetak/menaikkan token sendiri
|
|
139
168
|
- `/api/device-tokens*`, `/api/sync*` — identitas mesin & sync hub
|
|
169
|
+
- **Tulis** ke `/api/portal*` (`POST …/tickets`, `POST …/chat/sessions`, `POST …/messages`) — itu
|
|
170
|
+
bertindak **sebagai klien** (tiket multipart; chat menghabiskan kuota klien dan tertulis sebagai
|
|
171
|
+
ucapannya): peniruan identitas, bukan capability. Membaca portal sebagai klien tersedia lewat
|
|
172
|
+
capability `portal:read` (ADR-0195)
|
|
173
|
+
|
|
174
|
+
> Dulu cookie-only, kini capability **opt-in** (manusia yang mencentangnya di Settings → Akses AI
|
|
175
|
+
> Agent): `/api/webhooks*` → `webhooks:*` (ADR-0194), `/api/client-accounts*` → `clients:*`
|
|
176
|
+
> (ADR-0194), `/api/telegram/{settings,test,credentials}` → `telegram:admin` (ADR-0193), dan
|
|
177
|
+
> **baca** `/api/portal*` → `portal:read` (ADR-0195). Lihat tabel §3.
|
|
178
|
+
- `/api/auth/{status,setup,login,logout}` — cookie sesi & bootstrap akun pertama (setup token di
|
|
179
|
+
`HANOMAN_HOME`). Agen tak punya sesi cookie, jadi tak ada yang bisa dibungkus bermakna. (Pengelolaan
|
|
180
|
+
akun — `/api/auth/users*` dan `change-password` — **dibuka** lewat `access:*`; lihat §5b.)
|
|
181
|
+
- `/api/sync*` — sync mesin-ke-mesin; kredensialnya Bearer **device token**, bukan agent token
|
|
182
|
+
- `/api/device-tokens*`, `/api/sync*` — identitas mesin & sync hub. **Pengecualian sempit (ADR-0199):**
|
|
183
|
+
tiga permukaan UI sync (`GET /api/sync/conflicts`, `GET /api/sync/pending`, `POST /api/sync/now`) dan
|
|
184
|
+
`POST /api/sync/conflicts/:entity/:recordId/resolve` terbuka lewat domain `sync` (§3). SEMUA sisanya
|
|
185
|
+
— `/api/sync/{pull,bootstrap,push,logs,attachments,qa-attachments,ws,relay/ws}` — adalah kanal
|
|
186
|
+
mesin-ke-mesin ber-**Bearer device-token** yang ditegakkan route-nya sendiri; agent token bukan
|
|
187
|
+
device token, dan membungkusnya berarti menyerahkan kredensial mesin ke agen. `POST /api/ws-tickets`
|
|
188
|
+
(tiket WebSocket sekali-pakai untuk sesi cookie) juga cookie-only
|
|
140
189
|
- `/api/webhooks*` — memegang secret penandatanganan **dan** menentukan ke mana data workspace
|
|
141
190
|
mengalir keluar (ADR-0100)
|
|
142
191
|
- `/api/telegram/settings`, `/api/telegram/test`, `/api/telegram/credentials` — permukaan
|
|
@@ -150,6 +199,15 @@ token selalu **403**, apa pun capability-nya, dan tak ada capability yang bisa m
|
|
|
150
199
|
bisa memalsukan "sesi X bertanya Y" bisa menggerakkan lead atas nama sesi mana pun — itu peniruan
|
|
151
200
|
identitas, bukan capability. Agent token di sini menerima **401** (token sesi tak cocok), bukan
|
|
152
201
|
403: gate cookie mem-bypass path-nya dan route menegakkan tokennya sendiri
|
|
202
|
+
- `/api/terminal/workspace` — tata letak tab/pane **browser** satu akun manusia (revisi optimistik per
|
|
203
|
+
`req.user.id`); agent token tak punya akun, dan menulisnya hanya merusak layout operator
|
|
204
|
+
- Frame siar `presence` di `/api/events/ws` — grup itu **tidak** dikirim ke principal non-cookie,
|
|
205
|
+
walau kanalnya sendiri `GLOBAL_READ`. Agen membaca peta yang sama lewat `GET /api/presence`
|
|
206
|
+
(`presence:read`, §5b). `PUT /api/settings` tetap tak bisa mengubah kunci `remoteControl`/
|
|
207
|
+
`logShipping`/`logRetention`; jalurnya hanya `PUT /api/remote-control` dan `PUT /api/logs/retention`
|
|
208
|
+
- `POST /api/update/apply` dan tulis lain di bawah prefix status (`/api/limits`, `/api/update`,
|
|
209
|
+
`/api/events`, `/api/fs`, `/api/health`, serta — sejak ADR-0187 — `/api/models`, `/api/methods`,
|
|
210
|
+
`/api/codex`) — **baca**-nya terbuka untuk token mana pun, **tulis**-nya cookie-only
|
|
153
211
|
- `/api/presence` — peta pekerjaan yang sedang berjalan di **seluruh mesin** operator: sesi apa
|
|
154
212
|
hidup di device mana (SPEC-919/ADR-0147). Tak ada capability yang bisa berarti sesuatu untuk
|
|
155
213
|
agregat lintas-mesin, jadi ia cookie-only apa pun method-nya. Frame siar `presence` di
|
|
@@ -164,13 +222,58 @@ token selalu **403**, apa pun capability-nya, dan tak ada capability yang bisa m
|
|
|
164
222
|
mesin mana", dan agen yang bisa membacanya atau memendekkan retensinya bisa menyembunyikan atau
|
|
165
223
|
memalsukan seluruh jejak sesi. Cookie-only apa pun method-nya — beda dari `POST /api/sync/logs`
|
|
166
224
|
(pengiriman batch **dari** klien), yang tetap device-token seperti jalur sync lain
|
|
167
|
-
-
|
|
168
|
-
`/api/
|
|
169
|
-
|
|
225
|
+
- Tulis lain di bawah prefix status (`/api/limits`, `/api/update`, `/api/events`, `/api/fs`,
|
|
226
|
+
`/api/health`) — **baca**-nya terbuka untuk token mana pun, **tulis**-nya cookie-only. Satu-satunya
|
|
227
|
+
pengecualian (ADR-0200): `POST /api/update/apply` dan `POST /api/restart` lewat capability danger
|
|
228
|
+
`instance:admin` (§3), tidak lewat capability lain
|
|
170
229
|
|
|
171
230
|
Route yang tak dikenal peta juga **default cookie-only** (aman): endpoint baru tak pernah terbuka
|
|
172
231
|
karena kelalaian. Endpoint `/api/help*` (Help Center publik) punya otorisasi sendiri (kunci tiket)
|
|
173
|
-
dan tak memakai agent token
|
|
232
|
+
dan tak memakai agent token: ia anonim by design (honeypot + rate-limit dirancang untuk manusia
|
|
233
|
+
pelapor), jadi **tak ada tool MCP** untuknya (ADR-0201) — operator membaca tiket lewat
|
|
234
|
+
`hanoman_tickets_list`/`hanoman_ticket_get` dan URL publik lewat `hanoman_help_center_get`.
|
|
235
|
+
|
|
236
|
+
## 5b. Permukaan admin yang dibuka opt-in (ADR-0196..0198)
|
|
237
|
+
|
|
238
|
+
Sampai ADR-0196..0198, `/api/agent-tokens*`, `/api/device-tokens*`, `/api/auth/users*`,
|
|
239
|
+
`/api/auth/change-password`, `/api/remote-control`, `/api/devices/:id/relay/*`, `/api/logs*`,
|
|
240
|
+
`/api/presence`, dan metrik custom agent cookie-only. Pemilik repo meminta mereka dibungkus, jadi
|
|
241
|
+
keputusan lama itu dibalik **dengan capability baru yang opt-in** (tak ada capability lama yang
|
|
242
|
+
membukanya) dan **pagar yang ditegakkan di server**, bukan hanya di katalog tool:
|
|
243
|
+
|
|
244
|
+
| Hal | Aturan |
|
|
245
|
+
|---|---|
|
|
246
|
+
| Cetak agent token (`POST /api/agent-tokens`) | `capabilities` token baru HARUS himpunan-bagian milik pemanggil (`write ⊇ read`; danger hanya bila dimiliki persis). Selebihnya **403** `{ error, exceeds: [...] }` |
|
|
247
|
+
| Ubah agent token (`PATCH`) | `capabilities` baru ⊆ milik pemanggil, **dan** token target tak boleh lebih berkuasa dari pemanggil (agen lemah tak bisa menonaktifkan/menurunkan token yang manusia beri hak lebih) |
|
|
248
|
+
| Cabut agent token (`DELETE`) | aturan target yang sama |
|
|
249
|
+
| Token sendiri | **tak bisa** diubah atau dicabut oleh dirinya sendiri (403 `{ self: true }`) — minta manusia |
|
|
250
|
+
| Plaintext token | hanya di balasan `POST` (agent maupun device token); tak pernah di daftar atau di log audit; runtime MCP melepas topeng token HANYA untuk hasil sukses dua tool pencetak |
|
|
251
|
+
| Device token | dicetak atas nama akun admin yang menerbitkan agent token pemanggil (rantai `agent:<id>` ditelusuri); tanpa penerbit manusia → **409**. Agen hanya melihat/mencabut device token milik akun itu |
|
|
252
|
+
| Akun | membuat akun admin, menghapus akun, menyetel ulang password akun lain = `access:admin` (danger). Reset password agen: `{ userId, newPassword }` tanpa password lama, mencabut semua sesi akun itu, dan **tidak** menerbitkan cookie. Admin terakhir tetap tak bisa dihapus. `access:admin` setara menyerahkan seluruh instance |
|
|
253
|
+
| Grant jarak jauh (`PUT /api/remote-control`) | `control.capabilities` ⊆ milik pemanggil; mengubah lajur log (data keluar ke hub) menuntut juga `logs:write` |
|
|
254
|
+
| Relay perangkat | `relay:exec` (danger) untuk semua method; aktor ditandai `agent:<id>`; allowlist route ditegakkan mesin tujuan; WebSocket relay tidak tersedia untuk agen |
|
|
255
|
+
| Jejak | setiap aksi admin oleh agent token tercatat di log event (`kind` `admin.*`) tanpa rahasia; perubahan retensi log dicatat **sebelum** nilainya berubah |
|
|
256
|
+
|
|
257
|
+
Tool MCP-nya (semua tulis bermode `danger`, hanya tampil dengan `--danger`):
|
|
258
|
+
|
|
259
|
+
| Tool | Mode | Capability |
|
|
260
|
+
|---|---|---|
|
|
261
|
+
| `hanoman_agent_tokens_list`, `hanoman_agent_token_capabilities` | baca | `tokens:read` |
|
|
262
|
+
| `hanoman_agent_token_create`, `hanoman_agent_token_update`, `hanoman_agent_token_revoke` | danger | `tokens:write` |
|
|
263
|
+
| `hanoman_device_tokens_list` | baca | `devices:read` |
|
|
264
|
+
| `hanoman_device_token_create`, `hanoman_device_token_revoke` | danger | `devices:write` |
|
|
265
|
+
| `hanoman_users_list` | baca | `access:read` |
|
|
266
|
+
| `hanoman_user_create`, `hanoman_user_delete`, `hanoman_user_password_reset` | danger | `access:admin` |
|
|
267
|
+
| `hanoman_remote_control_get` | baca | `remote:read` |
|
|
268
|
+
| `hanoman_remote_control_set` | danger | `remote:control` |
|
|
269
|
+
| `hanoman_device_relay` | danger | `relay:exec` |
|
|
270
|
+
| `hanoman_device_access_request` | danger | `relay:exec` |
|
|
271
|
+
| `hanoman_device_access_requests` | danger | `relay:exec` |
|
|
272
|
+
| `hanoman_logs_search`, `hanoman_log_transcript`, `hanoman_logs_retention_get` | baca | `logs:read` |
|
|
273
|
+
| `hanoman_logs_retention_set` | danger | `logs:write` |
|
|
274
|
+
| `hanoman_presence_get` | baca | `presence:read` |
|
|
275
|
+
| `hanoman_agent_metrics` | baca | `metrics:read` |
|
|
276
|
+
| `hanoman_agent_invocation_disposition` | danger | `metrics:write` |
|
|
174
277
|
|
|
175
278
|
## 6. Endpoint yang paling sering dipakai
|
|
176
279
|
|
|
@@ -184,15 +287,19 @@ dan tak memakai agent token.
|
|
|
184
287
|
| `POST /api/specs` | `backlog:write` | buat backlog item — bentuk payload di §7. |
|
|
185
288
|
| `PATCH /api/specs/:id` | `backlog:write` | ubah item; konten hanya selagi belum dimulai. |
|
|
186
289
|
| `POST /api/specs/:id/done` | `backlog:write` | tandai item **selesai** tanpa menjalankan sesi — untuk pekerjaan yang beres di luar hanoman. Body `{ reason?: string (≤280), confirm?: boolean }`, keduanya opsional; balasannya `Spec` yang sudah `stage:"done"`. Tak menjalankan maupun menghentikan sesi apa pun. |
|
|
290
|
+
| `POST /api/specs/:id/reviewed` | `backlog:write` | tandai atau batalkan **sudah ditinjau** pada item yang sudah **selesai** (tool `hanoman_backlog_mark_reviewed`, ADR-0218). Body `{ reviewed?: boolean }`, default `true`; `false` membatalkan. Idempoten: menandai ulang tak mengubah stempelnya. Balasannya `Spec` dengan `reviewedAt`. 409 bila item belum selesai. Tak mengubah stage, diff, atau berkas. |
|
|
187
291
|
| `GET /api/specs/:id/attachments` | `backlog:read` | lampiran backlog item — gambar & dokumen yang dilampirkan manusia sebagai konteks kerja. |
|
|
188
292
|
| `POST /api/specs/:id/attachments` | `backlog:write` | unggah lampiran (`multipart/form-data`, beberapa berkas per request). Berkas yang ditolak **tak** menggagalkan yang lain — periksa `rejected[]`. |
|
|
189
293
|
| `GET /api/specs/:id/attachments/:attId` | `backlog:read` | byte satu lampiran. |
|
|
190
294
|
| `DELETE /api/specs/:id/attachments/:attId` | `backlog:write` | hapus satu lampiran. |
|
|
191
295
|
| `GET /api/specs/:id/docs` | `backlog:read` | dokumen yang ditulis sesi item itu. |
|
|
192
|
-
| `GET /api/specs/:id/review` | `backlog:read` | diff hasil kerja sesi. |
|
|
296
|
+
| `GET /api/specs/:id/review` | `backlog:read` | diff hasil kerja sesi. Bila belum ada worktree maupun commit: **200** dengan `empty: true` dan `reason` — `no-worktree` (belum pernah dimulai) atau `no-commit` (worktree sudah lenyap tanpa commit); `base`, `files`, dan `changed` kosong. Itu keadaan, bukan galat (ADR-0213). Sesi memakai pola yang sama (ADR-0215). |
|
|
297
|
+
| `DELETE /api/specs/:id/worktree` | `ide:git` (danger) | hapus worktree satu item (tool `hanoman_backlog_worktree_delete`, ADR-0213). Perubahan yang belum di-commit di dalamnya **hilang** dan sesi yang berjalan di sana ditutup; cabang dan commit tetap. Balasan **200** `{ name, ok, cleanup, closedSession?, error? }` — `ok: false` bila worktree tak ditemukan atau tak bisa dihapus. Capability-nya sama dengan `POST /api/projects/:id/worktrees/delete`, bukan `backlog:write`. |
|
|
193
298
|
| `GET /api/projects/:id/docs` | `docs:read` | index Source of Truth project. |
|
|
194
299
|
| `GET /api/projects/:id/docs/<path>` | `docs:read` | isi satu dokumen. |
|
|
195
300
|
| `GET /api/terminal/sessions` | `sessions:read` | sesi yang sedang hidup. |
|
|
301
|
+
| `GET /api/terminal/sessions/:id/review` | `sessions:read` | diff worktree sesi project-level (PRD). Bentuknya sama dengan review backlog item. Bila sesi tak punya worktree (`no-worktree`) atau worktree-nya sudah lenyap tanpa commit (`no-commit`): **200** dengan `empty: true` dan `reason`; `base`, `files`, dan `changed` kosong. Itu keadaan, bukan galat (ADR-0215, amandemen ADR-0213). Tool `hanoman_session_review`. |
|
|
302
|
+
| `GET /api/terminal/sessions/:id/review/<path>` | `sessions:read` | isi diff satu berkas. **409** bila tak ada sumber sama sekali (tak ada berkas untuk dibaca); 404 bila path di luar daftar. |
|
|
196
303
|
| `GET /api/notifications` | `notifications:read` | notifikasi. **Tanpa `limit` → 50 teratas**, bukan seluruhnya (lihat jebakan di §10). |
|
|
197
304
|
| `GET /api/tickets` | `support:read` | tiket Help Center. |
|
|
198
305
|
| `GET /api/lead/decisions` | `lead:read` | jejak keputusan hanoman-lead. Menerima `page`/`limit`; `take`/`skip` lama tetap jalan. |
|
|
@@ -250,9 +357,11 @@ capability-nya. Capability menjawab "boleh?", bukan "sebaiknya?".
|
|
|
250
357
|
| `POST /api/terminal/sessions` | melahirkan proses agen `--dangerously-skip-permissions` di sebuah worktree — **RCE efektif**. Batas satu-satunya adalah isolasi git worktree (ADR-0037). |
|
|
251
358
|
| `POST`/`PUT`/`DELETE` di bawah `/api/vps` | **remote exec** di server produksi. |
|
|
252
359
|
| `POST /api/lead/decisions` | putusannya bisa **menggerakkan sesi** (integrate ke main, menghentikan sesi) dan selalu melahirkan baris jejak permanen (ADR-0091/0098). |
|
|
360
|
+
| `POST /api/update/apply`, `POST /api/restart` | memasang paket npm dan **me-restart server** (ADR-0200): dashboard & koneksi MCP putus beberapa detik; pane tmux selamat. Selalu `confirm: false` dulu untuk membaca jumlah sesi hidup. |
|
|
361
|
+
| `POST /api/sync/conflicts/:entity/:recordId/resolve` | **membuang data satu sisi** (lokal atau hub) tanpa jalan pulang (ADR-0199). Baca `differs` dari `hanoman_sync_conflicts` dulu. |
|
|
253
362
|
|
|
254
|
-
Perlakukan `POST /api/specs/:id/integrate`, `DELETE /api/specs/:id`,
|
|
255
|
-
disiplin yang sama:
|
|
363
|
+
Perlakukan `POST /api/specs/:id/integrate`, `DELETE /api/specs/:id`, `DELETE /api/specs/:id/worktree`,
|
|
364
|
+
dan perubahan `stage` dengan disiplin yang sama: keempatnya mengubah sejarah git atau membuang pekerjaan. `POST /api/specs/:id/done`
|
|
256
365
|
tak menyentuh git, tetapi ia **menyatakan pekerjaan orang lain selesai** — pakai hanya bila kamu punya
|
|
257
366
|
buktinya, dan tulis buktinya di `reason`.
|
|
258
367
|
|
|
@@ -432,7 +541,20 @@ Panduan siap salin untuk keempat klien, berikut tabel tool → capability, ada d
|
|
|
432
541
|
| `hanoman_lead_decisions_list` | baca | `lead:read` |
|
|
433
542
|
| `hanoman_backlog_create`, `hanoman_backlog_update` | tulis | `backlog:write` |
|
|
434
543
|
| `hanoman_notifications_mark_read` | tulis | `notifications:write` |
|
|
544
|
+
| `hanoman_notification_mark_read` | tulis | `notifications:write` — satu notifikasi berdasarkan `id` (ADR-0204), idempoten |
|
|
545
|
+
| `hanoman_scheduler_queue_cancel`, `hanoman_scheduler_queue_requeue` | tulis | `settings:write`; requeue baris `failed` juga menuntut `sessions:spawn` (ADR-0204, 403 bila tak ada) |
|
|
435
546
|
| `hanoman_lead_ask` | tulis | `lead:write` |
|
|
547
|
+
| `hanoman_health`, `hanoman_models`, `hanoman_codex_version`, `hanoman_methods_status` | baca | — (`GLOBAL_READ`, ADR-0187) |
|
|
548
|
+
| `hanoman_activity_list` | baca | `activity:read` |
|
|
549
|
+
| `hanoman_activity_purge` | **berbahaya** | `activity:write` |
|
|
550
|
+
| `hanoman_portal_admin_chat_sessions`, `hanoman_portal_admin_chat_session_get` | baca | `portal-chat:read` |
|
|
551
|
+
| `hanoman_portal_admin_chat_export` | **berbahaya** | `portal-chat:read` |
|
|
552
|
+
| `hanoman_portal_admin_chat_prd_write` | **berbahaya** | `portal-chat:write` |
|
|
553
|
+
| `hanoman_sync_conflicts`, `hanoman_sync_pending` | baca | `sync:read` |
|
|
554
|
+
| `hanoman_sync_now` | tulis | `sync:write` |
|
|
555
|
+
| `hanoman_sync_conflict_resolve` | **danger** | `sync:resolve` |
|
|
556
|
+
| `hanoman_update_apply`, `hanoman_instance_restart` | **danger** | `instance:admin` |
|
|
557
|
+
| `hanoman_agent_guide` | baca | — (membaca halaman ini lewat `GET /api/agent-integration.md`, publik) |
|
|
436
558
|
|
|
437
559
|
### Yang sengaja TIDAK tersedia lewat MCP
|
|
438
560
|
|
|
@@ -442,18 +564,70 @@ penghapusan backlog, dan perubahan `stage`. Batasan ini ada di katalog toolnya,
|
|
|
442
564
|
token yang punya `sessions:write` sekalipun tak akan menemukan tool untuk memakainya. Lihat §8.
|
|
443
565
|
|
|
444
566
|
**Menjawab dialog sesi** (`GET /api/terminal/sessions/:id/dialog`,
|
|
445
|
-
`POST /api/terminal/sessions/:id/dialog/answer`,
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
567
|
+
`POST /api/terminal/sessions/:id/dialog/answer`, `POST /api/terminal/sessions/:id/dialog/takeover`)
|
|
568
|
+
dulu sengaja tak punya tool (SPEC-899 · ADR-0142: agen yang bisa menjawab `AskUserQuestion` bisa
|
|
569
|
+
menjawab pertanyaannya sendiri, dan gerbang "manusia terakhir yang memutuskan" runtuh). Sejak
|
|
570
|
+
**ADR-0190/ADR-0192** pemilik membungkusnya sebagai tool `hanoman_session_dialog_get|answer|takeover`
|
|
571
|
+
(semuanya `danger`, hanya muncul dengan `--danger`) — dengan gerbang di **server**, bukan di
|
|
572
|
+
deskripsi:
|
|
573
|
+
|
|
574
|
+
- capability **`sessions:decide`** (domain `decisions`), terpisah dari `sessions:write`; `steer` ke
|
|
575
|
+
pane yang sedang menampilkan dialog pun menuntutnya;
|
|
576
|
+
- **403** bila token ITU yang meluncurkan sesinya (`reason: "launcher"`), atau bila panggilan datang
|
|
577
|
+
dari dalam sesi hanoman — proses MCP yang diwarisi `HANOMAN_SESSION_ID` mengirim header
|
|
578
|
+
`x-hanoman-session` (`reason: "self"` / `"in-session"`);
|
|
579
|
+
- **409** `deciding` (hanoman-lead sedang menyusun keputusan; rebut dulu lewat `takeover`) dan
|
|
580
|
+
`answering` tetap berlaku. 403 mendahului 409 supaya penolakan tak membocorkan keadaan lead.
|
|
581
|
+
|
|
582
|
+
**Batas yang jujur:** server tak bisa membuktikan siapa di balik sebuah request Bearer — sesi tidak
|
|
583
|
+
menerima agent token dari hanoman, jadi tak ada ikatan token↔sesi. Header `x-hanoman-session`
|
|
584
|
+
bisa dilepas oleh klien yang sengaja memakai token yang sama lewat `curl`. Yang menutupnya adalah
|
|
585
|
+
praktik: **jangan pasang token ber-`sessions:decide` di konfigurasi yang dibaca sesi hanoman**
|
|
586
|
+
(mis. `~/.claude.json` tingkat-user diwarisi setiap sesi). Pakai token terpisah, di klien MCP yang
|
|
587
|
+
dijalankan manusia di luar sesi.
|
|
588
|
+
|
|
589
|
+
**Cronjob** (`/api/scheduler/crons*`, ADR-0191 membalik ADR-0112) kini punya tool
|
|
590
|
+
`hanoman_scheduler_crons_list|cron_runs` (baca, `scheduler:read`) dan
|
|
591
|
+
`hanoman_scheduler_cron_create|update|delete|run` (`danger`, `scheduler:write`). Karena sebuah cron
|
|
592
|
+
adalah `POST /terminal/sessions` yang ditunda, **create, update yang menyentuh prompt/agen/model/
|
|
593
|
+
effort/jadwal atau mengaktifkan, dan run** juga menuntut `sessions:spawn` — ditegakkan di handler
|
|
594
|
+
server (403 `need: sessions:spawn`), bukan hanya disebut di deskripsi. Mengganti nama, menonaktifkan,
|
|
595
|
+
dan menghapus cron cukup dengan `scheduler:write`.
|
|
596
|
+
|
|
597
|
+
**Lampiran, ekspor/impor QA, unggah IDE, dan arsip git** punya tool sejak ADR-0183 — lihat
|
|
598
|
+
[Transfer berkas](#transfer-berkas-adr-0183) di bawah. Yang tetap tak punya tool hanya WebSocket
|
|
599
|
+
(`/terminal/sessions/:id/ws`, `/events/ws`) dan tiga route `dialog` di atas.
|
|
600
|
+
|
|
601
|
+
### Transfer berkas (ADR-0183)
|
|
602
|
+
|
|
603
|
+
Tool MCP berbicara JSON teks, jadi **byte tak pernah lewat model**. Satu mekanisme seragam:
|
|
604
|
+
|
|
605
|
+
- **Unggah** — argumen `filePath` / `filePaths` / `files[].filePath` adalah path **lokal di mesin
|
|
606
|
+
tempat `hanoman mcp` berjalan**. Klien membacanya dan mengirimnya sebagai multipart.
|
|
607
|
+
- **Unduh** — byte disimpan ke `savePath` lokal (default: direktori sementara aman); jawabannya
|
|
608
|
+
`{savedTo, filename, bytes, mime, sha256}`. `overwrite:true` wajib untuk menimpa berkas yang ada.
|
|
609
|
+
Teks kecil (template Markdown, ekspor `md`/`csv`) dikembalikan **inline** oleh
|
|
610
|
+
`hanoman_qa_template_md` dan `hanoman_qa_report_export_text`.
|
|
611
|
+
- **Batas (ADR-0184)** — path hanya boleh di dalam direktori kerja MCP, direktori sementara MCP,
|
|
612
|
+
atau `HANOMAN_MCP_TRANSFER_DIRS`; `..`, symlink yang keluar, dan berkas rahasia (`.env`,
|
|
613
|
+
kunci SSH, `.git`, …) ditolak di kedua arah; ukuran dibatasi (default 100 MiB).
|
|
614
|
+
- **Mode (ADR-0186)** — tool yang menyentuh disk klien bermode `write` walau capability-nya `:read`,
|
|
615
|
+
sehingga `--read-only` tak pernah menyentuh disk.
|
|
616
|
+
|
|
617
|
+
| Tool | Mode | Capability |
|
|
618
|
+
|---|---|---|
|
|
619
|
+
| `hanoman_backlog_attachment_upload`, `hanoman_backlog_attachment_download` | tulis | `backlog:write` / `backlog:read` |
|
|
620
|
+
| `hanoman_ticket_attachment_download` | tulis | `support:read` |
|
|
621
|
+
| `hanoman_qa_attachment_upload`, `hanoman_qa_import`, `hanoman_qa_cases_import` | tulis | `qa:write` |
|
|
622
|
+
| `hanoman_qa_attachment_download`, `hanoman_qa_template_xlsx`, `hanoman_qa_report_export_file` | tulis | `qa:read` |
|
|
623
|
+
| `hanoman_qa_template_md`, `hanoman_qa_report_export_text` (inline) | baca | `qa:read` |
|
|
624
|
+
| `hanoman_ide_upload` | tulis | `ide:write` |
|
|
625
|
+
| `hanoman_ide_archive_download` | tulis | `ide:read` |
|
|
626
|
+
| `hanoman_session_attachment_upload` (PNG/JPEG/WebP ≤ 5 MB; jawabannya path di SERVER) | tulis | `sessions:write` |
|
|
627
|
+
| `hanoman_qa_report_delete`, `hanoman_qa_case_delete`, `hanoman_qa_finding_delete`, `hanoman_qa_attachment_delete` | **berbahaya** | `qa:delete` (ADR-0185) |
|
|
628
|
+
|
|
629
|
+
`qa:delete` adalah akses `danger` yang **tak diimplikasikan `qa:write`**: token lama ber-`qa:write`
|
|
630
|
+
tak lagi bisa menghapus data QA sampai manusia mencentang `qa:delete` di Settings → Akses AI Agent.
|
|
457
631
|
|
|
458
632
|
### Opsi
|
|
459
633
|
|
|
@@ -462,6 +636,8 @@ ber-`backlog:read`/`backlog:write` — yang tak dipajang adalah **tool**-nya.
|
|
|
462
636
|
| `HANOMAN_HOST` / `--host <url>` | **Wajib.** Instance yang dituju. Agent token diterbitkan per-instance — token dari instance lain selalu 401 di sini, dan MCP server menjelaskannya, bukan meneruskan 401 telanjang. |
|
|
463
637
|
| `HANOMAN_AGENT_TOKEN` | **Wajib.** Hanya dari env atau `~/.hanoman/agent-token` — **tak pernah** dari argumen baris perintah (ARGV terbaca proses lain di mesin yang sama). |
|
|
464
638
|
| `HANOMAN_MCP_READ_ONLY=1` / `--read-only` | Menyembunyikan seluruh tool tulis dari `tools/list`. |
|
|
639
|
+
| `HANOMAN_MCP_TRANSFER_DIRS` / `--transfer-dir <d>` | Direktori tambahan (dipisah `:`) yang boleh dibaca/ditulis tool transfer berkas, di luar direktori kerja MCP dan direktori sementara MCP. Akar filesystem ditolak. |
|
|
640
|
+
| `HANOMAN_MCP_TRANSFER_MAX_BYTES` / `--transfer-max-bytes <n>` | Plafon ukuran per berkas dan total per panggilan transfer. Default 100 MiB, maks 2 GiB. |
|
|
465
641
|
| `HANOMAN_MCP_MAX_BYTES` / `--max-bytes <n>` | Plafon ukuran balasan tool. Default 24576. Balasan yang dipotong ditandai `truncated: true` + `shown`/`total`. |
|
|
466
642
|
|
|
467
643
|
Skema tool berversi (`MCP_TOOL_SCHEMA_VERSION`, saat ini **1**) dan aditif dalam satu versi:
|
package/package.json
CHANGED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
-- ADR-0218 · penanda "sudah ditinjau" pada item selesai (filter Backlog "Perlu ditinjau").
|
|
2
|
+
--
|
|
3
|
+
-- Ditulis tangan (bukan `migrate dev`), pola SPEC-516/SPEC-804: worktree tetangga membuat
|
|
4
|
+
-- `migrate dev` me-reset DB saat ada drift. ADITIF murni — satu kolom NULLABLE tanpa default.
|
|
5
|
+
ALTER TABLE "Spec" ADD COLUMN "reviewedAt" DATETIME;
|
|
6
|
+
|
|
7
|
+
-- Backfill sekali-jalan: item yang SUDAH selesai dianggap sudah ditinjau pada `updatedAt`-nya.
|
|
8
|
+
-- Tanpa ini setiap item selesai yang pernah dikerjakan sesi (baseSha terisi) langsung masuk
|
|
9
|
+
-- "Perlu ditinjau" begitu migrasi jalan, dan filter yang sejak hari pertama berisi ratusan baris
|
|
10
|
+
-- tak lagi bisa dipercaya. Harganya diterima dan ditulis di ADR-0218: penanda hanya berlaku untuk
|
|
11
|
+
-- penyelesaian SETELAH migrasi ini. Item lama yang belum pernah ditinjau tetap bisa dibuka lewat
|
|
12
|
+
-- Review perubahan, hanya saja tidak lagi disorot oleh filter.
|
|
13
|
+
UPDATE "Spec" SET "reviewedAt" = "updatedAt" WHERE "stage" = 'done' AND "reviewedAt" IS NULL;
|
package/prisma/schema.prisma
CHANGED
|
@@ -75,6 +75,12 @@ model Spec {
|
|
|
75
75
|
// ditandai manual. Ditimpa tiap penandaan berikutnya: ia menjelaskan keadaan yang BERLAKU;
|
|
76
76
|
// riwayat transisi stage tinggal di SessionResult (ADR-0047). Ikut FIELDS.spec sync.
|
|
77
77
|
manualDone Json?
|
|
78
|
+
// ADR-0218 · kapan operator menandai item SELESAI ini sudah ditinjau ("Tandai ditinjau"). null =
|
|
79
|
+
// belum ditinjau, atau item belum selesai. Ditulis HANYA oleh `POST /specs/:id/reviewed`, dan
|
|
80
|
+
// di-reset ke null saat stage dikembalikan mundur dari `done` (PATCH stage, POST source). SENGAJA
|
|
81
|
+
// tak di-reset oleh recordCompletion: reconcile memanggilnya ulang untuk item yang sudah selesai,
|
|
82
|
+
// dan itu akan menghapus tanda yang baru dipasang. Ikut FIELDS.spec sync (ADR-0218).
|
|
83
|
+
reviewedAt DateTime?
|
|
78
84
|
// SPEC-447 · ADR-0093 · id spec lain yang harus SELESAI & commit-nya sudah ada di branch basis
|
|
79
85
|
// sebelum item ini boleh diluncurkan. Array JSON of string; null/[] = tak bergantung apa pun.
|
|
80
86
|
// Sengaja kolom, bukan tabel join: SQLite melarang scalar list, `Json` sudah dipakai `payload`,
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import{j as n,u as B,I as M,l as w,S as R,F as S}from"./index-DmnwLncG.js";import{R as C}from"./react-DfGhg7ZW.js";import"./markdown-CWEY34jk.js";import"./xterm-3VOAfa_q.js";function $({height:a,color:s,rod:t}){return n.jsxs("svg",{viewBox:"0 0 64 128",width:a*64/128,height:a,"aria-hidden":"true",focusable:"false",children:[n.jsx("path",{d:"M31.2 74 L30.2 127 L33.8 127 L32.8 74 Z",fill:t}),n.jsx("path",{d:"M51 79 L50.2 122 L53 122 L52.4 79 Z",fill:t}),n.jsx("path",{d:`M32 73
|
|
2
|
+
C 24 76, 19 88, 17.5 103 C 17 106, 20 107, 24 106.4
|
|
3
|
+
C 28 106, 30 104, 32 104 C 34 104, 36 106, 40 106.4 C 44 107, 47 106, 46.5 103
|
|
4
|
+
C 45 88, 40 76, 32 73 Z`,fill:s}),n.jsx("path",{d:"M32 74 C 29.5 66, 29.5 60, 31 53 L 37 53 C 38.5 60, 37.5 67, 34.5 74 Z",fill:s}),n.jsx("path",{d:`M31 53
|
|
5
|
+
C 28.5 48, 29 43.5, 31.5 40.5
|
|
6
|
+
C 28 38.5, 25 34, 25.5 28.5
|
|
7
|
+
C 26 22, 30 16.5, 36 15
|
|
8
|
+
C 34 20, 34.5 24, 37.5 27.5
|
|
9
|
+
C 41 31.5, 44.5 33.5, 46.5 37.5
|
|
10
|
+
C 48 40.5, 47 43.5, 44.5 44.5
|
|
11
|
+
C 46 46.5, 46.5 49, 45 51.5
|
|
12
|
+
C 43 54.5, 38 55, 34.5 53.8 Z`,fill:s}),n.jsx("path",{d:`M35 55 C 40 58, 44.5 63, 47.5 69 C 49.5 73, 51 76, 51.8 79.5 L 49.4 80.5
|
|
13
|
+
C 47 76.5, 44.5 72.5, 41.5 68.5 C 38.5 64.5, 35.5 61, 33 58.5 Z`,fill:s})]})}const N=a=>!!a.decision&&!a.deciding,y={wait:{color:"var(--amber-500)",rod:"var(--brass-300)"},run:{color:"var(--brass-400)",rod:"var(--brass-300)"},off:{color:"var(--term-dim)",rod:"var(--term-line)"}};function g({tone:a,name:s,fase:t,sesi:d,aksi:o,onClick:h}){return n.jsxs("button",{type:"button",className:`hn-dlg-row hn-dlg-row--${a}`,onClick:h,children:[n.jsx("span",{className:"hn-dlg-glyph","aria-hidden":"true",children:n.jsx($,{height:24,color:y[a].color,rod:y[a].rod})}),n.jsx("span",{className:"hn-dlg-name",children:s}),n.jsx("span",{className:"hn-dlg-fase",children:t}),d&&n.jsx("span",{className:"hn-dlg-sesi",children:d}),n.jsx("span",{className:"hn-dlg-aksi",children:o})]})}function K(a,s){return a?{tone:"ok",text:"Terhubung"}:s?{tone:"idle",text:"Jeda · tab tersembunyi"}:{tone:"err",text:"Koneksi terputus · menyambung ulang"}}function A({projects:a,backlog:s,sessions:t,onOpenSession:d,onOpenProject:o,onGoto:h,onExit:v,previous:l}){const u=B(),m=l?l.onBack:v,j=C.useRef(m);j.current=m,C.useEffect(()=>{const e=i=>{i.key==="Escape"&&j.current()};return window.addEventListener("keydown",e),()=>window.removeEventListener("keydown",e)},[]);const r=t.filter(e=>!e.exited).sort((e,i)=>e.id.localeCompare(i.id)),L=new Set(r.map(e=>e.projectId)),c=r.filter(N),x=r.filter(e=>!N(e)),p=a.filter(e=>!L.has(e.id)),I=new Map(s.map(e=>[e.id,e])),f=new Map(a.map(e=>[e.id,e.name])),b=e=>{const i=e.specId?I.get(e.specId):void 0;return i?w(R,i.stage):e.flow?w(S,e.flow):"Sesi terminal"},E=[r.length===0?"Tidak ada sesi berjalan":`${r.length} sesi berjalan`,c.length>0?`${c.length} menunggu jawabanmu`:null,`${a.length} project`].filter(Boolean).join(" · "),k=K(u.connected,u.paused);return n.jsxs("div",{className:"hn-dlg",children:[n.jsxs("header",{className:"hn-dlg-top",children:[n.jsxs("div",{className:"hn-dlg-head",children:[n.jsx("h2",{className:"hn-dlg-title",children:"Dalang Hanoman"}),n.jsx("p",{className:"hn-dlg-summary",children:E}),n.jsxs("p",{className:`hn-dlg-link hn-dlg-link--${k.tone}`,role:"status",children:[n.jsx("span",{className:"hn-dlg-dot","aria-hidden":"true"}),k.text]})]}),n.jsxs("div",{className:"hn-dlg-actions",children:[n.jsx("button",{type:"button",className:"hn-dlg-btn",onClick:()=>h("terminal"),children:"Buka terminal"}),n.jsx("button",{type:"button",className:"hn-dlg-exit",onClick:m,"aria-label":l?`Kembali ke ${l.label}`:"Keluar dari panggung (Esc)",title:l?`Kembali ke ${l.label}`:"Keluar dari panggung (Esc)",children:n.jsx(M,{name:l?"arrow-left":"x",size:18})})]})]}),c.length>0&&n.jsxs("section",{className:"hn-dlg-section","aria-labelledby":"dlg-wait",children:[n.jsx("h3",{id:"dlg-wait",className:"hn-dlg-rt",children:"Menunggu jawabanmu"}),n.jsx("ul",{className:"hn-dlg-list",children:c.map(e=>n.jsx("li",{children:n.jsx(g,{tone:"wait",name:f.get(e.projectId)??e.projectId,fase:b(e),sesi:e.specId??"",aksi:"Jawab",onClick:()=>d(e.id)})},e.id))})]}),n.jsxs("section",{className:"hn-dlg-section","aria-labelledby":"dlg-prj",children:[n.jsx("h3",{id:"dlg-prj",className:"hn-dlg-rt",children:"Project"}),a.length===0?n.jsxs("div",{className:"hn-dlg-empty",children:[n.jsx("div",{children:"Panggung sunyi — belum ada project di workspace."}),n.jsx("div",{className:"hn-dlg-empty-hint",children:"Tambahkan project dan sang dalang mengangkat wayangnya."})]}):x.length+p.length>0&&n.jsxs("ul",{className:"hn-dlg-list",children:[x.map(e=>n.jsx("li",{children:n.jsx(g,{tone:"run",name:f.get(e.projectId)??e.projectId,fase:b(e),sesi:e.specId??"",aksi:"Buka terminal",onClick:()=>d(e.id)})},e.id)),p.map(e=>n.jsx("li",{children:n.jsx(g,{tone:"off",name:e.name,fase:"Tidak ada sesi",sesi:"",aksi:"Buka project",onClick:()=>o(e)})},e.id))]})]})]})}export{A as DalangHanomanScreen};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
.hn-dlg{position:fixed;top:0;right:0;bottom:0;left:0;z-index:100;overflow-y:auto;overscroll-behavior:contain;background:var(--term-bg);color:var(--term-fg);padding:calc(var(--space-5) + var(--safe-top)) var(--space-6) calc(var(--space-4) + var(--safe-bottom))}.hn-dlg-top{display:flex;flex-wrap:wrap;align-items:flex-start;justify-content:space-between;gap:var(--space-3) var(--space-4)}.hn-dlg-head{flex:1 1 260px;min-width:0}.hn-dlg-title{margin:0;font-family:var(--font-display);font-size:var(--text-2xl);font-weight:var(--weight-semibold);line-height:var(--leading-tight);color:var(--term-fg)}.hn-dlg-summary{margin:var(--space-1) 0 0;font-size:var(--text-md);color:var(--term-fg)}.hn-dlg-link{display:inline-flex;align-items:center;gap:var(--space-2);margin:var(--space-1) 0 0;font-size:var(--text-xs);color:var(--term-dim)}.hn-dlg-dot{flex:none;width:7px;height:7px;margin:0;border-radius:50%;background:var(--term-dim)}.hn-dlg-link--ok .hn-dlg-dot{background:var(--leaf-500)}.hn-dlg-link--err .hn-dlg-dot{background:var(--clay-500)}.hn-dlg-actions{display:flex;align-items:center;gap:var(--space-2);flex:0 0 auto}.hn-dlg-btn{-webkit-appearance:none;-moz-appearance:none;appearance:none;min-height:var(--touch-target);padding:0 var(--space-4);display:inline-flex;align-items:center;border:1px solid var(--term-line);border-radius:var(--radius-md);background:none;color:var(--brass-300);font:var(--weight-medium) var(--text-sm)/1 var(--font-ui);cursor:pointer}.hn-dlg-btn:hover{border-color:var(--brass-700)}.hn-dlg-exit{-webkit-appearance:none;-moz-appearance:none;appearance:none;flex:none;width:var(--touch-target);height:var(--touch-target);padding:0;display:inline-flex;align-items:center;justify-content:center;border:1px solid var(--term-line);border-radius:var(--radius-md);background:none;color:var(--term-dim);cursor:pointer}.hn-dlg-exit:hover{color:var(--term-fg);border-color:var(--brass-700)}.hn-dlg-btn:focus-visible,.hn-dlg-exit:focus-visible,.hn-dlg-row:focus-visible{outline:2px solid var(--border-focus);outline-offset:2px}.hn-dlg-section{margin-top:var(--space-6);min-width:0}.hn-dlg-rt{margin:0 0 var(--space-2);font-family:var(--font-mono);font-size:var(--text-2xs);font-weight:var(--weight-medium);letter-spacing:var(--tracking-caps);text-transform:uppercase;color:var(--brass-300)}.hn-dlg-list{list-style:none;margin:0;padding:0;border-top:1px solid var(--term-line)}.hn-dlg-list>li{border-bottom:1px solid var(--term-line)}.hn-dlg-row{-webkit-appearance:none;-moz-appearance:none;appearance:none;width:100%;min-height:var(--touch-target);margin:0;display:flex;flex-wrap:wrap;align-items:center;gap:var(--space-1) var(--space-3);padding:var(--space-2) var(--space-2);border:0;background:none;color:inherit;font:inherit;text-align:left;cursor:pointer}.hn-dlg-row:hover{background:color-mix(in srgb,var(--term-fg) 4%,transparent)}.hn-dlg-glyph{flex:0 0 24px;display:inline-flex;justify-content:center}.hn-dlg-name{flex:1 1 160px;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-family:var(--font-mono);font-size:var(--text-sm);font-weight:var(--weight-medium);color:var(--term-fg)}.hn-dlg-fase{font-size:var(--text-xs);color:var(--term-fg)}.hn-dlg-sesi{font-family:var(--font-mono);font-size:var(--text-xs);color:var(--term-dim)}.hn-dlg-aksi{margin-left:auto;font-size:var(--text-xs);font-weight:var(--weight-medium);color:var(--brass-300);white-space:nowrap}.hn-dlg-row--wait .hn-dlg-aksi{color:var(--amber-500)}.hn-dlg-row--off .hn-dlg-name,.hn-dlg-row--off .hn-dlg-fase,.hn-dlg-row--off .hn-dlg-aksi{color:var(--term-dim)}.hn-dlg-empty{margin-top:var(--space-6);font-size:var(--text-md);color:var(--term-fg)}.hn-dlg-empty-hint{margin-top:var(--space-1);font-size:var(--text-sm);color:var(--term-dim)}@media (max-width: 767px){.hn-dlg{padding-inline:var(--page-gutter-x, var(--space-4))}.hn-dlg-aksi{margin-left:0}}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{j as e,M as r,
|
|
1
|
+
import{j as e,M as r,aO as d,aQ as a,m as x}from"./index-DmnwLncG.js";import"./react-DfGhg7ZW.js";function f({path:i,text:o,eyebrow:n,download:t,onClose:s}){const l=i.slice(i.lastIndexOf("/")+1)||i;return e.jsx(r,{open:!0,title:l,eyebrow:n??i,icon:"book-open",onClose:s,width:980,fillHeight:!0,children:e.jsxs("div",{style:{display:"flex",flexDirection:"column",height:"100%",minHeight:0},children:[t&&e.jsx("div",{style:{display:"flex",justifyContent:"flex-end",paddingBottom:6,borderBottom:"1px solid var(--border-hair)",marginBottom:8},children:e.jsx(d,{href:t})}),e.jsx("div",{"data-testid":"doc-preview-scroll",style:{flex:"1 1 0",minHeight:0,overflow:"auto",padding:"0 4px 8px"},children:o?e.jsx(a,{text:o,name:i}):e.jsx(x,{kind:"empty",icon:"file-text",title:"Berkas kosong",hint:i})})]})})}export{f as D};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
.hn-docs-index{display:flex;flex-direction:column;min-width:0;min-height:0;height:100%;background:var(--surface-card);border:1px solid var(--border-hair);border-radius:var(--radius-lg);overflow:hidden}.hn-docs-index-head{display:flex;align-items:center;justify-content:space-between;gap:var(--space-2);padding:var(--space-3) var(--space-3);border-bottom:1px solid var(--border-hair);min-width:0;flex-wrap:wrap}.hn-docs-index-actions{display:flex;align-items:center;gap:var(--space-2)}.hn-docs-index-tools{display:flex;flex-direction:column;gap:var(--space-2);padding:var(--space-2) var(--space-3) var(--space-3);border-bottom:1px solid var(--border-hair)}.hn-docs-cov{align-self:flex-start;font-family:var(--font-mono);font-size:var(--text-xs);color:var(--text-muted);background:var(--bone-200);border-radius:var(--radius-pill);padding:2px 10px}.hn-docs-tree-scroll{flex:1 1 auto;min-height:0;overflow:auto;padding:var(--space-1) var(--space-2) var(--space-2)}.hn-docs-tree{display:flex;flex-direction:column;min-width:0}.hn-docs-group{display:flex;flex-direction:column;gap:1px;padding-bottom:2px}.hn-docs-node{display:flex;align-items:center;gap:var(--space-2);width:100%;box-sizing:border-box;min-height:34px;padding:6px 8px;border-radius:var(--radius-sm);cursor:pointer;text-align:left;color:var(--text-body)}.hn-docs-node:hover{background:var(--bone-200)}.hn-docs-node:focus-visible{outline:2px solid var(--border-focus);outline-offset:-2px}.hn-docs-node-dir{font-weight:var(--weight-medium)}.hn-docs-dir-label{font-family:var(--font-mono);font-size:var(--text-sm);color:var(--text-strong);font-weight:var(--weight-medium)}.hn-docs-node-file.is-selected{background:var(--brass-100)}.hn-docs-file-label{font-family:var(--font-mono);font-size:var(--text-xs);color:var(--text-body);overflow-wrap:anywhere}.hn-docs-file-label.is-unlinked{color:var(--text-muted)}.hn-docs-node-file.is-selected .hn-docs-file-label{color:var(--brass-700);font-weight:var(--weight-semibold)}.hn-docs-unscored{margin-top:var(--space-2);padding-top:var(--space-2);border-top:1px solid var(--border-hair)}.hn-docs-unscored-label{padding:4px 8px;color:var(--text-subtle)}.hn-docs-results{display:flex;flex-direction:column;gap:1px}.hn-docs-result{display:flex;align-items:center;gap:var(--space-2);width:100%;box-sizing:border-box;min-height:34px;padding:6px 8px;border:0;border-radius:var(--radius-sm);background:transparent;color:var(--text-body);font:inherit;text-align:left;cursor:pointer;min-width:0}.hn-docs-result:hover{background:var(--bone-200)}.hn-docs-result.is-selected{background:var(--brass-100)}.hn-docs-result:focus-visible{outline:2px solid var(--border-focus);outline-offset:-2px}.hn-docs-result .hn-prj-mono{font-size:var(--text-xs);overflow-wrap:anywhere;min-width:0}.hn-docs-result-kind{margin-left:auto;flex:0 0 auto;font-size:var(--text-2xs);color:var(--text-subtle);text-transform:uppercase;letter-spacing:.3px}.hn-docs-viewer{display:flex;flex-direction:column;min-width:0;min-height:0;height:100%;background:var(--surface-card);border:1px solid var(--border-hair);border-radius:var(--radius-lg);overflow:hidden}.hn-docs-viewer-head{display:flex;align-items:center;gap:var(--space-3);flex:0 0 auto;flex-wrap:wrap;padding:var(--space-3) var(--space-4);border-bottom:1px solid var(--border-hair);min-width:0}.hn-docs-path{font-size:var(--text-sm);color:var(--text-strong);font-weight:var(--weight-medium);overflow-wrap:anywhere;min-width:0}.hn-docs-savebar{position:sticky;top:0;z-index:1;display:flex;align-items:center;gap:var(--space-2);flex-wrap:wrap;flex:0 0 auto;padding:var(--space-2) var(--space-4);background:var(--bone-100);border-bottom:1px solid var(--border-hair)}.hn-docs-error{flex:0 0 auto;padding:var(--space-2) var(--space-4);font-size:var(--text-sm);color:var(--clay-600);border-bottom:1px solid var(--border-hair)}.hn-docs-body{flex:1 1 auto;min-height:0;overflow:auto;padding:var(--space-2) var(--space-6) var(--space-8)}.hn-docs-read{max-width:72ch}.hn-docs-edit{display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1fr);flex:1 1 auto;min-height:0}.hn-docs-edit-pane{display:flex;flex-direction:column;min-height:0;min-width:0}.hn-docs-edit-pane+.hn-docs-edit-pane{border-left:1px solid var(--border-hair)}.hn-docs-edit-label{padding:var(--space-2) var(--space-3);border-bottom:1px solid var(--border-hair);background:var(--bone-100)}.hn-docs-textarea{flex:1 1 auto;width:100%;box-sizing:border-box;resize:none;border:0;min-height:320px;padding:var(--space-4) var(--space-4);font-family:var(--font-mono);font-size:var(--text-sm);line-height:1.7;color:var(--text-body);background:var(--surface-card)}.hn-docs-textarea:focus-visible{outline:2px solid var(--border-focus);outline-offset:-2px}.hn-docs-preview{flex:1 1 auto;overflow:auto;padding:var(--space-1) var(--space-6) var(--space-6);min-height:0}@media (max-width: 767px){.hn-docs-edit{grid-template-columns:minmax(0,1fr)}.hn-docs-edit-pane+.hn-docs-edit-pane{border-left:0;border-top:1px solid var(--border-hair)}.hn-docs-body{padding:var(--space-2) var(--space-4) var(--space-6)}.hn-docs-preview{padding:var(--space-1) var(--space-4) var(--space-4)}}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import{q as Ne,d as De,h as S,j as e,m as w,I as N,U as Se,Y as Ce,B as O,f as Ke,n as q,O as Le,aO as Me,aQ as te,D as $e,aT as ze,i as ie}from"./index-DmnwLncG.js";import{R as o}from"./react-DfGhg7ZW.js";import"./markdown-CWEY34jk.js";import"./xterm-3VOAfa_q.js";function de(n){let t={...n,kids:n.kids.map(de)};for(;!t.cat&&t.kids.length===1;){const i=t.kids[0];t={...i,label:t.label+"/"+i.label}}return t}function re(n){const t={path:"",kids:[]};for(const i of n){if(i.cat==="."){t.kids.push({path:".",label:".",cat:i,kids:[]});continue}let l=t;for(const u of i.cat.split("/")){const k=l.path?l.path+"/"+u:u;let m=l.kids.find(s=>s.path===k);m||(m={path:k,label:u,kids:[]},l.kids.push(m)),l=m}l.cat=i}return t.kids.map(de)}function le(n){const t=n.find(i=>i.scored&&i.linked)??n.find(i=>i.scored)??n[0];return t&&t.files[0]?`${t.cat}/${t.files[0]}`:""}function oe(n){return/(^|\/)adr(s)?(\/|$)/i.test(n)?"ADR":/(^|\/)prd(s)?(\/|$)/i.test(n)?"PRD":/(^|\/)spec(s|ification)?(\/|$)/i.test(n)?"Spec":"Lainnya"}const Fe=["PRD","Spec","ADR","Lainnya"],ue=n=>`dir:${n}`,Pe=(n,t)=>!!t&&t.startsWith(n.path+"/");function he({node:n,depth:t,ctx:i}){var h,p,y;const l=Pe(n,i.selected),[u,k]=o.useState(l);o.useEffect(()=>{l&&k(!0)},[l]);const m=((h=n.cat)==null?void 0:h.scored)??!0,s=((p=n.cat)==null?void 0:p.linked)??!0,v=ue(n.path),b=()=>k(d=>!d),c=d=>{d.key==="Enter"||d.key===" "?(d.preventDefault(),b()):d.key==="ArrowRight"&&!u?(d.preventDefault(),k(!0)):d.key==="ArrowLeft"&&u&&(d.preventDefault(),k(!1))};return e.jsxs(e.Fragment,{children:[e.jsxs("div",{role:"treeitem","aria-level":t+1,"aria-expanded":u,tabIndex:i.tabbable===v?0:-1,className:"hn-docs-node hn-docs-node-dir",style:{paddingLeft:6+t*12},onClick:b,onKeyDown:c,onFocus:()=>i.setFocusKey(v),children:[e.jsx(N,{name:u?"chevron-down":"chevron-right",size:14,color:"var(--text-subtle)"}),e.jsx(N,{name:"folder",size:15,color:m?s?"var(--brass-500)":"var(--clay-500)":"var(--text-subtle)"}),e.jsxs("span",{className:"hn-docs-dir-label",children:[n.label,"/"]}),n.cat&&m&&(n.cat.linked?e.jsx(N,{name:"link",size:13,color:"var(--leaf-600)"}):e.jsx(N,{name:"unlink",size:13,color:"var(--clay-500)"}))]}),u&&e.jsxs("div",{role:"group",className:"hn-docs-group",children:[n.kids.map(d=>e.jsx(he,{node:d,depth:t+1,ctx:i},d.path)),(y=n.cat)==null?void 0:y.files.map(d=>{const f=n.path+"/"+d,j=f===i.selected;return e.jsxs("div",{role:"treeitem","aria-level":t+2,"aria-selected":j,"aria-current":j?"true":void 0,tabIndex:i.tabbable===f?0:-1,className:`hn-docs-node hn-docs-node-file${j?" is-selected":""}`,style:{paddingLeft:18+t*12},onClick:()=>i.onSelect(f),onFocus:()=>i.setFocusKey(f),onKeyDown:g=>{(g.key==="Enter"||g.key===" ")&&(g.preventDefault(),i.onSelect(f))},children:[e.jsx(N,{name:"file-text",size:13,color:j?"var(--brass-700)":"var(--text-subtle)"}),e.jsx("span",{className:`hn-docs-file-label${!m||s?"":" is-unlinked"}`,children:d})]},d)})]})]})}function ce({label:n,nodes:t,selected:i,onSelect:l}){const[u,k]=o.useState(null),m=t[0]?ue(t[0].path):"",v={selected:i,onSelect:l,tabbable:u??(i||m),setFocusKey:c=>k(c)},b=c=>{if(c.key!=="ArrowDown"&&c.key!=="ArrowUp")return;const h=Array.from(c.currentTarget.querySelectorAll('[role="treeitem"]')),p=h.indexOf(document.activeElement);if(p<0)return;c.preventDefault();const y=h[Math.min(h.length-1,Math.max(0,p+(c.key==="ArrowDown"?1:-1)))];y==null||y.focus()};return e.jsx("div",{role:"tree","aria-label":n,className:"hn-docs-tree",onKeyDown:b,children:t.map(c=>e.jsx(he,{node:c,depth:0,ctx:v},c.path))})}function Ie({projectId:n,projectName:t,docStatus:i}){const[l,u]=o.useState([]),[k,m]=o.useState(0),[s,v]=Ne(ze("docs",n),"selected","",$e),[b,c]=o.useState({}),[h,p]=o.useState("preview"),[y,d]=o.useState("tree"),[f,j]=o.useState(""),[g,U]=o.useState(!1),[L,W]=o.useState(!1),[G,C]=o.useState(""),[$,Q]=o.useState(""),[D,z]=o.useState("all"),{confirm:J,dialog:me}=De(),[F,K]=o.useState("loading"),[pe,ke]=o.useState(0);o.useEffect(()=>{let a=!0;return K("loading"),S.getDocs(n).then(r=>{if(!a)return;const x=r.tree;u(x),m(r.coverage),c({}),p("preview"),K("ready"),C(""),Q(""),z("all"),v(le(x))}).catch(()=>{a&&(u([]),v(""),K("error"))}),()=>{a=!1}},[n,pe]),o.useEffect(()=>{if(!s||s in b)return;let a=!0;return S.getDoc(n,s).then(r=>{a&&c(x=>({...x,[s]:r.content}))}).catch(()=>{a&&c(r=>({...r,[s]:null}))}),()=>{a=!1}},[s,n,b]);const P=!!s&&!(s in b),A=s?b[s]===null:!1,fe=()=>c(a=>{const r={...a};return delete r[s],r}),V=a=>D==="all"||oe(a.cat)===D,xe=re(l.filter(a=>a.scored&&V(a))),Y=re(l.filter(a=>!a.scored&&V(a))),B=b[s]??"",be=s.includes("/")?s.slice(0,s.lastIndexOf("/")):".",T=l.find(a=>a.cat===be),_=s,E=h==="edit"&&f!==B,I=o.useMemo(()=>l.flatMap(a=>a.files.map(r=>({key:`${a.cat}/${r}`,scored:a.scored,linked:a.linked,kind:oe(a.cat)}))),[l]),R=$.trim().toLowerCase(),X=R?I.filter(a=>(D==="all"||a.kind===D)&&a.key.toLowerCase().includes(R)):[],ve=o.useMemo(()=>{const a={PRD:0,Spec:0,ADR:0,Lainnya:0};for(const r of I)a[r.kind]+=1;return a},[I]),ye=i==="broken"?"broken":i==="drift"?"drift":"ok";function Z(a){if(!E){p("preview"),a();return}J({title:"Buang perubahan?",eyebrow:s,message:"Isi dokumen yang kamu ubah belum disimpan.",impact:["Perubahan di editor hilang dan tak bisa dikembalikan dari dashboard."],confirmLabel:"Buang perubahan",cancelLabel:"Lanjut mengedit",tone:"danger",icon:"trash-2"}).then(r=>{r&&(p("preview"),a())})}function ge(){C(""),j(B),p("edit")}const ee=()=>Z(()=>{});async function ae(){if(!(L||!s)){W(!0),C("");try{await S.putDoc(n,s,f),c(a=>({...a,[s]:f})),p("preview")}catch(a){C(ie("menyimpan dokumen",a))}finally{W(!1)}}}function H(a){Z(()=>{v(a),p("preview"),d("viewer")})}async function se(){const a=await S.getDocs(n),r=a.tree;u(r),m(a.coverage),r.some(x=>x.files.some(M=>`${x.cat}/${M}`===s))||v(le(r))}async function ne(){if(!g){U(!0);try{await se()}catch{K("error")}finally{U(!1)}}}async function je(){if(!s)return;const a=s;let r=!1;try{r=await J({title:`Hapus ${a}?`,eyebrow:"Source of Truth",message:"Dokumen ini adalah Source of Truth project. Berkas aslinya di disk ikut dihapus.",impact:["Berkas di disk dihapus dan tak bisa dikembalikan dari dashboard.","Kategori indeks dihitung ulang setelah penghapusan."],confirmLabel:"Hapus dokumen",tone:"danger",icon:"trash-2",run:()=>S.deleteDoc(n,a)})}catch(x){C(ie("menghapus dokumen",x));return}if(r){c(x=>{const M={...x};return delete M[a],M});try{await se()}catch{K("error")}}}const we=F==="loading"?e.jsx(w,{kind:"loading",compact:!0,title:"Memuat indeks…"}):F==="error"?e.jsx(w,{kind:"error",compact:!0,title:"Gagal memuat indeks dokumen",hint:t,action:()=>ke(a=>a+1)}):l.length===0?e.jsx(w,{kind:"empty",compact:!0,icon:"folder-open",title:"Belum ada dokumen",hint:"Belum ada Markdown di repo ini.",action:ne,actionLabel:"Muat ulang",actionIcon:"radar"}):R?X.length===0?e.jsx(w,{kind:"empty",compact:!0,icon:"search",title:"Tak ada berkas cocok",hint:`Tak ada yang cocok dengan “${$.trim()}”.`}):e.jsx("div",{role:"list","aria-label":"Hasil pencarian berkas",className:"hn-docs-results",children:X.map(a=>e.jsxs("button",{type:"button","aria-current":a.key===s?"true":void 0,onClick:()=>H(a.key),className:`hn-docs-result${a.key===s?" is-selected":""}`,children:[e.jsx(N,{name:"file-text",size:13,color:"var(--text-subtle)"}),e.jsx("span",{className:"hn-prj-mono",children:a.key}),e.jsx("span",{className:"hn-docs-result-kind",children:a.kind})]},a.key))}):e.jsxs(e.Fragment,{children:[e.jsx(ce,{label:"Berkas dokumen",nodes:xe,selected:s,onSelect:H}),Y.length>0&&e.jsxs("div",{className:"hn-docs-unscored",children:[e.jsx("div",{className:"hn-eyebrow hn-docs-unscored-label",children:"Lainnya (tidak dinilai)"}),e.jsx(ce,{label:"Dokumen tidak dinilai",nodes:Y,selected:s,onSelect:H})]})]});return e.jsxs(e.Fragment,{children:[e.jsx(Se,{ariaLabel:"Panel Dokumen",active:y,onActiveChange:a=>d(a),masterWidth:288,className:"hn-workspace-panels",panels:[{id:"tree",label:"Indeks",className:"hn-panel-flex",content:e.jsxs("aside",{"aria-label":"Indeks dokumen",className:"hn-docs-index",children:[e.jsxs("div",{className:"hn-docs-index-head",children:[e.jsxs("span",{className:"hn-eyebrow",children:["dokumen · ",t]}),e.jsxs("div",{className:"hn-docs-index-actions",children:[e.jsx(Ce,{status:ye,size:"sm"}),e.jsx(O,{size:"sm",variant:"ghost",leftIcon:g?"loader":"radar",onClick:ne,disabled:g,children:g?"Memuat…":"Muat ulang"})]})]}),F==="ready"&&l.length>0&&e.jsxs("div",{className:"hn-docs-index-tools",children:[e.jsx(Ke,{size:"sm","aria-label":"Cari berkas dokumen",leftIcon:"search",value:$,placeholder:"mis. product/prd",onChange:a=>Q(a.target.value),className:"hn-prj-full"}),e.jsxs("div",{role:"group","aria-label":"Kategori dokumen",className:"hn-prj-chips",children:[e.jsx("button",{type:"button",className:"hn-prj-chip hn-touch-target","aria-pressed":D==="all",onClick:()=>z("all"),children:"Semua"}),Fe.map(a=>e.jsxs("button",{type:"button",className:"hn-prj-chip hn-touch-target","aria-pressed":D===a,onClick:()=>z(a),children:[a,e.jsx("span",{className:"hn-prj-chip-count",children:ve[a]})]},a))]}),e.jsxs("span",{className:"hn-docs-cov",title:"Persentase kategori yang terindeks",children:["Cakupan ",Math.round(k),"%"]})]}),e.jsx("div",{className:"hn-docs-tree-scroll",children:we})]})},{id:"viewer",label:"Dokumen",className:"hn-panel-flex",content:e.jsxs("section",{"aria-label":"Isi dokumen",className:"hn-docs-viewer",style:{display:"flex",flexDirection:"column",minHeight:0},children:[e.jsxs("div",{className:"hn-docs-viewer-head",children:[e.jsx(N,{name:"file-text",size:15,color:"var(--text-muted)"}),e.jsx("span",{className:"hn-prj-mono hn-docs-path",children:_}),T&&T.scored&&(T.linked?e.jsx(q,{tone:"ok",size:"sm",icon:"link",children:"terindeks"}):e.jsx(q,{tone:"err",size:"sm",icon:"unlink",children:"tak tertaut"})),e.jsx("span",{className:"hn-grow"}),e.jsxs("div",{role:"group","aria-label":"Mode dokumen",className:"hn-prj-seg",children:[e.jsx("button",{type:"button","aria-pressed":h==="preview",className:"hn-touch-target",onClick:()=>{h==="edit"&&ee()},children:"Baca"}),e.jsx("button",{type:"button","aria-pressed":h==="edit",className:"hn-touch-target",onClick:()=>{h!=="edit"&&ge()},disabled:!s||P||A,children:"Edit"})]}),h==="preview"&&e.jsx(Le,{label:"Aksi untuk dokumen",icon:"ellipsis",items:[{key:"delete",label:"Hapus dokumen…",icon:"trash-2",tone:"danger",disabled:!s,onSelect:()=>{je()}}],children:e.jsx(Me,{href:a=>S.docDownloadUrl(n,s,a),disabled:!s||P||A})})]}),h==="edit"&&e.jsxs("div",{role:"region","aria-label":"Simpan perubahan",className:"hn-docs-savebar",children:[E?e.jsx(q,{tone:"warn",size:"sm",children:"belum disimpan"}):e.jsx("span",{className:"hn-prj-hint",children:"Tak ada perubahan"}),e.jsx("span",{className:"hn-grow"}),e.jsx(O,{size:"sm",variant:"ghost",onClick:ee,disabled:L,children:"Batal"}),e.jsx(O,{size:"sm",leftIcon:"check",onClick:()=>void ae(),loading:L,disabled:L||!E,children:"Simpan"})]}),G&&e.jsx("div",{role:"alert",className:"hn-docs-error",children:G}),h==="preview"?e.jsx("div",{"data-testid":"doc-preview-scroll",className:"hn-docs-body",style:{flex:"1 1 auto",minHeight:0,overflow:"auto"},children:s?P?e.jsx(w,{kind:"loading",title:"Memuat dokumen…",hint:s}):A?e.jsx(w,{kind:"error",title:"Gagal memuat dokumen",hint:s,action:fe}):e.jsx("div",{className:"hn-docs-read",children:e.jsx(te,{text:B,name:s})}):e.jsx(w,{kind:"empty",icon:"file-text",title:"Tidak ada dokumen dipilih",hint:"Pilih file dari indeks di kiri."})}):e.jsxs("div",{className:"hn-docs-edit",children:[e.jsxs("div",{className:"hn-docs-edit-pane",children:[e.jsx("div",{className:"hn-docs-edit-label",children:e.jsx("span",{className:"hn-eyebrow",children:"Markdown"})}),e.jsx("textarea",{"aria-label":`Isi ${_}`,value:f,spellCheck:!1,onChange:a=>j(a.target.value),onKeyDown:a=>{(a.metaKey||a.ctrlKey)&&a.key.toLowerCase()==="s"&&(a.preventDefault(),ae())},placeholder:`# Judul dokumen
|
|
2
|
+
|
|
3
|
+
Paragraf pembuka…`,className:"hn-docs-textarea"})]}),e.jsxs("div",{className:"hn-docs-edit-pane",children:[e.jsx("div",{className:"hn-docs-edit-label",children:e.jsx("span",{className:"hn-eyebrow",children:"Pratinjau langsung"})}),e.jsx("div",{className:"hn-docs-preview",children:e.jsx(te,{text:f,name:s})})]})]})]})}]}),me]})}export{Ie as DocsWorkspace,re as buildTree,oe as docKind,le as firstDoc};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
pre code.hljs{display:block;overflow-x:auto;padding:1em}code.hljs{padding:3px 5px}/*!
|
|
2
|
+
Theme: GitHub
|
|
3
|
+
Description: Light theme as seen on github.com
|
|
4
|
+
Author: github.com
|
|
5
|
+
Maintainer: @Hirse
|
|
6
|
+
Updated: 2021-05-15
|
|
7
|
+
|
|
8
|
+
Outdated base version: https://github.com/primer/github-syntax-light
|
|
9
|
+
Current colors taken from GitHub's CSS
|
|
10
|
+
*/.hljs{color:#24292e;background:#fff}.hljs-doctag,.hljs-keyword,.hljs-meta .hljs-keyword,.hljs-template-tag,.hljs-template-variable,.hljs-type,.hljs-variable.language_{color:#d73a49}.hljs-title,.hljs-title.class_,.hljs-title.class_.inherited__,.hljs-title.function_{color:#6f42c1}.hljs-attr,.hljs-attribute,.hljs-literal,.hljs-meta,.hljs-number,.hljs-operator,.hljs-variable,.hljs-selector-attr,.hljs-selector-class,.hljs-selector-id{color:#005cc5}.hljs-regexp,.hljs-string,.hljs-meta .hljs-string{color:#032f62}.hljs-built_in,.hljs-symbol{color:#e36209}.hljs-comment,.hljs-code,.hljs-formula{color:#6a737d}.hljs-name,.hljs-quote,.hljs-selector-tag,.hljs-selector-pseudo{color:#22863a}.hljs-subst{color:#24292e}.hljs-section{color:#005cc5;font-weight:700}.hljs-bullet{color:#735c0f}.hljs-emphasis{color:#24292e;font-style:italic}.hljs-strong{color:#24292e;font-weight:700}.hljs-addition{color:#22863a;background-color:#f0fff4}.hljs-deletion{color:#b31d28;background-color:#ffeef0}.hn-ide-changes-bar{display:flex;align-items:center;gap:var(--space-2);flex-wrap:wrap;padding:var(--space-2) var(--space-3);border-bottom:1px solid var(--border-hair)}.hn-ide-changes-legend{flex:1 1 200px;min-width:0;font-size:var(--text-xs);color:var(--text-muted)}.hn-ide-changes-hint{margin:0;padding:var(--space-2) var(--space-3);font-size:var(--text-xs);color:var(--text-subtle);border-bottom:1px solid var(--border-hair)}.hn-ide-section-head{display:flex;align-items:center;gap:var(--space-1);padding:var(--space-2) var(--space-2)}.hn-ide-section-head .hn-eyebrow{flex:1 1 auto;min-width:0}.hn-ide-view-toggle{display:inline-flex;align-items:center;justify-content:center;padding:3px;border:none;border-radius:var(--radius-sm);background:transparent;cursor:pointer}.hn-ide-view-toggle[aria-pressed=true]{background:var(--brass-100)}.hn-ide-change-path{flex:1 1 auto;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-family:var(--font-mono);font-size:var(--text-xs);color:var(--text-body)}.hn-ide-change-count{font-family:var(--font-mono);font-size:var(--text-2xs);flex:0 0 auto}.hn-ide-empty-line{padding:var(--space-1) var(--space-2);font-size:var(--text-xs);color:var(--text-subtle)}.hn-ide-scroll{flex:1 1 auto;min-height:0;overflow:auto;padding:var(--space-2)}.hn-ide-files-pane{position:relative;display:flex;flex-direction:column;flex:1 1 auto;min-height:0;min-width:0}.hn-ide-split{position:absolute;top:0;bottom:0;right:-12px;width:12px;z-index:2;display:flex;align-items:center;justify-content:center;cursor:col-resize;touch-action:none}.hn-ide-split:before{content:"";width:2px;height:56px;border-radius:2px;background:var(--border-hair)}.hn-ide-split:hover:before,.hn-ide-split:focus-visible:before{background:var(--brass-500)}.hn-ide-split:focus-visible{outline:2px solid var(--border-focus);outline-offset:-2px;border-radius:4px}.hn-responsive-panels[data-split=false] .hn-ide-split{display:none}.hn-ide-files-bar{display:flex;align-items:center;gap:var(--space-1);flex-wrap:wrap;padding:var(--space-2) var(--space-2);border-bottom:1px solid var(--border-hair)}.hn-ide-files-eyebrow{flex:0 1 auto;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.hn-ide-dest{flex:1 1 auto;min-width:0;text-align:right;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-family:var(--font-mono);font-size:var(--text-2xs);color:var(--text-subtle)}.hn-ide-viewer-bar{display:flex;align-items:center;gap:var(--space-2);flex-wrap:wrap;padding:var(--space-2) var(--space-3);border-bottom:1px solid var(--border-hair)}.hn-ide-viewer-path{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-family:var(--font-mono);font-size:var(--text-sm);color:var(--text-strong)}.hn-ide-seg{display:inline-flex;gap:2px;background:var(--bone-100);border-radius:var(--radius-pill);padding:2px}.hn-ide-seg button{padding:4px 12px;border:none;cursor:pointer;border-radius:var(--radius-pill);font-size:var(--text-xs);background:transparent;color:var(--text-muted);font-weight:400}.hn-ide-seg button[aria-pressed=true]{background:var(--surface-card);color:var(--text-strong);font-weight:600}.hn-ide-graph-row:hover{background:var(--bone-100)}.hn-ide-graph-bar{display:flex;align-items:center;gap:var(--space-3);flex-wrap:wrap;padding:6px var(--space-3);border-bottom:1px solid var(--border-hair);font-size:var(--text-xs)}.hn-ide-graph-view{display:flex;align-items:center;gap:var(--space-3);flex-wrap:wrap;padding:6px var(--space-3);border-bottom:1px solid var(--border-hair);background:var(--surface-sunken)}.hn-ide-menu-group+.hn-ide-menu-group{border-top:1px solid var(--border-hair);margin-top:4px;padding-top:4px}.hn-ide-menu-group-title{padding:6px 10px 2px;font-size:var(--text-2xs);color:var(--text-subtle)}.hn-ide-menu-item:hover,.hn-ide-menu-item:focus-visible{background:var(--bone-200);outline:none}.hn-ide-cabang-bar{display:flex;align-items:center;gap:var(--space-2);flex-wrap:wrap;padding:var(--space-2) var(--space-3);border-bottom:1px solid var(--border-hair)}.hn-ide-cabang-title{flex:1 1 200px;min-width:0}.hn-ide-cabang-opts{display:flex;align-items:center;gap:var(--space-3);flex-wrap:wrap;padding:var(--space-2) var(--space-3);border-bottom:1px solid var(--border-hair);background:var(--surface-sunken)}.hn-ide-cabang-banner{display:flex;align-items:center;gap:var(--space-2);flex-wrap:wrap;padding:var(--space-2) var(--space-3);border-bottom:1px solid var(--border-hair);font-size:var(--text-xs)}.hn-ide-cabang-banner-text{flex:1 1 260px;min-width:0}.hn-ide-cabang-results{padding:var(--space-2) var(--space-3);border-bottom:1px solid var(--border-hair);font-size:var(--text-xs)}.hn-ide-cabang-results-sum{color:var(--text-strong)}.hn-ide-cabang-results-line{color:var(--text-muted);font-family:var(--font-mono)}.hn-ide-cabang-list{max-height:620px;overflow:auto}.hn-ide-cabang-pickall{display:flex;align-items:center;gap:var(--space-2);padding:var(--space-2) var(--space-3);border-bottom:1px solid var(--border-hair)}.hn-ide-cabang-meta{display:flex;align-items:center;gap:var(--space-2);flex-wrap:wrap;min-width:0;font-size:var(--text-xs);color:var(--text-muted)}.hn-ide-cabang-branch{font-family:var(--font-mono);color:var(--brass-700)}.hn-ide-cabang-sha{font-family:var(--font-mono);color:var(--text-subtle)}.hn-ide-cabang-when{font-size:var(--text-2xs);color:var(--text-subtle)}.hn-ide-cabang-link{padding:0;border:0;background:none;cursor:pointer;color:var(--brass-700);font:inherit}.hn-ide-cabang-more{padding:var(--space-2);text-align:center}@media (pointer: coarse),(max-width: 767px){.hn-ide-menu-item{min-height:44px}.hn-ide-split{width:20px;right:-16px}.hn-ide-cabang-list .hn-dense-row button{min-height:44px}}@media (max-width: 767px){.hn-ide-files-bar{flex-wrap:nowrap}.hn-ide-files-eyebrow{display:none}.hn-ide-dest{font-size:var(--text-2xs)}.hn-ide-viewer-bar{gap:var(--space-1)}}@media (prefers-reduced-motion: reduce){.hn-ide-split:before{transition:none}}.hn-ide-screen{min-width:0;display:flex;flex-direction:column;gap:16px}.hn-ide-scroll-body{flex:1 1 auto;min-height:0;overflow:auto}.hn-ide-section-gap{border-top:1px solid var(--border-hair);margin:var(--space-1) 0}.hn-ide-drop{outline:2px dashed var(--brass-500);outline-offset:-4px}.hn-ide-viewer-actions{display:flex;align-items:center;gap:var(--space-2);flex-wrap:wrap}.hn-ide-diff-pad{padding:var(--space-2) 0}.hn-ide-truncated{padding:var(--space-2) var(--space-3);font-size:var(--text-2xs);color:var(--text-subtle)}.hn-ide-source-pre{margin:0;padding:var(--space-3) var(--space-3);font-family:var(--font-mono);font-size:var(--text-xs);line-height:1.6;white-space:pre-wrap;word-break:break-word;color:var(--text-body)}.hn-ide-code-pre{margin:0;padding:var(--space-3);overflow:auto}.hn-ide-code{font-family:var(--font-mono);font-size:var(--text-xs);line-height:1.7}.hn-ide-md-pad{padding:var(--space-3) var(--space-3)}.hn-ide-editor{width:100%;min-height:clamp(240px,50dvh,560px);box-sizing:border-box;resize:vertical;border:none;padding:var(--space-3);font-family:var(--font-mono);font-size:var(--text-xs);line-height:1.7;color:var(--text-body);background:var(--surface-card)}.hn-ide-muted-line{font-size:var(--text-xs);color:var(--text-subtle);margin-bottom:var(--space-2)}.hn-ide-warn-line{font-size:var(--text-xs);color:var(--clay-600);margin-bottom:var(--space-2)}.hn-ide-pre-msg{font-family:var(--font-mono);font-size:var(--text-xs);white-space:pre-wrap;color:var(--text-muted);margin:0 0 var(--space-3)}.hn-ide-conflict-list{margin:0;font-family:var(--font-mono);font-size:var(--text-xs);max-height:220px;overflow:auto}.hn-ide-remote-row{display:flex;align-items:center;gap:var(--space-2);padding:5px 0;border-bottom:1px solid var(--border-hair)}.hn-ide-remote-name{font-family:var(--font-mono);font-size:var(--text-sm);font-weight:600;width:90px}.hn-ide-remote-url{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-family:var(--font-mono);font-size:var(--text-2xs);color:var(--text-muted)}.hn-ide-remote-add{display:flex;gap:var(--space-2);margin-top:var(--space-3)}@media (max-width: 640px){.hn-ide-remote-add{flex-direction:column;align-items:stretch}.hn-ide-remote-add>.hn-ide-input-100{width:100%}}.hn-ide-changes-bar .hn-ide-changes-legend{min-width:0}.hn-ide-head>.hn-ide-toolbar{flex-wrap:wrap;overflow:visible}.hn-ide-head>.hn-ide-toolbar>.hn-ide-proyek,.hn-ide-head>.hn-ide-toolbar>.hn-ide-ref{min-width:0;max-width:100%}.hn-ide-head>.hn-ide-toolbar>.hn-ide-proyek{flex:1 1 140px;max-width:min(100%,320px)}.hn-ide-head>.hn-ide-toolbar>.hn-ide-ref{flex:1 1 140px;max-width:min(100%,220px)}.hn-ide-head>.hn-ide-toolbar .hn-select select{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}@media (max-width: 767px){.hn-ide-head>.hn-ide-toolbar>.hn-ide-proyek{flex-basis:100%;max-width:100%}}.hn-ide-screen .hn-responsive-panel:focus{outline:none}.hn-ide-screen .hn-responsive-panels[data-split=false] .hn-responsive-panel:focus-visible{outline:2px solid var(--border-focus);outline-offset:-2px}.hn-ide-seg button{min-height:24px;min-width:24px;box-sizing:border-box;display:inline-flex;align-items:center;justify-content:center}@media (pointer: coarse),(max-width: 767px){.hn-ide-seg button{min-height:44px;min-width:44px}}.hn-git-graph-layout{display:grid;grid-template-columns:var(--git-cols, minmax(0, 1fr));gap:16px;align-items:start;min-width:0;--graph-lane-0: #a9791c;--graph-lane-1: #3b7a57;--graph-lane-2: #8a5a44;--graph-lane-3: #4a6fa5;--graph-lane-4: #7d5ba6;--graph-lane-5: #b0503a}.hn-git-panel{min-width:0}.hn-git-find{display:flex;align-items:center;gap:8px;flex-wrap:wrap;padding:8px 12px;border-bottom:1px solid var(--border-hair)}.hn-git-find-input{flex:1 1 0%;border:none;background:transparent;font-size:12.5px;font-family:var(--font-ui);color:var(--text-body)}.hn-git-find-count{font-family:var(--font-mono);font-size:11px;color:var(--text-subtle)}.hn-git-find-hint{margin-left:auto;font-size:11px;color:var(--text-subtle)}@media (pointer: coarse),(max-width: 640px){.hn-git-find-hint{display:none}}.hn-git-branch-select{font-size:12px;padding:3px 6px;border:1px solid var(--border-hair);border-radius:var(--radius-sm);background:var(--surface-card);color:var(--text-body)}.hn-git-stash-row{display:flex;align-items:center;gap:6px;flex-wrap:wrap;padding:6px 12px;border-bottom:1px solid var(--border-hair)}.hn-git-stash-label{margin-right:4px}.hn-git-stash-chip{font-family:var(--font-mono);font-size:10.5px;padding:2px 8px;border-radius:999px;border:none;cursor:pointer;background:var(--bone-200);color:var(--text-muted);flex:0 0 auto}.hn-git-rows{min-width:var(--graph-min-w, 0px)}.hn-git-scroll{background:linear-gradient(var(--surface-card),var(--surface-card)) left / 12px 100% no-repeat local,linear-gradient(var(--surface-card),var(--surface-card)) right / 12px 100% no-repeat local,linear-gradient(to right,color-mix(in srgb,var(--ink-900) 16%,transparent),transparent) left / 10px 100% no-repeat scroll,linear-gradient(to left,color-mix(in srgb,var(--ink-900) 16%,transparent),transparent) right / 10px 100% no-repeat scroll}.hn-ide-graph-row{display:flex;align-items:center;gap:10px;height:var(--row-h, 30px);padding:0 12px;cursor:pointer;border-bottom:1px solid var(--border-hair)}.hn-ide-graph-row.is-sel{background:var(--brass-100)}.hn-ide-graph-row.is-active{background:var(--brass-300)}.hn-ide-graph-row.is-hit{background:var(--bone-100)}.hn-graph-lane{flex:0 0 auto}.hn-graph-edge{fill:none;stroke:var(--lane);stroke-width:1.5px}.hn-graph-node{fill:var(--lane);stroke:var(--surface-card);stroke-width:1.5px}.hn-graph-node[data-merge=true]{fill:var(--surface-card);stroke:var(--lane)}.hn-graph-node--open{fill:none;stroke:var(--graph-lane-0)}.hn-git-main{display:flex;align-items:center;gap:6px;min-width:0;flex:1 1 0%}.hn-git-refs{display:contents}.hn-git-ref{font-family:var(--font-mono);font-size:10.5px;padding:1px 6px;cursor:pointer;border:none;border-radius:999px;background:var(--brass-100);color:var(--brass-700);flex:0 0 auto}.hn-git-ref--head{background:var(--brass-700);color:var(--bone-000)}.hn-git-tag{font-family:var(--font-mono);font-size:10.5px;padding:1px 6px 1px 4px;border:none;border-radius:999px;display:inline-flex;align-items:center;gap:3px;background:var(--leaf-100);color:var(--leaf-600);flex:0 0 auto}.hn-git-merge{flex:0 0 auto;font-family:var(--font-mono);font-size:10px;padding:0 5px;border-radius:999px;border:1px solid var(--border-strong);color:var(--text-muted)}.hn-git-subject{min-width:160px;padding:0;border:0;background:transparent;cursor:pointer;text-align:left;font-size:12.5px;color:var(--text-body);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.hn-git-subject--muted{color:var(--text-muted)}.hn-git-author{flex:0 0 auto;width:88px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;text-align:right;font-family:var(--font-mono);font-size:11px;color:var(--text-subtle)}.hn-git-date{flex:0 0 auto;width:40px;text-align:right;font-family:var(--font-mono);font-size:11px;color:var(--text-subtle)}.hn-git-wt{flex:0 0 auto;width:88px;text-align:right;font-family:var(--font-mono);font-size:11px;color:var(--text-subtle)}.hn-git-wt-gap{flex:0 0 auto;width:40px}.hn-git-text{display:contents}.hn-git-date-under{display:none}@media (max-width: 640px){.hn-git-author,.hn-git-wt,.hn-git-wt-gap,.hn-git-date{display:none}.hn-git-rows{min-width:max(0px,calc(var(--graph-min-w, 0px) - 148px))}.hn-git-main{flex-direction:column;align-items:stretch;justify-content:center;gap:0}.hn-git-text{display:flex;align-items:center;gap:8px;height:18px;min-width:0;order:-1;position:relative;z-index:1}.hn-git-main>.hn-git-text>button.hn-git-subject{flex:1 1 0%;min-width:0;min-height:0;height:18px;line-height:18px}.hn-git-date-under{display:block;flex:0 0 auto;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-family:var(--font-mono);font-size:10.5px;line-height:18px;color:var(--text-subtle)}.hn-git-refs{display:flex;align-items:center;gap:6px;min-width:0;height:26px}}.hn-git-uncommitted{flex:1 1 0%;border:0;padding:0;background:transparent;text-align:left;cursor:pointer;font-size:12.5px;font-style:italic;color:var(--text-muted)}.hn-git-uncommitted{min-height:24px;display:flex;align-items:center}.hn-git-uncommitted--static{cursor:default}.hn-git-more{display:flex;align-items:center;justify-content:center;gap:10px;padding:9px 12px;border-top:1px solid var(--border-hair)}.hn-git-more-count{font-size:11.5px;color:var(--text-subtle)}.hn-git-head{display:flex;align-items:center;justify-content:space-between;margin-bottom:8px}.hn-git-cluster{display:flex;align-items:center;gap:6px}.hn-git-detail-title{font-size:13px;color:var(--text-strong);font-weight:600;margin-bottom:4px}.hn-git-detail-meta{display:flex;align-items:center;gap:6px;font-size:11px;color:var(--text-subtle);margin-bottom:6px}.hn-git-avatar{border-radius:999px}.hn-git-detail-body{font-family:var(--font-mono);font-size:11.5px;white-space:pre-wrap;color:var(--text-muted);margin-bottom:10px}.hn-git-changed-head{display:flex;align-items:center;gap:6px;margin-bottom:6px}.hn-git-count{margin-bottom:6px}.hn-git-empty{font-size:12px;color:var(--text-subtle)}.hn-git-view-btn{display:flex;padding:3px;border:none;cursor:pointer;border-radius:4px;background:transparent}.hn-git-view-btn[aria-pressed=true]{background:var(--brass-100)}.hn-git-file{display:flex;align-items:center;gap:6px;padding:3px 6px}.hn-git-file-path{flex:1 1 0%;min-width:0;text-align:left;border:none;background:transparent;cursor:pointer;font-family:var(--font-mono);font-size:11.5px;color:var(--text-body);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.hn-git-compare-pill{position:fixed;bottom:max(16px,var(--safe-bottom));left:50%;transform:translate(-50%);z-index:140;background:var(--brass-500);color:#fff;padding:6px 14px;border-radius:999px;font-size:12.5px;box-shadow:var(--shadow-lg);display:flex;align-items:center;gap:10px}.hn-git-compare-close{border:none;background:transparent;color:#fff;cursor:pointer;font-weight:700}.hn-git-diff-body{height:100%;min-height:0;display:flex;flex-direction:column}.hn-git-diff-bar{flex:0 0 auto;display:flex;align-items:center;gap:12px;padding-bottom:10px;border-bottom:1px solid var(--border-hair)}.hn-git-diff-tabs{display:flex;gap:2px;background:var(--bone-100);border-radius:var(--radius-pill);padding:2px}.hn-git-diff-tab{padding:4px 12px;border:none;cursor:pointer;border-radius:var(--radius-pill);font-size:12px;text-transform:capitalize;background:transparent;color:var(--text-muted);font-weight:400}.hn-git-diff-tab--on{background:var(--surface-card);color:var(--text-strong);font-weight:600}.hn-git-diff-scroll{flex:1 1 auto;min-height:0;overflow:auto;padding:10px 0}.hn-git-md{padding:0 16px}.hn-git-source{margin:0;padding:0 16px;font-family:var(--font-mono);font-size:12.5px;line-height:1.6;white-space:pre-wrap;word-break:break-word;color:var(--text-body)}.hn-ide-input-full{width:100%}.hn-ide-input-100{width:100px}.hn-ide-screen--fill{flex:1 1 0;min-height:0}.hn-ide-tree-scroll{padding:8px;flex:1 1 auto;min-height:0;overflow:auto;outline-offset:-4px}.hn-ide-read-root{display:flex;flex-direction:column;gap:16px;flex:1 1 0;min-height:0}.hn-ide-read-head{flex:0 0 auto;padding:10px 14px;border-bottom:1px solid var(--border-hair)}.hn-ide-read-tree{padding:8px;flex:1 1 auto;min-height:0;overflow:auto}.hn-ide-read-divider{border-top:1px solid var(--border-hair);margin:6px 0}.hn-ide-read-section{padding:6px 8px;margin-top:8px;border-top:1px solid var(--border-hair)}.hn-ide-read-viewer-head{flex:0 0 auto;display:flex;align-items:center;gap:12px;padding:10px 16px;border-bottom:1px solid var(--border-hair)}.hn-ide-read-path{font-family:var(--font-mono);font-size:13px;color:var(--text-strong)}.hn-ide-read-diff{padding:10px 0}.hn-ide-read-md{padding:16px 20px}.hn-ide-read-pre{margin:0;padding:16px 18px;overflow:auto;font-family:var(--font-mono);font-size:12.5px;line-height:1.7}.hn-ide-read-from{padding:6px 14px;font-size:11.5px;color:var(--text-subtle)}.hn-ide-read-compare{padding:8px 14px;border-bottom:1px solid var(--border-hair)}.hn-ide-read-empty{font-size:12px;color:var(--text-subtle)}.hn-ide-read-cpath{font-family:var(--font-mono);font-size:11.5px;padding:2px 0}.hn-ide-read-alert{padding:8px 14px;font-size:12px;color:var(--status-err);border-bottom:1px solid var(--border-hair)}.hn-ide-read-list{padding:8px;overflow:auto}.hn-ide-read-commit{display:flex;flex-direction:column;align-items:flex-start;gap:2px;width:100%;padding:6px 10px;border:none;cursor:pointer;text-align:left;background:transparent}.hn-ide-read-commit--from{background:var(--brass-100)}.hn-ide-read-subject{font-family:var(--font-mono);font-size:12.5px}.hn-ide-read-meta{font-family:var(--font-mono);font-size:11px;color:var(--text-subtle)}.hn-git-refs>button.hn-git-ref,.hn-git-refs>button.hn-git-tag{position:relative;display:inline-flex;align-items:center;box-sizing:border-box;min-width:0;min-height:0;height:16px;max-width:min(100%,168px);padding:0 6px;line-height:14px;flex:0 0 auto}@media (max-width: 640px){.hn-git-refs>button.hn-git-ref,.hn-git-refs>button.hn-git-tag{max-width:72px;flex:0 1 auto}}.hn-git-refs>button.hn-git-ref:after,.hn-git-refs>button.hn-git-tag:after{content:"";position:absolute;left:0;right:0;top:50%;height:24px;transform:translateY(-50%)}.hn-git-ref-text{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.hn-git-main .hn-git-subject{min-height:24px}@media (pointer: coarse),(max-width: 767px){.hn-git-refs>button.hn-git-ref:after,.hn-git-refs>button.hn-git-tag:after{height:var(--touch-target)}.hn-git-main .hn-git-subject{min-height:var(--touch-target)}}.hn-ide-view-toggle{min-width:24px;min-height:24px;box-sizing:border-box}
|