Draw verbatim text on its own dark surface
A command, a tool's output and a code block in a reply are the one thing on this screen that is not somebody's prose, and they now say so: Mocha's Crust, which sits below Base, so the same colour is one clear step down both on the page where a reply is drawn and on the card where a tool call is. The renderer's code background was `surfaceVariant`, which is exactly a card's own fill -- a fenced block inside a tool call had no background at all, and one in a reply read as a step *up* out of the page. Tool output takes the monospace face with it. It is column-aligned far more often than it is prose -- a listing, a diff, a table of numbers -- and a proportional font silently destroys the alignment that carried the meaning. `RawBlock` is a composable rather than a modifier because the inset is part of it: monospace text against the edge of a tinted block reads as clipping. A call with neither a subject nor any other field draws nothing at all rather than an empty tinted rectangle. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
446d3b9c21
commit
e76617e210
5 files changed
+85
-10
No files matched your search
@@ -39,8 +39,12 @@ fun MarkdownText(text: String, replies: ParsedReplies, modifier: Modifier = Modi
|
|||||||
markdownColor(
|
markdownColor(
|
||||||
text = MaterialTheme.colorScheme.onSurface,
|
text = MaterialTheme.colorScheme.onSurface,
|
||||||
dividerColor = MaterialTheme.colorScheme.outlineVariant,
|
dividerColor = MaterialTheme.colorScheme.outlineVariant,
|
||||||
codeBackground = MaterialTheme.colorScheme.surfaceVariant,
|
// The dark surface every verbatim thing in this app sits on -- see [rawSurface],
|
||||||
inlineCodeBackground = MaterialTheme.colorScheme.surfaceVariant,
|
// and the tool call above this reply, which now matches. `surfaceVariant` was
|
||||||
|
// exactly a card's own fill, so a fenced block inside a tool call had no
|
||||||
|
// background at all and one in a reply read as a step *up* out of the page.
|
||||||
|
codeBackground = rawSurface,
|
||||||
|
inlineCodeBackground = rawSurface,
|
||||||
// The same tint a code block gets, rather than the renderer's 2%-alpha default:
|
// The same tint a code block gets, rather than the renderer's 2%-alpha default:
|
||||||
// two adjacent tints that differ by a fiftieth read as one flat block on a phone,
|
// two adjacent tints that differ by a fiftieth read as one flat block on a phone,
|
||||||
// so the table would have had a border-less grid and nothing saying where it began.
|
// so the table would have had a border-less grid and nothing saying where it began.
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
package com.example.aiapp
|
||||||
|
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.ColumnScope
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verbatim text, on the surface that says so: a command about to be run, what a tool printed.
|
||||||
|
*
|
||||||
|
* A composable rather than a modifier repeated at each site, because the inset is part of it --
|
||||||
|
* monospace text drawn hard against the edge of a tinted block reads as a clipping fault, and three
|
||||||
|
* copies of "clip, fill, pad" drift apart the first time one of them is adjusted.
|
||||||
|
*
|
||||||
|
* The colour is [rawSurface], which is also what a code block inside a reply is given; that is the
|
||||||
|
* point of having one name for it. Markdown's blocks are painted by the renderer rather than by
|
||||||
|
* this, since it draws its own, but they are the same colour on purpose.
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun RawBlock(modifier: Modifier = Modifier, content: @Composable ColumnScope.() -> Unit) {
|
||||||
|
Column(
|
||||||
|
modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
// Smaller than a card's radius, and deliberately: this sits *inside* one, and a
|
||||||
|
// rounded rectangle drawn at the same radius as the rounded rectangle behind it reads
|
||||||
|
// as a misprint rather than as nesting.
|
||||||
|
.clip(MaterialTheme.shapes.extraSmall)
|
||||||
|
.background(rawSurface)
|
||||||
|
.padding(horizontal = 8.dp, vertical = 6.dp),
|
||||||
|
content = content,
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -181,6 +181,22 @@ private const val WARNING_PERCENT = 75.0
|
|||||||
/** Close enough that the next turn may be the one that is refused. */
|
/** Close enough that the next turn may be the one that is refused. */
|
||||||
private const val OVER_LIMIT_PERCENT = 90.0
|
private const val OVER_LIMIT_PERCENT = 90.0
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The surface verbatim text sits on: a command, a tool's output, a code block in a reply.
|
||||||
|
*
|
||||||
|
* The darkest value in the palette rather than a step up from the page, and that is the whole point
|
||||||
|
* -- everything else on this screen is somebody's prose, and this is what a machine was handed and
|
||||||
|
* what it said back, character for character. Crust sits *below* Base, so the same colour reads as
|
||||||
|
* one clear step down both on the page, where a reply is drawn, and on a card, where a tool call
|
||||||
|
* is; a tint chosen upwards has to be picked twice and still collides with the card it lands on.
|
||||||
|
* The renderer's default code background was `surfaceVariant`, which is exactly a card's own fill
|
||||||
|
* -- so a code block inside a tool call had no background at all.
|
||||||
|
*
|
||||||
|
* One colour for all three, so "this is verbatim" is learnable once.
|
||||||
|
*/
|
||||||
|
val rawSurface: Color
|
||||||
|
@Composable get() = Mocha.Crust
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Catppuccin Mocha as a syntax theme, for the highlighter used on a tool call's input.
|
* Catppuccin Mocha as a syntax theme, for the highlighter used on a tool call's input.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package com.example.aiapp
|
package com.example.aiapp
|
||||||
|
|
||||||
import androidx.compose.foundation.horizontalScroll
|
import androidx.compose.foundation.horizontalScroll
|
||||||
import androidx.compose.foundation.layout.Column
|
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.rememberScrollState
|
import androidx.compose.foundation.rememberScrollState
|
||||||
@@ -106,11 +105,21 @@ fun parseToolInput(tool: String, input: String): ToolInput {
|
|||||||
return ToolInput(subject, language, description, timeout, rest)
|
return ToolInput(subject, language, description, timeout, rest)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** A tool call's input: its subject highlighted, its description, then whatever else it carried. */
|
/**
|
||||||
|
* A tool call's input: its subject highlighted, then whatever else it carried.
|
||||||
|
*
|
||||||
|
* On the dark surface every verbatim thing in the app sits on -- see [RawBlock]. Drawn as nothing
|
||||||
|
* at all when the call carried neither, rather than as an empty block: a tinted rectangle with
|
||||||
|
* nothing in it is a rendering fault, and it is the shape a tool with no input actually has.
|
||||||
|
*
|
||||||
|
* The description is *not* here. It is the tool's own prose about what it is doing, so it belongs
|
||||||
|
* with the reader's text rather than inside the machine's; [ToolCard] draws it above this.
|
||||||
|
*/
|
||||||
@Composable
|
@Composable
|
||||||
fun ToolInputView(tool: String, input: String, modifier: Modifier = Modifier) {
|
fun ToolInputView(tool: String, input: String, modifier: Modifier = Modifier) {
|
||||||
val parsed = remember(tool, input) { parseToolInput(tool, input) }
|
val parsed = remember(tool, input) { parseToolInput(tool, input) }
|
||||||
Column(modifier.fillMaxWidth()) {
|
if (parsed.subject == null && parsed.rest.isEmpty()) return
|
||||||
|
RawBlock(modifier) {
|
||||||
parsed.subject?.let { subject ->
|
parsed.subject?.let { subject ->
|
||||||
// Not wrapped: a wrapped command hides where its arguments end,
|
// Not wrapped: a wrapped command hides where its arguments end,
|
||||||
// and the long one is the one being read closely.
|
// and the long one is the one being read closely.
|
||||||
@@ -119,10 +128,7 @@ fun ToolInputView(tool: String, input: String, modifier: Modifier = Modifier) {
|
|||||||
style = MaterialTheme.typography.bodySmall,
|
style = MaterialTheme.typography.bodySmall,
|
||||||
fontFamily = FontFamily.Monospace,
|
fontFamily = FontFamily.Monospace,
|
||||||
softWrap = false,
|
softWrap = false,
|
||||||
modifier =
|
modifier = Modifier.fillMaxWidth().horizontalScroll(rememberScrollState()),
|
||||||
Modifier.padding(top = 4.dp)
|
|
||||||
.fillMaxWidth()
|
|
||||||
.horizontalScroll(rememberScrollState()),
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
parsed.rest.forEach {
|
parsed.rest.forEach {
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ import androidx.compose.ui.layout.onGloballyPositioned
|
|||||||
import androidx.compose.ui.platform.LocalDensity
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
import androidx.compose.ui.semantics.contentDescription
|
import androidx.compose.ui.semantics.contentDescription
|
||||||
import androidx.compose.ui.semantics.semantics
|
import androidx.compose.ui.semantics.semantics
|
||||||
|
import androidx.compose.ui.text.font.FontFamily
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.Dp
|
import androidx.compose.ui.unit.Dp
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
@@ -375,7 +376,17 @@ fun ToolCard(
|
|||||||
if (tool.output.isNotEmpty()) {
|
if (tool.output.isNotEmpty()) {
|
||||||
Spacer(Modifier.height(8.dp))
|
Spacer(Modifier.height(8.dp))
|
||||||
Text("Output", style = MaterialTheme.typography.labelSmall)
|
Text("Output", style = MaterialTheme.typography.labelSmall)
|
||||||
Text(tool.output, style = MaterialTheme.typography.bodySmall)
|
// What the tool printed, on the surface everything verbatim gets and in the
|
||||||
|
// face it was written for: this is column-aligned far more often than it is
|
||||||
|
// prose -- a directory listing, a diff, a table of numbers -- and a
|
||||||
|
// proportional font silently destroys the alignment that carried the meaning.
|
||||||
|
RawBlock(Modifier.padding(top = 2.dp)) {
|
||||||
|
Text(
|
||||||
|
tool.output,
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
fontFamily = FontFamily.Monospace,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Shown open or closed. A call that produced a picture is one
|
// Shown open or closed. A call that produced a picture is one
|
||||||
|
|||||||
Reference in new issue
Block a user