Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
275811a
feat: add herdr multiplexer support
salemsayed Jul 19, 2026
9f6d66c
feat: add herdr snapshot models and parser
salemsayed Jul 19, 2026
5a14b66
feat: add herdr control plane to terminal session engine
salemsayed Jul 19, 2026
063f20f
feat: add native herdr panel to tab manager and command palette
salemsayed Jul 19, 2026
858759e
feat: notify on herdr agent blocked/done and tune polling cadence
salemsayed Jul 19, 2026
872c559
feat: show herdr tabs in the terminal tab strip
salemsayed Jul 20, 2026
248aa8d
feat: parse herdr layout geometry in snapshot
salemsayed Jul 20, 2026
34a247a
feat: add herdr terminal frame codec
salemsayed Jul 20, 2026
34b7ab3
feat: stream herdr panes into native terminals
salemsayed Jul 20, 2026
9e5bafc
feat: route input to the focused herdr pane
salemsayed Jul 20, 2026
d97b1f1
feat: render herdr tabs as native splits
salemsayed Jul 20, 2026
fac3d4c
feat: add per-host herdr native splits toggle
salemsayed Jul 20, 2026
1cf0e14
fix: create herdr pane streams from reconciliation
salemsayed Jul 20, 2026
24febb0
perf: hold control on every visible herdr pane, route input to focused
salemsayed Jul 20, 2026
94befcf
feat: herdr switcher home on the connection tab
salemsayed Jul 20, 2026
d1bdda4
fix: apply current theme to newly created herdr pane VTs
salemsayed Jul 20, 2026
2d07124
feat: add multi-exec-channel support to the native SSH bridge
salemsayed Jul 20, 2026
893f343
perf: stream herdr panes and verbs over one shared SSH connection
salemsayed Jul 20, 2026
0907e23
fix: render herdr panes at native size instead of letterboxed
salemsayed Jul 20, 2026
ce803c9
fix: sync herdr pane geometry after the channel opens
salemsayed Jul 20, 2026
d506197
fix: size herdr split panes by edges so the last pane fits
salemsayed Jul 20, 2026
1364bfb
fix: correct herdr pane scroll direction and tap-to-focus keyboard
salemsayed Jul 20, 2026
ecd789c
fix: streamline native herdr connect and force the tab strip
salemsayed Jul 20, 2026
a28ac53
feat: show server connections in the herdr switcher home
salemsayed Jul 20, 2026
e572963
fix: label herdr home connections by server name, not tab alias
salemsayed Jul 20, 2026
4879f9b
fix: summarize workspace agent status by counts, not a single rollup
salemsayed Jul 20, 2026
8286493
feat: add a "needs you" attention section to the herdr home
salemsayed Jul 20, 2026
c3fc6cb
polish: herdr home density, pluralization, and title cleanup
salemsayed Jul 20, 2026
1dff2a2
fix: don't crash when a herdr control verb hits a dead connection
salemsayed Jul 20, 2026
84b4544
feat: allow herdr commands to target a named session
salemsayed Jul 20, 2026
21896b4
feat: create herdr workspaces from the switcher home
salemsayed Jul 20, 2026
64955dd
fix: skip the session picker for classic herdr too, not just native
salemsayed Jul 20, 2026
83c8a5b
fix: don't mirror herdr tabs in the strip for classic herdr mode
salemsayed Jul 20, 2026
93cb5ab
feat: add herdr pane split/close and workspace close verbs
salemsayed Jul 20, 2026
765c563
feat: create/destroy herdr panes, tabs, workspaces from native UI
salemsayed Jul 20, 2026
48a962f
fix: bootstrap single herdr pane at last measured geometry
salemsayed Jul 20, 2026
9b1a03a
feat: expand herdr workspaces inline to pick a tab or pane
salemsayed Jul 20, 2026
65601cd
polish: cleaner herdr split panes — dim inactive, no boxes
salemsayed Jul 20, 2026
4758175
feat: pinch zoom in herdr panes + smooth stepped font zoom
salemsayed Jul 20, 2026
6c44a5a
feat: show focused workspace name in herdr tab strip chip
salemsayed Jul 20, 2026
a97e0ac
feat: near-instant herdr tab switching — warm pane streams + optimist…
salemsayed Jul 20, 2026
1419293
fix: harden herdr stream lifecycles
salemsayed Jul 20, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
185 changes: 185 additions & 0 deletions android/app/schemas/com.jossephus.chuchu.data.db.AppDatabase/12.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
{
"formatVersion": 1,
"database": {
"version": 12,
"identityHash": "e1c59cbcdee1384ed9afa8331c0d766e",
"entities": [
{
"tableName": "host_profiles",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `host` TEXT NOT NULL, `port` INTEGER NOT NULL, `username` TEXT NOT NULL, `password` TEXT NOT NULL, `keyId` INTEGER, `keyPassphrase` TEXT NOT NULL, `transport` TEXT NOT NULL, `authMethod` TEXT NOT NULL, `requireAuthOnConnect` INTEGER NOT NULL DEFAULT 0, `postConnectCommand` TEXT, `multiplexer` TEXT DEFAULT NULL, `herdrNativeMode` INTEGER NOT NULL DEFAULT 1)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "host",
"columnName": "host",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "port",
"columnName": "port",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "username",
"columnName": "username",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "password",
"columnName": "password",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "keyId",
"columnName": "keyId",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "keyPassphrase",
"columnName": "keyPassphrase",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "transport",
"columnName": "transport",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "authMethod",
"columnName": "authMethod",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "requireAuthOnConnect",
"columnName": "requireAuthOnConnect",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "postConnectCommand",
"columnName": "postConnectCommand",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "multiplexer",
"columnName": "multiplexer",
"affinity": "TEXT",
"notNull": false,
"defaultValue": "NULL"
},
{
"fieldPath": "herdrNativeMode",
"columnName": "herdrNativeMode",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "1"
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
},
"indices": [
{
"name": "index_host_profiles_name",
"unique": false,
"columnNames": [
"name"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_host_profiles_name` ON `${TABLE_NAME}` (`name`)"
}
],
"foreignKeys": []
},
{
"tableName": "ssh_keys",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `algorithm` TEXT NOT NULL, `privateKeyPem` TEXT NOT NULL, `publicKeyOpenSsh` TEXT NOT NULL, `createdAtEpochMs` INTEGER NOT NULL)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "algorithm",
"columnName": "algorithm",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "privateKeyPem",
"columnName": "privateKeyPem",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "publicKeyOpenSsh",
"columnName": "publicKeyOpenSsh",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "createdAtEpochMs",
"columnName": "createdAtEpochMs",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
},
"indices": [
{
"name": "index_ssh_keys_name",
"unique": true,
"columnNames": [
"name"
],
"orders": [],
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_ssh_keys_name` ON `${TABLE_NAME}` (`name`)"
}
],
"foreignKeys": []
}
],
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'e1c59cbcdee1384ed9afa8331c0d766e')"
]
}
}
22 changes: 22 additions & 0 deletions android/app/src/main/java/com/jossephus/chuchu/MainActivity.kt
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package com.jossephus.chuchu

