diff --git a/docs/development.md b/docs/development.md index 0a6674460..3d994ed28 100644 --- a/docs/development.md +++ b/docs/development.md @@ -303,6 +303,8 @@ npm run build:app-deps # highlight -> protocol -> client -> expo-two-way-aud Use `npm run build:server` whenever you have changed any daemon/server-facing package and need clean cross-package types or runtime behavior. +The app Metro config disables Watchman and uses Metro's node crawler for exports. Keep that invariant unless you have verified production app exports on machines with and without Watchman installed; distro Watchman builds can differ in capabilities and change Metro's crawl behavior. + For tighter loops, you can rebuild a single workspace: - Changed `packages/protocol/src/*` or `packages/client/src/*`: `npm run build:client`. diff --git a/packages/app/metro.config.cjs b/packages/app/metro.config.cjs index 5a1169d86..59bb6ec7a 100644 --- a/packages/app/metro.config.cjs +++ b/packages/app/metro.config.cjs @@ -14,6 +14,12 @@ const customWebPlatform = (process.env.PASEO_WEB_PLATFORM ?? "") const config = getDefaultConfig(projectRoot); const defaultResolveRequest = config.resolver.resolveRequest ?? resolve; + +// Keep app exports deterministic across dev machines and CI. Metro's Watchman +// crawler behavior depends on the host Watchman build/capabilities, while the +// node crawler is the path used when Watchman is absent. +config.resolver.useWatchman = false; + const escapedAppSrcRoot = appSrcRoot .split(path.sep) .map((segment) => segment.replace(/[|\\{}()[\]^$+*?.]/g, "\\$&")) diff --git a/packages/app/src/agent-stream/view.tsx b/packages/app/src/agent-stream/view.tsx index a73a17358..6480686dc 100644 --- a/packages/app/src/agent-stream/view.tsx +++ b/packages/app/src/agent-stream/view.tsx @@ -52,6 +52,7 @@ import type { AgentScreenAgent } from "@/hooks/use-agent-screen-state-machine"; import { useSessionStore } from "@/stores/session-store"; import { useFileExplorerActions } from "@/hooks/use-file-explorer-actions"; import { useLoadOlderAgentHistory } from "@/hooks/use-load-older-agent-history"; +import { useSettings } from "@/hooks/use-settings"; import type { ToastApi } from "@/components/toast-host"; import type { DaemonClient } from "@getpaseo/client/internal/daemon-client"; import { ToolCallDetailsContent } from "@/components/tool-call-details"; @@ -318,6 +319,7 @@ const AgentStreamViewComponent = forwardRef settings.autoExpandReasoning); const viewportRef = useRef(null); const isMobile = useIsCompactFormFactor(); const streamRenderStrategy = useMemo( @@ -633,10 +635,12 @@ const AgentStreamViewComponent = forwardRef ); }, - [setInlineDetailsExpanded], + [autoExpandReasoning, setInlineDetailsExpanded], ); const renderToolCallItem = useCallback( diff --git a/packages/app/src/components/message.tsx b/packages/app/src/components/message.tsx index 6a8cf8922..6cdef2edd 100644 --- a/packages/app/src/components/message.tsx +++ b/packages/app/src/components/message.tsx @@ -3038,6 +3038,8 @@ interface ToolCallProps { onInlineDetailsHoverChange?: (hovered: boolean) => void; onInlineDetailsExpandedChange?: (expanded: boolean) => void; onOpenFilePath?: (filePath: string) => void; + defaultExpanded?: boolean; + forceInline?: boolean; } export const ToolCall = memo(function ToolCall({ @@ -3054,11 +3056,14 @@ export const ToolCall = memo(function ToolCall({ onInlineDetailsHoverChange, onInlineDetailsExpandedChange, onOpenFilePath, + defaultExpanded, + forceInline = false, }: ToolCallProps) { const { openToolCall } = useToolCallSheet(); - const [isExpanded, setIsExpanded] = useState(false); + const [isExpanded, setIsExpanded] = useState(defaultExpanded ?? false); const isMobile = useIsCompactFormFactor(); + const shouldRenderInline = !isMobile || forceInline; const effectiveDetail = useMemo(() => { if (detail) { @@ -3096,7 +3101,7 @@ export const ToolCall = memo(function ToolCall({ }, [presentation.openFilePath, onOpenFilePath]); const handleToggle = useCallback(() => { - if (isMobile) { + if (!shouldRenderInline) { openToolCall({ displayName: presentation.displayName, summary: presentation.summary, @@ -3109,7 +3114,7 @@ export const ToolCall = memo(function ToolCall({ setIsExpanded((prev) => !prev); } }, [ - isMobile, + shouldRenderInline, openToolCall, presentation.displayName, presentation.summary, @@ -3120,22 +3125,22 @@ export const ToolCall = memo(function ToolCall({ ]); useEffect(() => { - if (!onInlineDetailsHoverChange || isMobile || isExpanded) { + if (!onInlineDetailsHoverChange || !shouldRenderInline || isExpanded) { return; } onInlineDetailsHoverChange(false); - }, [isExpanded, isMobile, onInlineDetailsHoverChange]); + }, [isExpanded, shouldRenderInline, onInlineDetailsHoverChange]); useEffect(() => { if (!onInlineDetailsExpandedChange) { return; } - if (isMobile) { + if (!shouldRenderInline) { onInlineDetailsExpandedChange(false); return; } onInlineDetailsExpandedChange(isExpanded); - }, [isExpanded, isMobile, onInlineDetailsExpandedChange]); + }, [isExpanded, shouldRenderInline, onInlineDetailsExpandedChange]); useEffect(() => { if (!onInlineDetailsExpandedChange) { @@ -3148,7 +3153,7 @@ export const ToolCall = memo(function ToolCall({ // Render inline details for desktop const renderDetails = useCallback(() => { - if (isMobile) return null; + if (!shouldRenderInline) return null; return ( ); - }, [isMobile, effectiveDetail, presentation.errorText, presentation.isLoadingDetails]); + }, [shouldRenderInline, effectiveDetail, presentation.errorText, presentation.isLoadingDetails]); if (presentation.isPlan && effectiveDetail?.type === "plan") { return ( @@ -3175,10 +3180,10 @@ export const ToolCall = memo(function ToolCall({ label={presentation.displayName} secondaryLabel={presentation.summary} icon={presentation.icon} - isExpanded={!isMobile && isExpanded} + isExpanded={shouldRenderInline && isExpanded} onToggle={presentation.canOpenDetails ? handleToggle : undefined} onOpenFile={handleOpenFile} - renderDetails={presentation.canOpenDetails && !isMobile ? renderDetails : undefined} + renderDetails={presentation.canOpenDetails && shouldRenderInline ? renderDetails : undefined} isLoading={status === "running" || status === "executing"} isError={status === "failed"} isLastInSequence={isLastInSequence} @@ -3200,5 +3205,7 @@ function areToolCallPropsEqual(previous: ToolCallProps, next: ToolCallProps) { if (previous.isLastInSequence !== next.isLastInSequence) return false; if (previous.disableOuterSpacing !== next.disableOuterSpacing) return false; if (previous.onOpenFilePath !== next.onOpenFilePath) return false; + if (previous.defaultExpanded !== next.defaultExpanded) return false; + if (previous.forceInline !== next.forceInline) return false; return true; } diff --git a/packages/app/src/hooks/use-settings/index.ts b/packages/app/src/hooks/use-settings/index.ts index f492fac52..b1776783c 100644 --- a/packages/app/src/hooks/use-settings/index.ts +++ b/packages/app/src/hooks/use-settings/index.ts @@ -96,6 +96,8 @@ export interface UseSettingsReturn { resetSettings: () => Promise; } +type SettingsSelector = (settings: Settings) => TSelected; + export function useAppSettings(): UseAppSettingsReturn { const queryClient = useQueryClient(); const { data, isPending, error } = useQuery({ @@ -137,7 +139,11 @@ export function useAppSettings(): UseAppSettingsReturn { }; } -export function useSettings(): UseSettingsReturn { +export function useSettings(): UseSettingsReturn; +export function useSettings(selector: SettingsSelector): TSelected; +export function useSettings( + selector?: SettingsSelector, +): UseSettingsReturn | TSelected { const appSettings = useAppSettings(); const desktopSettings = useDesktopSettings(); @@ -177,6 +183,9 @@ export function useSettings(): UseSettingsReturn { if (updates.workspaceTitleSource !== undefined) { appUpdates.workspaceTitleSource = updates.workspaceTitleSource; } + if (updates.autoExpandReasoning !== undefined) { + appUpdates.autoExpandReasoning = updates.autoExpandReasoning; + } const promises: Promise[] = []; if (Object.keys(appUpdates).length > 0) { promises.push(appSettings.updateSettings(appUpdates)); @@ -210,13 +219,19 @@ export function useSettings(): UseSettingsReturn { await Promise.all(resets); }, [appSettings, desktopSettings]); + const settings = { + ...DEFAULT_APP_SETTINGS, + ...appSettings.settings, + manageBuiltInDaemon: desktopSettings.settings.daemon.manageBuiltInDaemon, + releaseChannel: desktopSettings.settings.releaseChannel, + }; + + if (selector) { + return selector(settings); + } + return { - settings: { - ...DEFAULT_APP_SETTINGS, - ...appSettings.settings, - manageBuiltInDaemon: desktopSettings.settings.daemon.manageBuiltInDaemon, - releaseChannel: desktopSettings.settings.releaseChannel, - }, + settings, isLoading: appSettings.isLoading || desktopSettings.isLoading, error: appSettings.error ?? desktopSettings.error, updateSettings, diff --git a/packages/app/src/hooks/use-settings/storage.ts b/packages/app/src/hooks/use-settings/storage.ts index 9bf897cef..116781227 100644 --- a/packages/app/src/hooks/use-settings/storage.ts +++ b/packages/app/src/hooks/use-settings/storage.ts @@ -39,6 +39,7 @@ export interface AppSettings { codeFontSize: number; // clamped px, default 12 syntaxTheme: SyntaxThemeId; // default "one" workspaceTitleSource: WorkspaceTitleSource; + autoExpandReasoning: boolean; } export interface Settings extends AppSettings { @@ -58,6 +59,7 @@ export const DEFAULT_CLIENT_SETTINGS: AppSettings = { codeFontSize: DEFAULT_CODE_FONT_SIZE, syntaxTheme: "one", workspaceTitleSource: "title", + autoExpandReasoning: false, }; export const DEFAULT_APP_SETTINGS: Settings = { @@ -204,6 +206,9 @@ function pickAppSettings(stored: Partial): Partial { ) { result.workspaceTitleSource = stored.workspaceTitleSource; } + if (typeof stored.autoExpandReasoning === "boolean") { + result.autoExpandReasoning = stored.autoExpandReasoning; + } return result; } diff --git a/packages/app/src/i18n/resources/ar.ts b/packages/app/src/i18n/resources/ar.ts index 22e3f5a5b..3f3deca5a 100644 --- a/packages/app/src/i18n/resources/ar.ts +++ b/packages/app/src/i18n/resources/ar.ts @@ -1471,6 +1471,10 @@ export const ar: TranslationResources = { description: "يتم الاحتفاظ بالخطوط في المخزن المؤقت الطرفي المدمج", accessibilityLabel: "خطوط التمرير Terminal", }, + autoExpandReasoning: { + label: "عرض التفكير دائماً", + description: "إظهار تفكير الوكيل وخطوات الاستدلال بشكل كامل بشكل افتراضي", + }, language: { label: "لغة", description: "لغة التطبيق", @@ -1552,6 +1556,9 @@ export const ar: TranslationResources = { auto: "نظام", }, }, + detailLevel: { + title: "مستوى التفاصيل", + }, fonts: { title: "الخطوط", systemDefault: "الافتراضي للنظام", diff --git a/packages/app/src/i18n/resources/en.ts b/packages/app/src/i18n/resources/en.ts index 6d5e66b02..3db321bbd 100644 --- a/packages/app/src/i18n/resources/en.ts +++ b/packages/app/src/i18n/resources/en.ts @@ -1478,6 +1478,10 @@ export const en = { description: "Lines kept in the built-in terminal buffer", accessibilityLabel: "Terminal scrollback lines", }, + autoExpandReasoning: { + label: "Always expand reasoning", + description: "Show agent thinking and chain-of-thought blocks fully expanded by default", + }, language: { label: "Language", description: "App language", @@ -1559,6 +1563,9 @@ export const en = { auto: "System", }, }, + detailLevel: { + title: "Detail level", + }, fonts: { title: "Fonts", systemDefault: "System default", diff --git a/packages/app/src/i18n/resources/es.ts b/packages/app/src/i18n/resources/es.ts index 4461abd9b..d4345c4d1 100644 --- a/packages/app/src/i18n/resources/es.ts +++ b/packages/app/src/i18n/resources/es.ts @@ -1511,6 +1511,11 @@ export const es: TranslationResources = { description: "Líneas mantenidas en el búfer de terminal incorporado", accessibilityLabel: "Líneas del historial de terminal", }, + autoExpandReasoning: { + label: "Siempre expandir razonamiento", + description: + "Mostrar los bloques de pensamiento y razonamiento del agente totalmente expandidos de forma predeterminada", + }, language: { label: "Idioma", description: "Idioma de la aplicación", @@ -1592,6 +1597,9 @@ export const es: TranslationResources = { auto: "Sistema", }, }, + detailLevel: { + title: "Nivel de detalle", + }, fonts: { title: "Fuentes", systemDefault: "Valor predeterminado del sistema", diff --git a/packages/app/src/i18n/resources/fr.ts b/packages/app/src/i18n/resources/fr.ts index 1bcf57ac2..a5f922ddc 100644 --- a/packages/app/src/i18n/resources/fr.ts +++ b/packages/app/src/i18n/resources/fr.ts @@ -1514,6 +1514,10 @@ export const fr: TranslationResources = { description: "Lignes conservées dans le tampon du terminal intégré", accessibilityLabel: "Lignes de défilementTerminal", }, + autoExpandReasoning: { + label: "Toujours afficher le raisonnement", + description: "Afficher le raisonnement de l'agent entièrement développé par défaut", + }, language: { label: "Langue", description: "Langue de l'application", @@ -1596,6 +1600,9 @@ export const fr: TranslationResources = { auto: "Système", }, }, + detailLevel: { + title: "Niveau de détail", + }, fonts: { title: "Polices", systemDefault: "Valeur par défaut du système", diff --git a/packages/app/src/i18n/resources/ja.ts b/packages/app/src/i18n/resources/ja.ts index 33546846a..fc219c447 100644 --- a/packages/app/src/i18n/resources/ja.ts +++ b/packages/app/src/i18n/resources/ja.ts @@ -1487,6 +1487,10 @@ export const ja: TranslationResources = { description: "組み込みターミナルバッファに保持する行数", accessibilityLabel: "ターミナルスクロールバック行数", }, + autoExpandReasoning: { + label: "常に思考プロセスを展開", + description: "デフォルトでAIのエージェント思考・推論ブロックを完全に展開して表示します", + }, language: { label: "言語", description: "アプリの言語", @@ -1568,6 +1572,9 @@ export const ja: TranslationResources = { auto: "システム", }, }, + detailLevel: { + title: "詳細レベル", + }, fonts: { title: "フォント", systemDefault: "システムデフォルト", diff --git a/packages/app/src/i18n/resources/pt-BR.ts b/packages/app/src/i18n/resources/pt-BR.ts index 5ee07a541..83a733fbf 100644 --- a/packages/app/src/i18n/resources/pt-BR.ts +++ b/packages/app/src/i18n/resources/pt-BR.ts @@ -1496,6 +1496,11 @@ export const ptBR: TranslationResources = { description: "Linhas mantidas no buffer do terminal integrado", accessibilityLabel: "Linhas do scrollback do terminal", }, + autoExpandReasoning: { + label: "Sempre expandir raciocínio", + description: + "Mostrar os blocos de pensamento e raciocínio do agente totalmente expandidos por padrão", + }, language: { label: "Idioma", description: "Idioma do app", @@ -1577,6 +1582,9 @@ export const ptBR: TranslationResources = { auto: "Sistema", }, }, + detailLevel: { + title: "Nível de detalhe", + }, fonts: { title: "Fontes", systemDefault: "Sistema padrão", diff --git a/packages/app/src/i18n/resources/ru.ts b/packages/app/src/i18n/resources/ru.ts index 16af21aa6..dd79f92a5 100644 --- a/packages/app/src/i18n/resources/ru.ts +++ b/packages/app/src/i18n/resources/ru.ts @@ -1501,6 +1501,11 @@ export const ru: TranslationResources = { description: "Строки, хранящиеся во встроенном буфере терминала.", accessibilityLabel: "Линии прокрутки Terminal", }, + autoExpandReasoning: { + label: "Всегда разворачивать размышления", + description: + "По умолчанию показывать блоки размышлений и логики агента полностью развернутыми", + }, language: { label: "Язык", description: "Язык приложения", @@ -1583,6 +1588,9 @@ export const ru: TranslationResources = { auto: "Система", }, }, + detailLevel: { + title: "Уровень детализации", + }, fonts: { title: "Шрифты", systemDefault: "Система по умолчанию", diff --git a/packages/app/src/i18n/resources/zh-CN.ts b/packages/app/src/i18n/resources/zh-CN.ts index 69792250b..9db623473 100644 --- a/packages/app/src/i18n/resources/zh-CN.ts +++ b/packages/app/src/i18n/resources/zh-CN.ts @@ -1453,6 +1453,10 @@ export const zhCN: TranslationResources = { description: "内置终端缓冲区保留的行数", accessibilityLabel: "终端回滚行数", }, + autoExpandReasoning: { + label: "始终展开推理过程", + description: "默认情况下完全展开 AI 的思考和推理过程", + }, language: { label: "语言", description: "应用语言", @@ -1534,6 +1538,9 @@ export const zhCN: TranslationResources = { auto: "系统", }, }, + detailLevel: { + title: "详细程度", + }, fonts: { title: "字体", systemDefault: "系统默认", diff --git a/packages/app/src/screens/settings/appearance/appearance-section.tsx b/packages/app/src/screens/settings/appearance/appearance-section.tsx index b21dc70c4..ed7ac596c 100644 --- a/packages/app/src/screens/settings/appearance/appearance-section.tsx +++ b/packages/app/src/screens/settings/appearance/appearance-section.tsx @@ -16,6 +16,7 @@ import { DropdownMenuSeparator, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; +import { Switch } from "@/components/ui/switch"; import { SettingsSection } from "@/screens/settings/settings-section"; import { MAX_CODE_FONT_SIZE, @@ -189,6 +190,28 @@ function ThemeRow({ value, onChange }: ThemeRowProps) { ); } +interface AutoExpandReasoningRowProps { + value: boolean; + onChange: (value: boolean) => void; +} + +function AutoExpandReasoningRow({ value, onChange }: AutoExpandReasoningRowProps) { + const { t } = useTranslation(); + return ( + + + + {t("settings.general.autoExpandReasoning.label")} + + + {t("settings.general.autoExpandReasoning.description")} + + + + + ); +} + // --------------------------------------------------------------------------- // Fonts: family text fields + numeric size fields (commit on blur/submit) // --------------------------------------------------------------------------- @@ -397,6 +420,13 @@ export function AppearanceSection() { [updateSettings], ); + const handleAutoExpandReasoningChange = useCallback( + (autoExpandReasoning: boolean) => { + void updateSettings({ autoExpandReasoning }); + }, + [updateSettings], + ); + const commitUiFontFamily = useCallback( (value: string) => { const sanitized = sanitizeFontFamily(value); @@ -477,6 +507,14 @@ export function AppearanceSection() { + + + + + {showFontFamilyRows ? (