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: 6 additions & 3 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,10 @@ The browser and native clients live in `app/`. Single Expo codebase targeting we

```bash
cd app
yarn web # web client served by Metro on http://localhost:8081, HMR
yarn ios # iOS simulator (Expo Go or dev client)
yarn android # Android emulator
yarn dev:web:local # web client on http://localhost:8081, HMR, aimux-dev daemon
yarn dev:native:local # Metro for an already-installed native dev build
yarn dev:ios:local # build/install/open the iOS simulator dev build, not Expo Go
yarn dev:android:local # build/install/open the Android emulator dev build
```

What triggers what:
Expand Down Expand Up @@ -99,6 +100,8 @@ For distribution, EAS produces native bundles via the `app/scripts/build.sh` pip
```bash
cd app
yarn build:testflight # TestFlight, iOS default channel
yarn build --android # Play/internal Android build
yarn build --all # iOS + Android
yarn build:production # production, App Store / Play
```

Expand Down
13 changes: 8 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -322,25 +322,28 @@ For GUI and daemon development, use the isolated `aimux-dev` runtime so your rea
aimux-dev daemon restart
aimux-dev daemon project-ensure --project /Users/sam/cs/glyde-frontend
cd app
yarn dev:local
yarn dev:web:local
```

See [docs/dev-runtime.md](docs/dev-runtime.md) for the full local GUI/backend loop.

```bash
cd app
yarn install
yarn web # open browser client (Metro serves it on http://localhost:8081)
yarn ios # iOS simulator
yarn android # Android emulator
yarn dev:web:local # browser client on http://localhost:8081
yarn dev:native:local # Metro for an installed native dev build
yarn dev:ios:local # build/install/open iOS simulator dev build, not Expo Go
yarn dev:android:local # build/install/open Android emulator dev build
```

The app talks to two HTTP surfaces:

- the global aimux daemon at `http://localhost:43190` for project discovery
- per-project metadata servers (port supplied via `/projects` response) for state, agent I/O, plans, and the `/events` SSE stream

For mobile use against a remote machine, set `EXPO_PUBLIC_AIMUX_DAEMON_URL=http://<machine>:43190` in `app/.env`.
For simulator-local development, the helper scripts use `http://127.0.0.1:43191`
for iOS and `http://10.0.2.2:43191` for Android. For mobile use against a remote
machine, set `EXPO_PUBLIC_AIMUX_DAEMON_URL=http://<machine>:43190` in `app/.env`.

Release pipeline (uses EAS):

Expand Down
2 changes: 2 additions & 0 deletions app/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,6 @@ EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_test_your_key_here

# Optional overrides
# EXPO_PUBLIC_AIMUX_DAEMON_URL=http://localhost:43191
# EXPO_PUBLIC_AIMUX_DAEMON_URL=http://127.0.0.1:43191 # iOS simulator
# EXPO_PUBLIC_AIMUX_DAEMON_URL=http://10.0.2.2:43191 # Android emulator
# EXPO_PUBLIC_AIMUX_RELAY_URL=wss://relay.aimux.app
39 changes: 25 additions & 14 deletions app/app/_layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import { Stack, useRouter, useSegments } from "expo-router";
import React, { useEffect } from "react";
import { useColorScheme } from "nativewind";
import { DarkTheme, DefaultTheme, ThemeProvider } from "@react-navigation/native";
import { StatusBar } from "expo-status-bar";
import { SafeAreaProvider } from "react-native-safe-area-context";
import { AuthProvider, LOCAL_MODE, useAuth } from "@/lib/auth";
import { useThemeEffect } from "@/lib/theme-effect";

