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
9 changes: 8 additions & 1 deletion apps/mobile/src/features/layout/inspector-host.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,14 @@ const PANE_HEADER_OPTIONS = { ...GLASS_HEADER_OPTIONS, headerShown: false };

function PaneHeader(props: { readonly store: InspectorBarStore }) {
const options = useSyncExternalStore(props.store.subscribe, props.store.getSnapshot);
return <V5StackHeader options={{ ...PANE_HEADER_OPTIONS, ...options }} canGoBack={false} />;
// Its own search identity, so opening the inspector doesn't morph the sidebar's search into it.
return (
<V5StackHeader
options={{ ...PANE_HEADER_OPTIONS, ...options }}
canGoBack={false}
searchIdentifier="inspector-search"
/>
);
}

function PaneScreenContent(props: {
Expand Down
159 changes: 98 additions & 61 deletions apps/mobile/src/features/threads/ThreadDetailScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,10 @@ import Animated, {
} from "react-native-reanimated";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import { useBarOverlap } from "../../components/ui/useBarInset";
import { useWorkspaceContentWidth } from "../layout/workspace-content-width";
import {
useWorkspaceContentWidth,
WorkspaceSettledContentWidthContext,
} from "../layout/workspace-content-width";
import { useAdaptiveWorkspaceLayout } from "../layout/adaptive-workspace-context";
import { useNativeWorkspaceColumnsSupported } from "../../native/NativeWorkspaceColumns";
import {
Expand Down Expand Up @@ -317,6 +320,17 @@ export interface ThreadDetailScreenProps {

// iOS questions overlay the controls; Android questions stack with child approvals.
const ComposerPanels = Platform.OS === "android" ? Animated.ScrollView : Animated.View;

/** Hands the feed the screen width measured for a native column; elsewhere keeps the inherited one. */
function ColumnWidth(props: { readonly width: number | null; readonly children: React.ReactNode }) {
return props.width === null ? (
props.children
) : (
<WorkspaceSettledContentWidthContext value={props.width}>
{props.children}
</WorkspaceSettledContentWidthContext>
);
}
const USAGE_LIMITS_PANEL_GAP = 12;
const USAGE_LIMITS_PANEL_MIN_HEIGHT = 44;

Expand Down Expand Up @@ -1022,8 +1036,13 @@ function ThreadDetailBody(
// Android lays the screen out below its opaque header and reports a zero
// header height, so the window height overstates the room by the header.
const [screenHeight, setScreenHeight] = useState<number | null>(null);
const [screenWidth, setScreenWidth] = useState<number | null>(null);
const handleScreenLayout = (event: LayoutChangeEvent) => {
setScreenHeight(event.nativeEvent.layout.height);
const width = Math.round(event.nativeEvent.layout.width);
setScreenWidth((current) =>
current !== null && Math.abs(current - width) <= 1 ? current : width,
);
};
const screenAreaHeight = screenHeight ?? windowHeight;
const draftViewportHeight = screenAreaHeight - navigationHeaderHeight - liveKeyboardHeight;
Expand Down Expand Up @@ -1211,6 +1230,13 @@ function ThreadDetailBody(
}
: { width: "auto", right: controlInsets.right },
);
// A native column resizes the screen before JS hears of it. The feed and the composer take the
// measured width in one render, so they change size together rather than the composer first.
const columnWidth = usesNativeWorkspaceColumns && isSplitLayout ? screenWidth : null;
const columnComposerWidthStyle =
columnWidth === null
? null
: { width: Math.max(0, columnWidth - controlInsets.left - controlInsets.right) };
const selectedInstanceId = props.selectedThread.modelSelection.instanceId;
useAtomMount(threadStreamHaptics(props.selectedThreadDetailRef));
const selectedProviderSkills = (() => {
Expand Down Expand Up @@ -1436,54 +1462,58 @@ function ThreadDetailBody(
)}
>
<WorkspaceMutationIdProvider threadRef={props.selectedThreadDetailRef}>
<ThreadFeed
toolCallVisibility={toolCallVisibility}
environmentId={props.environmentId}
threadId={props.selectedThread.id}
detailRef={props.selectedThreadDetailRef}
workspaceRoot={props.threadCwd}
feed={props.selectedThreadFeed}
pendingHandoff={props.pendingHandoff}
worktreeSetup={props.worktreeSetup}
setupWorkingStartedAt={props.setupWorkingStartedAt}
queuedMessages={props.queuedMessages}
dispatchingMessageId={props.dispatchingMessageId}
// A native subagent has no composer to edit a pending message in;
// Cancel on the edit banner would discard it.
onEditPendingMessage={
isProviderSubagent || pendingMessageEditBlocked ? null : handleEditPendingMessage
}
contentPresentation={props.contentPresentation}
threadTitle={props.selectedThread.title}
latestRun={props.activityRun}
activeWorkStartedAt={props.activeWorkStartedAt}
runlessWorkActive={props.runlessWorkActive ?? false}
listRef={listRef}
freeze={freeze}
anchorMessageId={anchorMessageId}
jumpToMessageId={jumpMessageLoaded ? jumpMessageId : null}
onJumpedToMessage={() => finishThreadMessageJump(selectedThreadKey)}
contentInsetEndAdjustment={combinedContentInsetEndAdjustment}
keyboardOffset={composerKeyboardOffset}
sendingWithKeyboard={sendingWithKeyboard}
fitShortFeedToViewport={questionInFlow && !isKeyboardVisible}
contentTopInset={0}
contentBottomInset={
questionInFlow
? 0
: estimatedOverlayHeight +
(showFloatingStatus ? FLOATING_WORKING_CONTROL_COVERAGE : 0)
}
contentMaxWidth={contentMaxWidth}
historyControls={props.historyControls}
contentSideInsets={nativeMetrics?.safeArea}
layoutVariant={layoutVariant}
usesAutomaticContentInsets={props.usesAutomaticContentInsets}
onHeaderMaterialVisibilityChange={props.onHeaderMaterialVisibilityChange}
onIsAtEndChange={setIsAtEnd}
skills={selectedProviderSkills}
onUseArtifactTemplate={handleUseArtifactTemplate}
/>
<ColumnWidth width={columnWidth}>
<ThreadFeed
toolCallVisibility={toolCallVisibility}
environmentId={props.environmentId}
threadId={props.selectedThread.id}
detailRef={props.selectedThreadDetailRef}
workspaceRoot={props.threadCwd}
feed={props.selectedThreadFeed}
pendingHandoff={props.pendingHandoff}
worktreeSetup={props.worktreeSetup}
setupWorkingStartedAt={props.setupWorkingStartedAt}
queuedMessages={props.queuedMessages}
dispatchingMessageId={props.dispatchingMessageId}
// A native subagent has no composer to edit a pending message in;
// Cancel on the edit banner would discard it.
onEditPendingMessage={
isProviderSubagent || pendingMessageEditBlocked
? null
: handleEditPendingMessage
}
contentPresentation={props.contentPresentation}
threadTitle={props.selectedThread.title}
latestRun={props.activityRun}
activeWorkStartedAt={props.activeWorkStartedAt}
runlessWorkActive={props.runlessWorkActive ?? false}
listRef={listRef}
freeze={freeze}
anchorMessageId={anchorMessageId}
jumpToMessageId={jumpMessageLoaded ? jumpMessageId : null}
onJumpedToMessage={() => finishThreadMessageJump(selectedThreadKey)}
contentInsetEndAdjustment={combinedContentInsetEndAdjustment}
keyboardOffset={composerKeyboardOffset}
sendingWithKeyboard={sendingWithKeyboard}
fitShortFeedToViewport={questionInFlow && !isKeyboardVisible}
contentTopInset={0}
contentBottomInset={
questionInFlow
? 0
: estimatedOverlayHeight +
(showFloatingStatus ? FLOATING_WORKING_CONTROL_COVERAGE : 0)
}
contentMaxWidth={contentMaxWidth}
historyControls={props.historyControls}
contentSideInsets={nativeMetrics?.safeArea}
layoutVariant={layoutVariant}
usesAutomaticContentInsets={props.usesAutomaticContentInsets}
onHeaderMaterialVisibilityChange={props.onHeaderMaterialVisibilityChange}
onIsAtEndChange={setIsAtEnd}
skills={selectedProviderSkills}
onUseArtifactTemplate={handleUseArtifactTemplate}
/>
</ColumnWidth>
</WorkspaceMutationIdProvider>
</RenderErrorBoundary>
</View>
Expand Down Expand Up @@ -1533,18 +1563,25 @@ function ThreadDetailBody(
pointerEvents="box-none"
style={
questionInFlow
? isSplitLayout
? composerWidthStyle
: undefined
: [
{
position: "absolute",
bottom: controlInsets.bottom,
left: controlInsets.left,
right: controlInsets.right,
},
composerWidthStyle,
]
? (columnComposerWidthStyle ?? (isSplitLayout ? composerWidthStyle : undefined))
: columnComposerWidthStyle
? [
{
position: "absolute",
bottom: controlInsets.bottom,
left: controlInsets.left,
},
columnComposerWidthStyle,
]
: [
{
position: "absolute",
bottom: controlInsets.bottom,
left: controlInsets.left,
right: controlInsets.right,
},
composerWidthStyle,
]
}
>
{/* No paddingTop here: the overlay's measured height becomes the
Expand Down
18 changes: 16 additions & 2 deletions apps/mobile/src/native/V5StackHeader.ios.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,8 @@ export function V5StackHeader(props: {
readonly column?: "primary" | "detail";
/** The workspace's compose action, shown as its own group in this column's bar. */
readonly onCompose?: () => void;
/** UIKit morphs search items that share an identifier between bars as columns change. */
readonly searchIdentifier?: string;
}) {
const { options } = props;
const theme = useMobileNavigationTheme();
Expand All @@ -182,7 +184,14 @@ export function V5StackHeader(props: {
const bottom = toolbar.flatMap<HeaderItem>((item, index) => {
if (item.type === "mailSearchToolbar") return [];
if (item.type === "searchBarPlacement")
return [{ type: "item", id: `toolbar-search:${index}`, searchBarPlacement: true }];
return [
{
type: "item",
id: `toolbar-search:${index}`,
searchBarPlacement: true,
...(props.searchIdentifier ? { identifier: props.searchIdentifier } : {}),
},
];
return convertItems([item], `toolbar:${index}`);
});
if (mailSearch) {
Expand All @@ -199,7 +208,12 @@ export function V5StackHeader(props: {
if (bottomSearch) {
if (spacesSearch && bottom.length)
bottom.push({ type: "spacer", id: "before-search", sizing: "fixed", width: 8 });
bottom.push({ type: "item", id: "home-search", searchBarPlacement: true });
bottom.push({
type: "item",
id: "home-search",
searchBarPlacement: true,
...(props.searchIdentifier ? { identifier: props.searchIdentifier } : {}),
});
if (spacesSearch && mailSearch.onComposePress)
bottom.push({ type: "spacer", id: "after-search", sizing: "fixed", width: 8 });
}
Expand Down
Loading