Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
3 changes: 2 additions & 1 deletion scripts/core-boundaries/rules/source/required-rules.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -5937,7 +5937,8 @@ export const requiredContentRules = [
message: 'missing remote chat history assembly delegation',
},
{
regex: /\bproject_remote_chat_user\(\s*turn\.user_message\.metadata\.as_ref\(\),\s*&prompt_visible_content\s*\)/,
regex:
/\bproject_remote_chat_user\(\s*turn\.user_message\.metadata\.as_ref\(\),\s*&prompt_visible_content,\s*read_image_pixels,?\s*\)/,
message: 'missing remote chat user projection owner delegation',
},
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import androidx.compose.ui.test.performClick
import androidx.compose.ui.test.performTextReplacement
import androidx.compose.ui.test.performTouchInput
import androidx.compose.ui.test.swipeDown
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.test.platform.app.InstrumentationRegistry
import com.openbitfun.mobile.app.ui.chat.CONVERSATION_LIST_TEST_TAG
Expand Down Expand Up @@ -288,6 +289,21 @@ class ConversationViewTest {
composeRule.onNodeWithTag(CHAT_STATUS_BAR_TEST_TAG).assertDoesNotExist()
}

/**
* A placeholder that outlives its subject reads as a hang, so the skeleton
* gives up on a transcript that never lands and lets the pane speak for
* itself. Matches HarmonyOS's `DeferredLoadingGate` cap.
*/
@Test
fun loadingSkeletonStopsStandingForATranscriptThatNeverArrives() {
composeRule.mainClock.autoAdvance = false
setConversationContent(state = { readyState(sessionId = "pending") })
composeRule.mainClock.advanceTimeBy(200)
composeRule.onNodeWithTag(CONVERSATION_LOADING_TEST_TAG).assertIsDisplayed()
composeRule.mainClock.advanceTimeBy(20_000)
composeRule.onNodeWithTag(CONVERSATION_LOADING_TEST_TAG).assertDoesNotExist()
}

@Test
fun composerShowsTheStoreDraftAndTypingDispatchesUpdateDraft() {
val intents = mutableListOf<RemoteSessionIntent>()
Expand Down Expand Up @@ -375,6 +391,44 @@ class ConversationViewTest {
composeRule.onNodeWithTag(COMPOSER_SEND_TEST_TAG).assertIsNotEnabled()
}

/**
* The composer and the header float over the transcript rather than sitting
* above and below it in a column, so the list runs the full height of the
* pane and these insets are the only thing keeping the first and last
* message out from under them. The insets are measured from the live
* overlays, so growing one — a composer going multiline, an image strip
* appearing — has to push the transcript rather than cover it.
*/
@Test
fun insetsKeepTheEndsOfTheTranscriptClearOfTheFloatingOverlays() {
val bottomInset = mutableStateOf(96.dp)
composeRule.setContent {
OpenBitFunTheme(dark = false) {
TimelineForTest(
rows = listOf(assistantRow("inset-marker")),
topInset = 72.dp,
bottomInset = bottomInset.value,
)
}
}
composeRule.waitForIdle()

val listBounds = composeRule.onNodeWithTag(CONVERSATION_LIST_TEST_TAG)
.getUnclippedBoundsInRoot()
val resting = composeRule.onNodeWithText("inset-marker", substring = true)
.getUnclippedBoundsInRoot()
assertTrue(resting.bottom <= listBounds.bottom - 96.dp + 1.dp)
assertTrue(resting.top >= listBounds.top)

composeRule.runOnIdle { bottomInset.value = 180.dp }
composeRule.waitForIdle()

val lifted = composeRule.onNodeWithText("inset-marker", substring = true)
.getUnclippedBoundsInRoot()
assertTrue(lifted.bottom <= listBounds.bottom - 180.dp + 1.dp)
assertTrue(lifted.bottom < resting.bottom)
}

private fun setConversationContent(
state: () -> RemoteSessionUiState.Ready,
phase: ConnectionPhase = ConnectionPhase.CONNECTED,
Expand Down Expand Up @@ -428,10 +482,17 @@ class ConversationViewTest {
)

@Composable
private fun TimelineForTest(rows: List<ConversationRow>, hasMoreMessages: Boolean = false) {
private fun TimelineForTest(
rows: List<ConversationRow>,
hasMoreMessages: Boolean = false,
topInset: Dp = 0.dp,
bottomInset: Dp = 0.dp,
) {
ConversationTimelineView(
rows = rows,
hasMoreMessages = hasMoreMessages,
topInset = topInset,
bottomInset = bottomInset,
onLoadOlder = {},
enabled = true,
onApproveTool = { _, _ -> },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -220,8 +220,8 @@ internal fun ComposerBar(
modifier = modifier
.fillMaxWidth()
.padding(
start = MobileDesignGeometry.ContentGutter,
end = MobileDesignGeometry.ContentGutter,
start = MobileDesignGeometry.ConversationOverlaySideInset,
end = MobileDesignGeometry.ConversationOverlaySideInset,
top = 8.dp,
bottom = 14.dp,
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import com.openbitfun.mobile.app.R
import com.openbitfun.mobile.core.feature.session.ConversationRow
Expand Down Expand Up @@ -85,6 +86,13 @@ internal fun ConversationTimelineView(
downloadEnabled: Boolean,
modifier: Modifier,
historyLoadState: HistoryLoadState = HistoryLoadState.IDLE,
/**
* How much of the pane the floating header and composer cover. The list
* runs the full height behind them, so without these the first and last
* messages would sit under a capsule and never come out from under it.
*/
topInset: Dp = 0.dp,
bottomInset: Dp = 0.dp,
) {
val listState = rememberLazyListState()
var stickToBottom by rememberSaveable { mutableStateOf(true) }
Expand Down Expand Up @@ -124,7 +132,12 @@ internal fun ConversationTimelineView(
LazyColumn(
state = listState,
modifier = Modifier.fillMaxSize().testTag(CONVERSATION_LIST_TEST_TAG),
contentPadding = PaddingValues(start = 20.dp, end = 20.dp, bottom = 12.dp),
contentPadding = PaddingValues(
start = 20.dp,
end = 20.dp,
top = topInset,
bottom = if (bottomInset > 0.dp) bottomInset else 12.dp,
),
verticalArrangement = Arrangement.spacedBy(12.dp, Alignment.Bottom),
) {
if (hasMoreMessages) {
Expand Down Expand Up @@ -172,7 +185,10 @@ internal fun ConversationTimelineView(
color = MaterialTheme.colorScheme.surface,
shadowElevation = 5.dp,
tonalElevation = 1.dp,
modifier = Modifier.align(Alignment.BottomCenter).offset(y = (-4).dp).size(42.dp),
// Sits above the floating composer rather than behind it.
modifier = Modifier.align(Alignment.BottomCenter)
.offset(y = -(bottomInset + 4.dp))
.size(42.dp),
) {
Box(contentAlignment = Alignment.Center) {
Icon(
Expand Down
Loading
Loading