Add daemon-wide system prompts (#1100)
* Add daemon-wide system prompt setting * Align settings textarea rendering * Polish daemon settings layout * Remove orchestrator prompt injection
This commit is contained in:
@@ -0,0 +1,58 @@
|
||||
import type { StyleProp, TextStyle } from "react-native";
|
||||
import { useMemo } from "react";
|
||||
import { TextInput, View } from "react-native";
|
||||
import { StyleSheet, useUnistyles } from "react-native-unistyles";
|
||||
import { settingsStyles } from "@/styles/settings";
|
||||
|
||||
interface SettingsTextAreaProps {
|
||||
accessibilityLabel: string;
|
||||
value: string;
|
||||
onChangeText: (text: string) => void;
|
||||
placeholder?: string;
|
||||
testID?: string;
|
||||
style?: StyleProp<TextStyle>;
|
||||
}
|
||||
|
||||
export function SettingsTextArea({
|
||||
accessibilityLabel,
|
||||
value,
|
||||
onChangeText,
|
||||
placeholder,
|
||||
testID,
|
||||
style,
|
||||
}: SettingsTextAreaProps) {
|
||||
const { theme } = useUnistyles();
|
||||
const inputStyle = useMemo(() => [styles.input, style], [style]);
|
||||
|
||||
return (
|
||||
<TextInput
|
||||
testID={testID}
|
||||
accessibilityLabel={accessibilityLabel}
|
||||
multiline
|
||||
value={value}
|
||||
onChangeText={onChangeText}
|
||||
placeholder={placeholder}
|
||||
placeholderTextColor={theme.colors.foregroundMuted}
|
||||
style={inputStyle}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function SettingsTextAreaCard(props: SettingsTextAreaProps) {
|
||||
return (
|
||||
<View style={settingsStyles.card}>
|
||||
<SettingsTextArea {...props} />
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create((theme) => ({
|
||||
input: {
|
||||
color: theme.colors.foreground,
|
||||
fontSize: theme.fontSize.sm,
|
||||
paddingVertical: theme.spacing[3],
|
||||
paddingHorizontal: theme.spacing[4],
|
||||
minHeight: 96,
|
||||
textAlignVertical: "top",
|
||||
},
|
||||
}));
|
||||
@@ -23,6 +23,7 @@ import { ExternalLink } from "@/components/ui/external-link";
|
||||
import { LoadingSpinner } from "@/components/ui/loading-spinner";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { AdaptiveModalSheet, type SheetHeader } from "@/components/adaptive-modal-sheet";
|
||||
import { SettingsTextAreaCard } from "@/components/settings-textarea";
|
||||
import { SettingsGroup } from "@/screens/settings/settings-group";
|
||||
import { SettingsSection } from "@/screens/settings/settings-section";
|
||||
import { settingsStyles } from "@/styles/settings";
|
||||
@@ -621,18 +622,13 @@ function ProjectConfigForm({
|
||||
testID="worktree-group"
|
||||
>
|
||||
<SettingsSection title="Setup" testID="worktree-setup-section" trailing={setupDocsLink}>
|
||||
<View style={settingsStyles.card}>
|
||||
<TextInput
|
||||
testID="worktree-setup-input"
|
||||
accessibilityLabel="Worktree setup commands"
|
||||
multiline
|
||||
value={draft.setupText}
|
||||
onChangeText={handleSetupChange}
|
||||
placeholder="npm install"
|
||||
placeholderTextColor={styles.placeholderColor.color}
|
||||
style={styles.lifecycleInput}
|
||||
/>
|
||||
</View>
|
||||
<SettingsTextAreaCard
|
||||
testID="worktree-setup-input"
|
||||
accessibilityLabel="Worktree setup commands"
|
||||
value={draft.setupText}
|
||||
onChangeText={handleSetupChange}
|
||||
placeholder="npm install"
|
||||
/>
|
||||
</SettingsSection>
|
||||
|
||||
<SettingsSection
|
||||
@@ -641,18 +637,13 @@ function ProjectConfigForm({
|
||||
trailing={teardownDocsLink}
|
||||
flush
|
||||
>
|
||||
<View style={settingsStyles.card}>
|
||||
<TextInput
|
||||
testID="worktree-teardown-input"
|
||||
accessibilityLabel="Worktree teardown commands"
|
||||
multiline
|
||||
value={draft.teardownText}
|
||||
onChangeText={handleTeardownChange}
|
||||
placeholder="docker compose down"
|
||||
placeholderTextColor={styles.placeholderColor.color}
|
||||
style={styles.lifecycleInput}
|
||||
/>
|
||||
</View>
|
||||
<SettingsTextAreaCard
|
||||
testID="worktree-teardown-input"
|
||||
accessibilityLabel="Worktree teardown commands"
|
||||
value={draft.teardownText}
|
||||
onChangeText={handleTeardownChange}
|
||||
placeholder="docker compose down"
|
||||
/>
|
||||
</SettingsSection>
|
||||
</SettingsGroup>
|
||||
|
||||
@@ -1008,18 +999,13 @@ function MetadataPromptSection({ promptKey, value, onChange, flush }: MetadataPr
|
||||
);
|
||||
return (
|
||||
<SettingsSection title={meta.title} testID={meta.sectionTestID} flush={flush}>
|
||||
<View style={settingsStyles.card}>
|
||||
<TextInput
|
||||
testID={meta.inputTestID}
|
||||
accessibilityLabel={meta.title}
|
||||
multiline
|
||||
value={value}
|
||||
onChangeText={handleChange}
|
||||
placeholder={meta.placeholder}
|
||||
placeholderTextColor={styles.placeholderColor.color}
|
||||
style={styles.lifecycleInput}
|
||||
/>
|
||||
</View>
|
||||
<SettingsTextAreaCard
|
||||
testID={meta.inputTestID}
|
||||
accessibilityLabel={meta.title}
|
||||
value={value}
|
||||
onChangeText={handleChange}
|
||||
placeholder={meta.placeholder}
|
||||
/>
|
||||
</SettingsSection>
|
||||
);
|
||||
}
|
||||
@@ -1344,14 +1330,6 @@ const styles = StyleSheet.create((theme) => ({
|
||||
errorBlock: {
|
||||
marginTop: theme.spacing[2],
|
||||
},
|
||||
lifecycleInput: {
|
||||
color: theme.colors.foreground,
|
||||
fontSize: theme.fontSize.sm,
|
||||
paddingVertical: theme.spacing[3],
|
||||
paddingHorizontal: theme.spacing[4],
|
||||
minHeight: 96,
|
||||
textAlignVertical: "top",
|
||||
},
|
||||
emptyScripts: {
|
||||
color: theme.colors.foregroundMuted,
|
||||
fontSize: theme.fontSize.sm,
|
||||
|
||||
@@ -4,6 +4,7 @@ import { Alert, Pressable, Text, View } from "react-native";
|
||||
import { StyleSheet, useUnistyles } from "react-native-unistyles";
|
||||
import { AdaptiveModalSheet, type SheetHeader } from "@/components/adaptive-modal-sheet";
|
||||
import { AdaptiveRenameModal } from "@/components/rename-modal";
|
||||
import { SettingsTextAreaCard } from "@/components/settings-textarea";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { LocalDaemonSection } from "@/desktop/components/desktop-updates-section";
|
||||
@@ -379,9 +380,9 @@ function ConnectionRow({
|
||||
function DaemonSection({ host, isLocalDaemon }: { host: HostProfile; isLocalDaemon: boolean }) {
|
||||
return (
|
||||
<>
|
||||
<SettingsSection title="Operations">
|
||||
<RestartDaemonCard host={host} />
|
||||
<SettingsSection title="Daemon settings">
|
||||
<InjectPaseoToolsCard serverId={host.serverId} />
|
||||
<AppendSystemPromptCard serverId={host.serverId} />
|
||||
</SettingsSection>
|
||||
{isLocalDaemon ? (
|
||||
<SettingsSection title="Pair devices">
|
||||
@@ -544,9 +545,9 @@ function InjectPaseoToolsCard({ serverId }: { serverId: string }) {
|
||||
<View style={settingsStyles.card} testID="host-page-inject-mcp-card">
|
||||
<View style={settingsStyles.row}>
|
||||
<View style={settingsStyles.rowContent}>
|
||||
<Text style={settingsStyles.rowTitle}>Inject Paseo tools</Text>
|
||||
<Text style={settingsStyles.rowTitle}>Enable Paseo tools</Text>
|
||||
<Text style={settingsStyles.rowHint}>
|
||||
Automatically inject Paseo MCP tools into new agents
|
||||
Agents will be able to manage worktrees, agents and schedules
|
||||
</Text>
|
||||
</View>
|
||||
<Switch
|
||||
@@ -559,6 +560,111 @@ function InjectPaseoToolsCard({ serverId }: { serverId: string }) {
|
||||
);
|
||||
}
|
||||
|
||||
function AppendSystemPromptCard({ serverId }: { serverId: string }) {
|
||||
const isConnected = useHostRuntimeIsConnected(serverId);
|
||||
const { config, patchConfig } = useDaemonConfig(serverId);
|
||||
const persistedPrompt = config?.appendSystemPrompt ?? "";
|
||||
const [draft, setDraft] = useState(persistedPrompt);
|
||||
const [isEditing, setIsEditing] = useState(false);
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
const header = useMemo<SheetHeader>(() => ({ title: "Append system prompt" }), []);
|
||||
|
||||
useEffect(() => {
|
||||
setDraft(persistedPrompt);
|
||||
}, [persistedPrompt]);
|
||||
|
||||
const hasChanges = draft !== persistedPrompt;
|
||||
|
||||
const handleOpen = useCallback(() => {
|
||||
setDraft(persistedPrompt);
|
||||
setIsEditing(true);
|
||||
}, [persistedPrompt]);
|
||||
|
||||
const handleClose = useCallback(() => {
|
||||
if (isSaving) return;
|
||||
setDraft(persistedPrompt);
|
||||
setIsEditing(false);
|
||||
}, [isSaving, persistedPrompt]);
|
||||
|
||||
const handleSave = useCallback(() => {
|
||||
setIsSaving(true);
|
||||
void patchConfig({ appendSystemPrompt: draft })
|
||||
.then(() => {
|
||||
setIsEditing(false);
|
||||
return;
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("[HostPage] Failed to save append system prompt", error);
|
||||
})
|
||||
.finally(() => setIsSaving(false));
|
||||
}, [draft, patchConfig]);
|
||||
|
||||
const handleReset = useCallback(() => {
|
||||
setDraft(persistedPrompt);
|
||||
}, [persistedPrompt]);
|
||||
|
||||
if (!isConnected) return null;
|
||||
|
||||
return (
|
||||
<>
|
||||
<View style={settingsStyles.card} testID="host-page-append-system-prompt-card">
|
||||
<View style={settingsStyles.row}>
|
||||
<View style={settingsStyles.rowContent}>
|
||||
<Text style={settingsStyles.rowTitle}>System prompt</Text>
|
||||
<Text style={settingsStyles.rowHint}>Added a system prompt to all agents</Text>
|
||||
</View>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onPress={handleOpen}
|
||||
testID="host-page-append-system-prompt-edit"
|
||||
>
|
||||
Edit
|
||||
</Button>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{isEditing ? (
|
||||
<AdaptiveModalSheet
|
||||
header={header}
|
||||
visible
|
||||
onClose={handleClose}
|
||||
testID="host-page-append-system-prompt-sheet"
|
||||
desktopMaxWidth={560}
|
||||
>
|
||||
<SettingsTextAreaCard
|
||||
testID="host-page-append-system-prompt-input"
|
||||
accessibilityLabel="Append system prompt"
|
||||
value={draft}
|
||||
onChangeText={setDraft}
|
||||
placeholder="Always keep replies concise."
|
||||
/>
|
||||
<View style={styles.appendPromptActions}>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onPress={handleReset}
|
||||
disabled={!hasChanges || isSaving}
|
||||
testID="host-page-append-system-prompt-reset"
|
||||
>
|
||||
Reset
|
||||
</Button>
|
||||
<Button
|
||||
variant="default"
|
||||
size="sm"
|
||||
onPress={handleSave}
|
||||
disabled={!hasChanges || isSaving}
|
||||
testID="host-page-append-system-prompt-save"
|
||||
>
|
||||
{isSaving ? "Saving..." : "Save"}
|
||||
</Button>
|
||||
</View>
|
||||
</AdaptiveModalSheet>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function PairDeviceRow() {
|
||||
const { theme } = useUnistyles();
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
@@ -631,6 +737,8 @@ function RemoveHostSection({ host, onRemoved }: { host: HostProfile; onRemoved?:
|
||||
|
||||
return (
|
||||
<SettingsSection title="Danger zone" testID="host-page-remove-host-card">
|
||||
<RestartDaemonCard host={host} />
|
||||
|
||||
<View style={settingsStyles.card}>
|
||||
<View style={settingsStyles.row}>
|
||||
<View style={settingsStyles.rowContent}>
|
||||
@@ -755,6 +863,11 @@ const styles = StyleSheet.create((theme) => ({
|
||||
gap: theme.spacing[2],
|
||||
marginTop: theme.spacing[4],
|
||||
},
|
||||
appendPromptActions: {
|
||||
flexDirection: "row",
|
||||
justifyContent: "flex-end",
|
||||
gap: theme.spacing[2],
|
||||
},
|
||||
emptyCard: {
|
||||
padding: theme.spacing[4],
|
||||
alignItems: "center",
|
||||
|
||||
@@ -202,7 +202,12 @@ const disabledCodexEntry: ProviderSnapshotEntry = {
|
||||
};
|
||||
|
||||
function makeConfig(providers: MutableDaemonConfig["providers"] = {}): MutableDaemonConfig {
|
||||
return { mcp: { injectIntoAgents: false }, providers, autoArchiveAfterMerge: false };
|
||||
return {
|
||||
mcp: { injectIntoAgents: false },
|
||||
providers,
|
||||
autoArchiveAfterMerge: false,
|
||||
appendSystemPrompt: "",
|
||||
};
|
||||
}
|
||||
|
||||
function descendants(el: HTMLElement): HTMLElement[] {
|
||||
|
||||
Reference in New Issue
Block a user