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
4 changes: 4 additions & 0 deletions TODO.md
Original file line number Diff line number Diff line change
Expand Up @@ -465,6 +465,10 @@ within the limits of the server APIs and mobile lifecycle.
- [ ] Implement supported key, OAuth, and command connection flows.
- [ ] Keep callback state and temporary secrets in SecureStore.
- [ ] Implement MCP list, add, remove, connect, disconnect, and resource catalog.
- [x] Add location-wide MCP list and runtime connect/disconnect from session
Location options, keeping the branch visible. Experimental generated
connect/disconnect operations are explicitly accepted for this slice;
add/remove, OAuth, resource browsing, and device verification remain.
- [ ] Implement saved-permission list and removal.
- [ ] Add reference browsing and web search where the current API supports it.
- [ ] Add read-only configuration and plugin inspection.
Expand Down
45 changes: 44 additions & 1 deletion apps/mobile/src/screens/app-shell.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { expect, jest, test } from "@jest/globals";
import { fireEvent, render, screen, waitFor } from "@testing-library/react-native";
import * as Clipboard from "expo-clipboard";
import { createElement } from "react";
import { Dimensions, Text } from "react-native";
import { Dimensions, Pressable, Text } from "react-native";

import { useConnections } from "../connections/connections-context";
import { useConnectionRuntime } from "../state/connection-runtime-context";
Expand Down Expand Up @@ -132,6 +132,49 @@ test("session tabs replace healthy connection and server labels but retain conne
expect(navigate).toHaveBeenCalledWith("Settings");
});

test("keeps branch and trailing location options together on narrow phones and large text", () => {
jest
.mocked(useConnections)
.mockReturnValue({ profiles: [], selectedProfileId: undefined } as never);
jest
.mocked(useConnectionRuntime)
.mockReturnValue({ reconnectAttempt: 0, status: "connected" } as never);
jest
.mocked(useWorkspaceSelection)
.mockReturnValue({ attentionCoverage: { completeness: "complete" }, pendingCount: 0 } as never);
const originalWindow = Dimensions.get("window");
const originalScreen = Dimensions.get("screen");
try {
for (const fontScale of [1, 2]) {
Dimensions.set({
window: { ...originalWindow, width: 320, fontScale },
screen: { ...originalScreen, width: 320, fontScale },
});
const view = render(
createElement(ShellFrame, {
active: "Workspace",
navigate: jest.fn(),
branch: { state: "known", name: "feature/a-long-branch-name" },
sessionTabs: { active: "session", onSelect: jest.fn() },
sessionOptions: createElement(
Pressable,
{ accessibilityRole: "button", accessibilityLabel: "Location options" },
createElement(Text, null, "..."),
),
}),
);
expect(
screen.getByRole("button", { name: "Current branch, feature/a-long-branch-name" }),
).toBeOnTheScreen();
expect(screen.getByRole("button", { name: "Location options" })).toBeOnTheScreen();
expect(screen.getByRole("tab", { name: "Changes" })).toBeOnTheScreen();
view.unmount();
}
} finally {
Dimensions.set({ window: originalWindow, screen: originalScreen });
}
});

test("reveals and copies the full session branch name", async () => {
jest.mocked(useConnections).mockReturnValue({
profiles: [{ id: "connection-1", name: "Test server" }],
Expand Down
113 changes: 66 additions & 47 deletions apps/mobile/src/screens/app-shell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -617,13 +617,15 @@ export function ShellFrame({
children,
hideConnectionBar,
navigate,
sessionOptions,
sessionTabs,
}: {
active: Section;
branch?: SessionBranch;
children?: ReactNode;
hideConnectionBar?: boolean;
navigate: (screen: Section) => void;
sessionOptions?: ReactNode;
sessionTabs?: { active: "session" | "changes"; onSelect: (tab: "session" | "changes") => void };
}) {
const { fontScale, width } = useWindowDimensions();
Expand Down Expand Up @@ -754,53 +756,65 @@ export function ShellFrame({
</Text>
</View>
)}
{branch ? (
branch.state === "known" && branch.name ? (
<Pressable
accessibilityHint="Opens details where you can copy the full branch name"
accessibilityLabel={`Current branch, ${branch.name}${branch.stale ? ", may be outdated" : ""}`}
accessibilityRole="button"
onPress={() => showDetail("branch")}
style={({ pressed }) => [
styles.branchControl,
largeText && styles.branchControlLargeText,
pressed && styles.branchControlPressed,
]}
>
<Feather
accessibilityElementsHidden
color={palette.dim}
importantForAccessibility="no-hide-descendants"
name="git-branch"
size={14}
/>
<Text
dynamicTypeRamp={typeRamp.subheading}
ellipsizeMode="middle"
numberOfLines={1}
style={styles.branchName}
>
{branch.name}
</Text>
</Pressable>
) : (
<View style={[styles.branchControl, largeText && styles.branchControlLargeText]}>
<Feather
accessibilityElementsHidden
color={palette.dim}
importantForAccessibility="no-hide-descendants"
name="git-branch"
size={14}
/>
<Text dynamicTypeRamp={typeRamp.subheading} style={styles.branchName}>
{branch.state === "loading"
? "Checking branch"
: branch.state === "none"
? "No branch"
: "Branch unavailable"}
</Text>
</View>
)
{branch || sessionOptions ? (
<View
style={[styles.locationControls, largeText && styles.locationControlsLargeText]}
>
{branch ? (
branch.state === "known" && branch.name ? (
<Pressable
accessibilityHint="Opens details where you can copy the full branch name"
accessibilityLabel={`Current branch, ${branch.name}${branch.stale ? ", may be outdated" : ""}`}
accessibilityRole="button"
onPress={() => showDetail("branch")}
style={({ pressed }) => [
styles.branchControl,
largeText && !sessionOptions && styles.branchControlLargeText,
pressed && styles.branchControlPressed,
]}
>
<Feather
accessibilityElementsHidden
color={palette.dim}
importantForAccessibility="no-hide-descendants"
name="git-branch"
size={14}
/>
<Text
dynamicTypeRamp={typeRamp.subheading}
ellipsizeMode="middle"
numberOfLines={1}
style={styles.branchName}
>
{branch.name}
</Text>
</Pressable>
) : (
<View
style={[
styles.branchControl,
largeText && !sessionOptions && styles.branchControlLargeText,
]}
>
<Feather
accessibilityElementsHidden
color={palette.dim}
importantForAccessibility="no-hide-descendants"
name="git-branch"
size={14}
/>
<Text dynamicTypeRamp={typeRamp.subheading} style={styles.branchName}>
{branch.state === "loading"
? "Checking branch"
: branch.state === "none"
? "No branch"
: "Branch unavailable"}
</Text>
</View>
)
) : null}
{sessionOptions}
</View>
) : null}
{!sessionTabs ? (
<Pressable
Expand Down Expand Up @@ -987,12 +1001,14 @@ export function WorkspaceStateCard({ state }: { state: ReturnType<typeof getWork
}

export function ActionButton({
accessibilityLabel,
disabled,
fullWidth,
label,
onPress,
secondary,
}: {
accessibilityLabel?: string;
disabled?: boolean;
fullWidth?: boolean;
label: string;
Expand All @@ -1001,6 +1017,7 @@ export function ActionButton({
}) {
return (
<Pressable
accessibilityLabel={accessibilityLabel}
accessibilityRole="button"
accessibilityState={{ disabled: Boolean(disabled) }}
disabled={disabled}
Expand All @@ -1024,6 +1041,8 @@ export function ActionButton({
}

const styles = StyleSheet.create({
locationControls: { flex: 1, minWidth: 0, flexDirection: "row", alignItems: "center" },
locationControlsLargeText: { flex: 0, width: "100%" },
sessionTabs: { flexDirection: "row", gap: space.sm },
sessionTab: {
minHeight: 44,
Expand Down
Loading
Loading