Lompat ke konten
dokumentasi.id
Commit 0da916f
v1
Star0

Ada commit baru di branch utama.

2a17915

9

Terminal, Split Pane, dan Workspace

Terminal Termimus menyusun tampilan sebagai pohon PaneNode. Setiap grup terminal — satu tab di header — memegang satu rootPane, dan pohon itu dibangun dari dua jenis node: PaneLeaf yang menyimpan tabIds serta activeTabId, dan PaneSplit yang menyimpan direction serta ratio untuk dua anak. Nilai direction adalah row untuk kiri-kanan dan column untuk atas-bawah. src/lib/layoutTree.ts:1-18

TerminalGroup hanya menambahkan id dan label opsional di atas satu rootPane. State selebihnya bersifat global: tabs, groups, activeGroupId, activePaneId, dan maximizedPaneId. src/stores/useSessionStore.ts:42-47

Pohon PaneNode dan operasinya

Seluruh operasi layout adalah fungsi murni yang menerima node dan mengembalikan node baru, bukan mutasi di tempat. PaneContainer cukup merender rekursif: leaf menjadi PaneView, split menjadi dua div berukuran calc(ratio% - 3px) yang dipisahkan SplitHandle. src/components/terminal/PaneContainer.tsx:11-60

OperasiPerilaku pada treeRujukan
createLeafPane, createSplitPaneleaf baru dengan pane-<ts>-<n>, split baru dengan split-<ts>-<n>; ratio default 50src/lib/layoutTree.ts:42-64
removeTabFromTreeleaf yang kosong menjadi null dan sibling mengambil alih split; activeTabId pindah ke tab terakhir yang tersisasrc/lib/layoutTree.ts:124-165
splitLeafNodememangkas newTabId dari posisi lamanya lebih dulu, lalu mengganti leaf target dengan PaneSplit berratio 50src/lib/layoutTree.ts:171-217
moveTabToPanemen-dock tab ke leaf target; jika tree habis, layout awal dibuat ulangsrc/lib/layoutTree.ts:222-261
updateSplitRatiomembulatkan dan mengurung rasio pada 15 sampai 85src/lib/layoutTree.ts:292-314
graph TD
  root["PaneSplit column ratio 50"] --> top["PaneSplit row ratio 50"]
  root --> bottom["PaneSplit row ratio 50"]
  top --> s0["PaneLeaf s0"]
  top --> s1["PaneLeaf s1"]
  bottom --> s2["PaneLeaf s2"]
  bottom --> s3["PaneLeaf s3"]
  style root fill:#f59e0b,color:#000
  style top fill:#f59e0b,color:#000
  style bottom fill:#f59e0b,color:#000
  style s0 fill:#336791,color:#fff
  style s1 fill:#336791,color:#fff
  style s2 fill:#336791,color:#fff
  style s3 fill:#336791,color:#fff

%% fig: Pohon PaneNode untuk layout grid-4
%% desc: Satu split column membungkus dua split row, dan setiap leaf memegang satu session ID dari openClusterGroup.
Fig. 1: Pohon PaneNode untuk layout grid-4Satu split column membungkus dua split row, dan setiap leaf memegang satu session ID dari openClusterGroup.

Workspace preset dan peluncuran atomik

Preset disimpan sebagai WorkspacePreset dengan layout: PresetLayoutType dan daftar PresetNode yang hanya berisi paneIndex serta hostId. src/lib/api.ts:156-187 Enam nilai layout yang tersedia dipetakan ke jumlah pane oleh getPaneCountForLayout, dan getLayoutMeta memberi label serta deskripsi untuk setiap nilai. src/stores/useWorkspaceStore.ts:12-26 src/stores/useWorkspaceStore.ts:28-71

layoutLabel getLayoutMetaPane
split-verticalSide by Side (1×2)2
split-horizontalTop & Bottom (2×1)2
grid-42×2 Quad Grid (4)4
split-1-21 Large + 2 Stacked3
split-2-12 Stacked + 1 Wide3
triple-columnTriple Column (1×3)3

