feat(app): add auto-expand reasoning setting

This commit is contained in:
Matt Cowger
2026-07-06 01:42:41 +00:00
parent b1d11fccd5
commit 3c79183f9a
13 changed files with 91 additions and 12 deletions

View File

@@ -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<AgentStreamViewHandle, AgentStreamVi
) {
const { t } = useTranslation();
const router = useRouter();
const { settings } = useSettings();
const viewportRef = useRef<StreamViewportHandle | null>(null);
const isMobile = useIsCompactFormFactor();
const streamRenderStrategy = useMemo(
@@ -633,10 +635,12 @@ const AgentStreamViewComponent = forwardRef<AgentStreamViewHandle, AgentStreamVi
args={item.text}
status={item.status === "ready" ? "completed" : "executing"}
isLastInSequence={layoutItem.isLastInToolSequence}
defaultExpanded={settings.autoExpandReasoning}
forceInline={settings.autoExpandReasoning}
/>
);
},
[setInlineDetailsExpanded],
[setInlineDetailsExpanded, settings.autoExpandReasoning],
);
const renderToolCallItem = useCallback(

View File

@@ -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<ToolCallDetail | undefined>(() => {
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 (
<ToolCallDetailsContent
detail={effectiveDetail}
@@ -3157,7 +3162,7 @@ export const ToolCall = memo(function ToolCall({
showLoadingSkeleton={presentation.isLoadingDetails}
/>
);
}, [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;
}

View File

@@ -177,6 +177,9 @@ export function useSettings(): UseSettingsReturn {
if (updates.workspaceTitleSource !== undefined) {
appUpdates.workspaceTitleSource = updates.workspaceTitleSource;
}
if (updates.autoExpandReasoning !== undefined) {
appUpdates.autoExpandReasoning = updates.autoExpandReasoning;
}
const promises: Promise<void>[] = [];
if (Object.keys(appUpdates).length > 0) {
promises.push(appSettings.updateSettings(appUpdates));

View File

@@ -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<AppSettings>): Partial<AppSettings> {
) {
result.workspaceTitleSource = stored.workspaceTitleSource;
}
if (typeof stored.autoExpandReasoning === "boolean") {
result.autoExpandReasoning = stored.autoExpandReasoning;
}
return result;
}

View File

@@ -1457,6 +1457,10 @@ export const ar: TranslationResources = {
description: "يتم الاحتفاظ بالخطوط في المخزن المؤقت الطرفي المدمج",
accessibilityLabel: "خطوط التمرير Terminal",
},
autoExpandReasoning: {
label: "عرض التفكير دائماً",
description: "إظهار تفكير الوكيل وخطوات الاستدلال بشكل كامل بشكل افتراضي",
},
language: {
label: "لغة",
description: "لغة التطبيق",

View File

@@ -1464,6 +1464,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",

View File

@@ -1497,6 +1497,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",

View File

@@ -1500,6 +1500,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",

View File

@@ -1473,6 +1473,10 @@ export const ja: TranslationResources = {
description: "組み込みターミナルバッファに保持する行数",
accessibilityLabel: "ターミナルスクロールバック行数",
},
autoExpandReasoning: {
label: "常に思考プロセスを展開",
description: "デフォルトでAIのエージェント思考・推論ブロックを完全に展開して表示します",
},
language: {
label: "言語",
description: "アプリの言語",

View File

@@ -1482,6 +1482,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",

View File

@@ -1487,6 +1487,11 @@ export const ru: TranslationResources = {
description: "Строки, хранящиеся во встроенном буфере терминала.",
accessibilityLabel: "Линии прокрутки Terminal",
},
autoExpandReasoning: {
label: "Всегда разворачивать размышления",
description:
"По умолчанию показывать блоки размышлений и логики агента полностью развернутыми",
},
language: {
label: "Язык",
description: "Язык приложения",

View File

@@ -1439,6 +1439,10 @@ export const zhCN: TranslationResources = {
description: "内置终端缓冲区保留的行数",
accessibilityLabel: "终端回滚行数",
},
autoExpandReasoning: {
label: "始终展开推理过程",
description: "默认情况下完全展开 AI 的思考和推理过程",
},
language: {
label: "语言",
description: "应用语言",

View File

@@ -67,6 +67,7 @@ import { AddHostModal } from "@/components/add-host-modal";
import { PairLinkModal } from "@/components/pair-link-modal";
import { KeyboardShortcutsSection } from "@/screens/settings/keyboard-shortcuts-section";
import { Button } from "@/components/ui/button";
import { Switch } from "@/components/ui/switch";
import { CommunityLinks } from "@/components/community-links";
import { SegmentedControl } from "@/components/ui/segmented-control";
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem } from "@/components/ui/dropdown-menu";
@@ -244,6 +245,7 @@ interface GeneralSectionProps {
handleServiceUrlBehaviorChange: (behavior: ServiceUrlBehavior) => void;
handleLanguageChange: (language: AppLanguage) => void;
handleTerminalScrollbackLinesChange: (lines: number) => void;
handleAutoExpandReasoningChange: (autoExpand: boolean) => void;
}
interface ServiceUrlBehaviorMenuItemProps {
@@ -300,6 +302,7 @@ function GeneralSection({
handleServiceUrlBehaviorChange,
handleLanguageChange,
handleTerminalScrollbackLinesChange,
handleAutoExpandReasoningChange,
}: GeneralSectionProps) {
const { t, i18n } = useTranslation();
const activeLocale = getActiveLocale(i18n.language);
@@ -433,6 +436,20 @@ function GeneralSection({
accessibilityLabel={t("settings.general.terminalScrollback.accessibilityLabel")}
/>
</View>
<View style={ROW_WITH_BORDER_STYLE}>
<View style={settingsStyles.rowContent}>
<Text style={settingsStyles.rowTitle}>
{t("settings.general.autoExpandReasoning.label")}
</Text>
<Text style={settingsStyles.rowHint}>
{t("settings.general.autoExpandReasoning.description")}
</Text>
</View>
<Switch
value={settings.autoExpandReasoning}
onValueChange={handleAutoExpandReasoningChange}
/>
</View>
</View>
</SettingsSection>
);
@@ -1180,6 +1197,13 @@ export default function SettingsScreen({ view, openAddHostIntent = null }: Setti
[updateSettings],
);
const handleAutoExpandReasoningChange = useCallback(
(autoExpandReasoning: boolean) => {
void updateSettings({ autoExpandReasoning });
},
[updateSettings],
);
const handlePlaybackTest = useCallback(async () => {
if (!voiceAudioEngine || isPlaybackTestRunning) {
return;
@@ -1385,6 +1409,7 @@ export default function SettingsScreen({ view, openAddHostIntent = null }: Setti
handleServiceUrlBehaviorChange={handleServiceUrlBehaviorChange}
handleLanguageChange={handleLanguageChange}
handleTerminalScrollbackLinesChange={handleTerminalScrollbackLinesChange}
handleAutoExpandReasoningChange={handleAutoExpandReasoningChange}
/>
);
case "daemon":