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: 9 additions & 0 deletions .macroscope/check-run-agents/effect-service-conventions.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,15 @@ Review changed TypeScript and directly affected call sites for the conventions b
- Keep implementation-specific names when an abstract port module contains one of several possible implementations, for example `makeCloudflaredRelayClient` and `layerCloudflared` in `RelayClient.ts`.
- `infra/relay/src/db.ts` is an intentional exception: an inline `Layer.succeed(RelayDb, db)` is acceptable without generic `make`/`layer` exports.

## Dependency acquisition and runtime boundaries

- Production service construction must acquire Effect service dependencies from the environment with `yield* Foo.Foo`, and its `make`/`layer` types must expose those requirements. Flag factories or constructors that accept `Foo["Service"]` (or a plain object whose methods return `Effect`) when that value is an implementation dependency owned by the service. Passing service instances explicitly is acceptable in tests and integration harnesses; passing pure configuration, immutable domain values, or deliberate callback strategies is not service injection.
- Do not hide dependencies in module globals, closures over singleton services, or `Layer.succeed` implementations that call runtime-backed or imperative APIs. Trace helpers used by a supposedly synchronous layer far enough to verify that asynchronous services are represented in the Effect environment.
- `ManagedRuntime.make`, `runPromise`, and `runPromiseExit` belong at explicit application/framework boundaries such as React, native callback, CLI, or HTTP adapters. Flag their use in domain services, repositories, persistence implementations, and service constructors. A clearly named imperative adapter may bridge an Effect service into a Promise API, but it must not become a dependency of another Effect service.
- Do not create per-feature managed runtimes or Atom runtimes to smuggle the same owned resource into multiple consumers. Compose the resource once in an application-owned layer/runtime and provide its context to integration runtimes.
- When acquisition can fail but a caller must retain fallback behavior, keep the failure typed in Effect rather than bypassing the layer through an imperative runtime. Model unavailability in service operations or with an explicit optional-service layer so downstream recovery remains visible and testable.
- During review, search touched code and affected call sites for service-instance parameters, `Layer.succeed`, `ManagedRuntime.make`, and `.runPromise`/`.runPromiseExit`. Verify that each occurrence is a legitimate test seam, pure value injection, or application boundary—not fake dependency injection or a hidden runtime.

## Errors and predicates

- Define service failures with `Schema.TaggedErrorClass` and structured attributes. Derive `message` from those attributes rather than storing an unstructured message as the only data.
Expand Down
36 changes: 35 additions & 1 deletion apps/mobile/app.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,16 @@ function readReleaseVersion(): string | undefined {
}
}

const dmSansFonts = {
regular: "@expo-google-fonts/dm-sans/400Regular/DMSans_400Regular.ttf",
medium: "@expo-google-fonts/dm-sans/500Medium/DMSans_500Medium.ttf",
bold: "@expo-google-fonts/dm-sans/700Bold/DMSans_700Bold.ttf",
Comment thread
wizzoapp[bot] marked this conversation as resolved.
} as const;

// These aliases match the fonts' PostScript names on iOS. Register the same
// names on Android so React Native and the native composer use one set of
// family names without waiting for runtime font loading.

