git.iko.soy/opencode-android
on master help sign in
Eliminate redundant string lookups and substring allocations on the streaming hot path
734c312iko
app/src/main/java/soy/iko/opencode/ui/chat/ChatScreen.kt+10−5
@@ -130,6 +130,14 @@ fun ChatScreen(
130 val shareContext = LocalContext.current
130 val shareContext = LocalContext.current
131 val shareLabel = stringResource(R.string.share)
131 val shareLabel = stringResource(R.string.share)
132 val defaultShareSubject = stringResource(R.string.share_subject)
132 val defaultShareSubject = stringResource(R.string.share_subject)
133+ val sessionLabel = stringResource(R.string.session)
134+ val defaultModelLabel = stringResource(R.string.default_model)
135+ val subtitle = remember(selectedModel?.modelLabel, selectedAgent, defaultModelLabel) {
136+ buildString {
137+ append(selectedModel?.modelLabel ?: defaultModelLabel)
138+ selectedAgent?.let { append(" · $it") }
139+ }
140+ }
133
141
134 // Connection profile (with auth) for rendering inline image attachments.
142 // Connection profile (with auth) for rendering inline image attachments.
135 // Collect as state so a server switch recomposes the image context.
143 // Collect as state so a server switch recomposes the image context.
@@ -197,12 +205,9 @@ fun ChatScreen(
197 Column(modifier = Modifier
205 Column(modifier = Modifier
198 .clickable(enabled = models.isNotEmpty(), role = Role.Button) { showModelPicker = true }
206 .clickable(enabled = models.isNotEmpty(), role = Role.Button) { showModelPicker = true }
199 ) {
207 ) {
200− Text(sessionTitle ?: stringResource(R.string.session))
208+ Text(sessionTitle ?: sessionLabel)
201 Text(
209 Text(
202− buildString {
210+ subtitle,
203− append(selectedModel?.modelLabel ?: stringResource(R.string.default_model))
204− selectedAgent?.let { append(" · $it") }
205− },
206 style = MaterialTheme.typography.labelSmall,
211 style = MaterialTheme.typography.labelSmall,
207 color = MaterialTheme.colorScheme.onSurfaceVariant,
212 color = MaterialTheme.colorScheme.onSurfaceVariant,
208 )
213 )
app/src/main/java/soy/iko/opencode/ui/chat/PartRenderer.kt+3−2
@@ -97,6 +97,8 @@ private fun ReasoningBlock(text: String, streaming: Boolean, modifier: Modifier)
97 var expanded by rememberSaveable { mutableStateOf(false) }
97 var expanded by rememberSaveable { mutableStateOf(false) }
98 val expandedState = stringResource(R.string.state_expanded)
98 val expandedState = stringResource(R.string.state_expanded)
99 val collapsedState = stringResource(R.string.state_collapsed)
99 val collapsedState = stringResource(R.string.state_collapsed)
100+ val thinkingLabel = stringResource(R.string.thinking)
101+ val thoughtsLabel = stringResource(R.string.thoughts)
100 Column(modifier = modifier.fillMaxWidth()) {
102 Column(modifier = modifier.fillMaxWidth()) {
101 Row(
103 Row(
102 modifier = Modifier
104 modifier = Modifier
@@ -110,7 +112,6 @@ private fun ReasoningBlock(text: String, streaming: Boolean, modifier: Modifier)
110 verticalAlignment = Alignment.CenterVertically,
112 verticalAlignment = Alignment.CenterVertically,
111 ) {
113 ) {
112 if (streaming) {
114 if (streaming) {
113− val thinkingLabel = stringResource(R.string.thinking)
114 CircularProgressIndicator(
115 CircularProgressIndicator(
115 Modifier.size(14.dp).semantics { contentDescription = thinkingLabel },
116 Modifier.size(14.dp).semantics { contentDescription = thinkingLabel },
116 strokeWidth = 2.dp,
117 strokeWidth = 2.dp,
@@ -125,7 +126,7 @@ private fun ReasoningBlock(text: String, streaming: Boolean, modifier: Modifier)
125 )
126 )
126 }
127 }
127 Text(
128 Text(
128− if (streaming) stringResource(R.string.thinking) else stringResource(R.string.thoughts),
129+ if (streaming) thinkingLabel else thoughtsLabel,
129 style = MaterialTheme.typography.labelMedium,
130 style = MaterialTheme.typography.labelMedium,
130 color = MaterialTheme.colorScheme.onSurfaceVariant,
131 color = MaterialTheme.colorScheme.onSurfaceVariant,
131 modifier = Modifier.padding(start = 6.dp),
132 modifier = Modifier.padding(start = 6.dp),
app/src/main/java/soy/iko/opencode/ui/components/MarkdownText.kt+3−2
@@ -91,12 +91,13 @@ fun MarkdownText(
91 val markdownState = rememberUpdatedState(markdown)
91 val markdownState = rememberUpdatedState(markdown)
92 // Keyed to a content-prefix so switching to a different message resets
92 // Keyed to a content-prefix so switching to a different message resets
93 // immediately instead of showing the old message for one frame.
93 // immediately instead of showing the old message for one frame.
94+ val prefix = markdown.take(32)
94− var renderedContent by remember(markdown.take(32)) { mutableStateOf(markdown) }
95+ var renderedContent by remember(prefix) { mutableStateOf(markdown) }
95 // conflate() coalesces a burst of token updates into a single emission so the
96 // conflate() coalesces a burst of token updates into a single emission so the
96 // collector sees only the latest value after the previous delay completes. Plain
97 // collector sees only the latest value after the previous delay completes. Plain
97 // collect (not collectLatest) is critical: collectLatest would cancel the delay
98 // collect (not collectLatest) is critical: collectLatest would cancel the delay
98 // on every new value, reintroducing the same starvation the keyed effect had.
99 // on every new value, reintroducing the same starvation the keyed effect had.
99− LaunchedEffect(markdown.take(32)) {
100+ LaunchedEffect(prefix) {
100 snapshotFlow { markdownState.value }
101 snapshotFlow { markdownState.value }
101 .conflate()
102 .conflate()
102 .collect { md ->
103 .collect { md ->