Expand Down Expand Up @@ -35,19 +37,28 @@ export default function RootLayout() {
const navTheme = colorScheme === "dark" ? DarkTheme : DefaultTheme;

return (
<ThemeProvider value={navTheme}>
<AuthProvider>
<AuthGate>
<Stack>
<Stack.Screen name="index" options={{ headerShown: false }} />
<Stack.Screen name="(main)" options={{ headerShown: false }} />
<Stack.Screen name="landing" options={{ headerShown: false }} />
<Stack.Screen name="cli-auth" options={{ headerShown: false }} />
<Stack.Screen name="sign-in" options={{ headerShown: false, presentation: "modal" }} />
<Stack.Screen name="sign-up" options={{ headerShown: false, presentation: "modal" }} />
</Stack>
</AuthGate>
</AuthProvider>
</ThemeProvider>
<SafeAreaProvider>
<ThemeProvider value={navTheme}>
<AuthProvider>
<AuthGate>
<StatusBar style={colorScheme === "dark" ? "light" : "dark"} />
<Stack>
<Stack.Screen name="index" options={{ headerShown: false }} />
<Stack.Screen name="(main)" options={{ headerShown: false }} />
<Stack.Screen name="landing" options={{ headerShown: false }} />
<Stack.Screen name="cli-auth" options={{ headerShown: false }} />
<Stack.Screen
name="sign-in"
options={{ headerShown: false, presentation: "modal" }}
/>
<Stack.Screen
name="sign-up"
options={{ headerShown: false, presentation: "modal" }}
/>
</Stack>
</AuthGate>
</AuthProvider>
</ThemeProvider>
</SafeAreaProvider>
);
}
Binary file added app/assets/images/adaptive-icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added app/assets/images/favicon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added app/assets/images/icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion app/components/AppShell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ export function AppShell({ children }: { children: React.ReactNode }) {
transform: [{ translateX }],
}}
>
<ProjectSidebar />
<ProjectSidebar showBottomNav={false} />
</Animated.View>
) : null}
</View>
Expand Down
26 changes: 17 additions & 9 deletions app/components/ChatComposer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,11 @@ export function ChatComposer({ serviceEndpoint, sessionId, token }: Props) {
setDraftImages((prev) => prev.filter((img) => img.attachmentId !== attachmentId));
}

function restoreDraftIfUntouched(text: string, images: DraftImage[]) {
setDraft((current) => (current.length > 0 ? current : text));
setDraftImages((current) => (current.length > 0 ? current : images));
}

