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;