feat(app): grouped project settings with docs links + scrollbar fix (#837)
* feat(app): group project settings into Worktree / Scripts / Metadata Adds SettingsGroup component (title + info tooltip + trailing slot) and restructures the project settings screen into three categorized groups with explanatory tooltips. Metadata prompt labels drop the "prompt" suffix now that they live under a "Metadata generation" group, and placeholders read as example instructions a real user would write. Applies useWebScrollbarStyle to the settings ScrollViews. * Add ExternalLink component and docs links to project settings
This commit is contained in:
@@ -19,9 +19,11 @@ import {
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import { Alert } from "@/components/ui/alert";
|
||||
import { ExternalLink } from "@/components/ui/external-link";
|
||||
import { LoadingSpinner } from "@/components/ui/loading-spinner";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { AdaptiveModalSheet } from "@/components/adaptive-modal-sheet";
|
||||
import { SettingsGroup } from "@/screens/settings/settings-group";
|
||||
import { SettingsSection } from "@/screens/settings/settings-section";
|
||||
import { settingsStyles } from "@/styles/settings";
|
||||
import { useProjects } from "@/hooks/use-projects";
|
||||
@@ -53,31 +55,41 @@ interface MetadataPromptField {
|
||||
|
||||
const METADATA_PROMPT_FIELDS: Record<MetadataPromptKey, MetadataPromptField> = {
|
||||
agentTitle: {
|
||||
title: "Agent title prompt",
|
||||
placeholder: "Keep titles short and action-oriented.",
|
||||
title: "Agent titles",
|
||||
placeholder: "Keep titles imperative and under 40 characters",
|
||||
sectionTestID: "metadata-prompt-agent-title-section",
|
||||
inputTestID: "metadata-prompt-agent-title-input",
|
||||
},
|
||||
branchName: {
|
||||
title: "Branch name prompt",
|
||||
placeholder: "Prefix branches with feat/ or fix/.",
|
||||
title: "Branch names",
|
||||
placeholder: "Prefix branches with feat/ or fix/, mb/ for personal branches",
|
||||
sectionTestID: "metadata-prompt-branch-name-section",
|
||||
inputTestID: "metadata-prompt-branch-name-input",
|
||||
},
|
||||
commitMessage: {
|
||||
title: "Commit message prompt",
|
||||
placeholder: "Use Conventional Commits.",
|
||||
title: "Commit messages",
|
||||
placeholder: "Use Conventional Commits with a scope",
|
||||
sectionTestID: "metadata-prompt-commit-message-section",
|
||||
inputTestID: "metadata-prompt-commit-message-input",
|
||||
},
|
||||
pullRequest: {
|
||||
title: "Pull request prompt",
|
||||
placeholder: "Include risk notes and a test plan.",
|
||||
title: "Pull requests",
|
||||
placeholder: "Lead with a one-paragraph summary, include a Test plan section",
|
||||
sectionTestID: "metadata-prompt-pull-request-section",
|
||||
inputTestID: "metadata-prompt-pull-request-input",
|
||||
},
|
||||
};
|
||||
|
||||
const WORKTREE_GROUP_INFO =
|
||||
"Commands that run when a worktree is created or torn down for this project";
|
||||
const WORKTREE_DOCS_URL = "https://paseo.sh/docs/worktrees";
|
||||
const WORKTREE_DOCS_TOOLTIP =
|
||||
"See docs for more details and the environment variables available to these commands";
|
||||
const SCRIPTS_GROUP_INFO =
|
||||
"Long-running services and one-off commands you can launch from any agent in this project";
|
||||
const METADATA_GROUP_INFO =
|
||||
"Project-specific instructions injected into the AI prompts Paseo uses to generate metadata — use them to enforce your team's conventions like branch naming, commit style, or PR format";
|
||||
|
||||
const NO_TARGET_MESSAGE = "We don't have an editable copy of this project on any connected host.";
|
||||
|
||||
const HOST_SWITCHER_LABEL = "Switch host";
|
||||
@@ -574,43 +586,82 @@ function ProjectConfigForm({
|
||||
[handleAddScript],
|
||||
);
|
||||
|
||||
const setupDocsLink = useMemo(
|
||||
() => (
|
||||
<ExternalLink
|
||||
href={WORKTREE_DOCS_URL}
|
||||
label="Docs"
|
||||
tooltip={WORKTREE_DOCS_TOOLTIP}
|
||||
testID="worktree-setup-docs-link"
|
||||
/>
|
||||
),
|
||||
[],
|
||||
);
|
||||
const teardownDocsLink = useMemo(
|
||||
() => (
|
||||
<ExternalLink
|
||||
href={WORKTREE_DOCS_URL}
|
||||
label="Docs"
|
||||
tooltip={WORKTREE_DOCS_TOOLTIP}
|
||||
testID="worktree-teardown-docs-link"
|
||||
/>
|
||||
),
|
||||
[],
|
||||
);
|
||||
|
||||
const isStale = writeError?.code === "stale_project_config";
|
||||
const isWriteFailed = writeError?.code === "write_failed";
|
||||
const saveDisabled = saveMutation.isPending || isStale || hasInvalidScripts;
|
||||
|
||||
return (
|
||||
<View>
|
||||
<SettingsSection title="Worktree setup" testID="worktree-setup-section">
|
||||
<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>
|
||||
</SettingsSection>
|
||||
<SettingsGroup
|
||||
title="Worktree lifecycle hooks"
|
||||
info={WORKTREE_GROUP_INFO}
|
||||
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>
|
||||
</SettingsSection>
|
||||
|
||||
<SettingsSection title="Worktree teardown" testID="worktree-teardown-section">
|
||||
<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>
|
||||
</SettingsSection>
|
||||
<SettingsSection
|
||||
title="Teardown"
|
||||
testID="worktree-teardown-section"
|
||||
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>
|
||||
</SettingsSection>
|
||||
</SettingsGroup>
|
||||
|
||||
<SettingsSection title="Scripts" testID="scripts-section" trailing={scriptsTrailing}>
|
||||
<SettingsGroup
|
||||
title="Scripts"
|
||||
info={SCRIPTS_GROUP_INFO}
|
||||
trailing={scriptsTrailing}
|
||||
testID="scripts-group"
|
||||
>
|
||||
<View style={settingsStyles.card} testID="scripts-list">
|
||||
{draft.scripts.length === 0 ? (
|
||||
<View style={settingsStyles.row}>
|
||||
@@ -628,16 +679,19 @@ function ProjectConfigForm({
|
||||
))
|
||||
)}
|
||||
</View>
|
||||
</SettingsSection>
|
||||
</SettingsGroup>
|
||||
|
||||
{METADATA_PROMPT_KEYS.map((key) => (
|
||||
<MetadataPromptSection
|
||||
key={key}
|
||||
promptKey={key}
|
||||
value={draft.metadataPrompts[key]}
|
||||
onChange={handleMetadataPromptChange}
|
||||
/>
|
||||
))}
|
||||
<SettingsGroup title="Metadata generation" info={METADATA_GROUP_INFO} testID="metadata-group">
|
||||
{METADATA_PROMPT_KEYS.map((key, index) => (
|
||||
<MetadataPromptSection
|
||||
key={key}
|
||||
promptKey={key}
|
||||
value={draft.metadataPrompts[key]}
|
||||
onChange={handleMetadataPromptChange}
|
||||
flush={index === METADATA_PROMPT_KEYS.length - 1}
|
||||
/>
|
||||
))}
|
||||
</SettingsGroup>
|
||||
|
||||
{isStale ? (
|
||||
<View style={styles.calloutWrap}>
|
||||
@@ -825,16 +879,17 @@ interface MetadataPromptSectionProps {
|
||||
promptKey: MetadataPromptKey;
|
||||
value: string;
|
||||
onChange: (key: MetadataPromptKey, text: string) => void;
|
||||
flush?: boolean;
|
||||
}
|
||||
|
||||
function MetadataPromptSection({ promptKey, value, onChange }: MetadataPromptSectionProps) {
|
||||
function MetadataPromptSection({ promptKey, value, onChange, flush }: MetadataPromptSectionProps) {
|
||||
const meta = METADATA_PROMPT_FIELDS[promptKey];
|
||||
const handleChange = useCallback(
|
||||
(text: string) => onChange(promptKey, text),
|
||||
[onChange, promptKey],
|
||||
);
|
||||
return (
|
||||
<SettingsSection title={meta.title} testID={meta.sectionTestID}>
|
||||
<SettingsSection title={meta.title} testID={meta.sectionTestID} flush={flush}>
|
||||
<View style={settingsStyles.card}>
|
||||
<TextInput
|
||||
testID={meta.inputTestID}
|
||||
|
||||
Reference in New Issue
Block a user