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:
Mohamed Boudra
2026-05-19 14:24:56 +00:00
committed by GitHub
parent da0e94fd18
commit 306601a0c3
21 changed files with 449 additions and 124 deletions
@@ -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,
+117 -4
View File
@@ -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[] {