openClusterGroup membangun seluruh pohon di dalam satu pemanggilan set: grid-4 menyusun dua split row di dalam satu split column, split-1-2 menaruh satu leaf di kiri dan split column di kanan, split-2-1 menaruh split row di atas, triple-column memakai ratio 33.3 untuk kolom pertama, dan split-vertical atau split-horizontal cukup satu split dengan dua leaf. Semua tab, grup, rootPane, dan activePaneId ditulis dalam satu update. src/stores/useSessionStore.ts:437-522

launchWorkspacePreset menyelesaikan semua hostId lebih dulu ke objek Host, melempar No valid hosts found for this workspace preset. jika tidak satu pun cocok, lalu meneruskan preset.layout, preset.broadcastOnLaunch, dan preset.name ke openClusterGroup. Preset tanpa node langsung mengembalikan false. src/lib/workspaceLauncher.ts:6-33

Alasan peluncuran dibuat satu langkah adalah geometri: komentar di modul menyebut tujuan ini untuk memastikan semua pane mount dengan dimensi kuadran finalnya, sehingga artefak resize SIGWINCH saat startup tidak muncul. src/lib/workspaceLauncher.ts:6-33

graph LR
  A["WorkspaceView.handleLaunchPreset"] --> B["launchWorkspacePreset"]
  B --> C["useHostStore.getState hosts"]
  C --> D["validNodes"]
  D --> E["useSessionStore.openClusterGroup"]
  E --> F["satu set groups + tabs + rootPane"]
  F --> G["PaneContainer merender tree final"]
  B -.->|"preset.nodes.length === 0"| X["return false"]
  D -.->|"tidak ada host cocok"| Y["throw No valid hosts found for this workspace preset."]
  style A fill:#f59e0b,color:#000
  style B fill:#f59e0b,color:#000
  style C fill:#336791,color:#fff
  style D fill:#8b5cf6,color:#fff
  style E fill:#8b5cf6,color:#fff
  style F fill:#336791,color:#fff
  style G fill:#2d7d46,color:#fff
  style X fill:#dc2626,color:#fff
  style Y fill:#dc2626,color:#fff

%% fig: Jalur launchWorkspacePreset sampai satu render
%% desc: Preset di-resolve ke host, dibangun sebagai satu tree utuh lewat openClusterGroup, lalu langsung dirender PaneContainer tanpa split bertahap.
Fig. 2: Jalur launchWorkspacePreset sampai satu renderPreset di-resolve ke host, dibangun sebagai satu tree utuh lewat openClusterGroup, lalu langsung dirender PaneContainer tanpa split bertahap.

Kebalikannya juga tersedia: handleLaunchPreset memanggil launchWorkspacePreset lalu membuka terminal, sedangkan handleSaveActiveSplit menebak layout dari jumlah leaf aktif dan menyusun PresetNode per pane. src/components/workspaces/WorkspaceView.tsx:110-150 TerminalWorkspace.handleSaveGroupSplit melakukan hal yang sama untuk grup yang sedang aktif, dengan nama Active Cluster (N Nodes). src/components/terminal/TerminalWorkspace.tsx:95-125

terminalPool dan siklus hidup xterm

Instance xterm tidak hidup di dalam React tree. Instance disimpan pada Map level modul bernama terminalPool, dengan kunci sessionId. src/components/terminal/XtermView.tsx:67-70 Saat XtermView mount, pool dicek lebih dulu: jika entry ada, term, fitAddon, searchAddon, dan lastSize dipakai ulang, lalu elemen DOM-nya dipindahkan ke container baru tanpa reconnect SSH. src/components/terminal/XtermView.tsx:534-557 Jika belum ada, Terminal baru dibuat bersama FitAddon dan SearchAddon, listener ssh-data, ssh-closed, dan ssh-progress dipasang, entry dimasukkan ke pool, lalu koneksi dimulai. src/components/terminal/XtermView.tsx:707-737

Pelepasan terjadi hanya saat tab ditutup. closeSession memanggil disposeTerminalSession sebelum memutus backend. src/stores/useSessionStore.ts:524-542 Di dalam disposeTerminalSession, highlighter dilepas, dataDisposable dan semua unlistenFns dibuang, term.dispose() dipanggil, elemen DOM dihapus dari induknya, dan entry dihapus dari pool. src/components/terminal/XtermView.tsx:309-352

