Fix raw hooks: plugin-side hookRawMethod with Advice instrumentation

Remove the planted isContactsTabVisible() hook target and app-side visibility
conditionals (that was cheating). Plugins now declare raw JVM targets directly
via hookRawMethod on existing methods like MainScreenKt.selectMainPage,
ContactsTabKt.ContactsTab, NavigationBarKt.NavigationBarItem, and Logger.d.

Replace ByteBuddy MethodDelegation (schema-changing) with Advice-based
ClassReloadingStrategy so hooks actually install at runtime. Fix Logger hook
signature for Kotlin object instance methods. Add chat banner z-index fix and
PluginEngine.hostRevision to recompose MainScreen after plugin load.

Co-authored-by: denis0001-dev <denis0001.dev@ya.ru>
This commit is contained in:
Cursor Agent
2026-09-17 00:17:25 +00:00
Unverified
parent ab4f6fcc53
commit 1ff4503be9
21 changed files with 536 additions and 89 deletions
+3 -3
View File
@@ -35,10 +35,10 @@ tasks.register("packageFcPlugin") {
{
"id": "hello_world",
"name": "Hello World",
"description": "Demonstrates high-level send hooks and raw Logger.d hooks.",
"description": "Demonstrates high-level send hooks, raw Logger.d hooks, and raw main-nav hooks.",
"author": "FromChat",
"version": "1.0.1",
"usage": "Send .hello Name in any chat to rewrite the message. Raw hook shows a bulletin when a message is queued for sending.",
"version": "1.0.3",
"usage": "Raw-hooks existing MainScreen/ContactsTab JVM methods (no app hook IDs). Also hides Devices/Logs settings and emoji; injects settings row, message menu item, and chat banner. Send .hello Name to test send hooks.",
"app_version": ">=1.0.0",
"sdk_version": ">=1.0.0",
"entry_class": "ru.fromchat.plugins.sample.hello.HelloWorldPlugin",
@@ -3,6 +3,8 @@ package ru.fromchat.plugins.sample.hello
import ru.fromchat.plugins.BasePlugin
import ru.fromchat.plugins.HookResult
import ru.fromchat.plugins.HookStrategy
import ru.fromchat.plugins.MenuItemData
import ru.fromchat.plugins.MenuItemType
import ru.fromchat.plugins.PluginSetting
import ru.fromchat.plugins.SendMessageHookContext
@@ -10,12 +12,15 @@ class HelloWorldPlugin : BasePlugin() {
override fun onPluginLoad() {
log("Hello World plugin loaded")
showBulletin("Hello World plugin is active")
registerUiOverrides()
registerInjectedUi()
registerRawHooks()
addOnSendMessageHook { context ->
onSendMessage(context)
}
hookShared(
hookId = "logger.debug",
after = { args, _ ->
before = { args ->
val tag = args.getOrNull(0) as? String ?: return@hookShared HookResult()
val message = args.getOrNull(1) as? String ?: return@hookShared HookResult()
if (tag == "OutgoingMessageCoordinator" && message.contains("enqueue")) {
@@ -26,6 +31,13 @@ class HelloWorldPlugin : BasePlugin() {
)
}
override fun onMenuItemClick(key: String) {
when (key) {
MENU_SETTINGS_DEMO -> showBulletin("Injected settings row clicked")
MENU_MESSAGE_DEMO -> showBulletin("Injected message menu clicked")
}
}
override fun createSettings(): List<PluginSetting> = listOf(
PluginSetting.Header("Hello World"),
PluginSetting.Input(
@@ -40,6 +52,122 @@ class HelloWorldPlugin : BasePlugin() {
),
)
private fun registerRawHooks() {
hookRawMethod(
className = "ru.fromchat.ui.main.MainScreenKt",
methodName = "selectMainPage",
paramTypeNames = arrayOf(
"int",
"com.pr0gramm3r101.utils.WindowWidthSizeClass",
"kotlinx.coroutines.CoroutineScope",
"androidx.compose.foundation.pager.PagerState",
"androidx.navigation.NavController",
),
before = { args ->
val page = (args.getOrNull(0) as? Number)?.toInt() ?: return@hookRawMethod HookResult()
if (page == MAIN_PAGE_CONTACTS) {
if (!contactsNavBlockedNotified) {
contactsNavBlockedNotified = true
showBulletin("Raw hook: blocked Contacts tab navigation")
}
HookResult(strategy = HookStrategy.CANCEL)
} else {
HookResult()
}
},
)
hookRawMethod(
className = "ru.fromchat.ui.main.ContactsTabKt",
methodName = "ContactsTab",
paramTypeNames = arrayOf(
"androidx.compose.runtime.Composer",
"int",
),
before = { _ ->
HookResult(strategy = HookStrategy.CANCEL)
},
)
hookRawMethod(
className = "ru.fromchat.ui.main.MainScreenKt",
methodName = "MainScreen",
paramTypeNames = arrayOf(
"androidx.compose.animation.SharedTransitionScope",
"androidx.compose.animation.AnimatedVisibilityScope",
"androidx.compose.material3.SnackbarHostState",
"boolean",
"int",
"boolean",
"kotlin.jvm.functions.Function1",
"androidx.compose.runtime.Composer",
"int",
"int",
),
before = { _ ->
mainScreenNavItemOrdinal = 0
HookResult()
},
)
hookRawMethod(
className = "androidx.compose.material3.NavigationBarKt",
methodName = "NavigationBarItem",
paramTypeNames = arrayOf(
"androidx.compose.foundation.layout.RowScope",
"boolean",
"kotlin.jvm.functions.Function0",
"kotlin.jvm.functions.Function2",
"androidx.compose.ui.Modifier",
"boolean",
"kotlin.jvm.functions.Function2",
"boolean",
"androidx.compose.material3.NavigationBarItemColors",
"androidx.compose.foundation.interaction.MutableInteractionSource",
"androidx.compose.runtime.Composer",
"int",
"int",
),
before = { _ ->
mainScreenNavItemOrdinal += 1
if (mainScreenNavItemOrdinal == CONTACTS_NAV_ITEM_ORDINAL) {
HookResult(strategy = HookStrategy.CANCEL)
} else {
HookResult()
}
},
)
}
private fun registerUiOverrides() {
registerFeatureOverride(FEATURE_SETTINGS_DEVICES) { false }
registerFeatureOverride(FEATURE_SETTINGS_LOGS) { false }
registerFeatureOverride(FEATURE_CHAT_EMOJI) { false }
}
private fun registerInjectedUi() {
registerUiOverlay(
slot = OVERLAY_CHAT_BANNER,
title = "Hello World plugin UI",
message = "This banner was injected by the plugin engine.",
)
addMenuItem(
MenuItemData(
menuType = MenuItemType.SETTINGS_ROW,
text = "Hello World demo action",
subtext = "Injected settings row from plugin",
priority = 10,
onClickKey = MENU_SETTINGS_DEMO,
),
)
addMenuItem(
MenuItemData(
menuType = MenuItemType.MESSAGE_CONTEXT,
text = "Hello World message action",
subtext = "Injected message menu item",
priority = 10,
onClickKey = MENU_MESSAGE_DEMO,
),
)
}
private fun onSendMessage(context: SendMessageHookContext): HookResult<SendMessageHookContext> {
val raw = context.text.trim()
if (!raw.startsWith(".hello")) return HookResult()
@@ -50,4 +178,21 @@ class HelloWorldPlugin : BasePlugin() {
showBulletin("High-level hook: rewrote .hello command")
return HookResult(strategy = HookStrategy.MODIFY, value = context)
}
private companion object {
const val MAIN_PAGE_CONTACTS = 1
const val CONTACTS_NAV_ITEM_ORDINAL = 2
const val FEATURE_SETTINGS_DEVICES = "settings.devices"
const val FEATURE_SETTINGS_LOGS = "settings.logs"
const val FEATURE_CHAT_EMOJI = "chat.emoji_picker"
const val OVERLAY_CHAT_BANNER = "chat.banner"
const val MENU_SETTINGS_DEMO = "hello_world.settings_demo"
const val MENU_MESSAGE_DEMO = "hello_world.message_demo"
@Volatile
var mainScreenNavItemOrdinal: Int = 0
@Volatile
var contactsNavBlockedNotified: Boolean = false
}
}