import { useState } from "react" import { View, Text, TextInput, TouchableOpacity, ScrollView, StyleSheet, useColorScheme, ActivityIndicator, Alert, } from "react-native" import { router } from "expo-router" import { Ionicons } from "@expo/vector-icons" import { useConnections } from "../../src/stores/connections" import type { ConnectionType } from "../../src/lib/types" export default function AddConnectionScreen() { const colorScheme = useColorScheme() const isDark = colorScheme === "dark" const { addConnection, testConnection } = useConnections() const [mode, setMode] = useState<"quick" | "advanced">("quick") const [type, setType] = useState("local") const [name, setName] = useState("") const [ip, setIp] = useState("") const [port, setPort] = useState("4096") const [url, setUrl] = useState("") const [directory, setDirectory] = useState("") const [username, setUsername] = useState("") const [password, setPassword] = useState("") const [isConnecting, setIsConnecting] = useState(false) const buildUrl = () => { if (mode === "advanced") return url.trim() const raw = ip.trim() if (!raw) return "" // Be forgiving about pasted values: a full URL, a host:port, or a // host with a trailing path. Extract scheme, host, and port so we // never produce "http://http://host:4096:4096". const schemeMatch = raw.match(/^(https?):\/\//i) const scheme = schemeMatch ? schemeMatch[1].toLowerCase() : "http" let rest = raw.replace(/^https?:\/\//i, "") rest = rest.split("/")[0] // drop any path/query let host = rest let pastedPort = "" const lastColon = rest.lastIndexOf(":") // Only treat trailing ":NNNN" as a port (ignore IPv6 colons / bare host) if (lastColon > -1 && /^\d+$/.test(rest.slice(lastColon + 1))) { host = rest.slice(0, lastColon) pastedPort = rest.slice(lastColon + 1) } const finalPort = pastedPort || port.trim() || "4096" return `${scheme}://${host}:${finalPort}` } const handleQuickConnect = async () => { const serverUrl = buildUrl() if (!serverUrl) { Alert.alert("Error", "Please enter your computer's IP address") return } setIsConnecting(true) // Test connection first const result = await testConnection( { id: "", name: name || "My Server", type: "local", url: serverUrl, username: username.trim() || undefined, }, password || undefined, ) if (result.ok) { // Save and go back await addConnection( { name: name.trim() || "My Server", type: "local", url: serverUrl, username: username.trim() || undefined, }, password || undefined, ) setIsConnecting(false) router.back() } else { setIsConnecting(false) Alert.alert( "Connection Failed", `Could not connect to ${serverUrl}\n\n${result.error || "Unknown error"}\n\nMake sure:\n1. OpenCode is running: opencode serve --hostname 0.0.0.0\n2. You're on the same network (or Tailscale is connected)\n3. The address is correct (use the IP for tailnet if MagicDNS isn't enabled)`, [{ text: "OK" }], ) } } const handleAdvancedSave = async () => { if (!name.trim()) { Alert.alert("Error", "Please enter a connection name") return } if (!url.trim()) { Alert.alert("Error", "Please enter a server URL") return } setIsConnecting(true) await addConnection( { name: name.trim(), type, url: url.trim(), directory: directory.trim() || undefined, username: username.trim() || undefined, }, password || undefined, ) setIsConnecting(false) router.back() } // Quick connect mode - simplified if (mode === "quick") { return ( Connect to OpenCode Enter your computer's IP address to connect {/* IP Address */} IP Address : {/* Optional name */} Name (optional) {/* Password if needed */} Password (if set on server) {/* Connect button */} {isConnecting ? ( ) : ( <> Connect )} {/* Help text */} How to find your IP: On your Mac, run:{"\n"} ipconfig getifaddr en0 Tailscale examples:{"\n"} http://100.64.12.34:4096 {"\n"} http://my-mac.tailnet.ts.net:4096 Use http:// for local and Tailscale addresses unless TLS is configured, then use https://. Make sure OpenCode is running:{"\n"} opencode serve --hostname 0.0.0.0 {/* Advanced mode link */} setMode("advanced")}> Advanced options (tunnels, cloud, auth) ) } // Advanced mode - full options return ( setMode("quick")}> Simple mode {/* Connection Type */} Connection Type {[ { type: "local" as const, label: "Local", icon: "wifi" as const }, { type: "tunnel" as const, label: "Tunnel", icon: "globe" as const }, { type: "cloud" as const, label: "Cloud", icon: "cloud" as const }, ].map((t) => ( setType(t.type)} > {t.label} ))} {/* Name */} Name {/* URL */} Server URL Local/Tailscale examples: http://100.64.12.34:4096 or{" "} http://my-mac.tailnet.ts.net:4096. Use https:// only when TLS is configured. {/* Directory */} Project Directory (Optional) Leave empty to use the server's current directory, or specify a path to work in a different folder. {/* Auth */} Authentication Username Password {/* Save */} {isConnecting ? ( ) : ( Save Connection )} ) } const styles = StyleSheet.create({ container: { flex: 1, backgroundColor: "#ffffff", }, containerDark: { backgroundColor: "#0a0a0a", }, content: { padding: 16, paddingBottom: 32, }, // Quick connect styles quickHeader: { alignItems: "center", paddingVertical: 24, }, quickTitle: { fontSize: 24, fontWeight: "700", color: "#0a0a0a", marginTop: 16, }, quickSubtitle: { fontSize: 15, color: "#666666", marginTop: 8, textAlign: "center", }, ipRow: { flexDirection: "row", alignItems: "center", gap: 8, }, ipInput: { flex: 1, }, ipColon: { fontSize: 20, fontWeight: "600", color: "#0a0a0a", }, portInput: { width: 80, }, connectButton: { flexDirection: "row", alignItems: "center", justifyContent: "center", gap: 8, padding: 16, borderRadius: 12, backgroundColor: "#0a0a0a", marginTop: 24, }, connectButtonDark: { backgroundColor: "#ffffff", }, connectButtonText: { fontSize: 18, fontWeight: "600", color: "#ffffff", }, connectButtonTextDark: { color: "#0a0a0a", }, helpBox: { backgroundColor: "#f5f5f5", borderRadius: 12, padding: 16, marginTop: 24, }, helpBoxDark: { backgroundColor: "#1a1a1a", }, helpTitle: { fontSize: 14, fontWeight: "600", color: "#0a0a0a", marginBottom: 8, }, helpText: { fontSize: 13, color: "#666666", lineHeight: 20, }, code: { fontFamily: "monospace", backgroundColor: "#e5e5e5", paddingHorizontal: 4, borderRadius: 4, }, advancedLink: { flexDirection: "row", alignItems: "center", justifyContent: "center", gap: 4, paddingVertical: 16, marginTop: 16, }, advancedLinkText: { fontSize: 14, color: "#666666", }, backToQuick: { flexDirection: "row", alignItems: "center", gap: 4, paddingVertical: 8, }, backToQuickText: { fontSize: 14, color: "#666666", }, // Original styles label: { fontSize: 14, fontWeight: "600", color: "#0a0a0a", marginTop: 16, marginBottom: 8, }, labelDark: { color: "#ffffff", }, typeContainer: { flexDirection: "row", gap: 8, }, typeOption: { flex: 1, flexDirection: "row", alignItems: "center", justifyContent: "center", padding: 12, borderRadius: 8, backgroundColor: "#f5f5f5", gap: 6, }, typeOptionDark: { backgroundColor: "#1a1a1a", }, typeOptionSelected: { backgroundColor: "#0a0a0a", }, typeOptionSelectedDark: { backgroundColor: "#ffffff", }, typeLabel: { fontSize: 13, fontWeight: "500", color: "#666666", }, textDark: { color: "#ffffff", }, typeLabelSelected: { color: "#ffffff", }, typeLabelSelectedDark: { color: "#0a0a0a", }, hint: { fontSize: 13, color: "#666666", marginTop: 8, }, hintDark: { color: "#888888", }, input: { backgroundColor: "#f5f5f5", borderRadius: 8, paddingHorizontal: 16, paddingVertical: 12, fontSize: 16, color: "#0a0a0a", }, inputDark: { backgroundColor: "#1a1a1a", color: "#ffffff", }, sectionTitle: { fontSize: 18, fontWeight: "600", color: "#0a0a0a", marginTop: 32, marginBottom: 8, }, })