async function handleSend() {
const trimmed = draft.trim();
if (!trimmed && draftImages.length === 0) return;
Expand All @@ -92,14 +97,16 @@ export function ChatComposer({ serviceEndpoint, sessionId, token }: Props) {
setError(null);
setSending(true);

const submittedText = trimmed;
const submittedImages = draftImages;
const clientMessageId = uuid();
const sendParts: AgentInputPart[] = [];
const historyParts: HistoryPart[] = [];
if (trimmed) {
sendParts.push({ type: "text", text: trimmed });
historyParts.push({ type: "text", text: trimmed });
if (submittedText) {
sendParts.push({ type: "text", text: submittedText });
historyParts.push({ type: "text", text: submittedText });
}
for (const img of draftImages) {
for (const img of submittedImages) {
sendParts.push({ type: "image", attachmentId: img.attachmentId, alt: img.filename });
historyParts.push({
type: "image",
Expand All @@ -119,6 +126,8 @@ export function ChatComposer({ serviceEndpoint, sessionId, token }: Props) {
deliveryState: "sending",
},
});
setDraft("");
setDraftImages([]);

try {
const result = await sendAgentInput(
Expand All @@ -134,6 +143,7 @@ export function ChatComposer({ serviceEndpoint, sessionId, token }: Props) {
patch: { deliveryState: "failed", deliveryError: msg },
});
setError(msg);
restoreDraftIfUntouched(submittedText, submittedImages);
return;
}
// Server operation state is queued|applied|submitted|failed; client tracks
Expand All @@ -152,11 +162,9 @@ export function ChatComposer({ serviceEndpoint, sessionId, token }: Props) {
: undefined,
},
});
if (deliveryState === "submitted") {
setDraft("");
setDraftImages([]);
} else {
if (deliveryState === "failed") {
setError(result.error ?? "The agent input operation failed.");
restoreDraftIfUntouched(submittedText, submittedImages);
}
} catch (e) {
const msg = e instanceof Error ? e.message : String(e);
Expand All @@ -166,6 +174,7 @@ export function ChatComposer({ serviceEndpoint, sessionId, token }: Props) {
patch: { deliveryState: "failed", deliveryError: msg },
});
setError(msg);
restoreDraftIfUntouched(submittedText, submittedImages);
} finally {
setSending(false);
}
Expand Down Expand Up @@ -203,7 +212,6 @@ export function ChatComposer({ serviceEndpoint, sessionId, token }: Props) {
onChangeText={setDraft}
onKeyPress={handleComposerKeyPress}
multiline
editable={!sending}
/>
<Button
variant="outline"
Expand Down
9 changes: 8 additions & 1 deletion app/components/MobileTabBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import React from "react";
import { Pressable, View } from "react-native";
import { usePathname, useRouter } from "expo-router";
import { Home, MessageSquare, Settings } from "lucide-react-native";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import { resolveChromeBottomInset } from "@/lib/native-safe-area";
import { Text } from "@/components/ui/text";
import { cn } from "@/lib/utils";

Expand All @@ -19,9 +21,14 @@ function isActive(pathname: string, route: string): boolean {
export function MobileTabBar() {
const router = useRouter();
const pathname = usePathname();
const insets = useSafeAreaInsets();
const bottomInset = resolveChromeBottomInset(insets.bottom);

return (
<View className="h-14 flex-row border-t border-border bg-card">
<View
className="flex-row border-t border-border bg-card"
style={{ height: 56 + bottomInset, paddingBottom: bottomInset }}
>
{TABS.map(({ label, route, Icon }) => {
const active = isActive(pathname, route);
return (
Expand Down
8 changes: 4 additions & 4 deletions app/components/ProjectSidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ function ProjectHeader({
</View>
<View className="flex-1 min-w-0">
<Text className="text-[10px] font-bold uppercase tracking-widest text-muted-foreground mb-1">
Project
All projects
</Text>
<Text
className="text-[16px] font-bold text-foreground"
Expand Down Expand Up @@ -427,7 +427,7 @@ function SidebarBottomNav() {

// ─── Top-level component ──────────────────────────────────────────────────

export function ProjectSidebar() {
export function ProjectSidebar({ showBottomNav = true }: { showBottomNav?: boolean }) {
const projects = useAtomValue(projectsAtom);
const selectedProject = useAtomValue(selectedProjectAtom);
const selectedProjectPath = useAtomValue(selectedProjectPathAtom);
Expand Down Expand Up @@ -486,7 +486,7 @@ export function ProjectSidebar() {
}

return (
<View className="w-80 border-r border-border bg-background">
<View className="w-80 flex-1 border-r border-border bg-background">
<ScrollView className="flex-1">
{pickerMode ? (
<ProjectPicker
Expand All @@ -508,7 +508,7 @@ export function ProjectSidebar() {
</>
)}
</ScrollView>
<SidebarBottomNav />
{showBottomNav ? <SidebarBottomNav /> : null}
</View>
);
}
9 changes: 8 additions & 1 deletion app/components/TopBar.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,23 @@
import React from "react";
import { View } from "react-native";
import { useAtomValue } from "jotai";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import { AuthMenu } from "@/components/AuthMenu";
import { RelayIndicator } from "@/components/RelayIndicator";
import { resolveChromeTopInset } from "@/lib/native-safe-area";
import { Text } from "@/components/ui/text";
import { relayConfiguredAtom } from "@/stores/relay";

export function TopBar({ left }: { left?: React.ReactNode }) {
const relayConfigured = useAtomValue(relayConfiguredAtom);
const insets = useSafeAreaInsets();
const topInset = resolveChromeTopInset(insets.top);

return (
<View className="z-30 h-14 flex-row items-center justify-between border-b border-border bg-card px-4">
<View
className="z-30 flex-row items-center justify-between border-b border-border bg-card px-4"
style={{ height: 56 + topInset, paddingTop: topInset }}
>
{left ? <View className="-ml-1 mr-2">{left}</View> : null}
<Text className="font-mono text-[17px] font-bold text-foreground">aimux</Text>
<View className="flex-1" />
Expand Down
19 changes: 19 additions & 0 deletions app/lib/native-safe-area.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { Platform } from "react-native";

// Expo dev-client reloads can briefly report zero iOS safe-area insets.
// These floors keep chrome clear of the Dynamic Island and home indicator
// until react-native-safe-area-context reports real device metrics.
export const IOS_MIN_TOP_INSET = 54;
export const IOS_MIN_BOTTOM_INSET = 24;

export function resolveChromeTopInset(topInset: number): number {
if (Platform.OS === "web") return 0;
if (Platform.OS === "ios") return Math.max(topInset, IOS_MIN_TOP_INSET);
return topInset;
}

export function resolveChromeBottomInset(bottomInset: number): number {
if (Platform.OS === "web") return 0;
if (Platform.OS === "ios") return Math.max(bottomInset, IOS_MIN_BOTTOM_INSET);
return bottomInset;
}
12 changes: 10 additions & 2 deletions app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,16 @@
"private": true,
"scripts": {
"start": "expo start",
"dev:local": "EXPO_PUBLIC_AIMUX_CONNECTION_MODE=local EXPO_PUBLIC_AIMUX_DAEMON_URL=http://localhost:43191 expo start --web",
"dev:relay": "EXPO_PUBLIC_AIMUX_CONNECTION_MODE=relay EXPO_PUBLIC_AIMUX_RELAY_URL=wss://relay.aimux.app expo start --web",
"dev:local": "yarn dev:web:local",
"dev:relay": "yarn dev:web:relay",
"dev:web:local": "EXPO_PUBLIC_AIMUX_CONNECTION_MODE=local EXPO_PUBLIC_AIMUX_DAEMON_URL=http://localhost:43191 expo start --web",
"dev:web:relay": "EXPO_PUBLIC_AIMUX_CONNECTION_MODE=relay EXPO_PUBLIC_AIMUX_RELAY_URL=wss://relay.aimux.app expo start --web",
"dev:native:local": "EXPO_PUBLIC_AIMUX_CONNECTION_MODE=local EXPO_PUBLIC_AIMUX_DAEMON_URL=http://127.0.0.1:43191 expo start --dev-client",
"dev:native:relay": "EXPO_PUBLIC_AIMUX_CONNECTION_MODE=relay EXPO_PUBLIC_AIMUX_RELAY_URL=wss://relay.aimux.app expo start --dev-client",
"dev:ios:local": "EXPO_PUBLIC_AIMUX_CONNECTION_MODE=local EXPO_PUBLIC_AIMUX_DAEMON_URL=http://127.0.0.1:43191 expo run:ios",
"dev:ios:relay": "EXPO_PUBLIC_AIMUX_CONNECTION_MODE=relay EXPO_PUBLIC_AIMUX_RELAY_URL=wss://relay.aimux.app expo run:ios",
"dev:android:local": "EXPO_PUBLIC_AIMUX_CONNECTION_MODE=local EXPO_PUBLIC_AIMUX_DAEMON_URL=http://10.0.2.2:43191 expo run:android",
"dev:android:relay": "EXPO_PUBLIC_AIMUX_CONNECTION_MODE=relay EXPO_PUBLIC_AIMUX_RELAY_URL=wss://relay.aimux.app expo run:android",
"android": "expo run:android",
"ios": "expo run:ios",
"web": "expo start --web",
Expand Down
Loading