Remove global settings test form

This commit is contained in:
Mohamed Boudra
2025-11-26 19:09:19 +01:00
parent 4f00e07284
commit 99b055d2f9
2 changed files with 7 additions and 285 deletions
+3 -282
View File
@@ -1,4 +1,4 @@
import { useState, useEffect, useRef, useCallback, useMemo } from "react";
import { useState, useEffect, useRef, useCallback } from "react";
import type { MutableRefObject } from "react";
import {
View,
@@ -59,31 +59,6 @@ const styles = StyleSheet.create((theme) => ({
fontWeight: theme.fontWeight.semibold,
marginBottom: theme.spacing[4],
},
hostSelectorRow: {
flexDirection: "row",
flexWrap: "wrap",
gap: theme.spacing[2],
marginBottom: theme.spacing[3],
},
hostSelectorChip: {
paddingVertical: theme.spacing[1],
paddingHorizontal: theme.spacing[3],
borderRadius: theme.borderRadius.full,
backgroundColor: theme.colors.card,
borderWidth: theme.borderWidth[1],
borderColor: theme.colors.border,
},
hostSelectorChipSelected: {
backgroundColor: theme.colors.foreground,
borderColor: theme.colors.foreground,
},
hostSelectorChipText: {
color: theme.colors.mutedForeground,
fontSize: theme.fontSize.xs,
},
hostSelectorChipTextSelected: {
color: theme.colors.background,
},
label: {
color: theme.colors.mutedForeground,
fontSize: theme.fontSize.sm,
@@ -96,51 +71,6 @@ const styles = StyleSheet.create((theme) => ({
borderRadius: theme.borderRadius.lg,
marginBottom: theme.spacing[2],
},
inputDisabled: {
opacity: theme.opacity[50],
},
helperText: {
color: theme.colors.mutedForeground,
fontSize: theme.fontSize.xs,
marginBottom: theme.spacing[3],
},
testButton: {
padding: theme.spacing[3],
borderRadius: theme.borderRadius.lg,
marginBottom: theme.spacing[3],
flexDirection: "row",
alignItems: "center",
justifyContent: "center",
backgroundColor: theme.colors.palette.blue[600],
},
testButtonDisabled: {
backgroundColor: theme.colors.muted,
},
testButtonText: {
color: theme.colors.palette.white,
fontWeight: theme.fontWeight.semibold,
marginLeft: theme.spacing[2],
},
testResultSuccess: {
padding: theme.spacing[3],
borderRadius: theme.borderRadius.lg,
backgroundColor: theme.colors.palette.green[900],
borderWidth: theme.borderWidth[1],
borderColor: theme.colors.palette.green[600],
},
testResultError: {
padding: theme.spacing[3],
borderRadius: theme.borderRadius.lg,
backgroundColor: theme.colors.palette.red[900],
borderWidth: theme.borderWidth[1],
borderColor: theme.colors.palette.red[600],
},
testResultTextSuccess: {
color: theme.colors.palette.green[200],
},
testResultTextError: {
color: theme.colors.palette.red[200],
},
settingCard: {
backgroundColor: theme.colors.card,
borderRadius: theme.borderRadius.lg,
@@ -350,53 +280,16 @@ export default function SettingsScreen() {
const { settings, isLoading: settingsLoading, updateSettings, resetSettings } = useAppSettings();
const { daemons, isLoading: daemonLoading, addDaemon, updateDaemon, removeDaemon } = useDaemonRegistry();
const { connectionStates, updateConnectionStatus } = useDaemonConnections();
const [selectedDaemonId, setSelectedDaemonId] = useState<string | null>(null);
const selectedDaemon = useMemo(() => {
if (!selectedDaemonId) {
return null;
}
return daemons.find((daemon) => daemon.id === selectedDaemonId) ?? null;
}, [daemons, selectedDaemonId]);
const selectedDaemonSession = useSessionForServer(selectedDaemonId);
const selectedDaemonConnection = selectedDaemonId ? connectionStates.get(selectedDaemonId) : null;
const selectedDaemonStatusLabel = selectedDaemonConnection ? formatConnectionStatus(selectedDaemonConnection.status) : null;
const [serverUrl, setServerUrl] = useState(selectedDaemon?.wsUrl ?? "");
const [useSpeaker, setUseSpeaker] = useState(settings.useSpeaker);
const [keepScreenOn, setKeepScreenOn] = useState(settings.keepScreenOn);
const [theme, setTheme] = useState<"dark" | "light" | "auto">(settings.theme);
const [hasChanges, setHasChanges] = useState(false);
const [isTesting, setIsTesting] = useState(false);
const [testResult, setTestResult] = useState<{
success: boolean;
message: string;
} | null>(null);
const [isDaemonFormVisible, setIsDaemonFormVisible] = useState(false);
const [daemonForm, setDaemonForm] = useState<{ id: string | null; label: string; wsUrl: string }>({ id: null, label: "", wsUrl: "" });
const [isSavingDaemon, setIsSavingDaemon] = useState(false);
const [daemonTestStates, setDaemonTestStates] = useState<Map<string, DaemonTestState>>(() => new Map());
const isLoading = settingsLoading || daemonLoading;
const baselineServerUrl = selectedDaemon?.wsUrl ?? "";
const isMountedRef = useRef(true);
const isServerConfigLocked = Boolean(selectedDaemon && !selectedDaemonSession);
const serverDescriptionText = selectedDaemon
? `${selectedDaemon.label}${selectedDaemonStatusLabel ? ` - ${selectedDaemonStatusLabel}` : ""}${
isServerConfigLocked ? " - Session unavailable" : ""
}`
: "Add a host to configure its server URL.";
const serverHelperText = isServerConfigLocked
? `Connect to ${selectedDaemon?.label ?? "this host"} to edit its server URL.`
: "Must be a valid WebSocket URL (ws:// or wss://)";
useEffect(() => {
if (daemons.length === 0) {
setSelectedDaemonId(null);
return;
}
if (!selectedDaemonId || !daemons.some((daemon) => daemon.id === selectedDaemonId)) {
setSelectedDaemonId(daemons[0].id);
}
}, [daemons, selectedDaemonId]);
useEffect(() => {
return () => {
@@ -510,10 +403,8 @@ export default function SettingsScreen() {
};
if (daemonForm.id) {
await updateDaemon(daemonForm.id, payload);
setSelectedDaemonId(daemonForm.id);
} else {
const created = await addDaemon(payload);
setSelectedDaemonId(created.id);
await addDaemon(payload);
}
handleCloseDaemonForm();
} catch (error) {
@@ -586,19 +477,14 @@ export default function SettingsScreen() {
setTheme(settings.theme);
}, [settings]);
useEffect(() => {
setServerUrl(selectedDaemon?.wsUrl ?? "");
}, [selectedDaemon?.wsUrl]);
// Track changes
useEffect(() => {
const changed =
serverUrl !== baselineServerUrl ||
useSpeaker !== settings.useSpeaker ||
keepScreenOn !== settings.keepScreenOn ||
theme !== settings.theme;
setHasChanges(changed);
}, [serverUrl, baselineServerUrl, useSpeaker, keepScreenOn, theme, settings]);
}, [useSpeaker, keepScreenOn, theme, settings]);
function validateServerUrl(url: string): boolean {
try {
@@ -609,49 +495,14 @@ export default function SettingsScreen() {
}
}
function deriveDaemonLabel(url: string): string {
try {
const parsed = new URL(url);
return parsed.hostname || "Host";
} catch {
return "Host";
}
}
async function handleSave() {
// Validate server URL
if (!validateServerUrl(serverUrl)) {
Alert.alert(
"Invalid URL",
"Server URL must be a valid WebSocket URL (ws:// or wss://)",
[{ text: "OK" }]
);
return;
}
try {
const trimmedUrl = serverUrl.trim();
await updateSettings({
useSpeaker,
keepScreenOn,
theme,
});
if (selectedDaemon) {
await updateDaemon(selectedDaemon.id, {
wsUrl: trimmedUrl,
label: selectedDaemon.label || deriveDaemonLabel(trimmedUrl),
});
setSelectedDaemonId(selectedDaemon.id);
} else {
const created = await addDaemon({
label: deriveDaemonLabel(trimmedUrl),
wsUrl: trimmedUrl,
});
setSelectedDaemonId(created.id);
}
Alert.alert(
"Settings Saved",
"Your settings have been saved successfully.",
@@ -700,48 +551,6 @@ export default function SettingsScreen() {
const restartConfirmationMessage =
"This will immediately stop the Voice Dev backend process. The app will disconnect until it restarts.";
async function handleTestConnection() {
if (isServerConfigLocked) {
Alert.alert(
"Session unavailable",
`${selectedDaemon?.label ?? "This host"} is not connected. Connect to it before testing the URL.`
);
return;
}
if (!validateServerUrl(serverUrl)) {
Alert.alert(
"Invalid URL",
"Server URL must be a valid WebSocket URL (ws:// or wss://)",
[{ text: "OK" }]
);
return;
}
setIsTesting(true);
setTestResult(null);
try {
await testServerConnection(serverUrl);
setTestResult({
success: true,
message: "Connection successful",
});
} catch (error) {
const message =
error instanceof Error
? error.message
: "Connection failed - check URL and network";
setTestResult({
success: false,
message,
});
} finally {
setIsTesting(false);
}
}
if (isLoading) {
return (
<View style={styles.loadingContainer}>
@@ -756,94 +565,6 @@ export default function SettingsScreen() {
<ScrollView style={styles.scrollView}>
<View style={styles.content}>
{/* Server Configuration */}
<View style={styles.section}>
<Text style={styles.sectionTitle}>Server Configuration</Text>
{daemons.length > 0 ? (
<View style={styles.hostSelectorRow}>
{daemons.map((daemon) => {
const isSelected = daemon.id === selectedDaemonId;
return (
<Pressable
key={daemon.id}
style={[styles.hostSelectorChip, isSelected && styles.hostSelectorChipSelected]}
onPress={() => setSelectedDaemonId(daemon.id)}
>
<Text
style={[styles.hostSelectorChipText, isSelected && styles.hostSelectorChipTextSelected]}
numberOfLines={1}
>
{daemon.label}
</Text>
</Pressable>
);
})}
</View>
) : null}
<Text style={styles.helperText}>{serverDescriptionText}</Text>
<Text style={styles.label}>WebSocket URL</Text>
<TextInput
style={[styles.input, isServerConfigLocked && styles.inputDisabled]}
placeholder="wss://example.com/ws"
placeholderTextColor={defaultTheme.colors.mutedForeground}
value={serverUrl}
onChangeText={(text) => {
setServerUrl(text);
setTestResult(null);
}}
autoCapitalize="none"
autoCorrect={false}
keyboardType="url"
editable={!isServerConfigLocked}
selectTextOnFocus={!isServerConfigLocked}
/>
<Text style={styles.helperText}>
{serverHelperText}
</Text>
{/* Test Connection Button */}
<Pressable
onPress={handleTestConnection}
disabled={isTesting || !validateServerUrl(serverUrl)}
style={[
styles.testButton,
(isTesting || !validateServerUrl(serverUrl) || isServerConfigLocked) &&
styles.testButtonDisabled,
]}
>
{isTesting ? (
<>
<ActivityIndicator size="small" color="#fff" />
<Text style={styles.testButtonText}>Testing...</Text>
</>
) : (
<Text style={styles.testButtonText}>Test Connection</Text>
)}
</Pressable>
{/* Test Result */}
{testResult && (
<View
style={
testResult.success
? styles.testResultSuccess
: styles.testResultError
}
>
<Text
style={
testResult.success
? styles.testResultTextSuccess
: styles.testResultTextError
}
>
{testResult.message}
</Text>
</View>
)}
</View>
{/* Host Management */}
<View style={styles.section}>
<Text style={styles.sectionTitle}>Hosts</Text>
+4 -3
View File
@@ -39,7 +39,8 @@ The multi-daemon infrastructure is in place: session directory with daemon-scope
- Create/import modal now requires explicit host selection (no first entry default) and `useAggregatedAgents` no longer fabricates a host id via `connectionStates.keys().next()`; verified with `npm run typecheck --workspace=@paseo/app`.
### 3. Simplify Settings Screen
- [ ] Remove the standalone "Test Connection" form/URL input at the top of settings.
- [x] Remove the standalone "Test Connection" form/URL input at the top of settings.
- Removed the global host selector/Test UI from `packages/app/src/app/settings.tsx` so configuration happens per-host within their cards/forms, and ran `npm run typecheck --workspace=@paseo/app`.
- [ ] Keep the per-host "Test" button in each host row (already exists).
### 4. Transparent Connection Management
@@ -52,9 +53,9 @@ The multi-daemon infrastructure is in place: session directory with daemon-scope
- [ ] Only show errors when the user tries to interact with an agent whose host is stopped.
- [ ] Update connection banners/indicators to show neutral "offline" state instead of error styling.
- [ ] Make the Git Diff offline/unavailable state neutral and stop instructing users to "connect" manually.
- Context: `packages/app/src/app/git-diff.tsx:233-241` still renders the destructive error container and tells users "Connect this host or switch to another one to continue," violating the auto-connect guidance.
- Context (review): `packages/app/src/app/git-diff.tsx:229-242` still renders the destructive error container and tells users "Connect this host or switch to another one to continue," so the UI still contradicts the auto-connect guidance even after the wording change.
- [ ] Update the File Explorer offline state to match the new philosophy (neutral messaging, no manual connect CTA).
- Context: `packages/app/src/app/file-explorer.tsx:690-698` presents the offline host as an error and asks users to "Connect this host and try again."
- Context (review): `packages/app/src/app/file-explorer.tsx:690-699` still uses the destructive error styling and the "Connect this host and try again" CTA instead of the neutral offline messaging the new philosophy calls for.
- [ ] Audit the agent detail flows for the same issue (offline agent screen + delete sheet) and replace the "connect this host" requirement with passive/offline messaging.
- Context: `packages/app/src/app/agent/[serverId]/[agentId].tsx:651-660` and `packages/app/src/components/agent-list.tsx:149` continue to show destructive states directing users to manually connect before managing agents.