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-181
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-472
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-603
| Operasi | Perilaku pada tree | Rujukan |
|---|---|---|
createLeafPane, createSplitPane | leaf baru dengan pane-<ts>-<n>, split baru dengan split-<ts>-<n>; ratio default 50 | src/lib/layoutTree.ts:42-644 |
removeTabFromTree | leaf yang kosong menjadi null dan sibling mengambil alih split; activeTabId pindah ke tab terakhir yang tersisa | src/lib/layoutTree.ts:124-1655 |
splitLeafNode | memangkas newTabId dari posisi lamanya lebih dulu, lalu mengganti leaf target dengan PaneSplit berratio 50 | src/lib/layoutTree.ts:171-2176 |
moveTabToPane | men-dock tab ke leaf target; jika tree habis, layout awal dibuat ulang | src/lib/layoutTree.ts:222-2617 |
updateSplitRatio | membulatkan dan mengurung rasio pada 15 sampai 85 | src/lib/layoutTree.ts:292-3148 |
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.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-1879 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-2610 src/stores/useWorkspaceStore.ts:28-7111
layout | Label getLayoutMeta | Pane |
|---|---|---|
split-vertical | Side by Side (1×2) | 2 |
split-horizontal | Top & Bottom (2×1) | 2 |
grid-4 | 2×2 Quad Grid (4) | 4 |
split-1-2 | 1 Large + 2 Stacked | 3 |
split-2-1 | 2 Stacked + 1 Wide | 3 |
triple-column | Triple 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-52212
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-3313
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-3313
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.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-15014 TerminalWorkspace.handleSaveGroupSplit melakukan hal yang sama untuk grup yang sedang aktif, dengan nama Active Cluster (N Nodes). src/components/terminal/TerminalWorkspace.tsx:95-12515
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-7016 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-55717 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-73718
Pelepasan terjadi hanya saat tab ditutup. closeSession memanggil disposeTerminalSession sebelum memutus backend. src/stores/useSessionStore.ts:524-54219 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-35220
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.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-14021 Di store, splitPane lebih dulu memangkas tab tersebut dari grup lain, menjalankan splitLeafNode, mempertahankan urutan grup, lalu mengosongkan maximizedPaneId. src/stores/useSessionStore.ts:702-75222
Drag tab memakai ambang 5 px sebelum startDragTab dipanggil, sehingga klik biasa tidak dianggap drag. src/components/terminal/PaneView.tsx:107-14021 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-4923 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-100824
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.Rasio split diatur langsung dari handle. SplitHandle mengubah posisi pointer menjadi persentase terhadap getBoundingClientRect induknya, lalu memanggil setSplitRatio. src/components/terminal/SplitHandle.tsx:16-3825 setSplitRatio meneruskan nilai itu ke updateSplitRatio, yang mengurungnya di 15 sampai 85. src/stores/useSessionStore.ts:790-80826 Maximize pane hanyalah flag maximizedPaneId yang di-toggle; nilainya dikosongkan lagi oleh setiap operasi split. src/stores/useSessionStore.ts:790-80826
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-89527 Daftar target itu yang dipakai term.onData untuk menulis ke beberapa sesi sekaligus. src/components/terminal/XtermView.tsx:534-55717
Resize, SIGWINCH, dan jebakan
| Parameter | Nilai | Rujukan |
|---|---|---|
Batas ratio | 15 sampai 85, dibulatkan | src/lib/layoutTree.ts:292-3148 |
ratio default split baru | 50 | src/lib/layoutTree.ts:42-644 |
ratio akar triple-column | 33.3 | src/stores/useSessionStore.ts:437-52212 |
| Ambang mulai drag | 5 px | src/components/terminal/PaneView.tsx:107-14021 |
| Ambang zona drop | 0.22 dan 0.78 dari sisi | src/components/terminal/DropZoneOverlay.tsx:25-4923 |
| Debounce resize | 50 ms | src/components/terminal/XtermView.tsx:740-77528 |
| Ukuran container minimum | 100 px per sisi | src/components/terminal/XtermView.tsx:740-77528 |
| Ukuran PTY minimum | cols >= 20 dan rows >= 5 | src/components/terminal/XtermView.tsx:740-77528 |
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-77528
⚠️ Jangan menyembunyikan terminal dengan
display: none.TerminalWorkspacememang tidak pernah di-unmount;Appmenyembunyikannya denganvisibility: hiddendanpointerEvents: nonesupaya geometri sesi tidak kolaps ke 0×0 dan memicu SIGWINCH palsu yang merusakhtopatau TUI curses. src/App.tsx:163-16929
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-9630
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-10131
| Shortcut | Aksi |
|---|---|
Escape | keluar dari mode maximize |
Alt+B | toggle broadcast untuk grup aktif |
Alt+S | toggle sidebar snippet |
Ketiganya dipasang sebagai listener keydown global di TerminalWorkspace, dan Alt+B memakai toggleGroupBroadcast(activeGroupId || undefined). src/components/terminal/TerminalWorkspace.tsx:71-9032
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.