diff --git a/apps/mobile/src/components/working-indicator.tsx b/apps/mobile/src/components/working-indicator.tsx
new file mode 100644
index 0000000..51abe31
--- /dev/null
+++ b/apps/mobile/src/components/working-indicator.tsx
@@ -0,0 +1,68 @@
+import { useEffect, useState } from "react";
+import { AccessibilityInfo, AppState, StyleSheet, View } from "react-native";
+import Animated, { type CSSAnimationKeyframes, steps } from "react-native-reanimated";
+
+import { palette } from "../theme";
+
+// OpenCode's terminal dot spinner, rendered as pixels instead of font-dependent glyphs.
+const frames = [0x0b, 0x19, 0x39, 0x38, 0x3c, 0x34, 0x26, 0x27, 0x07, 0x0f];
+const dots = [0, 1, 2, 3, 4, 5].map((bit) => ({
+ bit,
+ animation: Object.fromEntries(
+ [...frames, frames[0]].map((frame, index) => [
+ `${index * 10}%`,
+ { opacity: ((frame ?? 0) & (1 << bit)) !== 0 ? 1 : 0 },
+ ]),
+ ) as CSSAnimationKeyframes,
+}));
+
+export function WorkingIndicator() {
+ const [reducedMotion, setReducedMotion] = useState(true);
+ const [foreground, setForeground] = useState(AppState.currentState === "active");
+
+ useEffect(() => {
+ let mounted = true;
+ void AccessibilityInfo.isReduceMotionEnabled()
+ .then((enabled) => {
+ if (mounted) setReducedMotion(enabled);
+ })
+ .catch(() => undefined);
+ const motion = AccessibilityInfo.addEventListener("reduceMotionChanged", setReducedMotion);
+ const app = AppState.addEventListener("change", (state) => setForeground(state === "active"));
+ return () => {
+ mounted = false;
+ motion.remove();
+ app.remove();
+ };
+ }, []);
+
+ return (
+
+ {dots.map(({ bit, animation }) => (
+
+ ))}
+
+ );
+}
+
+const styles = StyleSheet.create({
+ icon: { width: 10, height: 16, flexShrink: 0 },
+ dot: { position: "absolute", width: 4, height: 4, backgroundColor: palette.signal },
+});
diff --git a/apps/mobile/src/screens/diff-screen.test.tsx b/apps/mobile/src/screens/diff-screen.test.tsx
index a3c876a..0a68cb3 100644
--- a/apps/mobile/src/screens/diff-screen.test.tsx
+++ b/apps/mobile/src/screens/diff-screen.test.tsx
@@ -53,7 +53,7 @@ test("renders an authoritative working-tree diff", async () => {
"Current working tree. This may include changes made after the selected tool call.",
),
).toBeOnTheScreen();
- expect(screen.getByText("+new value")).toHaveStyle({ backgroundColor: "#18230E" });
+ expect(screen.getByText("+new value")).toHaveStyle({ backgroundColor: "#172B38" });
expect(screen.getByText("-old value")).toHaveStyle({ backgroundColor: "#2A1714" });
expect(mockGetDiff).toHaveBeenCalledWith(
{},
diff --git a/apps/mobile/src/screens/session-execution-panel.tsx b/apps/mobile/src/screens/session-execution-panel.tsx
index 9c1d91c..3636149 100644
--- a/apps/mobile/src/screens/session-execution-panel.tsx
+++ b/apps/mobile/src/screens/session-execution-panel.tsx
@@ -6,6 +6,7 @@ import type {
import type { ReactNode } from "react";
import { Pressable, ScrollView, StyleSheet, Text, View } from "react-native";
+import { WorkingIndicator } from "../components/working-indicator";
import { palette, radius, space, typeRamp } from "../theme";
import { permissionActionExplanation } from "./permission-presentation";
import {
@@ -76,9 +77,17 @@ export function SessionExecutionPanel({
{active ? (
-
+ {permissions.length > 0 || formRequests ? (
+
+ ) : (
+
+ )}
- {permissions.length > 0 ? "Waiting for permission" : "OpenCode is working"}
+ {permissions.length > 0
+ ? "Waiting for permission"
+ : formRequests
+ ? "Waiting for input"
+ : "OpenCode is working"}
diff --git a/apps/mobile/src/screens/session-transcript.test.tsx b/apps/mobile/src/screens/session-transcript.test.tsx
index 3b03d4b..fbc4e7c 100644
--- a/apps/mobile/src/screens/session-transcript.test.tsx
+++ b/apps/mobile/src/screens/session-transcript.test.tsx
@@ -206,7 +206,7 @@ test("opens HTTP and HTTPS transcript URLs as confirmed external links", () => {
);
const secureLink = screen.getByRole("link", { name: "https://example.test/docs" });
- expect(secureLink).toHaveStyle({ color: "#B6F26C", textDecorationLine: "underline" });
+ expect(secureLink).toHaveStyle({ color: "#36C5E5", textDecorationLine: "underline" });
expect(screen.getByRole("link", { name: "http://localhost:4096/status" })).toBeOnTheScreen();
expect(screen.getByRole("link", { name: "https://assistant.test/guide" })).toHaveStyle({
fontWeight: "800",
@@ -267,7 +267,7 @@ test("renders fenced assistant code without markdown fence markers", () => {
expect(screen.getByText("Then inspect the stack.")).toBeOnTheScreen();
expect(screen.queryByText(/```/)).toBeNull();
expect(screen.getByLabelText("Code block, gdb")).toHaveStyle({
- backgroundColor: "#121614",
+ backgroundColor: "#241830",
borderWidth: 1,
});
});
diff --git a/apps/mobile/src/screens/workspace-screen.tsx b/apps/mobile/src/screens/workspace-screen.tsx
index 491f1b9..7933625 100644
--- a/apps/mobile/src/screens/workspace-screen.tsx
+++ b/apps/mobile/src/screens/workspace-screen.tsx
@@ -38,6 +38,7 @@ import {
} from "react-native";
import ReanimatedSwipeable from "react-native-gesture-handler/ReanimatedSwipeable";
import { KeyboardStickyView } from "react-native-keyboard-controller";
+import { WorkingIndicator } from "../components/working-indicator";
import { useConnections } from "../connections/connections-context";
import type { RootStackParamList } from "../navigation/root-navigation";
import { useConnectionRuntime } from "../state/connection-runtime-context";
@@ -1145,6 +1146,7 @@ function SessionRow({
pressed && styles.pressed,
]}
>
+ {active && !blocked ? : null}
@@ -1202,6 +1204,7 @@ function SessionRow({
style={({ pressed }) => [styles.childRow, pressed && styles.pressed]}
>
+ {child.active && child.attentionCount === 0 ? : null}
{child.session.title || "Untitled child session"}
diff --git a/apps/mobile/src/theme.ts b/apps/mobile/src/theme.ts
index 2f80cd7..6c747d1 100644
--- a/apps/mobile/src/theme.ts
+++ b/apps/mobile/src/theme.ts
@@ -1,12 +1,12 @@
export const palette = {
- background: "#0B0D0C",
- border: "#2A302D",
- card: "#121614",
+ background: "#100B18",
+ border: "#392C48",
+ card: "#241830",
danger: "#FF8E7A",
- dim: "#8E9993",
- ink: "#F0F4F1",
- signal: "#B6F26C",
- signalDark: "#18230E",
+ dim: "#A69BAF",
+ ink: "#EAE2F0",
+ signal: "#36C5E5",
+ signalDark: "#172B38",
warm: "#FFB86B",
} as const;