graph TD
  M["mount XtermView"] --> Q{"terminalPool.get sessionId"}
  Q -->|"entry ada"| R["reuse term fitAddon searchAddon"]
  Q -->|"entry belum ada"| N["new Terminal + FitAddon + SearchAddon"]
  N --> P["terminalPool.set sessionId entry"]
  P --> RO["ResizeObserver debounce 50 ms"]
  R --> RO
  RO --> RS["api.resizeSsh sessionId cols rows"]
  RO --> D["closeSession"]
  D --> DS["disposeTerminalSession"]
  DS --> X["terminalPool.delete sessionId"]
  style M fill:#f59e0b,color:#000
  style Q fill:#8b5cf6,color:#fff
  style R fill:#2d7d46,color:#fff
  style N fill:#2d7d46,color:#fff
  style P fill:#336791,color:#fff
  style RO fill:#8b5cf6,color:#fff
  style RS fill:#336791,color:#fff
  style D fill:#dc2626,color:#fff
  style DS fill:#dc2626,color:#fff
  style X fill:#dc2626,color:#fff

%% fig: Siklus hidup entry terminalPool
%% desc: Mount memakai ulang entry yang ada atau membuat entry baru, resize dikirim dari ResizeObserver, dan entry hanya dihapus saat closeSession memanggil disposeTerminalSession.
Fig. 3: Siklus hidup entry terminalPoolMount memakai ulang entry yang ada atau membuat entry baru, resize dikirim dari ResizeObserver, dan entry hanya dihapus saat closeSession memanggil disposeTerminalSession.

Tab, drag-and-drop, dan broadcast

Split dari dalam pane tidak selalu membuat sesi baru. PaneView.handleSplitRight memeriksa apakah pane punya tab lain; jika ada, tab itu yang dipindahkan ke kanan lewat splitPane(pane.id, otherTabId, "row", "second"). src/components/terminal/PaneView.tsx:107-140 Di store, splitPane lebih dulu memangkas tab tersebut dari grup lain, menjalankan splitLeafNode, mempertahankan urutan grup, lalu mengosongkan maximizedPaneId. src/stores/useSessionStore.ts:702-752

Drag tab memakai ambang 5 px sebelum startDragTab dipanggil, sehingga klik biasa tidak dianggap drag. src/components/terminal/PaneView.tsx:107-140 Saat drag aktif, DropZoneOverlay menghitung posisi relatif pointer: di bawah 0.22 berarti left, di atas 0.78 berarti right atau bottom, dan sisanya center untuk dock. src/components/terminal/DropZoneOverlay.tsx:25-49 endDragTab memetakan zona ke operasi tree: left menjadi splitPane(..., "row", "first"), right menjadi "row", "second", top menjadi "column", "first", bottom menjadi "column", "second", dan center menjadi moveTabToPane. src/stores/useSessionStore.ts:985-1008

graph TD
  E["endDragTab"] --> Z{"dragTarget.zone"}
  Z -->|"left"| SL["splitPane row first"]
  Z -->|"right"| SR["splitPane row second"]
  Z -->|"top"| ST["splitPane column first"]
  Z -->|"bottom"| SB["splitPane column second"]
  Z -->|"center"| CT["moveTabToPane"]
  SL --> T["splitLeafNode"]
  SR --> T
  ST --> T
  SB --> T
  CT --> H["helper moveTabToPane di layoutTree"]
  T --> C["removeTabFromTree lalu replaceInTree"]
  style E fill:#f59e0b,color:#000
  style Z fill:#8b5cf6,color:#fff
  style SL fill:#2d7d46,color:#fff
  style SR fill:#2d7d46,color:#fff
  style ST fill:#2d7d46,color:#fff
  style SB fill:#2d7d46,color:#fff
  style CT fill:#2d7d46,color:#fff
  style T fill:#336791,color:#fff
  style H fill:#336791,color:#fff
  style C fill:#336791,color:#fff