const config: ExpoConfig = {
name: variant.appName,
slug: "t3-code",
Expand Down Expand Up @@ -161,8 +171,32 @@ const config: ExpoConfig = {
favicon: "./assets/favicon.png",
},
plugins: [
"expo-font",
[
"expo-font",
{
ios: {
fonts: [dmSansFonts.regular, dmSansFonts.medium, dmSansFonts.bold],
},
android: {
fonts: [
{
fontFamily: "DMSans-Regular",
fontDefinitions: [{ path: dmSansFonts.regular, weight: 400 }],
},
{
fontFamily: "DMSans-Medium",
fontDefinitions: [{ path: dmSansFonts.medium, weight: 500 }],
},
{
fontFamily: "DMSans-Bold",
fontDefinitions: [{ path: dmSansFonts.bold, weight: 700 }],
},
],
},
},
],
"expo-secure-store",
"expo-sqlite",
["@clerk/expo", { theme: "./clerk-theme.json" }],
"expo-web-browser",
[
Expand Down
12 changes: 8 additions & 4 deletions apps/mobile/eas.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@
"APP_VARIANT": "development",
"EXPO_OWNER": "adam0410",
"EXPO_PROJECT_ID": "c5d56501-d2d8-444d-8b4a-b54ead1ba0f7",
"EXPO_UPDATES_URL": "https://u.expo.dev/c5d56501-d2d8-444d-8b4a-b54ead1ba0f7"
"EXPO_UPDATES_URL": "https://u.expo.dev/c5d56501-d2d8-444d-8b4a-b54ead1ba0f7",
"NODE_OPTIONS": "--max-old-space-size=4096"
},
"channel": "development",
"developmentClient": true,
Expand All @@ -23,7 +24,8 @@
"APP_VARIANT": "preview",
"EXPO_OWNER": "adam0410",
"EXPO_PROJECT_ID": "c5d56501-d2d8-444d-8b4a-b54ead1ba0f7",
"EXPO_UPDATES_URL": "https://u.expo.dev/c5d56501-d2d8-444d-8b4a-b54ead1ba0f7"
"EXPO_UPDATES_URL": "https://u.expo.dev/c5d56501-d2d8-444d-8b4a-b54ead1ba0f7",
"NODE_OPTIONS": "--max-old-space-size=4096"
},
"channel": "preview",
"environment": "preview",
Expand All @@ -39,7 +41,8 @@
"EXPO_OWNER": "adam0410",
"EXPO_PROJECT_ID": "c5d56501-d2d8-444d-8b4a-b54ead1ba0f7",
"EXPO_UPDATES_URL": "https://u.expo.dev/c5d56501-d2d8-444d-8b4a-b54ead1ba0f7",
"MOBILE_VERSION_POLICY": "fingerprint"
"MOBILE_VERSION_POLICY": "fingerprint",
"NODE_OPTIONS": "--max-old-space-size=4096"
},
"channel": "preview",
"environment": "preview",
Expand All @@ -55,7 +58,8 @@
"APP_VARIANT": "production",
"EXPO_OWNER": "adam0410",
"EXPO_PROJECT_ID": "c5d56501-d2d8-444d-8b4a-b54ead1ba0f7",
"EXPO_UPDATES_URL": "https://u.expo.dev/c5d56501-d2d8-444d-8b4a-b54ead1ba0f7"
"EXPO_UPDATES_URL": "https://u.expo.dev/c5d56501-d2d8-444d-8b4a-b54ead1ba0f7",
"NODE_OPTIONS": "--max-old-space-size=4096"
},
"channel": "production",
"environment": "production",
Expand Down
15 changes: 9 additions & 6 deletions apps/mobile/global.css
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@
/* Inputs */
--color-input: #ffffff;
--color-input-border: rgba(0, 0, 0, 0.1);
--color-sidebar-search: rgba(118, 118, 128, 0.12);
--color-placeholder: #a3a3a3;

/* Icons */
Expand Down Expand Up @@ -144,6 +145,7 @@
/* Inputs */
--color-input: #141414;
--color-input-border: rgba(255, 255, 255, 0.08);
--color-sidebar-search: rgba(118, 118, 128, 0.24);
--color-placeholder: #8e8e93;

/* Icons */
Expand Down Expand Up @@ -194,9 +196,10 @@

