feat(terminal): replace ghostty-web with an in-repo libghostty-vt adapter

The terminal ran on the ghostty-web npm package plus a hand-written patch,
and every rendering bug (recycled rows, duplicated reflow fragments, prompt
artifacts) had to be worked around from outside. The emulator now is the
official libghostty-vt C ABI compiled to WebAssembly, driven by a browser
adapter ported from T3 Code (MIT, notice in LICENSE-T3CODE) and owned in
packages/ui/src/lib/ghostty. The artifact is reproducible with
scripts/build-libghostty-wasm.sh, including a workaround for Zig 0.15.2 on
macOS 27 SDKs.

On top of the port: one WASM instance per page with every tab kept mounted
and hidden tabs paused; history replayed at the PTY size it was drawn for;
shells spawned only after the first fitted grid so zsh never prints the
PROMPT_SP marker; box drawing, block elements and Powerline arrows drawn
procedurally to the exact cell so TUI borders and block logos have no gaps
between rows; a software-rasterized canvas so Gecko renders every tab's text
with the same smoothing; the symbols-only Nerd Font bundled instead of a CDN
fetch; touch selection and scrolling driven through the surface API; a copy
button in the tab strip for touch hosts; localized aria labels.

Testing: bun tests run the real WASM (reflow, palette, replay isolation,
recycled rows, box glyph geometry); viewport and view tests use a surface
double; verified in Chromium and Zen (windowed and headless) for crisp text,
new tabs, panel reopen, resize and box glyph rendering; package type-check,
oxlint/eslint on new files, web build.
This commit is contained in:
Bohdan Triapitsyn
2026-09-07 12:18:22 +03:00
parent 3132d1361a
commit 39fa8c1917
61 changed files with 6678 additions and 990 deletions
+4
View File
@@ -1646,6 +1646,7 @@ export const dict = {
'terminalView.tabs.closeTabTitle': 'Registerkarte schließen',
'terminalView.tabs.newTabTitle': 'Neue Registerkarte',
'terminalView.viewport.inputAria': 'Terminal-Eingabe',
'terminalView.viewport.scrollbarAria': 'Terminal-Verlauf',
'directoryExplorerDialog.title': 'Projektverzeichnis hinzufügen',
'directoryExplorerDialog.description': 'Wählen Sie einen Ordner aus, der als Projekt hinzugefügt werden soll.',
'directoryExplorerDialog.toggle.showHidden': 'Versteckte anzeigen',
@@ -2979,6 +2980,9 @@ export const dict = {
'quota.window.completions': 'Vervollständigungen',
'quota.window.premiumInteractions': 'KI-Guthaben',
'terminalView.actions.attachSelection': 'Ausgewählte Ausgabe anhängen',
'terminalView.actions.copySelection': 'Ausgewählte Ausgabe kopieren',
'terminalView.toast.selectionCopied': 'Ausgabe kopiert',
'terminalView.toast.copyFailed': 'Kopieren fehlgeschlagen',
'terminalView.actions.restart': 'Terminal neu starten',
'chat.message.terminalContext': '{terminal}, Zeilen {start}-{end}',
'chat.message.context.codeComment': 'Kommentar zu {file}, Zeilen {start}-{end}',
+4
View File
@@ -8,6 +8,9 @@ export const dict = {
...linearIssuePickerI18n.en,
...linearPanelI18n.en,
'terminalView.actions.attachSelection': 'Attach selected output',
'terminalView.actions.copySelection': 'Copy selected output',
'terminalView.toast.selectionCopied': 'Output copied',
'terminalView.toast.copyFailed': 'Copy failed',
'terminalView.actions.restart': 'Restart terminal',
'chat.message.terminalContext': '{terminal}, lines {start}-{end}',
'chat.message.context.codeComment': 'Comment on {file}, lines {start}-{end}',
@@ -1847,6 +1850,7 @@ export const dict = {
'terminalView.tabs.closeTabTitle': 'Close tab',
'terminalView.tabs.newTabTitle': 'New tab',
'terminalView.viewport.inputAria': 'Terminal input',
'terminalView.viewport.scrollbarAria': 'Terminal scrollback',
'directoryExplorerDialog.title': 'Add project directory',
'directoryExplorerDialog.description': 'Choose a folder to add as a project.',
'directoryExplorerDialog.toggle.showHidden': 'Show hidden',
+4
View File
@@ -9,6 +9,9 @@ export const dict: Record<I18nKey, string> = {
...linearIssuePickerI18n.es,
...linearPanelI18n.es,
'terminalView.actions.attachSelection': 'Adjuntar salida seleccionada',
'terminalView.actions.copySelection': 'Copiar salida seleccionada',
'terminalView.toast.selectionCopied': 'Salida copiada',
'terminalView.toast.copyFailed': 'Error al copiar',
'terminalView.actions.restart': 'Reiniciar terminal',
'chat.message.terminalContext': '{terminal}, líneas {start}-{end}',
'chat.message.context.codeComment': 'Comentario en {file}, líneas {start}-{end}',
@@ -1825,6 +1828,7 @@ export const dict: Record<I18nKey, string> = {
"terminalView.tabs.closeTabTitle": "Cerrar pestaña",
"terminalView.tabs.newTabTitle": "Nueva pestaña",
"terminalView.viewport.inputAria": "Entrada de terminal",
"terminalView.viewport.scrollbarAria": "Historial del terminal",
"directoryExplorerDialog.title": "Añadir directorio de proyecto",
"directoryExplorerDialog.description": "Elige una carpeta para añadir como proyecto.",
"directoryExplorerDialog.toggle.showHidden": "Mostrar ocultos",
+4
View File
@@ -8,6 +8,9 @@ export const dict = {
...linearIssuePickerI18n.fr,
...linearPanelI18n.fr,
'terminalView.actions.attachSelection': 'Joindre la sortie sélectionnée',
'terminalView.actions.copySelection': 'Copier la sortie sélectionnée',
'terminalView.toast.selectionCopied': 'Sortie copiée',
'terminalView.toast.copyFailed': 'Échec de la copie',
'terminalView.actions.restart': 'Redémarrer le terminal',
'chat.message.terminalContext': '{terminal}, lignes {start}-{end}',
'chat.message.context.codeComment': 'Commentaire sur {file}, lignes {start}-{end}',
@@ -1604,6 +1607,7 @@ export const dict = {
'terminalView.tabs.closeTabTitle': 'Fermer l\'onglet',
'terminalView.tabs.newTabTitle': 'Nouvel onglet',
'terminalView.viewport.inputAria': 'Entrée de borne',
'terminalView.viewport.scrollbarAria': 'Historique du terminal',
'directoryExplorerDialog.title': 'Ajouter un répertoire de projet',
'directoryExplorerDialog.description': 'Choisissez un dossier à ajouter en tant que projet.',
'directoryExplorerDialog.toggle.showHidden': 'Afficher masqué',
+4
View File
@@ -9,6 +9,9 @@ export const dict: Record<I18nKey, string> = {
...linearIssuePickerI18n.ja,
...linearPanelI18n.ja,
'terminalView.actions.attachSelection': '選択した出力を添付',
'terminalView.actions.copySelection': '選択した出力をコピー',
'terminalView.toast.selectionCopied': '出力をコピーしました',
'terminalView.toast.copyFailed': 'コピーに失敗しました',
'terminalView.actions.restart': 'ターミナルを再起動',
'chat.message.terminalContext': '{terminal}、{start}〜{end}行',
'chat.message.context.codeComment': '{file} の {start}〜{end} 行へのコメント',
@@ -1843,6 +1846,7 @@ export const dict: Record<I18nKey, string> = {
'terminalView.tabs.closeTabTitle': 'タブを閉じる',
'terminalView.tabs.newTabTitle': '新しいタブ',
'terminalView.viewport.inputAria': 'ターミナル入力',
'terminalView.viewport.scrollbarAria': 'ターミナルのスクロールバック',
'directoryExplorerDialog.title': 'プロジェクトディレクトリを追加',
'directoryExplorerDialog.description': 'プロジェクトとして追加するフォルダを選択してください。',
'directoryExplorerDialog.toggle.showHidden': '隠しファイルを表示',
+4
View File
@@ -9,6 +9,9 @@ export const dict: Record<I18nKey, string> = {
...linearIssuePickerI18n.ko,
...linearPanelI18n.ko,
'terminalView.actions.attachSelection': '선택한 출력 첨부',
'terminalView.actions.copySelection': '선택한 출력 복사',
'terminalView.toast.selectionCopied': '출력을 복사했습니다',
'terminalView.toast.copyFailed': '복사 실패',
'terminalView.actions.restart': '터미널 다시 시작',
'chat.message.terminalContext': '{terminal}, {start}-{end}행',
'chat.message.context.codeComment': '{file} {start}-{end}행에 대한 댓글',
@@ -1849,6 +1852,7 @@ export const dict: Record<I18nKey, string> = {
'terminalView.tabs.closeTabTitle': '탭 닫기',
'terminalView.tabs.newTabTitle': '새 탭',
'terminalView.viewport.inputAria': '터미널 입력',
'terminalView.viewport.scrollbarAria': '터미널 스크롤백',
'directoryExplorerDialog.title': '프로젝트 디렉터리 추가',
'directoryExplorerDialog.description': '프로젝트로 추가할 폴더를 선택하세요.',
'directoryExplorerDialog.toggle.showHidden': '숨김 항목 표시',
+4
View File
@@ -9,6 +9,9 @@ export const dict: Record<I18nKey, string> = {
...linearIssuePickerI18n.pl,
...linearPanelI18n.pl,
'terminalView.actions.attachSelection': 'Dołącz zaznaczone dane wyjściowe',
'terminalView.actions.copySelection': 'Kopiuj zaznaczone dane wyjściowe',
'terminalView.toast.selectionCopied': 'Skopiowano dane wyjściowe',
'terminalView.toast.copyFailed': 'Kopiowanie nie powiodło się',
'terminalView.actions.restart': 'Uruchom terminal ponownie',
'chat.message.terminalContext': '{terminal}, wiersze {start}-{end}',
'chat.message.context.codeComment': 'Komentarz do {file}, wiersze {start}-{end}',
@@ -3043,6 +3046,7 @@ export const dict: Record<I18nKey, string> = {
'terminalView.tabs.closeTabTitle': 'Close tab',
'terminalView.tabs.newTabTitle': 'New tab',
'terminalView.viewport.inputAria': 'Terminal input',
'terminalView.viewport.scrollbarAria': 'Historia terminala',
'textarea.resizeHandleAria': 'Resize textarea',
'updateDialog.actions.copied': 'Skopiowano!',
'updateDialog.actions.copyCommand': 'Kopiuj polecenie',
@@ -9,6 +9,9 @@ export const dict: Record<I18nKey, string> = {
...linearIssuePickerI18n['pt-BR'],
...linearPanelI18n['pt-BR'],
'terminalView.actions.attachSelection': 'Anexar saída selecionada',
'terminalView.actions.copySelection': 'Copiar saída selecionada',
'terminalView.toast.selectionCopied': 'Saída copiada',
'terminalView.toast.copyFailed': 'Falha ao copiar',
'terminalView.actions.restart': 'Reiniciar terminal',
'chat.message.terminalContext': '{terminal}, linhas {start}-{end}',
'chat.message.context.codeComment': 'Comentário em {file}, linhas {start}-{end}',
@@ -1825,6 +1828,7 @@ export const dict: Record<I18nKey, string> = {
"terminalView.tabs.closeTabTitle": "Fechar aba",
"terminalView.tabs.newTabTitle": "Nova aba",
"terminalView.viewport.inputAria": "Entrada de terminal",
"terminalView.viewport.scrollbarAria": "Histórico do terminal",
"directoryExplorerDialog.title": "Adicionar diretório de projeto",
"directoryExplorerDialog.description": "Escolha uma pasta para adicionar como projeto.",
"directoryExplorerDialog.toggle.showHidden": "Mostrar ocultos",
+4
View File
@@ -8,6 +8,9 @@ export const dict = {
...linearIssuePickerI18n.tr,
...linearPanelI18n.tr,
'terminalView.actions.attachSelection': 'Seçili çıktıyı ekle',
'terminalView.actions.copySelection': 'Seçili çıktıyı kopyala',
'terminalView.toast.selectionCopied': 'Çıktı kopyalandı',
'terminalView.toast.copyFailed': 'Kopyalama başarısız',
'terminalView.actions.restart': 'Terminali yeniden başlat',
'chat.message.terminalContext': '{terminal}, {start}-{end}. satırlar',
'chat.chatInput.terminalContext': '{terminal}, {start}-{end}. satırlar',
@@ -1809,6 +1812,7 @@ export const dict = {
'terminalView.tabs.closeTabTitle': 'Sekmeyi kapat',
'terminalView.tabs.newTabTitle': 'Yeni sekme',
'terminalView.viewport.inputAria': 'Terminal girişi',
'terminalView.viewport.scrollbarAria': 'Terminal geçmişi',
'directoryExplorerDialog.title': 'Proje dizini ekle',
'directoryExplorerDialog.description': 'Proje olarak eklemek için bir klasör seçin.',
'directoryExplorerDialog.toggle.showHidden': 'Gizli dosyaları göster',
+4
View File
@@ -9,6 +9,9 @@ export const dict: Record<I18nKey, string> = {
...linearIssuePickerI18n.uk,
...linearPanelI18n.uk,
'terminalView.actions.attachSelection': 'Прикріпити вибраний вивід',
'terminalView.actions.copySelection': 'Скопіювати вибраний вивід',
'terminalView.toast.selectionCopied': 'Вивід скопійовано',
'terminalView.toast.copyFailed': 'Не вдалося скопіювати',
'terminalView.actions.restart': 'Перезапустити термінал',
'chat.message.terminalContext': '{terminal}, рядки {start}-{end}',
'chat.message.context.codeComment': 'Коментар до {file}, рядки {start}-{end}',
@@ -1825,6 +1828,7 @@ export const dict: Record<I18nKey, string> = {
"terminalView.tabs.closeTabTitle": "Закрити вкладку",
"terminalView.tabs.newTabTitle": "Нова вкладка",
"terminalView.viewport.inputAria": "Ввід терміналу",
"terminalView.viewport.scrollbarAria": "Історія термінала",
"directoryExplorerDialog.title": "Додати каталог проєкту",
"directoryExplorerDialog.description": "Виберіть папку, щоб додати її як проєкт.",
"directoryExplorerDialog.toggle.showHidden": "Показати приховані",
@@ -9,6 +9,9 @@ export const dict: Record<I18nKey, string> = {
...linearIssuePickerI18n['zh-CN'],
...linearPanelI18n['zh-CN'],
'terminalView.actions.attachSelection': '附加所选输出',
'terminalView.actions.copySelection': '复制所选输出',
'terminalView.toast.selectionCopied': '已复制输出',
'terminalView.toast.copyFailed': '复制失败',
'terminalView.actions.restart': '重启终端',
'chat.message.terminalContext': '{terminal},第 {start}-{end} 行',
'chat.message.context.codeComment': '对 {file} 第 {start}-{end} 行的评论',
@@ -1813,6 +1816,7 @@ export const dict: Record<I18nKey, string> = {
'terminalView.tabs.closeTabTitle': '关闭标签页',
'terminalView.tabs.newTabTitle': '新建标签页',
'terminalView.viewport.inputAria': '终端输入',
'terminalView.viewport.scrollbarAria': '终端回滚历史',
'directoryExplorerDialog.title': '添加项目目录',
'directoryExplorerDialog.description': '选择一个文件夹添加为项目。',
'directoryExplorerDialog.toggle.showHidden': '显示隐藏项',
@@ -9,6 +9,9 @@ export const dict: Record<I18nKey, string> = {
...linearIssuePickerI18n['zh-TW'],
...linearPanelI18n['zh-TW'],
'terminalView.actions.attachSelection': '附加所選輸出',
'terminalView.actions.copySelection': '複製所選輸出',
'terminalView.toast.selectionCopied': '已複製輸出',
'terminalView.toast.copyFailed': '複製失敗',
'terminalView.actions.restart': '重新啟動終端',
'chat.message.terminalContext': '{terminal},第 {start}-{end} 行',
'chat.message.context.codeComment': '對 {file} 第 {start}-{end} 行的評論',
@@ -1817,6 +1820,7 @@ export const dict: Record<I18nKey, string> = {
'terminalView.tabs.closeTabTitle': '關閉分頁',
'terminalView.tabs.newTabTitle': '新增分頁',
'terminalView.viewport.inputAria': '終端機輸入',
'terminalView.viewport.scrollbarAria': '終端機回捲歷史',
'directoryExplorerDialog.title': '新增專案目錄',
'directoryExplorerDialog.description': '選擇一個資料夾新增為專案。',
'directoryExplorerDialog.toggle.showHidden': '顯示隱藏項目',