mirror of
https://github.com/getpaseo/paseo.git
synced 2026-11-02 22:52:58 +00:00
fix(app): finish setup on its last step and confirm undoing orchestration
In the compact layout Orchestration is the last step, and Finish closed setup without applying its switches. The last step's work now runs before Finish, the same as Continue. Turning off skills now asks first, as Settings does. Removing the CLI keeps a paseo command this app didn't install and says so. A failed Paseo tools update shows a toast. Adding suggested projects no longer advances when a row was checked while the add ran. Chat sender attribution and the message preview toggle go back to the sm button: md padding is a breakpoint value, and on web its media-query rule beat the zero right padding that aligns the sender with its bubble.
This commit is contained in:
@@ -8,6 +8,7 @@ import {
|
||||
expectSetupStep,
|
||||
finishSetup,
|
||||
goToSetupStep,
|
||||
hostGivesAgentsPaseoTools,
|
||||
openAppAsNewUser,
|
||||
openRelayPairingFromSetup,
|
||||
closeRelayPairing,
|
||||
@@ -233,3 +234,35 @@ test("setup choices carry over to Settings and the phone step pairs over relay",
|
||||
await host.close();
|
||||
}
|
||||
});
|
||||
|
||||
test("in the compact layout, Finish on Orchestration applies its switches", async ({ page }) => {
|
||||
await page.setViewportSize({ width: 390, height: 844 });
|
||||
const host = await startSetupHost("compact");
|
||||
// The compact sheet is the only surface on screen, and Orchestration is its last step.
|
||||
const step = (title: string) =>
|
||||
expect(page.getByRole("heading", { name: title, exact: true })).toBeVisible({
|
||||
timeout: 60_000,
|
||||
});
|
||||
const primary = (label: string) => page.getByRole("button", { name: label, exact: true });
|
||||
|
||||
try {
|
||||
await openAppAsNewUser(page, host);
|
||||
await step("Welcome to Paseo");
|
||||
await primary("Continue").click();
|
||||
await step("Your agents");
|
||||
await primary("Continue").click();
|
||||
await step("Add your projects");
|
||||
await primary("Continue").click();
|
||||
|
||||
// The setup host starts with Paseo tools off; the switch starts on.
|
||||
expect(await hostGivesAgentsPaseoTools(host)).toBe(false);
|
||||
await step("Let agents use Paseo");
|
||||
await expect(paseoToolsSwitch(page)).toBeChecked();
|
||||
await primary("Finish").click();
|
||||
|
||||
await expect.poll(() => hostGivesAgentsPaseoTools(host), { timeout: 30_000 }).toBe(true);
|
||||
await expect(page.getByRole("heading", { name: "Let agents use Paseo" })).toHaveCount(0);
|
||||
} finally {
|
||||
await host.close();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -3,7 +3,9 @@ import { mkdir, mkdtemp, rm, utimes, writeFile } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import path from "node:path";
|
||||
import { expect, type Locator, type Page } from "@playwright/test";
|
||||
import type { DaemonClient as InternalDaemonClient } from "@getpaseo/client/internal/daemon-client";
|
||||
import { addProjectFlow, addProjectFlowInput, chooseAddProjectMethod } from "./add-project-flow";
|
||||
import { connectDaemonClient } from "./daemon-client-loader";
|
||||
import { buildCreateAgentPreferences, buildSeededHost } from "./daemon-registry";
|
||||
import { startIsolatedHostDaemon, type IsolatedHostDaemonOptions } from "./isolated-host-daemon";
|
||||
|
||||
@@ -80,6 +82,18 @@ export function paseoToolsSwitch(scope: Page | Locator): Locator {
|
||||
return scope.getByRole("switch", { name: "Inject Paseo tools", exact: true });
|
||||
}
|
||||
|
||||
/** Whether the host gives its agents Paseo tools, read from the host itself. */
|
||||
export async function hostGivesAgentsPaseoTools(host: { port: number }): Promise<boolean> {
|
||||
const client = await connectDaemonClient<
|
||||
Pick<InternalDaemonClient, "connect" | "close" | "getDaemonConfig">
|
||||
>({ clientIdPrefix: "app-e2e-onboarding", port: host.port });
|
||||
try {
|
||||
return (await client.getDaemonConfig()).config.mcp.injectIntoAgents !== false;
|
||||
} finally {
|
||||
await client.close();
|
||||
}
|
||||
}
|
||||
|
||||
export async function finishSetup(page: Page): Promise<void> {
|
||||
await setupModal(page).getByRole("button", { name: "Finish", exact: true }).click();
|
||||
await expect(setupModal(page)).toHaveCount(0);
|
||||
|
||||
@@ -10,6 +10,7 @@ import { useToast } from "@/contexts/toast-context";
|
||||
import { useFetchQuery } from "@/data/query";
|
||||
import { useHostFeature } from "@/runtime/host-features";
|
||||
import { useHostRuntimeClient, useHostRuntimeIsConnected } from "@/runtime/host-runtime";
|
||||
import { confirmDialog } from "@/utils/confirm-dialog";
|
||||
|
||||
export type AgentSkillsController = ReturnType<typeof useAgentSkills>;
|
||||
|
||||
@@ -94,14 +95,21 @@ export function useAgentSkills(serverId: string) {
|
||||
),
|
||||
[reconcileAsync],
|
||||
);
|
||||
const runUninstall = useCallback(
|
||||
() =>
|
||||
uninstallAsync().then(
|
||||
(status) => status.state === "not-installed",
|
||||
() => false,
|
||||
),
|
||||
[uninstallAsync],
|
||||
);
|
||||
// Uninstalling deletes whole skill folders, including anything the user added inside them, so
|
||||
// it always asks first. Declining resolves false, as a failure does.
|
||||
const runUninstall = useCallback(async () => {
|
||||
const confirmed = await confirmDialog({
|
||||
title: t("settings.host.skills.uninstallTitle"),
|
||||
message: t("settings.host.skills.uninstallMessage"),
|
||||
confirmLabel: t("settings.host.skills.actions.uninstall"),
|
||||
destructive: true,
|
||||
});
|
||||
if (!confirmed) return false;
|
||||
return uninstallAsync().then(
|
||||
(status) => status.state === "not-installed",
|
||||
() => false,
|
||||
);
|
||||
}, [t, uninstallAsync]);
|
||||
const saveSelection = useCallback(
|
||||
(selection: AgentSkillSelection, confirmedRemovals: readonly string[] = []) =>
|
||||
saveAsync({ selection, confirmedRemovals }),
|
||||
|
||||
@@ -56,6 +56,9 @@ export function CommunityLinks() {
|
||||
const styles = StyleSheet.create(() => ({
|
||||
row: {
|
||||
flexDirection: "row",
|
||||
// Shrinks and wraps rather than running off a narrow phone screen.
|
||||
flexShrink: 1,
|
||||
flexWrap: "wrap",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
gap: 0,
|
||||
|
||||
@@ -41,6 +41,7 @@ export function MessagePreview({ children }: { children: ReactNode }) {
|
||||
{overflowing ? (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
style={[styles.toggle, expanded && styles.expandedToggle]}
|
||||
accessibilityState={accessibilityState}
|
||||
onPress={toggle}
|
||||
@@ -76,6 +77,7 @@ export function MessageSender({
|
||||
return (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
leftIcon={renderProviderIcon}
|
||||
style={styles.sender}
|
||||
textStyle={styles.senderText}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useCallback } from "react";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useToast } from "@/contexts/toast-context";
|
||||
import {
|
||||
getCliInstallStatus,
|
||||
installCli,
|
||||
@@ -21,7 +22,10 @@ interface CliInstall {
|
||||
status: InstallStatus | null;
|
||||
isLoading: boolean;
|
||||
isWorking: boolean;
|
||||
/** Installs or removes the CLI; resolves to whether it worked. Failures are reported. */
|
||||
/**
|
||||
* Installs or removes the CLI; resolves to whether it worked. Failures are reported. Removing
|
||||
* keeps a paseo command this app did not install, says so, and resolves true.
|
||||
*/
|
||||
setInstalled: (installed: boolean) => Promise<boolean>;
|
||||
refresh: () => void;
|
||||
}
|
||||
@@ -30,6 +34,7 @@ export function useCliInstall(): CliInstall {
|
||||
const { t } = useTranslation();
|
||||
const queryClient = useQueryClient();
|
||||
const reportError = useDesktopIpcErrorReporter();
|
||||
const toast = useToast();
|
||||
const available = shouldUseDesktopDaemon();
|
||||
|
||||
const statusQuery = useQuery<InstallStatus, Error>({
|
||||
@@ -64,13 +69,21 @@ export function useCliInstall(): CliInstall {
|
||||
});
|
||||
const { mutateAsync, isPending } = mutation;
|
||||
|
||||
// Removing only ever deletes the CLI this app installed. A paseo command put there some other way
|
||||
// stays, and that still counts as done: the user is told rather than left with a switch that
|
||||
// silently won't turn off.
|
||||
const setInstalled = useCallback(
|
||||
(installed: boolean) =>
|
||||
mutateAsync(installed).then(
|
||||
(status) => status.installed === installed,
|
||||
(status) => {
|
||||
if (status.installed === installed) return true;
|
||||
if (installed) return false;
|
||||
toast.show(t("desktop.integrations.cli.keptForeign"));
|
||||
return true;
|
||||
},
|
||||
() => false,
|
||||
),
|
||||
[mutateAsync],
|
||||
[mutateAsync, t, toast],
|
||||
);
|
||||
const refresh = useCallback(() => {
|
||||
void refetch();
|
||||
|
||||
@@ -1542,6 +1542,7 @@ export const ar: TranslationResources = {
|
||||
statusFailed: "غير قادر على التحقق من حالة تثبيت CLI.",
|
||||
installFailed: "غير قادر على تثبيت PaseoCLI.",
|
||||
uninstallFailed: "تعذر إلغاء تثبيت Paseo CLI.",
|
||||
keptForeign: "تم الإبقاء على أمر paseo لم يثبته Paseo. احذفه بنفسك لإلغاء تثبيته.",
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -2788,6 +2789,8 @@ export const ar: TranslationResources = {
|
||||
description: "علّم وكلاءك كيفية استخدام أدوات Paseo",
|
||||
updateAvailable: "التحديث متاح",
|
||||
updateTitle: "تحديث مهارات Paseo ؟",
|
||||
uninstallTitle: "إلغاء تثبيت مهارات Paseo ؟",
|
||||
uninstallMessage: "يزيل جميع مهارات تنسيق Paseo من ~/.agents ، ~/.claude ، ~/.codex.",
|
||||
choose: "اختيار المهارات",
|
||||
chooseAll: "كل المهارات",
|
||||
chooseAllHint: "أبقِ جميع المهارات المرفقة مثبتة، بما فيها ما يُضاف لاحقًا.",
|
||||
@@ -2805,6 +2808,7 @@ export const ar: TranslationResources = {
|
||||
update: "تحديث",
|
||||
working: "عمل...",
|
||||
remove: "إزالة",
|
||||
uninstall: "إلغاء التثبيت",
|
||||
save: "حفظ",
|
||||
saving: "جارٍ الحفظ...",
|
||||
cancel: "يلغي",
|
||||
@@ -2822,6 +2826,7 @@ export const ar: TranslationResources = {
|
||||
title: "تمكين أدوات Paseo",
|
||||
hint: "سيتمكن الوكلاء من إدارة أشجار العمل والوكلاء والجداول الزمنية",
|
||||
accessibilityLabel: "حقن أدوات Paseo",
|
||||
updateFailed: "تعذر تحديث أدوات Paseo.",
|
||||
},
|
||||
systemPrompt: {
|
||||
title: "موجه النظام",
|
||||
|
||||
@@ -1566,6 +1566,8 @@ export const en = {
|
||||
statusFailed: "Unable to check CLI install status.",
|
||||
installFailed: "Unable to install the Paseo CLI.",
|
||||
uninstallFailed: "Unable to uninstall the Paseo CLI.",
|
||||
keptForeign:
|
||||
"Kept a paseo command that Paseo didn't install. Remove it yourself to uninstall it.",
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -2883,6 +2885,9 @@ export const en = {
|
||||
saveSelectionFailed: "Unable to save the orchestration skills selection.",
|
||||
updateAvailable: "Update available",
|
||||
updateTitle: "Update Paseo skills?",
|
||||
uninstallTitle: "Uninstall Paseo skills?",
|
||||
uninstallMessage:
|
||||
"Removes all Paseo orchestration skills from ~/.agents, ~/.claude, ~/.codex on this host.",
|
||||
choose: "Choose skills",
|
||||
chooseAll: "All skills",
|
||||
chooseAllHint: "Keep every bundled skill installed, including ones added later.",
|
||||
@@ -2896,6 +2901,7 @@ export const en = {
|
||||
update: "Update",
|
||||
working: "Working...",
|
||||
remove: "Remove",
|
||||
uninstall: "Uninstall",
|
||||
save: "Save",
|
||||
saving: "Saving...",
|
||||
cancel: "Cancel",
|
||||
@@ -2913,6 +2919,7 @@ export const en = {
|
||||
title: "Enable Paseo tools",
|
||||
hint: "Agents will be able to manage worktrees, agents and schedules",
|
||||
accessibilityLabel: "Inject Paseo tools",
|
||||
updateFailed: "Unable to update Paseo tools.",
|
||||
},
|
||||
systemPrompt: {
|
||||
title: "System prompt",
|
||||
|
||||
@@ -1587,6 +1587,8 @@ export const es: TranslationResources = {
|
||||
statusFailed: "No se puede verificar el estado de instalación deCLI.",
|
||||
installFailed: "No se puede instalar elPaseoCLI.",
|
||||
uninstallFailed: "No se pudo desinstalar la CLI de Paseo.",
|
||||
keptForeign:
|
||||
"Se conservó un comando paseo que Paseo no instaló. Elimínalo tú mismo para desinstalarlo.",
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -2809,6 +2811,9 @@ export const es: TranslationResources = {
|
||||
description: "Enseña a tus agentes a usar las herramientas de Paseo",
|
||||
updateAvailable: "Actualización disponible",
|
||||
updateTitle: "¿Actualizar las habilidades dePaseo?",
|
||||
uninstallTitle: "¿Desinstalar las habilidadesPaseo?",
|
||||
uninstallMessage:
|
||||
"Elimina todas las habilidades de orquestaciónPaseode ~/.agents, ~/.claude, ~/.codex.",
|
||||
choose: "Elegir habilidades",
|
||||
chooseAll: "Todas las habilidades",
|
||||
chooseAllHint:
|
||||
@@ -2827,6 +2832,7 @@ export const es: TranslationResources = {
|
||||
update: "Actualizar",
|
||||
working: "Laboral...",
|
||||
remove: "Eliminar",
|
||||
uninstall: "Desinstalar",
|
||||
save: "Guardar",
|
||||
saving: "Guardando...",
|
||||
cancel: "Cancelar",
|
||||
@@ -2844,6 +2850,7 @@ export const es: TranslationResources = {
|
||||
title: "Habilitar herramientasPaseo",
|
||||
hint: "Los agentes podrán gestionar árboles de trabajo, agentes y horarios.",
|
||||
accessibilityLabel: "Inyectar herramientasPaseo",
|
||||
updateFailed: "No se pudieron actualizar las herramientas de Paseo.",
|
||||
},
|
||||
systemPrompt: {
|
||||
title: "Aviso del sistema",
|
||||
|
||||
@@ -1583,6 +1583,8 @@ export const fr: TranslationResources = {
|
||||
statusFailed: "Impossible de vérifier l’installation de la CLI.",
|
||||
installFailed: "Impossible d’installer la CLI Paseo.",
|
||||
uninstallFailed: "Impossible de désinstaller la CLI Paseo.",
|
||||
keptForeign:
|
||||
"Une commande paseo qui n’a pas été installée par Paseo a été conservée. Supprimez-la vous-même pour la désinstaller.",
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -2813,6 +2815,9 @@ export const fr: TranslationResources = {
|
||||
description: "Apprenez à vos agents à utiliser les outils Paseo",
|
||||
updateAvailable: "Mise à jour disponible",
|
||||
updateTitle: "Mettre à jour les skills Paseo ?",
|
||||
uninstallTitle: "Désinstaller les skills Paseo ?",
|
||||
uninstallMessage:
|
||||
"Supprime tous les skills d’orchestration Paseo de ~/.agents, ~/.claude et ~/.codex sur cet hôte.",
|
||||
choose: "Choisir les skills",
|
||||
chooseAll: "Tous les skills",
|
||||
chooseAllHint:
|
||||
@@ -2831,6 +2836,7 @@ export const fr: TranslationResources = {
|
||||
update: "Mettre à jour",
|
||||
working: "En cours…",
|
||||
remove: "Supprimer",
|
||||
uninstall: "Désinstaller",
|
||||
save: "Enregistrer",
|
||||
saving: "Enregistrement…",
|
||||
cancel: "Annuler",
|
||||
@@ -2848,6 +2854,7 @@ export const fr: TranslationResources = {
|
||||
title: "Activer les outils Paseo",
|
||||
hint: "Les agents pourront gérer les worktrees, les agents et les planifications",
|
||||
accessibilityLabel: "Injecter les outils Paseo",
|
||||
updateFailed: "Impossible de mettre à jour les outils Paseo.",
|
||||
},
|
||||
systemPrompt: {
|
||||
title: "Prompt système",
|
||||
|
||||
@@ -1558,6 +1558,8 @@ export const ja: TranslationResources = {
|
||||
statusFailed: "CLIのインストール状態を確認できません。",
|
||||
installFailed: "Paseo CLIをインストールできません。",
|
||||
uninstallFailed: "Paseo CLI をアンインストールできません。",
|
||||
keptForeign:
|
||||
"Paseoがインストールしていないpaseoコマンドは残しました。アンインストールするには手動で削除してください。",
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -2777,6 +2779,9 @@ export const ja: TranslationResources = {
|
||||
description: "エージェントに Paseo ツールの使い方を教えます",
|
||||
updateAvailable: "更新が利用可能",
|
||||
updateTitle: "Paseoスキルを更新しますか?",
|
||||
uninstallTitle: "Paseoスキルをアンインストールしますか?",
|
||||
uninstallMessage:
|
||||
"~/.agents、~/.claude、~/.codexからすべてのPaseoオーケストレーションスキルを削除します。",
|
||||
choose: "スキルを選択",
|
||||
chooseAll: "すべてのスキル",
|
||||
chooseAllHint:
|
||||
@@ -2795,6 +2800,7 @@ export const ja: TranslationResources = {
|
||||
update: "更新",
|
||||
working: "処理中...",
|
||||
remove: "削除",
|
||||
uninstall: "アンインストール",
|
||||
save: "保存",
|
||||
saving: "保存中...",
|
||||
cancel: "キャンセル",
|
||||
@@ -2812,6 +2818,7 @@ export const ja: TranslationResources = {
|
||||
title: "Paseoツールを有効にする",
|
||||
hint: "エージェントがワークツリー、エージェント、スケジュールを管理できるようになります",
|
||||
accessibilityLabel: "Paseoツールを有効にする",
|
||||
updateFailed: "Paseoツールを更新できませんでした。",
|
||||
},
|
||||
systemPrompt: {
|
||||
title: "システムプロンプト",
|
||||
|
||||
@@ -1551,6 +1551,8 @@ export const ko: TranslationResources = {
|
||||
statusFailed: "CLI 설치 상태를 확인할 수 없습니다.",
|
||||
installFailed: "Paseo CLI를 설치할 수 없습니다.",
|
||||
uninstallFailed: "Paseo CLI를 제거할 수 없습니다.",
|
||||
keptForeign:
|
||||
"Paseo가 설치하지 않은 paseo 명령은 그대로 두었습니다. 제거하려면 직접 삭제하세요.",
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -2764,6 +2766,9 @@ export const ko: TranslationResources = {
|
||||
description: "에이전트에게 Paseo 도구 사용법을 알려 줍니다",
|
||||
updateAvailable: "업데이트 사용 가능",
|
||||
updateTitle: "Paseo 스킬을 업데이트할까요?",
|
||||
uninstallTitle: "Paseo 스킬을 제거할까요?",
|
||||
uninstallMessage:
|
||||
"~/.agents, ~/.claude, ~/.codex에서 모든 Paseo 오케스트레이션 스킬을 제거합니다.",
|
||||
choose: "스킬 선택",
|
||||
chooseAll: "모든 스킬",
|
||||
chooseAllHint: "나중에 추가되는 항목을 포함하여 모든 번들 스킬을 설치된 상태로 유지합니다.",
|
||||
@@ -2781,6 +2786,7 @@ export const ko: TranslationResources = {
|
||||
update: "업데이트",
|
||||
working: "작업 중...",
|
||||
remove: "제거",
|
||||
uninstall: "제거",
|
||||
save: "저장",
|
||||
saving: "저장 중...",
|
||||
cancel: "취소",
|
||||
@@ -2798,6 +2804,7 @@ export const ko: TranslationResources = {
|
||||
title: "Paseo 도구 활성화",
|
||||
hint: "에이전트가 워크트리, 에이전트, 일정을 관리할 수 있습니다",
|
||||
accessibilityLabel: "Paseo 도구 주입",
|
||||
updateFailed: "Paseo 도구를 업데이트할 수 없습니다.",
|
||||
},
|
||||
systemPrompt: {
|
||||
title: "시스템 프롬프트",
|
||||
|
||||
@@ -1572,6 +1572,8 @@ export const ptBR: TranslationResources = {
|
||||
statusFailed: "Não foi possível verificar o status de instalação da CLI.",
|
||||
installFailed: "Não foi possível instalar a CLI do Paseo.",
|
||||
uninstallFailed: "Não foi possível desinstalar a CLI do Paseo.",
|
||||
keptForeign:
|
||||
"Um comando paseo que o Paseo não instalou foi mantido. Remova-o manualmente para desinstalar.",
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -2790,6 +2792,9 @@ export const ptBR: TranslationResources = {
|
||||
description: "Ensine seus agentes a usar as ferramentas do Paseo",
|
||||
updateAvailable: "Atualização disponível",
|
||||
updateTitle: "Atualizar Paseo skills?",
|
||||
uninstallTitle: "Desinstalar Paseo skills?",
|
||||
uninstallMessage:
|
||||
"Remove todas as skills de orquestração do Paseo de ~/.agents, ~/.claude e ~/.codex.",
|
||||
choose: "Escolher skills",
|
||||
chooseAll: "Todas as skills",
|
||||
chooseAllHint:
|
||||
@@ -2808,6 +2813,7 @@ export const ptBR: TranslationResources = {
|
||||
update: "Atualizar",
|
||||
working: "Trabalhando...",
|
||||
remove: "Remover",
|
||||
uninstall: "Desinstalar",
|
||||
save: "Salvar",
|
||||
saving: "Salvando...",
|
||||
cancel: "Cancelar",
|
||||
@@ -2825,6 +2831,7 @@ export const ptBR: TranslationResources = {
|
||||
title: "Ativar ferramentas do Paseo",
|
||||
hint: "Agentes poderão gerenciar worktrees, agentes e agendamentos",
|
||||
accessibilityLabel: "Injetar ferramentas do Paseo",
|
||||
updateFailed: "Não foi possível atualizar as ferramentas do Paseo.",
|
||||
},
|
||||
systemPrompt: {
|
||||
title: "Prompt do sistema",
|
||||
|
||||
@@ -1569,6 +1569,8 @@ export const ru: TranslationResources = {
|
||||
statusFailed: "Не удалось проверить состояние установки CLI.",
|
||||
installFailed: "Не удалось установить Paseo CLI.",
|
||||
uninstallFailed: "Не удалось удалить Paseo CLI.",
|
||||
keptForeign:
|
||||
"Команда paseo, установленная не Paseo, оставлена. Чтобы удалить её, сделайте это вручную.",
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -2802,6 +2804,9 @@ export const ru: TranslationResources = {
|
||||
unsupported: "Обновите этот хост, чтобы управлять навыками оркестрации",
|
||||
updateAvailable: "Доступно обновление",
|
||||
updateTitle: "Обновить навыки Paseo?",
|
||||
uninstallTitle: "Удалить навыки Paseo?",
|
||||
uninstallMessage:
|
||||
"Удаляет все навыки оркестрации Paseo из ~/.agents, ~/.claude и ~/.codex на этом хосте.",
|
||||
choose: "Выбрать навыки",
|
||||
chooseAll: "Все навыки",
|
||||
chooseAllHint: "Устанавливать все встроенные навыки, включая добавленные позже.",
|
||||
@@ -2819,6 +2824,7 @@ export const ru: TranslationResources = {
|
||||
update: "Обновить",
|
||||
working: "Выполняется...",
|
||||
remove: "Удалить",
|
||||
uninstall: "Удалить",
|
||||
save: "Сохранить",
|
||||
saving: "Сохранение...",
|
||||
cancel: "Отмена",
|
||||
@@ -2836,6 +2842,7 @@ export const ru: TranslationResources = {
|
||||
title: "Включить инструменты Paseo",
|
||||
hint: "Агенты смогут управлять worktree, агентами и расписаниями.",
|
||||
accessibilityLabel: "Добавить инструменты Paseo",
|
||||
updateFailed: "Не удалось обновить инструменты Paseo.",
|
||||
},
|
||||
systemPrompt: {
|
||||
title: "Системный промпт",
|
||||
|
||||
@@ -1526,6 +1526,7 @@ export const zhCN: TranslationResources = {
|
||||
statusFailed: "无法检查 CLI 安装状态。",
|
||||
installFailed: "无法安装 Paseo CLI。",
|
||||
uninstallFailed: "无法卸载 Paseo CLI。",
|
||||
keptForeign: "已保留一个不是由 Paseo 安装的 paseo 命令。如需卸载,请手动删除。",
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -2722,6 +2723,8 @@ export const zhCN: TranslationResources = {
|
||||
description: "教你的代理如何使用 Paseo 工具",
|
||||
updateAvailable: "有更新可用",
|
||||
updateTitle: "更新 Paseo skills?",
|
||||
uninstallTitle: "卸载 Paseo skills?",
|
||||
uninstallMessage: "会从 ~/.agents、~/.claude、~/.codex 移除所有 Paseo 编排 skills。",
|
||||
choose: "选择 skills",
|
||||
chooseAll: "全部 skills",
|
||||
chooseAllHint: "保持安装所有内置 skills,包括以后新增的。",
|
||||
@@ -2739,6 +2742,7 @@ export const zhCN: TranslationResources = {
|
||||
update: "更新",
|
||||
working: "处理中...",
|
||||
remove: "移除",
|
||||
uninstall: "卸载",
|
||||
save: "保存",
|
||||
saving: "正在保存...",
|
||||
cancel: "取消",
|
||||
@@ -2756,6 +2760,7 @@ export const zhCN: TranslationResources = {
|
||||
title: "启用 Paseo tools",
|
||||
hint: "Agent 将能够管理 worktree、Agent 和计划",
|
||||
accessibilityLabel: "注入 Paseo tools",
|
||||
updateFailed: "无法更新 Paseo 工具。",
|
||||
},
|
||||
systemPrompt: {
|
||||
title: "System prompt",
|
||||
|
||||
@@ -42,24 +42,27 @@ export function OnboardingModal({ model, state }: { model: OnboardingModel; stat
|
||||
}, [model]);
|
||||
const handleBack = useCallback(() => model.back(), [model]);
|
||||
const { visit } = state;
|
||||
const handleNext = useCallback(() => model.next(visit), [model, visit]);
|
||||
// Moving on from the last step finishes setup; any step's own work runs before that.
|
||||
const handleAdvance = useCallback(() => {
|
||||
if (isLast) void model.complete();
|
||||
else model.next(visit);
|
||||
}, [isLast, model, visit]);
|
||||
const handleAddSuggestions = useCallback(() => {
|
||||
void (async () => {
|
||||
if (await addChecked()) model.next(visit);
|
||||
if (await addChecked()) handleAdvance();
|
||||
})();
|
||||
}, [addChecked, model, visit]);
|
||||
}, [addChecked, handleAdvance]);
|
||||
const handleApplyOrchestration = useCallback(() => {
|
||||
void (async () => {
|
||||
if (await applyOrchestration()) model.next(visit);
|
||||
if (await applyOrchestration()) handleAdvance();
|
||||
})();
|
||||
}, [applyOrchestration, model, visit]);
|
||||
}, [applyOrchestration, handleAdvance]);
|
||||
const primary = usePrimaryAction({
|
||||
step,
|
||||
isLast,
|
||||
projectSuggestions,
|
||||
orchestration,
|
||||
onFinish: handleComplete,
|
||||
onNext: handleNext,
|
||||
onAdvance: handleAdvance,
|
||||
onAddSuggestions: handleAddSuggestions,
|
||||
onApplyOrchestration: handleApplyOrchestration,
|
||||
});
|
||||
@@ -150,15 +153,15 @@ export function OnboardingModal({ model, state }: { model: OnboardingModel; stat
|
||||
/**
|
||||
* The footer's main button. On Projects, with folders checked, it adds them and moves on only once
|
||||
* every one was added. On Orchestration it applies the switches and moves on once they all took.
|
||||
* Otherwise it continues, or finishes on the last step.
|
||||
* Otherwise it moves on. Moving on from the last step finishes setup, so that step's work still
|
||||
* runs first; Orchestration is last in the compact layout.
|
||||
*/
|
||||
function usePrimaryAction(input: {
|
||||
step: OnboardingStepId;
|
||||
isLast: boolean;
|
||||
projectSuggestions: ProjectSuggestionsController;
|
||||
orchestration: OrchestrationSetupController;
|
||||
onFinish: () => void;
|
||||
onNext: () => void;
|
||||
onAdvance: () => void;
|
||||
onAddSuggestions: () => void;
|
||||
onApplyOrchestration: () => void;
|
||||
}): { label: string; busy: boolean; onPress: () => void } {
|
||||
@@ -168,15 +171,12 @@ function usePrimaryAction(input: {
|
||||
isLast,
|
||||
projectSuggestions,
|
||||
orchestration,
|
||||
onFinish,
|
||||
onNext,
|
||||
onAdvance,
|
||||
onAddSuggestions,
|
||||
onApplyOrchestration,
|
||||
} = input;
|
||||
const { checkedCount, isAdding } = projectSuggestions;
|
||||
if (isLast) {
|
||||
return { label: t("onboarding.setup.finish"), busy: false, onPress: onFinish };
|
||||
}
|
||||
const advanceLabel = isLast ? t("onboarding.setup.finish") : t("onboarding.setup.continue");
|
||||
// Rows leave the list as they are added, so the count can reach zero while adding.
|
||||
if (step === "projects" && (isAdding || checkedCount > 0)) {
|
||||
return {
|
||||
@@ -188,13 +188,9 @@ function usePrimaryAction(input: {
|
||||
};
|
||||
}
|
||||
if (step === "orchestration") {
|
||||
return {
|
||||
label: t("onboarding.setup.continue"),
|
||||
busy: orchestration.isApplying,
|
||||
onPress: onApplyOrchestration,
|
||||
};
|
||||
return { label: advanceLabel, busy: orchestration.isApplying, onPress: onApplyOrchestration };
|
||||
}
|
||||
return { label: t("onboarding.setup.continue"), busy: false, onPress: onNext };
|
||||
return { label: advanceLabel, busy: false, onPress: onAdvance };
|
||||
}
|
||||
|
||||
function StepRail({
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { useCallback, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { areSkillsInstalled, useAgentSkills, type AgentSkillsController } from "@/agent-skills";
|
||||
import { useToast } from "@/contexts/toast-context";
|
||||
import { useCliInstall } from "@/desktop/hooks/use-install-status";
|
||||
import { useDaemonConfig } from "@/hooks/use-daemon-config";
|
||||
|
||||
@@ -16,7 +18,10 @@ export interface OrchestrationSetupController {
|
||||
/** The skills row's picker and Update act on the host directly, through this. */
|
||||
skills: AgentSkillsController;
|
||||
isApplying: boolean;
|
||||
/** Brings the host in line with the choices; resolves true only when every one took. */
|
||||
/**
|
||||
* Brings the host in line with the choices; resolves true only when every one took. Each failure
|
||||
* is shown to the user.
|
||||
*/
|
||||
apply: () => Promise<boolean>;
|
||||
}
|
||||
|
||||
@@ -25,6 +30,8 @@ export interface OrchestrationSetupController {
|
||||
* computer until Continue applies them, the way checked projects are added.
|
||||
*/
|
||||
export function useOrchestrationSetup(serverId: string): OrchestrationSetupController {
|
||||
const { t } = useTranslation();
|
||||
const toast = useToast();
|
||||
const skills = useAgentSkills(serverId);
|
||||
const cli = useCliInstall();
|
||||
const { config, patchConfig } = useDaemonConfig(serverId);
|
||||
@@ -40,27 +47,39 @@ export function useOrchestrationSetup(serverId: string): OrchestrationSetupContr
|
||||
setChoices((current) => ({ ...current, [choice]: on }));
|
||||
}, []);
|
||||
|
||||
const toolsOn = config?.mcp.injectIntoAgents !== false;
|
||||
const skillsInstalled = areSkillsInstalled(skills.status);
|
||||
const cliInstalled = cli.status?.installed ?? false;
|
||||
// What the host and this computer have now, or null while it is still being read. A choice is
|
||||
// skipped only when it is known to match; an unknown state is always written.
|
||||
const toolsOn = config ? config.mcp.injectIntoAgents !== false : null;
|
||||
const skillsInstalled = skills.status ? areSkillsInstalled(skills.status) : null;
|
||||
const cliInstalled = cli.status ? cli.status.installed : null;
|
||||
const { reconcile, uninstall } = skills;
|
||||
const { setInstalled: setCliInstalled } = cli;
|
||||
const offersSkills = offered.skills;
|
||||
const offersCli = offered.cli;
|
||||
|
||||
const setTools = useCallback(
|
||||
(on: boolean) =>
|
||||
patchConfig({ mcp: { injectIntoAgents: on } })
|
||||
.then(Boolean, () => false)
|
||||
.then((done) => {
|
||||
if (!done) toast.error(t("settings.host.orchestration.enableTools.updateFailed"));
|
||||
return done;
|
||||
}),
|
||||
[patchConfig, t, toast],
|
||||
);
|
||||
const setSkillsInstalled = useCallback(
|
||||
() => (choices.skills ? reconcile() : uninstall()),
|
||||
[choices.skills, reconcile, uninstall],
|
||||
(on: boolean) => (on ? reconcile() : uninstall()),
|
||||
[reconcile, uninstall],
|
||||
);
|
||||
|
||||
const apply = useCallback(async () => {
|
||||
setIsApplying(true);
|
||||
try {
|
||||
const results = await Promise.all([
|
||||
choices.tools === toolsOn
|
||||
choices.tools === toolsOn ? true : setTools(choices.tools),
|
||||
!offersSkills || choices.skills === skillsInstalled
|
||||
? true
|
||||
: patchConfig({ mcp: { injectIntoAgents: choices.tools } }).then(Boolean, () => false),
|
||||
!offersSkills || choices.skills === skillsInstalled ? true : setSkillsInstalled(),
|
||||
: setSkillsInstalled(choices.skills),
|
||||
!offersCli || choices.cli === cliInstalled ? true : setCliInstalled(choices.cli),
|
||||
]);
|
||||
return results.every(Boolean);
|
||||
@@ -70,7 +89,7 @@ export function useOrchestrationSetup(serverId: string): OrchestrationSetupContr
|
||||
}, [
|
||||
choices,
|
||||
toolsOn,
|
||||
patchConfig,
|
||||
setTools,
|
||||
offersSkills,
|
||||
skillsInstalled,
|
||||
setSkillsInstalled,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useCallback, useMemo, useState } from "react";
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import { useOpenProject } from "@/hooks/use-open-project";
|
||||
import { useProjectRootPaths } from "@/stores/session-store-hooks";
|
||||
@@ -34,7 +34,10 @@ export interface ProjectSuggestionsController {
|
||||
list: SuggestionList;
|
||||
checkedCount: number;
|
||||
isAdding: boolean;
|
||||
/** Adds every checked folder. Resolves true when all of them were added. */
|
||||
/**
|
||||
* Adds every checked folder. Resolves true when all of them were added and no new row arrived
|
||||
* checked meanwhile.
|
||||
*/
|
||||
addChecked: () => Promise<boolean>;
|
||||
}
|
||||
|
||||
@@ -77,9 +80,17 @@ export function useProjectSuggestions(serverId: string): ProjectSuggestionsContr
|
||||
mutationFn: (rootPaths: string[]) => addSuggestedProjects(rootPaths, openProject),
|
||||
onSuccess: setAddFailures,
|
||||
});
|
||||
// A provider can answer while the add runs and bring in rows that start checked. Those were not
|
||||
// part of the add, so the step stays put and offers to add them rather than moving on.
|
||||
const latestChecked = useRef(checkedRootPaths);
|
||||
useEffect(() => {
|
||||
latestChecked.current = checkedRootPaths;
|
||||
}, [checkedRootPaths]);
|
||||
const addChecked = useCallback(async () => {
|
||||
const attempted = new Set(checkedRootPaths);
|
||||
const failures = await mutateAsync(checkedRootPaths);
|
||||
return failures.size === 0;
|
||||
const arrivedChecked = latestChecked.current.some((rootPath) => !attempted.has(rootPath));
|
||||
return failures.size === 0 && !arrivedChecked;
|
||||
}, [mutateAsync, checkedRootPaths]);
|
||||
|
||||
const list = useMemo((): SuggestionList => {
|
||||
|
||||
Reference in New Issue
Block a user