Archive merged PR workspaces from settings (#1313)
* Add workspace settings for merged PR cleanup * Fix worktree branch ahead status * Handle PR worktree upstream status * Remove stale checkout tracking field * Update settings E2E host section slugs * Treat unknown upstream as unsafe for auto-archive
This commit is contained in:
@@ -2,13 +2,13 @@ import { useLocalSearchParams } from "expo-router";
|
||||
import { useMemo } from "react";
|
||||
import { HostRouteBootstrapBoundary } from "@/components/host-route-bootstrap-boundary";
|
||||
import SettingsScreen from "@/screens/settings-screen";
|
||||
import { type HostSectionSlug, isHostSectionSlug } from "@/utils/host-routes";
|
||||
import { normalizeHostSectionSlug } from "@/utils/host-routes";
|
||||
|
||||
export default function SettingsHostSectionRoute() {
|
||||
const params = useLocalSearchParams<{ serverId?: string; hostSection?: string }>();
|
||||
const serverId = typeof params.serverId === "string" ? params.serverId.trim() : "";
|
||||
const rawSection = typeof params.hostSection === "string" ? params.hostSection : "";
|
||||
const section: HostSectionSlug = isHostSectionSlug(rawSection) ? rawSection : "connections";
|
||||
const section = normalizeHostSectionSlug(rawSection) ?? "connections";
|
||||
const view = useMemo(() => ({ kind: "host" as const, serverId, section }), [serverId, section]);
|
||||
|
||||
return (
|
||||
|
||||
@@ -30,7 +30,7 @@ import {
|
||||
Palette,
|
||||
Server,
|
||||
Network,
|
||||
Workflow,
|
||||
Bot,
|
||||
Boxes,
|
||||
Keyboard,
|
||||
Stethoscope,
|
||||
@@ -93,9 +93,10 @@ import { THINKING_TONE_NATIVE_PCM_BASE64 } from "@/utils/thinking-tone.native-pc
|
||||
import { useVoiceAudioEngineOptional } from "@/contexts/voice-context";
|
||||
import {
|
||||
HostConnectionsPage,
|
||||
HostDaemonPage,
|
||||
HostOrchestrationPage,
|
||||
HostAgentsPage,
|
||||
HostSettingsPage,
|
||||
HostProvidersPage,
|
||||
HostWorkspacesPage,
|
||||
} from "@/screens/settings/host-page";
|
||||
import ProjectsScreen from "@/screens/projects-screen";
|
||||
import ProjectSettingsScreen from "@/screens/project-settings-screen";
|
||||
@@ -148,9 +149,10 @@ interface HostSectionItem {
|
||||
|
||||
const HOST_SECTION_ITEMS: HostSectionItem[] = [
|
||||
{ id: "connections", label: "Connections", icon: Network },
|
||||
{ id: "orchestration", label: "Orchestration", icon: Workflow },
|
||||
{ id: "agents", label: "Agents", icon: Bot },
|
||||
{ id: "workspaces", label: "Workspaces", icon: FolderGit2 },
|
||||
{ id: "providers", label: "Providers", icon: Boxes },
|
||||
{ id: "daemon", label: "Daemon", icon: Server },
|
||||
{ id: "host", label: "Host", icon: Server },
|
||||
];
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -1304,12 +1306,14 @@ export default function SettingsScreen({ view }: SettingsScreenProps) {
|
||||
switch (view.section) {
|
||||
case "connections":
|
||||
return <HostConnectionsPage serverId={view.serverId} />;
|
||||
case "orchestration":
|
||||
return <HostOrchestrationPage serverId={view.serverId} />;
|
||||
case "agents":
|
||||
return <HostAgentsPage serverId={view.serverId} />;
|
||||
case "workspaces":
|
||||
return <HostWorkspacesPage serverId={view.serverId} />;
|
||||
case "providers":
|
||||
return <HostProvidersPage serverId={view.serverId} />;
|
||||
case "daemon":
|
||||
return <HostDaemonPage serverId={view.serverId} onHostRemoved={handleHostRemoved} />;
|
||||
case "host":
|
||||
return <HostSettingsPage serverId={view.serverId} onHostRemoved={handleHostRemoved} />;
|
||||
}
|
||||
}
|
||||
if (view.kind === "projects") {
|
||||
|
||||
@@ -188,7 +188,7 @@ export function HostConnectionsPage({ serverId }: { serverId: string }) {
|
||||
);
|
||||
}
|
||||
|
||||
export function HostOrchestrationPage({ serverId }: { serverId: string }) {
|
||||
export function HostAgentsPage({ serverId }: { serverId: string }) {
|
||||
const host = useHostProfile(serverId);
|
||||
const isConnected = useHostRuntimeIsConnected(serverId);
|
||||
|
||||
@@ -199,13 +199,36 @@ export function HostOrchestrationPage({ serverId }: { serverId: string }) {
|
||||
return (
|
||||
<View>
|
||||
{isConnected ? (
|
||||
<SettingsSection title="Orchestration">
|
||||
<SettingsSection title="Agents">
|
||||
<InjectPaseoToolsCard serverId={serverId} />
|
||||
<AppendSystemPromptCard serverId={serverId} />
|
||||
</SettingsSection>
|
||||
) : (
|
||||
<View style={EMPTY_CARD_STYLE}>
|
||||
<Text style={styles.emptyText}>Connect to this host to manage orchestration</Text>
|
||||
<Text style={styles.emptyText}>Connect to this host to manage agents</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
export function HostWorkspacesPage({ serverId }: { serverId: string }) {
|
||||
const host = useHostProfile(serverId);
|
||||
const isConnected = useHostRuntimeIsConnected(serverId);
|
||||
|
||||
if (!host) {
|
||||
return <HostNotFound />;
|
||||
}
|
||||
|
||||
return (
|
||||
<View>
|
||||
{isConnected ? (
|
||||
<SettingsSection title="Workspaces">
|
||||
<AutoArchiveMergedWorkspacesCard serverId={serverId} />
|
||||
</SettingsSection>
|
||||
) : (
|
||||
<View style={EMPTY_CARD_STYLE}>
|
||||
<Text style={styles.emptyText}>Connect to this host to manage workspaces</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
@@ -226,7 +249,7 @@ export function HostProvidersPage({ serverId }: { serverId: string }) {
|
||||
);
|
||||
}
|
||||
|
||||
export function HostDaemonPage({
|
||||
export function HostSettingsPage({
|
||||
serverId,
|
||||
onHostRemoved,
|
||||
}: {
|
||||
@@ -626,6 +649,45 @@ function InjectPaseoToolsCard({ serverId }: { serverId: string }) {
|
||||
);
|
||||
}
|
||||
|
||||
function AutoArchiveMergedWorkspacesCard({ serverId }: { serverId: string }) {
|
||||
const isConnected = useHostRuntimeIsConnected(serverId);
|
||||
const { config, patchConfig } = useDaemonConfig(serverId);
|
||||
|
||||
const handleValueChange = useCallback(
|
||||
(next: boolean) => {
|
||||
void patchConfig({ autoArchiveAfterMerge: next }).catch((error) => {
|
||||
console.error("[HostPage] Failed to update auto-archive after merge", error);
|
||||
Alert.alert(
|
||||
"Unable to update workspaces",
|
||||
error instanceof Error ? error.message : String(error),
|
||||
);
|
||||
});
|
||||
},
|
||||
[patchConfig],
|
||||
);
|
||||
|
||||
if (!isConnected) return null;
|
||||
|
||||
return (
|
||||
<View style={settingsStyles.card} testID="host-page-auto-archive-merged-workspaces-card">
|
||||
<View style={settingsStyles.row}>
|
||||
<View style={settingsStyles.rowContent}>
|
||||
<Text style={settingsStyles.rowTitle}>Archive merged PR workspaces</Text>
|
||||
<Text style={settingsStyles.rowHint}>
|
||||
Automatically archive clean Paseo workspaces after their pull request is merged
|
||||
</Text>
|
||||
</View>
|
||||
<Switch
|
||||
value={config?.autoArchiveAfterMerge === true}
|
||||
onValueChange={handleValueChange}
|
||||
accessibilityLabel="Archive merged PR workspaces"
|
||||
testID="host-page-auto-archive-merged-workspaces-switch"
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
function AppendSystemPromptCard({ serverId }: { serverId: string }) {
|
||||
const isConnected = useHostRuntimeIsConnected(serverId);
|
||||
const { config, patchConfig } = useDaemonConfig(serverId);
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
decodeWorkspaceIdFromPathSegment,
|
||||
encodeFilePathForPathSegment,
|
||||
encodeWorkspaceIdForPathSegment,
|
||||
normalizeHostSectionSlug,
|
||||
parseHostAgentRouteFromPathname,
|
||||
parseHostWorkspaceOpenIntentFromPathname,
|
||||
parseHostWorkspaceRouteFromPathname,
|
||||
@@ -160,3 +161,18 @@ describe("projects settings routes", () => {
|
||||
expect(decodeURIComponent(segment)).toBe(projectKey);
|
||||
});
|
||||
});
|
||||
|
||||
describe("host settings section slugs", () => {
|
||||
it("keeps current host settings sections", () => {
|
||||
expect(normalizeHostSectionSlug("connections")).toBe("connections");
|
||||
expect(normalizeHostSectionSlug("agents")).toBe("agents");
|
||||
expect(normalizeHostSectionSlug("workspaces")).toBe("workspaces");
|
||||
expect(normalizeHostSectionSlug("providers")).toBe("providers");
|
||||
expect(normalizeHostSectionSlug("host")).toBe("host");
|
||||
});
|
||||
|
||||
it("maps old host settings sections to their new names", () => {
|
||||
expect(normalizeHostSectionSlug("orchestration")).toBe("agents");
|
||||
expect(normalizeHostSectionSlug("daemon")).toBe("host");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -391,14 +391,32 @@ export function isSettingsSectionSlug(value: string): value is SettingsSectionSl
|
||||
return (SETTINGS_SECTION_SLUGS as readonly string[]).includes(value);
|
||||
}
|
||||
|
||||
export const HOST_SECTION_SLUGS = ["connections", "orchestration", "providers", "daemon"] as const;
|
||||
export const HOST_SECTION_SLUGS = [
|
||||
"connections",
|
||||
"agents",
|
||||
"workspaces",
|
||||
"providers",
|
||||
"host",
|
||||
] as const;
|
||||
|
||||
export type HostSectionSlug = (typeof HOST_SECTION_SLUGS)[number];
|
||||
|
||||
const LEGACY_HOST_SECTION_SLUGS: Record<string, HostSectionSlug> = {
|
||||
orchestration: "agents",
|
||||
daemon: "host",
|
||||
};
|
||||
|
||||
export function isHostSectionSlug(value: string): value is HostSectionSlug {
|
||||
return (HOST_SECTION_SLUGS as readonly string[]).includes(value);
|
||||
}
|
||||
|
||||
export function normalizeHostSectionSlug(value: string): HostSectionSlug | null {
|
||||
if (isHostSectionSlug(value)) {
|
||||
return value;
|
||||
}
|
||||
return LEGACY_HOST_SECTION_SLUGS[value] ?? null;
|
||||
}
|
||||
|
||||
export function buildSettingsRoute() {
|
||||
return "/settings" as const;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user