/* ─── Typography ────────────────────────────────────────────────────── */
@theme {
--font-sans: "DMSans_400Regular";
--font-medium: "DMSans_500Medium";
--font-bold: "DMSans_700Bold";
/* Keep these native family names aligned with app.config.ts. */
--font-sans: "DMSans-Regular";
--font-medium: "DMSans-Medium";
--font-bold: "DMSans-Bold";

/* Keep this scale aligned with src/lib/typography.ts for native style props. */
--text-3xs: 11px;
Expand All @@ -221,14 +224,14 @@

/* ─── Custom utilities ──────────────────────────────────────────────── */
@utility font-t3-medium {
font-family: "DMSans_500Medium";
font-family: var(--font-medium);
}

@utility font-t3-bold {
font-family: "DMSans_700Bold";
font-family: var(--font-bold);
}

@utility font-t3-extrabold {
font-family: "DMSans_700Bold";
font-family: var(--font-bold);
font-weight: 800;
}
Original file line number Diff line number Diff line change
Expand Up @@ -300,7 +300,7 @@ public final class T3ComposerEditorView: ExpoView, UITextViewDelegate {
skillText: "#a21caf",
fileTint: "#737373"
)
private var fontFamily = "DMSans_400Regular"
private var fontFamily = "DMSans-Regular"
private var fontSize: CGFloat = 14
private var lineHeight: CGFloat = 20
private var contentInsetVertical: CGFloat = 0
Expand Down Expand Up @@ -608,7 +608,7 @@ public final class T3ComposerEditorView: ExpoView, UITextViewDelegate {
iconImage: UIImage?,
style: ComposerChipStyle
) -> UIImage {
let font = UIFont(name: "DMSans_500Medium", size: max(12, fontSize - 2))
let font = UIFont(name: "DMSans-Medium", size: max(12, fontSize - 2))
?? UIFont.systemFont(ofSize: max(12, fontSize - 2), weight: .medium)
let fallbackIcon = UIImage(
systemName: iconName,
Expand Down
3 changes: 3 additions & 0 deletions apps/mobile/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
"scripts": {
"dev": "expo start --clear",
"dev:client": "APP_VARIANT=development expo start --dev-client --scheme t3code-dev --clear",
"dev:client:preview": "eas env:exec preview 'EXPO_NO_DOTENV=1 APP_VARIANT=preview expo start --dev-client --scheme t3code-preview --clear --lan'",
"start": "expo start",
"start:dev": "APP_VARIANT=development expo start",
"start:preview": "APP_VARIANT=preview expo start",
Expand Down Expand Up @@ -79,13 +80,15 @@
"expo-font": "~56.0.7",
"expo-glass-effect": "~56.0.4",
"expo-haptics": "~56.0.3",
"expo-image": "~56.0.11",
"expo-image-picker": "~56.0.18",
"expo-linking": "~56.0.14",
"expo-network": "~56.0.5",
"expo-notifications": "~56.0.18",
"expo-paste-input": "^0.1.15",
"expo-secure-store": "~56.0.4",
"expo-splash-screen": "~56.0.10",
"expo-sqlite": "~56.0.5",
"expo-symbols": "~56.0.6",
"expo-updates": "~56.0.19",
"expo-web-browser": "~56.0.5",
Expand Down
17 changes: 3 additions & 14 deletions apps/mobile/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,13 @@
import {
DMSans_400Regular,
DMSans_500Medium,
DMSans_700Bold,
useFonts,
} from "@expo-google-fonts/dm-sans";
import * as Linking from "expo-linking";
import * as SplashScreen from "expo-splash-screen";
import { useEffect } from "react";
import { StatusBar, useColorScheme } from "react-native";
import { GestureHandlerRootView } from "react-native-gesture-handler";
import { KeyboardProvider } from "react-native-keyboard-controller";
import { SafeAreaProvider } from "react-native-safe-area-context";
import { createStaticNavigation, DarkTheme, DefaultTheme } from "@react-navigation/native";

import { RegistryContext } from "@effect/atom-react";
import { useEffect } from "react";
import { CloudAuthProvider } from "./features/cloud/CloudAuthProvider";
import { AppearancePreferencesProvider } from "./features/settings/appearance/AppearancePreferencesProvider";
import { RootStack } from "./Stack";
Expand All @@ -29,17 +23,12 @@ const appLinking = {
const Navigation = createStaticNavigation(RootStack);

export default function App() {
const [fontsLoaded] = useFonts({
DMSans_400Regular,
DMSans_500Medium,
DMSans_700Bold,
});
const colorScheme = useColorScheme();
const statusBarBg = useThemeColor("--color-status-bar");

useEffect(() => {
if (fontsLoaded) SplashScreen.hide();
}, [fontsLoaded]);
SplashScreen.hide();
}, []);

return (
<RegistryContext.Provider value={appAtomRegistry}>
Expand Down
8 changes: 8 additions & 0 deletions apps/mobile/src/Stack.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ import { NewTaskDraftRouteScreen } from "./features/threads/NewTaskDraftRouteScr
import { NewTaskFlowProvider } from "./features/threads/new-task-flow-provider";
import { NewTaskRouteScreen } from "./features/threads/NewTaskRouteScreen";
import { SettingsAppearanceRouteScreen } from "./features/settings/SettingsAppearanceRouteScreen";
import { SettingsClientStorageRouteScreen } from "./features/settings/SettingsClientStorageRouteScreen";
import { SettingsAuthRouteScreen } from "./features/settings/SettingsAuthRouteScreen";
import { SettingsEnvironmentsRouteScreen } from "./features/settings/SettingsEnvironmentsRouteScreen";
import { SettingsRouteScreen } from "./features/settings/SettingsRouteScreen";
Expand Down Expand Up @@ -147,6 +148,13 @@ const SettingsSheetStack = createNativeStackNavigator({
title: "Appearance",
},
}),
SettingsClientStorage: createNativeStackScreen({
screen: SettingsClientStorageRouteScreen,
linking: "client-storage",
options: {
title: "Client Storage",
},
}),
SettingsAuth: createNativeStackScreen({
screen: SettingsAuthRouteScreen,
linking: "auth",
Expand Down
14 changes: 3 additions & 11 deletions apps/mobile/src/components/AppText.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import {
type TextInputProps as RNTextInputProps,
type TextProps as RNTextProps,
} from "react-native";
import { useThemeColor } from "../lib/useThemeColor";

import { cn } from "../lib/cn";

Expand All @@ -18,7 +17,7 @@ export function AppText({ className, ...props }: AppTextProps) {
return <RNText className={cn("font-sans text-foreground", className)} {...props} />;
}

export type AppTextInputProps = RNTextInputProps & {
export type AppTextInputProps = Omit<RNTextInputProps, "placeholderTextColor"> & {
readonly className?: string;
readonly ref?: React.Ref<RNTextInput>;
};
Expand All @@ -27,22 +26,15 @@ export type AppTextInputProps = RNTextInputProps & {
* Thin wrapper around RN TextInput with default input styling.
* Uses Uniwind className — no manual style parsing.
*/
export function AppTextInput({
className,
placeholderTextColor,
ref,
...props
}: AppTextInputProps) {
const placeholderColor = useThemeColor("--color-placeholder");

export function AppTextInput({ className, ref, ...props }: AppTextInputProps) {
return (
<RNTextInput
ref={ref}
className={cn(
"min-h-13.5 rounded-2xl border border-input-border bg-input px-3.5 py-3 font-sans text-base text-foreground",
className,
)}
placeholderTextColor={placeholderTextColor ?? placeholderColor}
placeholderTextColorClassName="accent-placeholder"
{...props}
/>
);
Expand Down
9 changes: 2 additions & 7 deletions apps/mobile/src/components/BrandMark.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,9 @@ export function BrandMark(props: { readonly compact?: boolean; readonly stageLab
/>
<View className="gap-1">
<View className="flex-row items-center gap-2">
<Text className="text-lg font-t3-bold text-foreground" style={{ letterSpacing: -0.4 }}>
T3 Code
</Text>
<Text className="text-lg font-t3-bold tracking-[-0.4px] text-foreground">T3 Code</Text>
<View className="rounded-full bg-subtle px-2 py-1">
<Text
className="text-3xs font-t3-bold uppercase text-foreground-muted"
style={{ letterSpacing: 1.1 }}
>
<Text className="text-3xs font-t3-bold tracking-[1.1px] uppercase text-foreground-muted">
{stageLabel}
</Text>
</View>
Expand Down
14 changes: 4 additions & 10 deletions apps/mobile/src/components/ComposerAttachmentStrip.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,14 +39,14 @@ export function ComposerAttachmentStrip(props: ComposerAttachmentStripProps) {
horizontal
showsHorizontalScrollIndicator={false}
keyboardShouldPersistTaps="always"
style={{ flexGrow: 0 }}
className="grow-0"
>
<View style={{ flexDirection: "row", gap: 10 }}>
<View className="flex-row gap-2.5">
{props.attachments.map((image) => (
<View
key={image.id}
className="relative"
style={{
position: "relative",
paddingTop: removeButtonGutter,
paddingRight: removeButtonGutter,
}}
Expand All @@ -66,16 +66,10 @@ export function ComposerAttachmentStrip(props: ComposerAttachmentStripProps) {
/>
</Pressable>
<Pressable
className="absolute h-[22px] w-[22px] items-center justify-center rounded-[11px] bg-black/55"
style={{
position: "absolute",
top: removeButtonPlacement === "gutter" ? 0 : 4,
right: removeButtonPlacement === "gutter" ? 0 : 4,
width: 22,
height: 22,
borderRadius: 11,
backgroundColor: "rgba(0,0,0,0.55)",
alignItems: "center",
justifyContent: "center",
}}
hitSlop={6}
onPress={() => props.onRemove(image.id)}
Expand Down
6 changes: 2 additions & 4 deletions apps/mobile/src/components/ComposerToolbarTrigger.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,9 @@ export function ComposerToolbarRow(props: {
}) {
return (
<View
className="flex-row items-center gap-1.5"
style={[
{
alignItems: "center",
flexDirection: "row",
gap: 6,
paddingBottom: props.paddingBottom ?? 8,
paddingHorizontal: props.paddingHorizontal ?? 6,
paddingTop: props.paddingTop ?? 8,
Expand Down Expand Up @@ -89,7 +87,7 @@ export function ComposerToolbarScroller(props: {
}, []);

return (
<View className="min-w-0 flex-1" style={{ position: "relative" }}>
<View className="relative min-w-0 flex-1">
<ScrollView
horizontal
keyboardShouldPersistTaps="always"
Expand Down
Loading
Loading