%% fig: Pemetaan zona drop ke operasi layout
%% desc: Setiap zona drop di DropZoneOverlay berakhir pada satu arah split atau pemindahan tab ke pane yang sudah ada.
Fig. 4: Pemetaan zona drop ke operasi layoutSetiap zona drop di DropZoneOverlay berakhir pada satu arah split atau pemindahan tab ke pane yang sudah ada.

Rasio split diatur langsung dari handle. SplitHandle mengubah posisi pointer menjadi persentase terhadap getBoundingClientRect induknya, lalu memanggil setSplitRatio. src/components/terminal/SplitHandle.tsx:16-38 setSplitRatio meneruskan nilai itu ke updateSplitRatio, yang mengurungnya di 15 sampai 85. src/stores/useSessionStore.ts:790-808 Maximize pane hanyalah flag maximizedPaneId yang di-toggle; nilainya dikosongkan lagi oleh setiap operasi split. src/stores/useSessionStore.ts:790-808

Broadcast masukan bersifat per grup. getBroadcastTargetSessionIds mencari grup pemilik sessionId, mengembalikan [sessionId] jika grup itu tidak ada di broadcastGroupIds, dan jika aktif mengembalikan activeTabId setiap leaf yang connected. src/stores/useSessionStore.ts:873-895 Daftar target itu yang dipakai term.onData untuk menulis ke beberapa sesi sekaligus. src/components/terminal/XtermView.tsx:534-557

Resize, SIGWINCH, dan jebakan

ParameterNilaiRujukan
Batas ratio15 sampai 85, dibulatkansrc/lib/layoutTree.ts:292-314
ratio default split baru50src/lib/layoutTree.ts:42-64
ratio akar triple-column33.3src/stores/useSessionStore.ts:437-522
Ambang mulai drag5 pxsrc/components/terminal/PaneView.tsx:107-140
Ambang zona drop0.22 dan 0.78 dari sisisrc/components/terminal/DropZoneOverlay.tsx:25-49
Debounce resize50 mssrc/components/terminal/XtermView.tsx:740-775
Ukuran container minimum100 px per sisisrc/components/terminal/XtermView.tsx:740-775
Ukuran PTY minimumcols >= 20 dan rows >= 5src/components/terminal/XtermView.tsx:740-775

ResizeObserver tidak langsung mengirim resize. Setiap perubahan dijalankan lewat setTimeout 50 ms, container di bawah 100 px diabaikan, dan api.resizeSsh hanya dipanggil ketika cols atau rows benar-benar berubah. src/components/terminal/XtermView.tsx:740-775

⚠️ Jangan menyembunyikan terminal dengan display: none. TerminalWorkspace memang tidak pernah di-unmount; App menyembunyikannya dengan visibility: hidden dan pointerEvents: none supaya geometri sesi tidak kolaps ke 0×0 dan memicu SIGWINCH palsu yang merusak htop atau TUI curses. src/App.tsx:163-169

Jebakan serupa muncul saat membuat sesi baru di dalam split. openSessionInSplit sengaja menggabungkan pembuatan sesi dan pemisahan pane dalam satu update, karena openSession yang diikuti splitPane terpisah membuat XtermView mount, mulai koneksi, lalu remount dan kehilangan log progres. src/stores/useSessionStore.ts:83-96

Header pane hanya dirender ketika tree benar-benar split dan tidak ada pane yang maximized; pada satu pane, header jendela atas menjadi satu-satunya tab bar. src/components/terminal/PaneView.tsx:96-101

ShortcutAksi
Escapekeluar dari mode maximize
Alt+Btoggle broadcast untuk grup aktif
Alt+Stoggle sidebar snippet

Ketiganya dipasang sebagai listener keydown global di TerminalWorkspace, dan Alt+B memakai toggleGroupBroadcast(activeGroupId || undefined). src/components/terminal/TerminalWorkspace.tsx:71-90

Siklus koneksi SSH itu sendiri, termasuk event ssh-progress dan ssh-closed yang dipakai di sini, dijelaskan pada Siklus Hidup Koneksi SSH. Format penyimpanan preset di database dibahas pada Skema Database Lokal.