Commit 84ee14c6 authored by Mahmoud Aglan's avatar Mahmoud Aglan

feat(chat): render prose as prose, tool calls as steps; restore the original mark

Two problems from the first real use of the app.

RENDERING. Every line of a run was drawn as flat monospace text, so the parts
that were markdown arrived as their source: tables as rows of pipe characters,
`**bold**` as asterisks, headings as hashes. That is the "gibberish between real
replies" — there was never any gibberish, only unrendered markdown sitting
between the parts that happened to contain none.

The server already emits a structured line vocabulary (▶ header, ⚙ tool,
→ result, · thinking, ■ footer, everything else prose). AgentOutput now groups
those lines into blocks and renders each as what it is:

  prose   -> MarkdownBody, with tables, code blocks, lists and headings
  tool    -> a compact one-line step, its result collapsed behind it
  thinking-> a single pulsing marker; consecutive ones collapse
  header  -> present but recessive; useful only when something goes wrong

A tool result is attached to the step above it rather than printed inline, so a
500-character command output no longer sits between two paragraphs. Tap to
expand it, long-press the step to copy the command.

THE ICON. I rebuilt the flame by hand instead of taking it from the APK that was
sitting in front of me the whole time — a worse copy of an asset I already had.
The original icon.png, icon_foreground.png and flame.png are lifted from the old
build's flutter_assets and are now both the launcher icon and the in-app mark,
loaded from the same file so the two cannot drift.
Co-Authored-By: 's avatarClaude Opus 5 (1M context) <noreply@anthropic.com>
parent e92220ba
...@@ -10,6 +10,10 @@ android { ...@@ -10,6 +10,10 @@ android {
ndkVersion = flutter.ndkVersion ndkVersion = flutter.ndkVersion
compileOptions { compileOptions {
// flutter_local_notifications uses java.time APIs that do not exist on
// older Android runtimes; desugaring backports them. Without this the
// AAR metadata check fails the build outright.
isCoreLibraryDesugaringEnabled = true
sourceCompatibility = JavaVersion.VERSION_17 sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17 targetCompatibility = JavaVersion.VERSION_17
} }
...@@ -47,3 +51,7 @@ kotlin { ...@@ -47,3 +51,7 @@ kotlin {
flutter { flutter {
source = "../.." source = "../.."
} }
dependencies {
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.1.4")
}
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/>
<foreground>
<inset
android:drawable="@drawable/ic_launcher_foreground"
android:inset="16%" />
</foreground>
</adaptive-icon>
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="ic_launcher_background">#0B0B0D</color>
</resources>
\ No newline at end of file
This source diff could not be displayed because it is too large. You can view the blob instead.
...@@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME ...@@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-all.zip distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-all.zip
assets/icon.png

13.1 KB | W: | H:

assets/icon.png

84.7 KB | W: | H:

assets/icon.png
assets/icon.png
assets/icon.png
assets/icon.png
  • 2-up
  • Swipe
  • Onion skin
assets/icon_foreground.png

8.93 KB | W: | H:

assets/icon_foreground.png

128 KB | W: | H:

assets/icon_foreground.png
assets/icon_foreground.png
assets/icon_foreground.png
assets/icon_foreground.png
  • 2-up
  • Swipe
  • Onion skin
...@@ -416,12 +416,9 @@ class _Bubble extends StatelessWidget { ...@@ -416,12 +416,9 @@ class _Bubble extends StatelessWidget {
), ),
], ],
), ),
if (message.content.isNotEmpty) ...[ if (message.content.isNotEmpty || running) ...[
const SizedBox(height: 10), const SizedBox(height: 10),
AgentOutput(text: message.content), AgentOutput(text: message.content, running: running),
] else if (running) ...[
const SizedBox(height: 10),
const Text('Starting…', style: TextStyle(color: Ember.textFaint, fontSize: 13)),
], ],
], ],
), ),
......
This diff is collapsed.
...@@ -2,35 +2,43 @@ import 'package:flutter/material.dart'; ...@@ -2,35 +2,43 @@ import 'package:flutter/material.dart';
import '../core/theme.dart'; import '../core/theme.dart';
/// The app mark: a flame on a dark rounded tile. Drawn rather than shipped as a /// The app mark — the original Son of Anton flame, carried over from the first
/// bitmap so it stays sharp at every size and the gradient matches the theme /// build's assets rather than redrawn. It is the identity the app is known by;
/// tokens instead of drifting from them. /// a "close enough" reconstruction is not the same thing.
///
/// Uses the same `assets/flame.png` that generates the launcher icon, so the
/// icon on the home screen and the mark inside the app cannot drift apart.
class FlameMark extends StatelessWidget { class FlameMark extends StatelessWidget {
const FlameMark({super.key, this.size = 40, this.tile = true}); const FlameMark({super.key, this.size = 40, this.tile = true});
final double size; final double size;
/// Whether to draw the dark rounded plate behind the flame. Off when the mark
/// sits on a surface that already provides its own contrast.
final bool tile; final bool tile;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final flame = SizedBox( final flame = Image.asset(
width: size * 0.56, 'assets/flame.png',
height: size * 0.56, width: size * (tile ? 0.62 : 1.0),
child: CustomPaint(painter: _FlamePainter()), height: size * (tile ? 0.62 : 1.0),
filterQuality: FilterQuality.medium,
); );
if (!tile) return flame; if (!tile) return flame;
return Container( return Container(
width: size, width: size,
height: size, height: size,
decoration: BoxDecoration( decoration: BoxDecoration(
color: const Color(0xFF121216), color: const Color(0xFF0A0A0E),
borderRadius: BorderRadius.circular(size * 0.28), borderRadius: BorderRadius.circular(size * 0.28),
border: Border.all(color: Ember.border), border: Border.all(color: Ember.border),
boxShadow: [ boxShadow: [
BoxShadow( BoxShadow(
color: Ember.flame.withValues(alpha: 0.22), color: Ember.flame.withValues(alpha: 0.20),
blurRadius: size * 0.35, blurRadius: size * 0.34,
spreadRadius: -size * 0.08, spreadRadius: -size * 0.10,
), ),
], ],
), ),
...@@ -38,52 +46,3 @@ class FlameMark extends StatelessWidget { ...@@ -38,52 +46,3 @@ class FlameMark extends StatelessWidget {
); );
} }
} }
class _FlamePainter extends CustomPainter {
@override
void paint(Canvas canvas, Size s) {
final w = s.width, h = s.height;
// Outer flame: a teardrop with a curled left shoulder.
final outer = Path()
..moveTo(w * 0.50, 0)
..cubicTo(w * 0.86, h * 0.26, w * 1.00, h * 0.46, w * 0.94, h * 0.66)
..cubicTo(w * 0.88, h * 0.89, w * 0.71, h * 1.00, w * 0.50, h * 1.00)
..cubicTo(w * 0.29, h * 1.00, w * 0.12, h * 0.89, w * 0.06, h * 0.66)
..cubicTo(w * 0.00, h * 0.44, w * 0.18, h * 0.30, w * 0.30, h * 0.14)
..cubicTo(w * 0.34, h * 0.34, w * 0.42, h * 0.40, w * 0.46, h * 0.36)
..cubicTo(w * 0.52, h * 0.30, w * 0.50, h * 0.14, w * 0.50, 0)
..close();
canvas.drawPath(
outer,
Paint()
..shader = const LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [Ember.flameSoft, Ember.flame],
).createShader(Rect.fromLTWH(0, 0, w, h)),
);
// Inner core, brighter and offset low — what reads as heat.
final core = Path()
..moveTo(w * 0.52, h * 0.42)
..cubicTo(w * 0.72, h * 0.56, w * 0.74, h * 0.70, w * 0.62, h * 0.82)
..cubicTo(w * 0.52, h * 0.92, w * 0.34, h * 0.88, w * 0.30, h * 0.74)
..cubicTo(w * 0.27, h * 0.62, w * 0.38, h * 0.54, w * 0.52, h * 0.42)
..close();
canvas.drawPath(
core,
Paint()
..shader = const LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [Color(0xFFFFD68A), Ember.amber],
).createShader(Rect.fromLTWH(0, h * 0.4, w, h * 0.6)),
);
}
@override
bool shouldRepaint(covariant CustomPainter old) => false;
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment