Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion apps/mobile/src/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { NotificationRoutingProvider } from "./notifications/notification-routin
import { AppLockProvider } from "./security/app-lock-context";
import { ConnectionRuntimeProvider } from "./state/connection-runtime-context";
import { FollowedProjectsProvider } from "./state/followed-projects-context";
import { TranscriptPreferencesProvider } from "./state/transcript-preferences";
import { migrateMobileDatabase, mobileDatabaseName } from "./storage/database";
import { palette } from "./theme";
import { AppUpdateBanner, AppUpdateCard, AppUpdatesProvider } from "./updates/app-updates";
Expand Down Expand Up @@ -61,7 +62,9 @@ export default function App() {
<NotificationRoutingProvider>
<NavigationContainer ref={rootNavigationRef} theme={navigationTheme}>
<View style={styles.appRoot}>
<RootNavigation />
<TranscriptPreferencesProvider>
<RootNavigation />
</TranscriptPreferencesProvider>
<AppUpdateBanner />
</View>
</NavigationContainer>
Expand Down
9 changes: 8 additions & 1 deletion apps/mobile/src/navigation/workspace-header-actions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,14 @@ function MenuButton({
}

const styles = StyleSheet.create({
actions: { alignItems: "center", flexDirection: "row" },
actions: {
alignItems: "center",
backgroundColor: palette.card,
borderColor: palette.border,
borderRadius: 24,
borderWidth: StyleSheet.hairlineWidth,
flexDirection: "row",
},
disclosure: { color: palette.dim, fontSize: 18, marginLeft: space.sm },
menuButton: {
alignItems: "center",
Expand Down
38 changes: 38 additions & 0 deletions apps/mobile/src/screens/app-shell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import {
import { useAppLock } from "../security/app-lock-context";
import { useConnectionRuntime } from "../state/connection-runtime-context";
import type { ConnectionTransportStatus } from "../state/connection-transport-coordinator";
import { useTranscriptPreferences } from "../state/transcript-preferences";
import { useWorkspaceSelection } from "../state/workspace-selection-context";
import { palette, radius, space, typeRamp, usesLargeTextLayout } from "../theme";
import { AppUpdateCard } from "../updates/app-updates";
Expand Down Expand Up @@ -246,6 +247,7 @@ export function PendingInteractionsScreen({ navigation }: ScreenProps<"Pending">
}

export function SettingsScreen({ navigation }: ScreenProps<"Settings">) {
const transcript = useTranscriptPreferences();
const db = useSQLiteContext();
const appLock = useAppLock();
const runtime = useConnectionRuntime();
Expand Down Expand Up @@ -337,6 +339,42 @@ export function SettingsScreen({ navigation }: ScreenProps<"Settings">) {

<AppUpdateCard />

<View style={styles.settingCard}>
<View style={styles.settingText}>
<Text style={styles.cardTitle}>Detailed transcript</Text>
<Text style={styles.cardCopy}>
{transcript.detailed
? "Detailed: individual tool executions and system notices."
: "Compact: group activity and collapse routine details."}
</Text>
</View>
<Switch
accessibilityLabel="Detailed transcript"
disabled={transcript.busy}
value={transcript.detailed}
onValueChange={(detailed) => void transcript.update({ detailed })}
/>
</View>
<View style={styles.settingCard}>
<View style={styles.settingText}>
<Text style={styles.cardTitle}>Show reasoning</Text>
<Text style={styles.cardCopy}>
Display model reasoning when provided by the server.
</Text>
</View>
<Switch
accessibilityLabel="Show reasoning"
disabled={transcript.busy}
value={transcript.reasoning}
onValueChange={(reasoning) => void transcript.update({ reasoning })}
/>
</View>
{transcript.error ? (
<Text accessibilityRole="alert" style={styles.errorText}>
Transcript preferences could not be loaded or saved. Try changing the setting again.
</Text>
) : null}

<View style={styles.settingCard}>
<View style={styles.settingText}>
<Text style={styles.cardTitle}>App lock</Text>
Expand Down
18 changes: 8 additions & 10 deletions apps/mobile/src/screens/session-composer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -761,7 +761,7 @@ const styles = StyleSheet.create({
input: {
color: palette.ink,
flex: 1,
fontSize: 16,
fontSize: 17,
lineHeight: 23,
},
inputCollapsed: { height: 42, paddingHorizontal: space.sm, paddingVertical: 0 },
Expand Down Expand Up @@ -803,7 +803,7 @@ const styles = StyleSheet.create({
minHeight: 44,
paddingHorizontal: 4,
},
selectorLabel: { color: palette.ink, flexShrink: 1, fontSize: 12, fontWeight: "700" },
selectorLabel: { color: palette.dim, flexShrink: 1, fontSize: 12, fontWeight: "500" },
selectorRow: { alignItems: "center", gap: space.xs, paddingRight: space.xs },
selectorScroller: { flex: 1 },
sendButton: {
Expand All @@ -817,26 +817,24 @@ const styles = StyleSheet.create({
sendButtonDisabled: { backgroundColor: palette.border, opacity: 0.68 },
shell: {
backgroundColor: palette.background,
borderTopColor: palette.border,
borderTopWidth: StyleSheet.hairlineWidth,
gap: space.xs,
paddingHorizontal: space.sm,
paddingVertical: space.xs,
paddingHorizontal: 12,
paddingVertical: 8,
},
surface: {
backgroundColor: palette.card,
borderColor: palette.border,
borderWidth: 1,
borderWidth: StyleSheet.hairlineWidth,
overflow: "hidden",
},
surfaceCollapsed: {
borderRadius: 999,
minHeight: 52,
minHeight: 60,
paddingLeft: 12,
paddingRight: 5,
paddingRight: 8,
},
surfaceExpanded: {
borderRadius: 26,
borderRadius: 28,
gap: 2,
paddingBottom: 6,
paddingHorizontal: 14,
Expand Down
74 changes: 70 additions & 4 deletions apps/mobile/src/screens/session-transcript.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,76 @@ import { Alert, Linking, View } from "react-native";

import { resetTranscriptPerformanceMetrics } from "../state/transcript-performance";
import { markdownPalette, palette } from "../theme";
import { SessionTranscriptRow } from "./session-transcript";
import { groupTranscriptMessages, SessionTranscriptRow } from "./session-transcript";

afterEach(resetTranscriptPerformanceMetrics);

test("cross-message grouping respects replies, errors, reasoning visibility and detailed mode", () => {
const original = messages.find((message) => message.type === "assistant");
if (!original) throw new Error("fixture");
const tool = original.content.find(
(part) => part.type === "tool" && part.state.status === "completed",
);
if (!tool) throw new Error("fixture");
const first = { ...original, retry: undefined, content: [tool] };
// Remove the retry field so this fixture represents a normal successful execution.
const { retry: _retry, ...clean } = first;
const second = { ...clean, id: "msg_second" };
const reasoning = {
...clean,
id: "msg_reasoning",
content: [{ type: "reasoning" as const, text: "Thinking" }],
};
const reply = { ...clean, id: "msg_reply", content: [{ type: "text" as const, text: "Reply" }] };
expect(groupTranscriptMessages([clean, reasoning, second], false, false)).toMatchObject([
{ type: "activity-group", count: 2 },
]);
expect(groupTranscriptMessages([clean, reasoning, second], false, true)).toHaveLength(3);
expect(groupTranscriptMessages([clean, reply, second], false, false)).toHaveLength(3);
expect(groupTranscriptMessages([clean, second], true, false)).toEqual([clean, second]);
const failed = { ...second, error: { type: "ToolError", message: "Failed" } };
expect(groupTranscriptMessages([clean, failed], false, false)).toEqual([clean, failed]);
});

test("reasoning can be hidden without hiding replies or tool failures", () => {
const message = messages.find((item) => item.type === "assistant");
if (!message) throw new Error("fixture");
const view = render(<SessionTranscriptRow message={message} showReasoning={false} />);
expect(screen.getByText("Answer")).toBeOnTheScreen();
expect(screen.getByText("tool failed")).toBeOnTheScreen();
expect(screen.queryByText("Reasoning detail")).toBeNull();
view.rerender(<SessionTranscriptRow message={message} showReasoning />);
expect(screen.getByText("Reasoning detail")).toBeOnTheScreen();
});

test("compact system notices expand and detailed mode shows their content", () => {
const message = messages.find((item) => item.type === "synthetic");
if (!message) throw new Error("fixture");
const view = render(<SessionTranscriptRow message={message} />);
expect(screen.queryByText("Generated")).toBeNull();
fireEvent.press(screen.getByRole("button", { name: /Generated context/ }));
expect(screen.getByText("Generated")).toBeOnTheScreen();
view.rerender(<SessionTranscriptRow detailed message={message} />);
expect(screen.getByText("Generated")).toBeOnTheScreen();
});

test("detailed mode renders grouped tool executions individually", () => {
const original = messages.find((item) => item.type === "assistant");
if (original?.type !== "assistant") throw new Error("fixture");
const tool = original.content.find(
(part) => part.type === "tool" && part.state.status === "completed",
);
if (tool?.type !== "tool") throw new Error("fixture");
const message = {
...original,
content: [tool, { ...tool, id: "tool-second", name: "second-tool" }],
};
const view = render(<SessionTranscriptRow message={message} />);
expect(screen.queryByText("Used Second-tool")).toBeNull();
view.rerender(<SessionTranscriptRow detailed message={message} />);
expect(screen.getByText("Used Second-tool")).toBeOnTheScreen();
});

test.each([
{ outcome: "succeeded", label: "Turn completed" },
{ outcome: "failed", label: "Turn failed" },
Expand Down Expand Up @@ -130,7 +196,7 @@ test("renders every current message and tool state with large details collapsed"
render(
<View>
{messages.map((message) => (
<SessionTranscriptRow key={message.id} message={message} />
<SessionTranscriptRow detailed key={message.id} message={message} />
))}
</View>,
);
Expand Down Expand Up @@ -213,7 +279,7 @@ test("opens HTTP and HTTPS transcript URLs as confirmed external links", () => {
});
expect(screen.getByRole("link", { name: "http://localhost:4096/status" })).toBeOnTheScreen();
expect(screen.getByRole("link", { name: "https://assistant.test/guide" })).toHaveStyle({
fontWeight: "800",
fontWeight: "700",
});

fireEvent.press(secureLink);
Expand Down Expand Up @@ -317,7 +383,7 @@ test("renders finished short reasoning inline with bold markdown", () => {

expect(screen.getByText("THOUGHT")).toBeOnTheScreen();
expect(screen.queryByText("THINKING")).toBeNull();
expect(screen.getByText("Adding mocks to repository tests")).toHaveStyle({ fontWeight: "800" });
expect(screen.getByText("Adding mocks to repository tests")).toHaveStyle({ fontWeight: "700" });
expect(screen.queryByText(/\*\*/)).toBeNull();
expect(screen.queryByRole("button", { name: /Thought/ })).toBeNull();
});
Expand Down
Loading
Loading