From 8f35fc78a093050af4871fb56dca7b125e788a27 Mon Sep 17 00:00:00 2001 From: Shivang Date: Sun, 27 Sep 2026 14:50:29 -0700 Subject: [PATCH 1/5] feat(api): add optional terminal titlebar action bridge --- build.gradle.kts | 3 +- .../boss/plugin/ui/TerminalTitleBarBridge.kt | 36 +++++++++++++++++++ 2 files changed, 38 insertions(+), 1 deletion(-) create mode 100644 src/main/kotlin/ai/rever/boss/plugin/ui/TerminalTitleBarBridge.kt diff --git a/build.gradle.kts b/build.gradle.kts index e6b0ad9..464788b 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -595,7 +595,8 @@ group = "ai.rever.boss.plugin.bundled" // ApiClassLoader/minApiVersion, no host release. // // Next release (1.0.94): optional native browser title-bar navigation bridge. -version = "1.0.94" +// 1.0.95 adds the optional, window-scoped terminal native-titlebar action bridge. +version = "1.0.95" java { toolchain { diff --git a/src/main/kotlin/ai/rever/boss/plugin/ui/TerminalTitleBarBridge.kt b/src/main/kotlin/ai/rever/boss/plugin/ui/TerminalTitleBarBridge.kt new file mode 100644 index 0000000..5db539f --- /dev/null +++ b/src/main/kotlin/ai/rever/boss/plugin/ui/TerminalTitleBarBridge.kt @@ -0,0 +1,36 @@ +package ai.rever.boss.plugin.ui + +import androidx.compose.runtime.mutableStateMapOf +import androidx.compose.ui.graphics.vector.ImageVector + +/** UI-thread-only, window-scoped terminal controls hosted by an optional native titlebar. */ +class TerminalTitleBarAction( + val id: String, + val label: String, + val symbol: String, + val icon: ImageVector, + val active: Boolean, + val onClick: () -> Unit, +) + +/** Unsupported hosts leave the window unclaimed, retaining the terminal's floating controls. */ +object TerminalTitleBarBridge { + private data class Entry(val windowId: String, val active: Boolean, val actions: List) + private val windows = mutableStateMapOf() + private val entries = mutableStateMapOf() + + fun hostWindow(windowId: String, enabled: Boolean) { + if (enabled) windows[windowId] = true else windows.remove(windowId) + } + + fun isHosted(windowId: String): Boolean = windows[windowId] == true + + fun publish(windowId: String, owner: Any, active: Boolean, actions: List) { + entries[owner] = Entry(windowId, active, actions) + } + + fun remove(owner: Any) { entries.remove(owner) } + + fun actions(windowId: String): List = + entries.values.lastOrNull { it.windowId == windowId && it.active }?.actions.orEmpty() +} From 9e82a75c5751187e21857185679786b2acd664c5 Mon Sep 17 00:00:00 2001 From: Shivang Date: Sun, 27 Sep 2026 16:17:46 -0700 Subject: [PATCH 2/5] fix: record terminal titlebar API snapshot --- api/boss-plugin-api.api | 21 ++++++++++++++ .../boss/plugin/ui/TerminalTitleBarBridge.kt | 28 +++++++++++++++---- 2 files changed, 44 insertions(+), 5 deletions(-) diff --git a/api/boss-plugin-api.api b/api/boss-plugin-api.api index 66fe959..0df7c5c 100644 --- a/api/boss-plugin-api.api +++ b/api/boss-plugin-api.api @@ -7967,6 +7967,27 @@ public final class ai/rever/boss/plugin/ui/ContextMenuItemData { public fun toString ()Ljava/lang/String; } +public final class ai/rever/boss/plugin/ui/TerminalTitleBarAction { + public static final field $stable I + public fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Landroidx/compose/ui/graphics/vector/ImageVector;ZLkotlin/jvm/functions/Function0;)V + public final fun getActive ()Z + public final fun getIcon ()Landroidx/compose/ui/graphics/vector/ImageVector; + public final fun getId ()Ljava/lang/String; + public final fun getLabel ()Ljava/lang/String; + public final fun getOnClick ()Lkotlin/jvm/functions/Function0; + public final fun getSymbol ()Ljava/lang/String; +} + +public final class ai/rever/boss/plugin/ui/TerminalTitleBarBridge { + public static final field $stable I + public static final field INSTANCE Lai/rever/boss/plugin/ui/TerminalTitleBarBridge; + public final fun actions (Ljava/lang/String;)Ljava/util/List; + public final fun hostWindow (Ljava/lang/String;Z)V + public final fun isHosted (Ljava/lang/String;)Z + public final fun publish (Ljava/lang/String;Ljava/lang/Object;ZLjava/util/List;)V + public final fun remove (Ljava/lang/Object;)V +} + public final class ai/rever/boss/plugin/workspace/BreadcrumbConfig { public static final field $stable I public static final field Companion Lai/rever/boss/plugin/workspace/BreadcrumbConfig$Companion; diff --git a/src/main/kotlin/ai/rever/boss/plugin/ui/TerminalTitleBarBridge.kt b/src/main/kotlin/ai/rever/boss/plugin/ui/TerminalTitleBarBridge.kt index 5db539f..90d100d 100644 --- a/src/main/kotlin/ai/rever/boss/plugin/ui/TerminalTitleBarBridge.kt +++ b/src/main/kotlin/ai/rever/boss/plugin/ui/TerminalTitleBarBridge.kt @@ -15,22 +15,40 @@ class TerminalTitleBarAction( /** Unsupported hosts leave the window unclaimed, retaining the terminal's floating controls. */ object TerminalTitleBarBridge { - private data class Entry(val windowId: String, val active: Boolean, val actions: List) + private data class Entry( + val windowId: String, + val active: Boolean, + val actions: List, + ) + private val windows = mutableStateMapOf() private val entries = mutableStateMapOf() - fun hostWindow(windowId: String, enabled: Boolean) { + fun hostWindow( + windowId: String, + enabled: Boolean, + ) { if (enabled) windows[windowId] = true else windows.remove(windowId) } fun isHosted(windowId: String): Boolean = windows[windowId] == true - fun publish(windowId: String, owner: Any, active: Boolean, actions: List) { + fun publish( + windowId: String, + owner: Any, + active: Boolean, + actions: List, + ) { entries[owner] = Entry(windowId, active, actions) } - fun remove(owner: Any) { entries.remove(owner) } + fun remove(owner: Any) { + entries.remove(owner) + } fun actions(windowId: String): List = - entries.values.lastOrNull { it.windowId == windowId && it.active }?.actions.orEmpty() + entries.values + .lastOrNull { it.windowId == windowId && it.active } + ?.actions + .orEmpty() } From 793d12feeb0e6889f2328793419d0a4d31f5706b Mon Sep 17 00:00:00 2001 From: Shivang Date: Sun, 27 Sep 2026 16:39:00 -0700 Subject: [PATCH 3/5] feat(api): host terminal controls independently of tabs --- api/boss-plugin-api.api | 3 +++ .../boss/plugin/ui/TerminalTitleBarBridge.kt | 24 +++++++++++++++++++ 2 files changed, 27 insertions(+) diff --git a/api/boss-plugin-api.api b/api/boss-plugin-api.api index 0df7c5c..52a5a40 100644 --- a/api/boss-plugin-api.api +++ b/api/boss-plugin-api.api @@ -7981,11 +7981,14 @@ public final class ai/rever/boss/plugin/ui/TerminalTitleBarAction { public final class ai/rever/boss/plugin/ui/TerminalTitleBarBridge { public static final field $stable I public static final field INSTANCE Lai/rever/boss/plugin/ui/TerminalTitleBarBridge; + public final fun Content (Ljava/lang/String;Landroidx/compose/runtime/Composer;I)V public final fun actions (Ljava/lang/String;)Ljava/util/List; public final fun hostWindow (Ljava/lang/String;Z)V public final fun isHosted (Ljava/lang/String;)Z public final fun publish (Ljava/lang/String;Ljava/lang/Object;ZLjava/util/List;)V + public final fun registerProvider (Ljava/lang/Object;Lkotlin/jvm/functions/Function3;)V public final fun remove (Ljava/lang/Object;)V + public final fun unregisterProvider (Ljava/lang/Object;)V } public final class ai/rever/boss/plugin/workspace/BreadcrumbConfig { diff --git a/src/main/kotlin/ai/rever/boss/plugin/ui/TerminalTitleBarBridge.kt b/src/main/kotlin/ai/rever/boss/plugin/ui/TerminalTitleBarBridge.kt index 90d100d..10cc133 100644 --- a/src/main/kotlin/ai/rever/boss/plugin/ui/TerminalTitleBarBridge.kt +++ b/src/main/kotlin/ai/rever/boss/plugin/ui/TerminalTitleBarBridge.kt @@ -1,5 +1,7 @@ package ai.rever.boss.plugin.ui +import androidx.compose.runtime.Composable +import androidx.compose.runtime.key import androidx.compose.runtime.mutableStateMapOf import androidx.compose.ui.graphics.vector.ImageVector @@ -21,6 +23,28 @@ object TerminalTitleBarBridge { val actions: List, ) + private val providers = mutableStateMapOf Unit>() + + fun registerProvider( + owner: Any, + content: @Composable (String) -> Unit, + ) { + providers[owner] = content + } + + fun unregisterProvider(owner: Any) { + providers.remove(owner) + } + + @Composable + fun Content(windowId: String) { + if (isHosted(windowId)) { + providers.entries.lastOrNull()?.let { (owner, content) -> + key(owner, windowId) { content(windowId) } + } + } + } + private val windows = mutableStateMapOf() private val entries = mutableStateMapOf() From e2318798640be5f162232a58d841a0c8b69eeea3 Mon Sep 17 00:00:00 2001 From: Shivang Date: Sun, 27 Sep 2026 17:12:08 -0700 Subject: [PATCH 4/5] feat(api): bridge window-scoped live call panels --- api/boss-plugin-api.api | 4 +++ .../boss/plugin/ui/TerminalTitleBarBridge.kt | 32 +++++++++++++++++++ 2 files changed, 36 insertions(+) diff --git a/api/boss-plugin-api.api b/api/boss-plugin-api.api index 52a5a40..0020ac7 100644 --- a/api/boss-plugin-api.api +++ b/api/boss-plugin-api.api @@ -7981,13 +7981,17 @@ public final class ai/rever/boss/plugin/ui/TerminalTitleBarAction { public final class ai/rever/boss/plugin/ui/TerminalTitleBarBridge { public static final field $stable I public static final field INSTANCE Lai/rever/boss/plugin/ui/TerminalTitleBarBridge; + public final fun CallBarContent (Ljava/lang/String;Landroidx/compose/runtime/Composer;I)V public final fun Content (Ljava/lang/String;Landroidx/compose/runtime/Composer;I)V public final fun actions (Ljava/lang/String;)Ljava/util/List; + public final fun hasCallBar (Ljava/lang/String;)Z public final fun hostWindow (Ljava/lang/String;Z)V public final fun isHosted (Ljava/lang/String;)Z public final fun publish (Ljava/lang/String;Ljava/lang/Object;ZLjava/util/List;)V + public final fun publishCallBar (Ljava/lang/String;Ljava/lang/Object;Lkotlin/jvm/functions/Function2;)V public final fun registerProvider (Ljava/lang/Object;Lkotlin/jvm/functions/Function3;)V public final fun remove (Ljava/lang/Object;)V + public final fun removeCallBar (Ljava/lang/Object;)V public final fun unregisterProvider (Ljava/lang/Object;)V } diff --git a/src/main/kotlin/ai/rever/boss/plugin/ui/TerminalTitleBarBridge.kt b/src/main/kotlin/ai/rever/boss/plugin/ui/TerminalTitleBarBridge.kt index 10cc133..7ff47ca 100644 --- a/src/main/kotlin/ai/rever/boss/plugin/ui/TerminalTitleBarBridge.kt +++ b/src/main/kotlin/ai/rever/boss/plugin/ui/TerminalTitleBarBridge.kt @@ -45,6 +45,38 @@ object TerminalTitleBarBridge { } } + private class CallBar( + val windowId: String, + val content: @Composable () -> Unit, + ) + + private val callBars = mutableStateMapOf() + + fun publishCallBar( + windowId: String, + owner: Any, + content: @Composable () -> Unit, + ) { + val previous = callBars[owner] + if (previous?.windowId != windowId || previous.content !== content) { + callBars[owner] = CallBar(windowId, content) + } + } + + fun removeCallBar(owner: Any) { + callBars.remove(owner) + } + + fun hasCallBar(windowId: String): Boolean = callBars.values.any { it.windowId == windowId } + + @Composable + fun CallBarContent(windowId: String) { + callBars.values + .lastOrNull { it.windowId == windowId } + ?.content + ?.invoke() + } + private val windows = mutableStateMapOf() private val entries = mutableStateMapOf() From dbef8041da2c7d24c5218f4a84cb166d36c748b4 Mon Sep 17 00:00:00 2001 From: Shivang Date: Sun, 27 Sep 2026 17:35:47 -0700 Subject: [PATCH 5/5] fix(api): define terminal bridge ownership and release version --- build.gradle.kts | 6 +- .../boss/plugin/ui/TerminalTitleBarBridge.kt | 100 ++++++++++---- .../plugin/ui/TerminalTitleBarBridgeTest.kt | 124 ++++++++++++++++++ 3 files changed, 200 insertions(+), 30 deletions(-) create mode 100644 src/test/kotlin/ai/rever/boss/plugin/ui/TerminalTitleBarBridgeTest.kt diff --git a/build.gradle.kts b/build.gradle.kts index 464788b..d2e2dcf 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -594,9 +594,11 @@ group = "ai.rever.boss.plugin.bundled" // CLI selection, rather than asking a consumer to duplicate routing policy. New types only: // ApiClassLoader/minApiVersion, no host release. // -// Next release (1.0.94): optional native browser title-bar navigation bridge. +// 1.0.94: optional native browser title-bar navigation bridge. // 1.0.95 adds the optional, window-scoped terminal native-titlebar action bridge. -version = "1.0.95" +version = "1.0.94" +// Local integration builds may stage the next artifact without advancing release CI twice. +providers.gradleProperty("localApiVersion").orNull?.let { version = it } java { toolchain { diff --git a/src/main/kotlin/ai/rever/boss/plugin/ui/TerminalTitleBarBridge.kt b/src/main/kotlin/ai/rever/boss/plugin/ui/TerminalTitleBarBridge.kt index 7ff47ca..be6e57e 100644 --- a/src/main/kotlin/ai/rever/boss/plugin/ui/TerminalTitleBarBridge.kt +++ b/src/main/kotlin/ai/rever/boss/plugin/ui/TerminalTitleBarBridge.kt @@ -1,11 +1,17 @@ package ai.rever.boss.plugin.ui +import ai.rever.boss.plugin.api.HostImplemented import androidx.compose.runtime.Composable import androidx.compose.runtime.key +import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.mutableStateMapOf import androidx.compose.ui.graphics.vector.ImageVector -/** UI-thread-only, window-scoped terminal controls hosted by an optional native titlebar. */ +/** + * Native control: [symbol] is an SF Symbol name, [icon] its Compose/vector fallback, and [active] + * is the service's selected state. Preserve this constructor; add compatible overloads instead. + */ +@HostImplemented class TerminalTitleBarAction( val id: String, val label: String, @@ -15,7 +21,21 @@ class TerminalTitleBarAction( val onClick: () -> Unit, ) -/** Unsupported hosts leave the window unclaimed, retaining the terminal's floating controls. */ +/** + * UI-thread-only bridge shared parent-first with BossConsole. API 1.0.95 introduces these types; + * native hosting requires a supporting host. Future members require a host release/minBossVersion. + * Unsupported hosts leave windows unclaimed, retaining terminal floating controls. + * + * The terminal plugin registers one provider, pairing registration with unregister on unload. + * Each composed provider pairs publish/remove and publishCallBar/removeCallBar on disposal. + * Publish from effects with stable callbacks. Owners are compared by identity, never equals. + * The most recently changed registration/publication wins when owners overlap; removing it + * restores the previous owner. Entry [publish] active means eligible, unlike action selected state. + * Hosts release their window claims on disposal; plugin cleanup cannot release a host claim. + */ +@HostImplemented +// One cohesive public bridge has three paired lifecycle protocols; keep existing ABI call sites. +@Suppress("TooManyFunctions") object TerminalTitleBarBridge { private data class Entry( val windowId: String, @@ -23,13 +43,21 @@ object TerminalTitleBarBridge { val actions: List, ) - private val providers = mutableStateMapOf Unit>() + private data class CallBar( + val windowId: String, + val content: @Composable () -> Unit, + ) + + private val providers = TerminalOwners<@Composable (String) -> Unit>() + private val callBars = TerminalOwners() + private val entries = TerminalOwners() + private val windows = mutableStateMapOf() fun registerProvider( owner: Any, content: @Composable (String) -> Unit, ) { - providers[owner] = content + providers.put(owner, content) } fun unregisterProvider(owner: Any) { @@ -39,47 +67,33 @@ object TerminalTitleBarBridge { @Composable fun Content(windowId: String) { if (isHosted(windowId)) { - providers.entries.lastOrNull()?.let { (owner, content) -> - key(owner, windowId) { content(windowId) } + providers.last()?.let { entry -> + key(entry.key, windowId) { entry.value(windowId) } } } } - private class CallBar( - val windowId: String, - val content: @Composable () -> Unit, - ) - - private val callBars = mutableStateMapOf() - fun publishCallBar( windowId: String, owner: Any, content: @Composable () -> Unit, ) { - val previous = callBars[owner] - if (previous?.windowId != windowId || previous.content !== content) { - callBars[owner] = CallBar(windowId, content) - } + callBars.put(owner, CallBar(windowId, content)) } fun removeCallBar(owner: Any) { callBars.remove(owner) } - fun hasCallBar(windowId: String): Boolean = callBars.values.any { it.windowId == windowId } + fun hasCallBar(windowId: String): Boolean = callBars.last { it.windowId == windowId } != null @Composable fun CallBarContent(windowId: String) { - callBars.values - .lastOrNull { it.windowId == windowId } - ?.content - ?.invoke() + callBars.last { it.windowId == windowId }?.let { entry -> + key(entry.key, windowId) { entry.value.content() } + } } - private val windows = mutableStateMapOf() - private val entries = mutableStateMapOf() - fun hostWindow( windowId: String, enabled: Boolean, @@ -95,7 +109,7 @@ object TerminalTitleBarBridge { active: Boolean, actions: List, ) { - entries[owner] = Entry(windowId, active, actions) + entries.put(owner, Entry(windowId, active, actions)) } fun remove(owner: Any) { @@ -103,8 +117,38 @@ object TerminalTitleBarBridge { } fun actions(windowId: String): List = - entries.values - .lastOrNull { it.windowId == windowId && it.active } + entries + .last { it.windowId == windowId && it.active } + ?.value ?.actions .orEmpty() } + +/** Identity-owned, explicitly ordered state. Unchanged effects do not invalidate composition. */ +private class TerminalOwners { + // Wrapper identity also gives Compose key() identity semantics for equal-but-distinct owners. + class Owned( + val owner: Any, + val value: T, + val key: Any = Any(), + ) + + private val values = mutableStateListOf>() + + fun put( + owner: Any, + value: T, + ) { + val index = values.indexOfFirst { it.owner === owner } + if (index >= 0 && values[index].value == value) return + val key = if (index >= 0) values.removeAt(index).key else Any() + values.add(Owned(owner, value, key)) + } + + fun remove(owner: Any) { + val index = values.indexOfFirst { it.owner === owner } + if (index >= 0) values.removeAt(index) + } + + fun last(predicate: (T) -> Boolean = { true }): Owned? = values.lastOrNull { predicate(it.value) } +} diff --git a/src/test/kotlin/ai/rever/boss/plugin/ui/TerminalTitleBarBridgeTest.kt b/src/test/kotlin/ai/rever/boss/plugin/ui/TerminalTitleBarBridgeTest.kt new file mode 100644 index 0000000..e81cce8 --- /dev/null +++ b/src/test/kotlin/ai/rever/boss/plugin/ui/TerminalTitleBarBridgeTest.kt @@ -0,0 +1,124 @@ +package ai.rever.boss.plugin.ui + +import androidx.compose.runtime.AbstractApplier +import androidx.compose.runtime.Composition +import androidx.compose.runtime.Recomposer +import androidx.compose.runtime.SideEffect +import androidx.compose.runtime.remember +import kotlinx.coroutines.runBlocking +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.unit.dp +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertTrue + +class TerminalTitleBarBridgeTest { + @Test + fun `provider arbitration is ordered and callbar owners have separate remembered state`() = runBlocking { + val first = EqualOwner() + val second = EqualOwner() + val recomposer = Recomposer(coroutineContext) + val composition = Composition(EmptyApplier(), recomposer) + var provider = "" + var value = "" + fun callBar(label: String): @androidx.compose.runtime.Composable () -> Unit = { + val remembered = remember { label } + SideEffect { value = remembered } + } + try { + TerminalTitleBarBridge.hostWindow("composed", true) + TerminalTitleBarBridge.registerProvider(first) { SideEffect { provider = "first" } } + TerminalTitleBarBridge.registerProvider(second) { SideEffect { provider = "second" } } + composition.setContent { TerminalTitleBarBridge.Content("composed") } + assertEquals("second", provider) + TerminalTitleBarBridge.unregisterProvider(second) + composition.setContent { TerminalTitleBarBridge.Content("composed") } + assertEquals("first", provider) + TerminalTitleBarBridge.publishCallBar("composed", first, callBar("first")) + composition.setContent { TerminalTitleBarBridge.CallBarContent("composed") } + assertEquals("first", value) + TerminalTitleBarBridge.publishCallBar("composed", second, callBar("second")) + composition.setContent { TerminalTitleBarBridge.CallBarContent("composed") } + assertEquals("second", value) + TerminalTitleBarBridge.removeCallBar(second) + composition.setContent { TerminalTitleBarBridge.CallBarContent("composed") } + assertEquals("first", value) + } finally { + composition.dispose() + recomposer.cancel() + TerminalTitleBarBridge.unregisterProvider(first) + TerminalTitleBarBridge.unregisterProvider(second) + TerminalTitleBarBridge.removeCallBar(first) + TerminalTitleBarBridge.removeCallBar(second) + TerminalTitleBarBridge.hostWindow("composed", false) + } + } + + private class EmptyApplier : AbstractApplier(Unit) { + override fun insertTopDown(index: Int, instance: Unit) = Unit + override fun insertBottomUp(index: Int, instance: Unit) = Unit + override fun remove(index: Int, count: Int) = Unit + override fun move(from: Int, to: Int, count: Int) = Unit + override fun onClear() = Unit + } + + private class EqualOwner { + override fun equals(other: Any?) = other is EqualOwner + override fun hashCode() = 0 + } + + private fun action(id: String) = TerminalTitleBarAction( + id, id, "", ImageVector.Builder(id, 1.dp, 1.dp, 1f, 1f).build(), false, {}, + ) + + @Test + fun `changed publications win and equal owners remain independent`() { + val first = EqualOwner() + val second = EqualOwner() + val a = listOf(action("first")) + val b = listOf(action("second")) + val changed = listOf(action("changed")) + try { + TerminalTitleBarBridge.publish("one", first, true, a) + TerminalTitleBarBridge.publish("one", second, true, b) + assertEquals(b, TerminalTitleBarBridge.actions("one")) + TerminalTitleBarBridge.publish("one", first, true, a) + assertEquals(b, TerminalTitleBarBridge.actions("one"), "unchanged effects do not steal ownership") + TerminalTitleBarBridge.publish("one", first, true, changed) + assertEquals(changed, TerminalTitleBarBridge.actions("one")) + TerminalTitleBarBridge.remove(first) + assertEquals(b, TerminalTitleBarBridge.actions("one")) + TerminalTitleBarBridge.publish("two", second, true, b) + assertTrue(TerminalTitleBarBridge.actions("one").isEmpty()) + assertEquals(b, TerminalTitleBarBridge.actions("two")) + TerminalTitleBarBridge.publish("two", second, false, b) + assertTrue(TerminalTitleBarBridge.actions("two").isEmpty()) + } finally { + TerminalTitleBarBridge.remove(first) + TerminalTitleBarBridge.remove(second) + } + } + + @Test + fun `callbar cleanup is identity scoped and cannot revoke host claim`() { + val first = EqualOwner() + val second = EqualOwner() + try { + TerminalTitleBarBridge.hostWindow("call-window", true) + TerminalTitleBarBridge.publishCallBar("call-window", first) {} + TerminalTitleBarBridge.publishCallBar("call-window", second) {} + TerminalTitleBarBridge.removeCallBar(first) + assertTrue(TerminalTitleBarBridge.hasCallBar("call-window")) + assertFalse(TerminalTitleBarBridge.hasCallBar("other")) + TerminalTitleBarBridge.removeCallBar(second) + assertFalse(TerminalTitleBarBridge.hasCallBar("call-window")) + assertTrue(TerminalTitleBarBridge.isHosted("call-window")) + } finally { + TerminalTitleBarBridge.removeCallBar(first) + TerminalTitleBarBridge.removeCallBar(second) + TerminalTitleBarBridge.hostWindow("call-window", false) + } + assertFalse(TerminalTitleBarBridge.isHosted("call-window")) + } +}