import android.app.Application
import android.content.Intent
import android.os.Bundle
import androidx.activity.SystemBarStyle
import androidx.activity.compose.setContent
Expand All @@ -13,12 +15,16 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.fragment.app.FragmentActivity
import androidx.lifecycle.lifecycleScope
import com.jossephus.chuchu.data.repository.SettingsRepository
import com.jossephus.chuchu.service.terminal.HerdrAgentNotifier
import com.jossephus.chuchu.service.terminal.TerminalSessionRepository
import com.jossephus.chuchu.ui.ApplicationNavController
import com.jossephus.chuchu.ui.theme.ChuColors
import com.jossephus.chuchu.ui.theme.ChuTheme
import com.jossephus.chuchu.ui.theme.GhosttyThemeRegistry
import com.jossephus.chuchu.ui.theme.resolveActiveThemeName
import kotlinx.coroutines.launch

class MainActivity : FragmentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
Expand All @@ -30,6 +36,22 @@ class MainActivity : FragmentActivity() {
setContent {
AppRoot()
}
handleHerdrNotificationIntent(intent)
}

override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
setIntent(intent)
handleHerdrNotificationIntent(intent)
}

private fun handleHerdrNotificationIntent(intent: Intent?) {
val sessionId = intent?.getStringExtra(HerdrAgentNotifier.EXTRA_TAB_SESSION_ID) ?: return
val herdrTabId = intent.getStringExtra(HerdrAgentNotifier.EXTRA_HERDR_TAB_ID) ?: return
val repository = TerminalSessionRepository.getInstance(application as Application)
repository.selectTab(sessionId)
val tab = repository.tabs.value.firstOrNull { it.id == sessionId } ?: return
lifecycleScope.launch { tab.engine.herdrFocusTab(herdrTabId) }
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ data class BackupHostProfile(
val requireAuthOnConnect: Boolean,
val postConnectCommand: String?,
val multiplexer: MultiplexerType?,
val herdrNativeMode: Boolean = true,
) {
fun toEntity(
idOverride: Long = id,
Expand All @@ -82,6 +83,7 @@ data class BackupHostProfile(
requireAuthOnConnect = requireAuthOnConnect,
postConnectCommand = postConnectCommand,
multiplexer = multiplexer,
herdrNativeMode = herdrNativeMode,
)

companion object {
Expand All @@ -99,6 +101,7 @@ data class BackupHostProfile(
requireAuthOnConnect = host.requireAuthOnConnect,
postConnectCommand = host.postConnectCommand,
multiplexer = host.multiplexer,
herdrNativeMode = host.herdrNativeMode,
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,15 @@ import com.jossephus.chuchu.model.SshKey

@Database(
entities = [HostProfile::class, SshKey::class],
version = 11,
version = 12,
autoMigrations = [
AutoMigration(from = 5, to = 6),
AutoMigration(from = 6, to = 7, spec = AppDatabase.Migration6To7::class),
AutoMigration(from = 7, to = 8),
AutoMigration(from = 8, to = 9),
AutoMigration(from = 9, to = 10, spec = AppDatabase.Migration9To10::class),
AutoMigration(from = 10, to = 11),
AutoMigration(from = 11, to = 12),
],
)
@TypeConverters(Converters::class)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,10 @@ class SettingsRepository(context: Context) {
private val _localShellEnabled = MutableStateFlow(prefs.getBoolean(KEY_LOCAL_SHELL_ENABLED, true))
val localShellEnabled: StateFlow<Boolean> = _localShellEnabled.asStateFlow()

private val _herdrNotificationsEnabled =
MutableStateFlow(prefs.getBoolean(KEY_HERDR_NOTIFICATIONS, true))
val herdrNotificationsEnabled: StateFlow<Boolean> = _herdrNotificationsEnabled.asStateFlow()

private val _terminalTabMode = MutableStateFlow(
parseTabMode(prefs.getString(KEY_TAB_MODE, TerminalTabMode.Classic.name)),
)
Expand Down Expand Up @@ -131,6 +135,11 @@ class SettingsRepository(context: Context) {
_localShellEnabled.value = enabled
}

fun setHerdrNotificationsEnabled(enabled: Boolean) {
prefs.edit().putBoolean(KEY_HERDR_NOTIFICATIONS, enabled).apply()
_herdrNotificationsEnabled.value = enabled
}

fun setThemeMode(mode: ThemeMode) {
prefs.edit().putString(KEY_THEME_MODE, mode.name).apply()
_themeMode.value = mode
Expand Down Expand Up @@ -193,6 +202,7 @@ class SettingsRepository(context: Context) {
private const val KEY_APP_LOCK_ENABLED = "app_lock_enabled"
private const val KEY_REQUIRE_AUTH_ON_CONNECT = "require_auth_on_connect"
private const val KEY_LOCAL_SHELL_ENABLED = "local_shell_enabled"
private const val KEY_HERDR_NOTIFICATIONS = "herdr_notifications_enabled"
private const val KEY_THEME_MODE = "theme_mode"
private const val KEY_LIGHT_THEME = "light_theme_name"
private const val KEY_TERMINAL_FONT_SIZE = "terminal_font_size_sp"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,6 @@ data class HostProfile(
val postConnectCommand: String? = null,
@ColumnInfo(defaultValue = "NULL")
val multiplexer: MultiplexerType? = null,
@ColumnInfo(defaultValue = "1")
val herdrNativeMode: Boolean = true,
)
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ enum class MultiplexerType(
Tmux("tmux", "tmux", true),
Zellij("zellij", "zellij", false),
Zmx("zmx", "zmx", true),
Herdr("herdr", "herdr", true),
;

companion object {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import com.jossephus.chuchu.model.Transport

object ChuchuBackupCodec {
const val FORMAT_VERSION: Int = 1
const val PAYLOAD_VERSION: Int = 2
const val PAYLOAD_VERSION: Int = 3
const val KDF_ID_PBKDF2_HMAC_SHA1: Int = 1
const val CIPHER_ID_AES_256_GCM: Int = 1
const val KDF_ITERATIONS: Int = 210_000
Expand Down Expand Up @@ -102,6 +102,7 @@ object ChuchuBackupCodec {
writer.writeBoolean(host.requireAuthOnConnect)
writeNullableStringField(host.postConnectCommand, "post-connect command")
writeNullableStringField(host.multiplexer?.id, "multiplexer")
writer.writeBoolean(host.herdrNativeMode)
}

val encoded = writer.toByteArray()
Expand Down Expand Up @@ -173,6 +174,7 @@ object ChuchuBackupCodec {
} else {
null
}
val herdrNativeMode = if (version >= 3) reader.readBoolean() else true
if (transport != Transport.LocalShell) {
add(
BackupHostProfile(
Expand All @@ -189,6 +191,7 @@ object ChuchuBackupCodec {
requireAuthOnConnect = requireAuthOnConnect,
postConnectCommand = postConnectCommand,
multiplexer = multiplexer,
herdrNativeMode = herdrNativeMode,
),
)
}
Expand Down
Loading