Skip to content
Open
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
61 changes: 51 additions & 10 deletions apps/mobile/src/features/settings/SettingsFollowUpRouteScreen.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { ScreenScrollView as ScrollView } from "../../components/ui/ScreenScrollView";
import { useAtomSet, useAtomValue } from "@effect/atom-react";
import { AsyncResult } from "effect/reactivity";
import type { TranscriptView } from "@t3tools/contracts";
import { Platform, View } from "react-native";
import { useSafeAreaInsets } from "react-native-safe-area-context";

Expand Down Expand Up @@ -28,6 +29,28 @@ const FOLLOW_UP_OPTIONS: ReadonlyArray<{
},
];

const TRANSCRIPT_VIEW_OPTIONS: ReadonlyArray<{
readonly view: TranscriptView;
readonly label: string;
readonly description: string;
}> = [
{
view: "normal",
label: "Normal",
description: "Each finished turn folds its work behind “Worked for”.",
},
{
view: "thinking",
label: "Thinking",
description: "Every turn opens with its tool calls and intermediate messages.",
},
{
view: "verbose",
label: "Verbose",
description: "Every turn opens, and so does each tool call's thinking text and output.",
},
];

export function SettingsFollowUpRouteScreen() {
const insets = useSafeAreaInsets();
const preferencesResult = useAtomValue(mobilePreferencesAtom);
Expand All @@ -36,32 +59,50 @@ export function SettingsFollowUpRouteScreen() {
const selectedBehavior = AsyncResult.isSuccess(preferencesResult)
? (preferencesResult.value.followUpBehavior ?? DEFAULT_FOLLOW_UP_BEHAVIOR)
: null;
const selectedTranscriptView = AsyncResult.isSuccess(preferencesResult)
? (preferencesResult.value.transcriptView ?? "normal")
: null;

const content = (
<ScrollView
contentInsetAdjustmentBehavior="automatic"
showsVerticalScrollIndicator={false}
className="flex-1"
contentContainerClassName="gap-3 px-5 pt-4"
contentContainerClassName="gap-6 px-5 pt-4"
contentContainerStyle={{ paddingBottom: Math.max(insets.bottom, 18) + 18 }}
>
<SettingsSection title="While the agent is running">
{FOLLOW_UP_OPTIONS.map((option, index) => (
<View className="gap-3">
<SettingsSection title="While the agent is running">
{FOLLOW_UP_OPTIONS.map((option, index) => (
<SettingsChoiceRow
key={option.behavior}
label={option.label}
description={option.description}
selected={selectedBehavior === option.behavior}
separated={index > 0}
disabled={!preferencesReady}
onPress={() => savePreferences({ followUpBehavior: option.behavior })}
/>
))}
</SettingsSection>
<Text className="px-2 text-sm text-foreground-muted">
Long-press the send button to use the other option for a single message. With a hardware
keyboard, hold Command while sending.
</Text>
</View>
<SettingsSection title="Transcript view">
{TRANSCRIPT_VIEW_OPTIONS.map((option, index) => (
<SettingsChoiceRow
key={option.behavior}
key={option.view}
label={option.label}
description={option.description}
selected={selectedBehavior === option.behavior}
selected={selectedTranscriptView === option.view}
separated={index > 0}
disabled={!preferencesReady}
onPress={() => savePreferences({ followUpBehavior: option.behavior })}
onPress={() => savePreferences({ transcriptView: option.view })}
/>
))}
</SettingsSection>
<Text className="px-2 text-sm text-foreground-muted">
Long-press the send button to use the other option for a single message. With a hardware
keyboard, hold Command while sending.
</Text>
</ScrollView>
);

Expand Down
13 changes: 12 additions & 1 deletion apps/mobile/src/features/settings/settings-search.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,18 @@ const SETTINGS_SEARCH_TERMS = {
"auto-resume",
"thread behavior",
],
SettingsFollowUp: ["follow-ups", "queue", "steer", "while the agent is running"],
SettingsFollowUp: [
"follow-ups",
"queue",
"steer",
"while the agent is running",
"transcript view",
"normal",
"thinking",
"verbose",
"expand all turns",
"worked for",
],
SettingsArchive: ["archive", "archived", "restore", "unarchive"],
SettingsProviderAccounts: [
"provider accounts",
Expand Down
6 changes: 6 additions & 0 deletions apps/mobile/src/features/threads/ThreadDetailScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ import {
import { formatModelSlugName, resolveSelectableModel } from "@t3tools/shared/model";
import { isProviderNativeSubagentThread } from "@t3tools/contracts";
import type { QueuedRunEdit } from "../../state/queued-run-edit";
import { mobilePreferencesAtom } from "../../state/preferences";
import type { FollowUpBehavior } from "../../lib/followUpBehavior";
import type { ActiveTurnComposerAction } from "@t3tools/client-runtime/state/composer-dispatch";
import * as Haptics from "expo-haptics";
Expand Down Expand Up @@ -457,6 +458,10 @@ function ThreadDetailBody(props: ThreadDetailScreenProps) {
threadId: props.selectedThread.id,
});
const composerError = useAtomValue(threadComposerErrorsAtom)[selectedThreadKey]?.message ?? null;
const preferences = useAtomValue(mobilePreferencesAtom);
const transcriptView = AsyncResult.isSuccess(preferences)
? (preferences.value.transcriptView ?? "normal")
: "normal";
const queuedCount = useThreadQueuedCount(props.selectedThreadDetailRef);
const turnSubagents = useThreadTurnSubagents(props.selectedThreadDetailRef);
const agentsSegment = resolveSubagentPillSegment(turnSubagents);
Expand Down Expand Up @@ -1394,6 +1399,7 @@ function ThreadDetailBody(props: ThreadDetailScreenProps) {
isProviderSubagent || pendingMessageEditBlocked ? null : handleEditPendingMessage
}
contentPresentation={props.contentPresentation}
transcriptView={transcriptView}
threadTitle={props.selectedThread.title}
latestRun={props.activityRun}
activeWorkStartedAt={props.activeWorkStartedAt}
Expand Down
62 changes: 52 additions & 10 deletions apps/mobile/src/features/threads/ThreadFeed.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ import {
type RunId,
type ScheduledTaskId,
type ScopedThreadRef,
type TranscriptView,
} from "@t3tools/contracts";
import { renderAssistantCitationsAsText } from "@t3tools/shared/assistantCitations";
import { encodeComposerContextFragment } from "@t3tools/shared/composerContextClipboard";
Expand Down Expand Up @@ -83,6 +84,12 @@ import {
markdownImageSourceFragment,
} from "@t3tools/client-runtime/markdown-images";
import { resolveViewedImageAsset } from "@t3tools/client-runtime/work-log/presentation";
import {
disclosureOpen,
transcriptFoldsStartOpen,
transcriptRowsStartOpen,
withDisclosureOpen,
} from "@t3tools/client-runtime/work-log/disclosure";
import {
renderCodexInlineDirectivesAsMarkdown,
splitCodexArtifactTemplateMarkdown,
Expand Down Expand Up @@ -351,6 +358,7 @@ export interface ThreadFeedProps {
readonly feed: ReadonlyArray<ThreadFeedEntry>;
readonly pendingHandoff?: HandoffEndpoints | null;
readonly contentPresentation: ThreadContentPresentation;
readonly transcriptView: TranscriptView;
readonly latestRun: ThreadFeedLatestRun | null;
readonly activeWorkStartedAt: string | null;
readonly runlessWorkActive?: boolean;
Expand Down Expand Up @@ -1664,6 +1672,8 @@ function renderFeedEntry(
> & {
readonly copiedRowId: string | null;
readonly expandedWorkRows: Record<string, boolean>;
readonly foldsStartOpen: boolean;
readonly rowsStartOpen: boolean;
readonly workRowSizing: ReturnType<typeof deriveThreadWorkLogSizing>;
readonly workGroupScrollPositions: Map<string, ThreadWorkGroupScrollPosition>;
readonly terminalAssistantMessageIds: ReadonlySet<string>;
Expand Down Expand Up @@ -2313,6 +2323,8 @@ function renderFeedEntry(
anchorKey={entry.id}
copiedRowId={props.copiedRowId}
expandedRows={props.expandedWorkRows}
foldsStartOpen={props.foldsStartOpen}
rowsStartOpen={props.rowsStartOpen}
rowSizing={props.workRowSizing}
scrollPositions={props.workGroupScrollPositions}
iconSubtleColor={iconSubtleColor}
Expand Down Expand Up @@ -2569,6 +2581,7 @@ function useThreadFeedPresentation(
props: ThreadFeedProps,
expandedTurnIds: ReadonlySet<RunId>,
expandedWorkGroups: Record<string, boolean>,
transcriptView: TranscriptView,
) {
const [feedCache] = useState(createThreadFeedCache);
return appendPendingThreadMessages(
Expand All @@ -2584,6 +2597,7 @@ function useThreadFeedPresentation(
props.activeWorkStartedAt,
props.runlessWorkActive ?? false,
feedCache,
transcriptView,
),
props.feed,
props.queuedMessages,
Expand Down Expand Up @@ -2677,7 +2691,25 @@ export const ThreadFeed = memo(function ThreadFeed({
expandedWorkRows: {},
expandedTurnIds: new Set(),
});
const { copiedRowId, expandedWorkGroups, expandedWorkRows, expandedTurnIds } = interactionState;
const { copiedRowId } = interactionState;
let { expandedWorkGroups, expandedWorkRows, expandedTurnIds } = interactionState;
const { transcriptView } = props;
const foldsStartOpen = transcriptFoldsStartOpen(transcriptView);
const rowsStartOpen = transcriptRowsStartOpen(transcriptView);
// Switching the view starts every disclosure from the new default.
const [disclosureView, setDisclosureView] = useState(transcriptView);
if (disclosureView !== transcriptView) {
setDisclosureView(transcriptView);
expandedWorkGroups = {};
expandedWorkRows = {};
expandedTurnIds = new Set();
setInteractionState((current) => ({
...current,
expandedWorkGroups,
expandedWorkRows,
expandedTurnIds,
}));
}
const markdownLinkRequest = useRef<AbortController | null>(null);
const onInlinePlaybackStart = () => {
markdownLinkRequest.current?.abort();
Expand Down Expand Up @@ -3236,18 +3268,18 @@ export const ThreadFeed = memo(function ThreadFeed({
const { jumpToMessageId } = props;
const jumpReady = !!jumpToMessageId && loadedListKey === `${feedThreadKey}:filled`;
const presentedFeed = appendGuestPendingMessages(
useThreadFeedPresentation(props, expandedTurnIds, expandedWorkGroups),
useThreadFeedPresentation(props, expandedTurnIds, expandedWorkGroups, transcriptView),
guestShare,
);
const jumpRunId =
jumpReady &&
!presentedFeed.some((entry) => entry.type === "message" && entry.id === jumpToMessageId)
? messageFoldRunId(props.feed, jumpToMessageId)
: null;
if (jumpRunId && !expandedTurnIds.has(jumpRunId)) {
if (jumpRunId && !disclosureOpen(expandedTurnIds, jumpRunId, foldsStartOpen)) {
setInteractionState((current) => ({
...current,
expandedTurnIds: new Set(current.expandedTurnIds).add(jumpRunId),
expandedTurnIds: withDisclosureOpen(current.expandedTurnIds, jumpRunId, foldsStartOpen),
}));
}
const setupAnchorIndex = presentedFeed.findIndex(
Expand Down Expand Up @@ -3365,15 +3397,16 @@ export const ThreadFeed = memo(function ThreadFeed({
}
return new Set(terminalIdsByTurn.values());
})();
// An interrupted run unfolds; a run that is no longer latest folds back.
// An interrupted run unfolds; a run that is no longer latest folds back
// unless every fold starts open.
const [previousLatestRun, setPreviousLatestRun] = useState(props.latestRun);
if (previousLatestRun !== props.latestRun) {
setPreviousLatestRun(props.latestRun);
const latest = props.latestRun;
const previous = previousLatestRun;
if (latest && previous) {
if (latest.runId !== previous.runId) {
if (expandedTurnIds.has(previous.runId)) {
if (!foldsStartOpen && expandedTurnIds.has(previous.runId)) {
setInteractionState((current) => {
const next = new Set(current.expandedTurnIds);
next.delete(previous.runId);
Expand All @@ -3383,7 +3416,11 @@ export const ThreadFeed = memo(function ThreadFeed({
} else if (previous.status === "running" && latest.status === "interrupted") {
setInteractionState((current) => ({
...current,
expandedTurnIds: new Set(current.expandedTurnIds).add(latest.runId),
expandedTurnIds: withDisclosureOpen(
current.expandedTurnIds,
latest.runId,
foldsStartOpen,
),
}));
}
}
Expand Down Expand Up @@ -3486,15 +3523,16 @@ export const ThreadFeed = memo(function ThreadFeed({
}));
};

// Lone rows share their group's disclosure state so opened details survive a second call joining.
// Lone rows share their group's disclosure state so opened details survive a second call
// joining. An expanded transcript already opens that group.
const onToggleWorkRow = (rowId: string, anchorKey?: string, groupId?: string) => {
suspendEndScrollMaintenanceForDisclosure(anchorKey ?? null);
setInteractionState((current) => {
const expanded = !(current.expandedWorkRows[rowId] ?? false);
return {
...current,
expandedWorkRows: { ...current.expandedWorkRows, [rowId]: expanded },
...(groupId === undefined
...(groupId === undefined || foldsStartOpen
? {}
: { expandedWorkGroups: { ...current.expandedWorkGroups, [groupId]: expanded } }),
};
Expand Down Expand Up @@ -3551,7 +3589,9 @@ export const ThreadFeed = memo(function ThreadFeed({
return undefined;
}
return entry.activities.some(
(activity) => activity.prominent || expandedWorkRows[activity.id],
(activity) =>
activity.prominent ||
(activity.canExpand && (expandedWorkRows[activity.id] ?? false) !== rowsStartOpen),
)
? undefined
: collapsedWorkLogHeight(entry.activities, entry.continuesWorkLog);
Expand Down Expand Up @@ -3599,6 +3639,8 @@ export const ThreadFeed = memo(function ThreadFeed({
threadId: props.threadId,
copiedRowId,
expandedWorkRows,
foldsStartOpen,
rowsStartOpen,
workRowSizing,
workGroupScrollPositions,
terminalAssistantMessageIds,
Expand Down
16 changes: 10 additions & 6 deletions apps/mobile/src/features/threads/thread-command-output.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
import type { EnvironmentId, ThreadId, TurnItemId } from "@t3tools/contracts";
import { TERMINAL_PALETTES, terminalOutputSpans } from "@t3tools/shared/terminalOutput";
import type React from "react";
import { useMemo, useRef } from "react";
import { ScrollView, View } from "react-native";
import { View } from "react-native";
import type Animated from "react-native-reanimated";
import { useAnimatedRef } from "react-native-reanimated";

import { AppText as Text } from "../../components/ui/AppText";
import { useServerConfigs } from "../../state/entities";
import { orchestrationEnvironment } from "../../state/orchestration";
import { useEnvironmentQuery } from "../../state/query";
import { WorkLogScrollView } from "./thread-work-log-scroll";

const FOLLOW_SLACK = 8;

Expand All @@ -23,14 +25,15 @@ export function ThreadCommandOutput(props: {
readonly threadId: ThreadId;
readonly itemId: TurnItemId;
readonly themeAppearance: "light" | "dark";
readonly fadeColor: string;
}) {
const { data, error } = useEnvironmentQuery(
orchestrationEnvironment.v2.commandOutput({
environmentId: props.environmentId,
input: { threadId: props.threadId, itemId: props.itemId },
}),
);
const scrollRef = useRef<React.ComponentRef<typeof ScrollView>>(null);
const scrollRef = useAnimatedRef<Animated.ScrollView>();
const followRef = useRef(true);
const text = data?.output.text ?? "";
const spans = useMemo(
Expand All @@ -56,8 +59,9 @@ export function ThreadCommandOutput(props: {
{data?.output.truncated ? (
<Text className="pb-0.5 text-3xs text-foreground-subtle">Earlier output not shown</Text>
) : null}
<ScrollView
ref={scrollRef}
<WorkLogScrollView
scrollRef={scrollRef}
fadeColor={props.fadeColor}
nestedScrollEnabled
directionalLockEnabled
showsVerticalScrollIndicator
Expand Down Expand Up @@ -86,7 +90,7 @@ export function ThreadCommandOutput(props: {
),
)}
</Text>
</ScrollView>
</WorkLogScrollView>
</View>
);
}
Loading
Loading