Remove global settings test form
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user