mirror of
https://github.com/getpaseo/paseo.git
synced 2026-11-10 10:25:03 +00:00
Let plugins contribute settings screens (#4357)
* feat(plugins): contribute settings screens and shared controls * fix(plugins): localize settings and verify shared UI consumers * test(composer): unmount draft test roots after each case * test(app): keep launch terminals alive until cleanup The fixed ten-second profile lifetime could expire before a busy CI browser attached or finished revisiting the composer. Keep the real process waiting for input until project teardown terminates it. * test(server): await the initial snapshot before a buffered ref event Watcher setup can read checkout facts before the initial refresh publishes a snapshot. Wait for publication before injecting the event so the test consistently exercises a second refresh.
This commit is contained in:
@@ -0,0 +1,14 @@
|
||||
# Settings example
|
||||
|
||||
Install this directory with `paseo plugin install /absolute/path/to/plugin-examples/settings`.
|
||||
Open Settings → Plugins → settings-example → Display, or run **Configure agent monitor** in Command Center.
|
||||
|
||||
The select and switch save immediately. The title editor keeps a draft and its original revision,
|
||||
so another client's save produces a conflict instead of overwriting its changes. Close and reopen
|
||||
the editor to discard the draft and use the latest settings.
|
||||
|
||||
Every UI component is a plain named import. The custom preview shares the same values without
|
||||
using a form wrapper. Values are shared by clients connected to the same host.
|
||||
|
||||
The same screen on [Android](screenshots/android.png), [Electron](screenshots/electron.png),
|
||||
and [compact web](screenshots/web-compact.png).
|
||||
@@ -0,0 +1,148 @@
|
||||
import { useCallback, useMemo, useState } from "react";
|
||||
import { Text, View } from "react-native";
|
||||
import { useSettings, type PluginSurfaceProps, type SettingsState } from "@getpaseo/plugin";
|
||||
import {
|
||||
SettingsAction,
|
||||
SettingsCard,
|
||||
SettingsInput,
|
||||
SettingsRow,
|
||||
SettingsSection,
|
||||
SettingsSelect,
|
||||
SettingsSwitch,
|
||||
} from "@getpaseo/plugin/ui";
|
||||
import { preferences } from "../shared/preferences";
|
||||
|
||||
const grouping = [
|
||||
{ label: "Project", value: "project" },
|
||||
{ label: "Workspace", value: "workspace" },
|
||||
{ label: "None", value: "none" },
|
||||
] as const;
|
||||
type Preferences = Extract<SettingsState<typeof preferences.schema>, { status: "ready" }>;
|
||||
|
||||
function TitleEditor({ settings, onClose }: { settings: Preferences; onClose(): void }) {
|
||||
// Keep the draft's revision so another client's save produces a conflict.
|
||||
const [draft, setDraft] = useState(() => ({
|
||||
values: settings.values,
|
||||
revision: settings.revision,
|
||||
}));
|
||||
const changeTitle = useCallback(
|
||||
(title: string) =>
|
||||
setDraft((current) => ({ ...current, values: { ...current.values, title } })),
|
||||
[],
|
||||
);
|
||||
const saveTitle = useCallback(async () => {
|
||||
if (await settings.save(draft.values, draft.revision)) onClose();
|
||||
}, [settings, draft, onClose]);
|
||||
return (
|
||||
<SettingsCard>
|
||||
<SettingsInput
|
||||
label="Monitor title"
|
||||
initialValue={draft.values.title}
|
||||
onChangeText={changeTitle}
|
||||
disabled={settings.saving}
|
||||
error={settings.saveError}
|
||||
/>
|
||||
<SettingsAction
|
||||
label="Title changes"
|
||||
actionLabel="Save title"
|
||||
disabled={settings.saving}
|
||||
onPress={saveTitle}
|
||||
/>
|
||||
<SettingsAction
|
||||
label="Discard title changes"
|
||||
actionLabel="Discard"
|
||||
disabled={settings.saving}
|
||||
onPress={onClose}
|
||||
/>
|
||||
</SettingsCard>
|
||||
);
|
||||
}
|
||||
|
||||
function DisplayControls({
|
||||
settings,
|
||||
theme,
|
||||
}: {
|
||||
settings: Preferences;
|
||||
theme: PluginSurfaceProps["theme"];
|
||||
}) {
|
||||
const style = useMemo(() => ({ color: theme.colors.foreground }), [theme]);
|
||||
const [editingTitle, setEditingTitle] = useState(false);
|
||||
const closeTitle = useCallback(() => {
|
||||
setEditingTitle(false);
|
||||
void settings.reload();
|
||||
}, [settings]);
|
||||
const toggleTitle = useCallback(() => setEditingTitle((value) => !value), []);
|
||||
const changeGrouping = useCallback(
|
||||
(groupBy: Preferences["values"]["groupBy"]) => {
|
||||
void settings.save({ ...settings.values, groupBy }, settings.revision);
|
||||
},
|
||||
[settings],
|
||||
);
|
||||
const changeMetadata = useCallback(
|
||||
(showMetadata: boolean) => {
|
||||
void settings.save({ ...settings.values, showMetadata }, settings.revision);
|
||||
},
|
||||
[settings],
|
||||
);
|
||||
return (
|
||||
<SettingsSection title="Display">
|
||||
<SettingsCard>
|
||||
<SettingsSelect
|
||||
label="Group agents by"
|
||||
value={settings.values.groupBy}
|
||||
options={grouping}
|
||||
disabled={settings.saving}
|
||||
onValueChange={changeGrouping}
|
||||
/>
|
||||
<SettingsSwitch
|
||||
label="Show metadata"
|
||||
value={settings.values.showMetadata}
|
||||
disabled={settings.saving}
|
||||
onValueChange={changeMetadata}
|
||||
/>
|
||||
<SettingsAction
|
||||
label="Monitor title"
|
||||
actionLabel={editingTitle ? "Close editor" : "Edit title"}
|
||||
disabled={settings.saving}
|
||||
onPress={toggleTitle}
|
||||
/>
|
||||
</SettingsCard>
|
||||
{editingTitle ? <TitleEditor settings={settings} onClose={closeTitle} /> : null}
|
||||
{settings.saveError && !editingTitle ? (
|
||||
<Text accessibilityRole="alert" style={style}>
|
||||
{settings.saveError}
|
||||
</Text>
|
||||
) : null}
|
||||
<SettingsRow label="Preview">
|
||||
<View>
|
||||
<Text style={style}>{settings.values.title}</Text>
|
||||
<Text style={style}>Grouped by {settings.values.groupBy}</Text>
|
||||
<Text style={style}>
|
||||
{settings.values.showMetadata ? "Metadata visible" : "Metadata hidden"}
|
||||
</Text>
|
||||
</View>
|
||||
</SettingsRow>
|
||||
</SettingsSection>
|
||||
);
|
||||
}
|
||||
|
||||
export function DisplaySettings({ theme }: PluginSurfaceProps) {
|
||||
const settings = useSettings(preferences);
|
||||
const style = useMemo(() => ({ color: theme.colors.foreground }), [theme]);
|
||||
if (settings.status === "loading") return <Text style={style}>Loading settings…</Text>;
|
||||
if (settings.status !== "ready")
|
||||
return (
|
||||
<SettingsSection title="Display">
|
||||
<Text style={style}>{settings.error}</Text>
|
||||
<SettingsAction label="Try again" actionLabel="Reload" onPress={settings.reload} />
|
||||
{settings.status === "invalid" ? (
|
||||
<SettingsAction
|
||||
label="Restore default settings"
|
||||
actionLabel="Reset"
|
||||
onPress={settings.reset}
|
||||
/>
|
||||
) : null}
|
||||
</SettingsSection>
|
||||
);
|
||||
return <DisplayControls settings={settings} theme={theme} />;
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import type { PluginClientContext } from "@getpaseo/plugin";
|
||||
import { DisplaySettings } from "./client/display-settings";
|
||||
|
||||
export default function contribute(client: PluginClientContext) {
|
||||
client.addSettingsScreen({
|
||||
id: "display",
|
||||
title: "Display",
|
||||
icon: "SlidersHorizontal",
|
||||
Component: DisplaySettings,
|
||||
});
|
||||
client.addCommandCenterItem({
|
||||
id: "settings",
|
||||
title: "Configure agent monitor",
|
||||
icon: "Settings",
|
||||
context: "global",
|
||||
onSelect({ openSettings }) {
|
||||
openSettings("display");
|
||||
},
|
||||
});
|
||||
return () => {};
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import type { PluginServerContext } from "@getpaseo/plugin";
|
||||
import { preferences } from "./shared/preferences";
|
||||
|
||||
export default function contribute(server: PluginServerContext) {
|
||||
server.registerSettings(preferences);
|
||||
return () => {};
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
{ "id": "settings-example" }
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 74 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 46 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 20 KiB |
@@ -0,0 +1,13 @@
|
||||
import { defineSettings } from "@getpaseo/plugin";
|
||||
import { z } from "zod";
|
||||
|
||||
export const preferences = defineSettings({
|
||||
id: "display",
|
||||
scope: "host",
|
||||
version: 1,
|
||||
schema: z.object({
|
||||
groupBy: z.enum(["project", "workspace", "none"]).default("project"),
|
||||
showMetadata: z.boolean().default(true),
|
||||
title: z.string().trim().min(1, "Enter a title").max(40).default("Agent monitor"),
|
||||
}),
|
||||
});
|
||||
Reference in New Issue
Block a user