update to build proc

This commit is contained in:
Ryan Vogel
2026-03-31 13:58:57 -04:00
parent 28aebb2772
commit 776e61d1ec
10 changed files with 707 additions and 89 deletions

View File

@@ -37,8 +37,9 @@ import * as FileSystem from "expo-file-system/legacy"
import { fetch as expoFetch } from "expo/fetch"
import { buildPermissionCardModel } from "@/lib/pending-permissions"
import { unregisterRelayDevice } from "@/lib/relay-client"
import { useMdnsDiscovery } from "@/hooks/use-mdns-discovery"
import { useMonitoring, type MonitorJob, type PermissionDecision } from "@/hooks/use-monitoring"
import { looksLikeLocalHost, useServerSessions } from "@/hooks/use-server-sessions"
import { DEFAULT_RELAY_URL, looksLikeLocalHost, useServerSessions } from "@/hooks/use-server-sessions"
import { ensureNotificationPermissions, getDevicePushToken } from "@/notifications/monitoring-notifications"
const CONTROL_HEIGHT = 86
@@ -229,6 +230,20 @@ function formatSessionUpdated(updatedMs: number): string {
}
}
function formatWorkingDirectory(directory?: string): string {
if (!directory) return "Not available"
if (directory.startsWith("/Users/")) {
const segments = directory.split("/")
if (segments.length >= 4) {
const tail = segments.slice(3).join("/")
return tail.length > 0 ? `~/${tail}` : "~"
}
}
return directory
}
type DropdownMode = "none" | "server" | "session"
type Pair = {
@@ -638,10 +653,17 @@ export default function DictationScreen() {
findServerForSession,
} = useServerSessions()
const { discoveredServers, discoveryStatus, discoveryError, discoveryAvailable, refreshDiscovery } = useMdnsDiscovery(
{
enabled: onboardingComplete && localNetworkPermissionState !== "denied",
},
)
const {
beginMonitoring,
activePermissionRequest,
devicePushToken,
latestAssistantContext,
latestAssistantResponse,
monitorJob,
monitorStatus,
@@ -1755,7 +1777,29 @@ export default function DictationScreen() {
const agentStateText = hasAssistantResponse ? latestAssistantResponse : "Waiting for agent…"
const shouldShowSend = hasCompletedSession && hasTranscript && !hasPendingPermission
const activeServer = servers.find((s) => s.id === activeServerId) ?? null
const discoveredServerOptions = useMemo(() => {
const saved = new Set(servers.map((server) => server.url.replace(/\/+$/, "")))
return discoveredServers.filter((server) => !saved.has(server.url.replace(/\/+$/, "")))
}, [discoveredServers, servers])
const discoveredServerEmptyLabel =
discoveryStatus === "error"
? "Unable to discover local servers"
: discoveryStatus === "scanning"
? "Scanning local network..."
: "No local servers found"
const activeSession = activeServer?.sessions.find((s) => s.id === activeSessionId) ?? null
let currentSessionModelLabel = "Not available"
if (latestAssistantContext?.modelID) {
currentSessionModelLabel = latestAssistantContext.modelID
if (latestAssistantContext.providerID) {
currentSessionModelLabel = `${latestAssistantContext.providerID}/${latestAssistantContext.modelID}`
}
}
const currentSessionDirectory = latestAssistantContext?.workingDirectory ?? activeSession?.directory
const currentSessionUpdated = activeSession ? formatSessionUpdated(activeSession.updated) : ""
const sessionList = activeSession
? (activeServer?.sessions ?? []).filter((session) => session.id !== activeSession.id)
: (activeServer?.sessions ?? [])
const canSendToSession = !!activeServer && activeServer.status === "online" && !!activeSession
const isReplyingToActivePermission =
activePermissionRequest !== null && respondingPermissionID === activePermissionRequest.id
@@ -1956,8 +2000,8 @@ export default function DictationScreen() {
],
}))
const menuRows =
effectiveDropdownMode === "server" ? Math.max(servers.length, 1) : Math.max(activeServer?.sessions.length ?? 0, 1)
const serverMenuRows = 2 + Math.max(servers.length, 1) + Math.max(discoveredServerOptions.length, 1)
const menuRows = effectiveDropdownMode === "server" ? serverMenuRows : Math.max(activeServer?.sessions.length ?? 0, 1)
const expandedRowsHeight = Math.min(menuRows, DROPDOWN_VISIBLE_ROWS) * 42
const dropdownFooterExtraHeight =
effectiveDropdownMode === "server"
@@ -2069,10 +2113,11 @@ export default function DictationScreen() {
}
if (next === "server") {
refreshAllServerHealth()
refreshDiscovery()
}
return next
})
}, [refreshAllServerHealth])
}, [refreshAllServerHealth, refreshDiscovery])
const toggleSessionMenu = useCallback(() => {
if (!activeServer || activeServer.status !== "online") return
@@ -2158,6 +2203,20 @@ export default function DictationScreen() {
[devicePushToken, removeServer, serversRef],
)
const handleConnectDiscoveredServer = useCallback(
(url: string) => {
const ok = addServer(url, DEFAULT_RELAY_URL, "")
if (!ok) {
Alert.alert("Could not add server", "The discovered server could not be added. Try scanning the QR code.")
return
}
setDropdownMode("none")
void Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success).catch(() => {})
},
[addServer],
)
const handleStartScan = useCallback(async () => {
scanLockRef.current = false
const current =
@@ -2467,9 +2526,9 @@ export default function DictationScreen() {
}
const onboardingSteps = [
{
title: "Allow mic access.",
title: "Microphone access.",
body: "Control only listens while you hold the record button.",
primaryLabel: microphonePermissionState === "pending" ? "Requesting microphone..." : "Allow microphone",
primaryLabel: microphonePermissionState === "pending" ? "Requesting microphone access..." : "Continue",
primaryDisabled: microphonePermissionState === "pending",
secondaryLabel: "Continue without granting",
visualTag: "MIC",
@@ -2480,7 +2539,7 @@ export default function DictationScreen() {
{
title: "Turn on notifications.",
body: "Get alerts when your OpenCode run finishes, fails, or needs your attention.",
primaryLabel: notificationPermissionState === "pending" ? "Requesting notifications..." : "Allow notifications",
primaryLabel: notificationPermissionState === "pending" ? "Requesting notification access..." : "Continue",
primaryDisabled: notificationPermissionState === "pending",
secondaryLabel: "Continue without granting",
visualTag: "PUSH",
@@ -2489,9 +2548,9 @@ export default function DictationScreen() {
visualTagStyle: styles.onboardingVisualTagNotifications,
},
{
title: "Enable local network.",
title: "Local network access.",
body: "This lets Control discover your machine on the same network.",
primaryLabel: localNetworkPermissionState === "pending" ? "Requesting local network..." : "Allow local network",
primaryLabel: localNetworkPermissionState === "pending" ? "Requesting local network access..." : "Continue",
primaryDisabled: localNetworkPermissionState === "pending",
secondaryLabel: "Continue without granting",
visualTag: "LAN",
@@ -2501,10 +2560,10 @@ export default function DictationScreen() {
},
{
title: "Pair your computer.",
body: "Start `opencode serve` on your computer, then scan the QR code to pair.",
primaryLabel: "Scan OpenCode QR",
body: "Start `opencode serve --mdns` on your computer. Control can discover nearby servers automatically, or you can scan a QR code.",
primaryLabel: "Scan OpenCode QR (optional)",
primaryDisabled: false,
secondaryLabel: "I will do this later",
secondaryLabel: "Skip and use discovery",
visualTag: "PAIR",
visualSurfaceStyle: styles.onboardingVisualSurfacePair,
visualOrbStyle: styles.onboardingVisualOrbPair,
@@ -2705,52 +2764,133 @@ export default function DictationScreen() {
bounces={false}
>
{effectiveDropdownMode === "server" ? (
servers.length === 0 ? (
<Text style={styles.serverEmptyText}>No servers yet</Text>
) : (
servers.map((server) => (
<Pressable
key={server.id}
onPress={() => handleSelectServer(server.id)}
style={({ pressed }) => [styles.serverRow, pressed && styles.serverRowPressed]}
>
<View
style={[
styles.serverStatusDot,
server.status === "online" ? styles.serverStatusActive : styles.serverStatusOffline,
]}
/>
<Text style={styles.serverNameText}>{server.name}</Text>
<Pressable onPress={() => handleDeleteServer(server.id)} hitSlop={8}>
<Text style={styles.serverDeleteIcon}></Text>
<>
<Text style={styles.serverGroupLabel}>Saved:</Text>
{servers.length === 0 ? (
<Text style={[styles.serverEmptyText, styles.serverGroupEmptyText]}>No saved servers</Text>
) : (
servers.map((server) => (
<Pressable
key={server.id}
onPress={() => handleSelectServer(server.id)}
style={({ pressed }) => [styles.serverRow, pressed && styles.serverRowPressed]}
>
<View
style={[
styles.serverStatusDot,
server.status === "online" ? styles.serverStatusActive : styles.serverStatusOffline,
]}
/>
<Text style={styles.serverNameText}>{server.name}</Text>
<Pressable onPress={() => handleDeleteServer(server.id)} hitSlop={8}>
<Text style={styles.serverDeleteIcon}></Text>
</Pressable>
</Pressable>
</Pressable>
))
)
))
)}
<View style={styles.serverGroupHeaderRow}>
<Text style={styles.serverGroupLabel}>Discovered:</Text>
{discoveryStatus === "scanning" ? <ActivityIndicator size="small" color="#8790A3" /> : null}
</View>
{!discoveryAvailable ? (
<Text style={[styles.serverEmptyText, styles.serverGroupEmptyText]}>
Discovery unavailable in this build
</Text>
) : discoveredServerOptions.length === 0 ? (
<Text style={[styles.serverEmptyText, styles.serverGroupEmptyText]}>
{discoveredServerEmptyLabel}
</Text>
) : (
discoveredServerOptions.map((server, index) => (
<Pressable
key={server.id}
onPress={() => handleConnectDiscoveredServer(server.url)}
style={({ pressed }) => [
styles.serverRow,
index === discoveredServerOptions.length - 1 && styles.serverRowLast,
pressed && styles.serverRowPressed,
]}
>
<View style={[styles.serverStatusDot, styles.serverStatusChecking]} />
<View style={styles.discoveredServerCopy}>
<Text style={styles.serverNameText} numberOfLines={1}>
{server.name}
</Text>
<Text style={styles.discoveredServerMeta} numberOfLines={1} ellipsizeMode="middle">
{server.url}
</Text>
</View>
<Text style={styles.discoveredServerAction}>Connect</Text>
</Pressable>
))
)}
{discoveryStatus === "error" && discoveryError ? (
<Text style={styles.discoveryErrorText} numberOfLines={1} ellipsizeMode="tail">
{discoveryError}
</Text>
) : null}
</>
) : activeServer ? (
activeServer.sessions.length === 0 ? (
activeServer.sessionsLoading ? null : (
<Text style={styles.serverEmptyText}>No sessions available</Text>
)
) : (
activeServer.sessions.map((session, index) => (
<Pressable
key={session.id}
onPress={() => handleSelectSession(session.id)}
style={({ pressed }) => [
styles.serverRow,
index === activeServer.sessions.length - 1 && styles.serverRowLast,
pressed && styles.serverRowPressed,
]}
>
<View style={[styles.serverStatusDot, styles.serverStatusActive]} />
<Text style={styles.serverNameText} numberOfLines={1}>
{session.title}
<>
{activeSession ? (
<>
<View style={styles.currentSessionSummary}>
<Text style={styles.currentSessionLabel}>Current session</Text>
<View style={styles.currentSessionMetaRow}>
<Text style={styles.currentSessionMetaKey}>Working dir</Text>
<Text style={styles.currentSessionMetaValue} numberOfLines={1} ellipsizeMode="middle">
{formatWorkingDirectory(currentSessionDirectory)}
</Text>
</View>
<View style={styles.currentSessionMetaRow}>
<Text style={styles.currentSessionMetaKey}>Model</Text>
<Text style={styles.currentSessionMetaValue} numberOfLines={1} ellipsizeMode="middle">
{currentSessionModelLabel}
</Text>
</View>
<View style={styles.currentSessionMetaRow}>
<Text style={styles.currentSessionMetaKey}>Updated</Text>
<Text style={styles.currentSessionMetaValue}>{currentSessionUpdated || "Just now"}</Text>
</View>
</View>
<View style={styles.currentSessionDivider} />
</>
) : null}
{sessionList.length === 0 ? (
activeServer.sessionsLoading ? null : (
<Text style={styles.serverEmptyText}>
{activeSession ? "No other sessions available" : "No sessions available"}
</Text>
<Text style={styles.sessionUpdatedText}>{formatSessionUpdated(session.updated)}</Text>
</Pressable>
))
)
)
) : (
sessionList.map((session, index) => (
<Pressable
key={session.id}
onPress={() => handleSelectSession(session.id)}
style={({ pressed }) => [
styles.serverRow,
index === sessionList.length - 1 && styles.serverRowLast,
pressed && styles.serverRowPressed,
]}
>
<View style={[styles.serverStatusDot, styles.serverStatusActive]} />
<Text style={styles.serverNameText} numberOfLines={1}>
{session.title}
</Text>
<Text style={styles.sessionUpdatedText}>{formatSessionUpdated(session.updated)}</Text>
</Pressable>
))
)}
</>
) : (
<Text style={styles.serverEmptyText}>Select a server first</Text>
)}
@@ -3756,12 +3896,68 @@ const styles = StyleSheet.create({
dropdownListContent: {
paddingBottom: 2,
},
currentSessionSummary: {
paddingHorizontal: 4,
paddingTop: 2,
paddingBottom: 8,
gap: 5,
},
currentSessionLabel: {
color: "#A3ACC0",
fontSize: 12,
fontWeight: "700",
letterSpacing: 0.4,
textTransform: "uppercase",
},
currentSessionMetaRow: {
flexDirection: "row",
alignItems: "center",
gap: 8,
},
currentSessionMetaKey: {
width: 74,
color: "#7C8599",
fontSize: 12,
fontWeight: "600",
},
currentSessionMetaValue: {
flex: 1,
color: "#D7DCE6",
fontSize: 13,
fontWeight: "500",
},
currentSessionDivider: {
width: "100%",
height: 1,
backgroundColor: "#222733",
marginBottom: 4,
},
serverGroupHeaderRow: {
flexDirection: "row",
alignItems: "center",
justifyContent: "space-between",
marginTop: 8,
},
serverGroupLabel: {
color: "#8F97AA",
fontSize: 12,
fontWeight: "700",
letterSpacing: 0.4,
textTransform: "uppercase",
paddingHorizontal: 4,
paddingVertical: 4,
},
serverEmptyText: {
color: "#6F7686",
fontSize: 13,
fontSize: 14,
textAlign: "center",
paddingVertical: 10,
},
serverGroupEmptyText: {
textAlign: "left",
paddingHorizontal: 4,
paddingVertical: 8,
},
serverRow: {
flexDirection: "row",
alignItems: "center",
@@ -3794,15 +3990,36 @@ const styles = StyleSheet.create({
serverNameText: {
flex: 1,
color: "#D6DAE4",
fontSize: 14,
fontSize: 16,
fontWeight: "500",
},
sessionUpdatedText: {
color: "#8E96A8",
fontSize: 12,
fontSize: 14,
fontWeight: "500",
marginLeft: 8,
},
discoveredServerCopy: {
flex: 1,
gap: 2,
},
discoveredServerMeta: {
color: "#818A9E",
fontSize: 12,
fontWeight: "500",
},
discoveredServerAction: {
color: "#B9C2D8",
fontSize: 13,
fontWeight: "700",
},
discoveryErrorText: {
color: "#7D8598",
fontSize: 11,
fontWeight: "500",
paddingHorizontal: 4,
paddingTop: 4,
},
serverDeleteIcon: {
color: "#8C93A3",
fontSize: 15,
@@ -3845,7 +4062,7 @@ const styles = StyleSheet.create({
sessionMenuActionText: {
flex: 1,
color: "#D6DAE4",
fontSize: 14,
fontSize: 16,
fontWeight: "500",
},
statusLeft: {