diff --git a/src/components/prose/GenerationThoughts.tsx b/src/components/prose/GenerationThoughts.tsx index e9140c38..c2331503 100644 --- a/src/components/prose/GenerationThoughts.tsx +++ b/src/components/prose/GenerationThoughts.tsx @@ -1,6 +1,8 @@ +import { useEffect, useState } from 'react' import { ChainOfThought, ChainOfThoughtContent, + ChainOfThoughtHeader, ChainOfThoughtStep, } from '@/components/ui/chain-of-thought' import { Loader2, Brain, Wrench, CheckCircle2, PenLine, FileText } from 'lucide-react' @@ -31,33 +33,38 @@ export function GenerationThoughts({ steps, streaming, hasText, + defaultExpanded = true, }: { steps: ThoughtStep[] streaming: boolean hasText: boolean + defaultExpanded?: boolean }) { // Determine if reasoning is still actively streaming (no text yet, last step is reasoning) const lastStep = steps[steps.length - 1] const isThinking = streaming && !hasText && lastStep?.type === 'reasoning' + // Open per the story setting while generating, then collapse once it completes + const [open, setOpen] = useState(defaultExpanded) + useEffect(() => { + setOpen(streaming ? defaultExpanded : false) + }, [streaming, defaultExpanded]) return (
- + + {isThinking ? 'Thinking' : 'Thoughts'} {steps.map((step, i) => { if (step.type === 'reasoning') { + // Reasoning text sits directly under the header — the header is its label return ( - -
- {step.text} -
-
+ {step.text} +
) } if (step.type === 'prewriter-text') { diff --git a/src/components/prose/ProseBlock.tsx b/src/components/prose/ProseBlock.tsx index 2d108c10..ac46dced 100644 --- a/src/components/prose/ProseBlock.tsx +++ b/src/components/prose/ProseBlock.tsx @@ -119,6 +119,10 @@ export const ProseBlock = memo(function ProseBlock({ void isFirst void isLast const queryClient = useQueryClient() + const { data: story } = useQuery({ + queryKey: ['story', storyId], + queryFn: () => api.stories.get(storyId), + }) const [actionMode, setActionMode] = useState<'regenerate' | null>(null) const [showUndo, setShowUndo] = useState(false) const [isStreamingAction, setIsStreamingAction] = useState(false) @@ -567,6 +571,7 @@ export const ProseBlock = memo(function ProseBlock({ steps={actionThoughtSteps} streaming={isStreamingAction} hasText={!!streamedActionText} + defaultExpanded={story?.settings.expandThoughtsByDefault ?? true} /> )} (null) const isNearBottomRef = useRef(true) const queryClient = useQueryClient() + const { data: story } = useQuery({ + queryKey: ['story', storyId], + queryFn: () => api.stories.get(storyId), + }) // Track whether user is near the bottom of the scroll area useEffect(() => { @@ -148,6 +152,7 @@ function StreamingSection({ steps={thoughtSteps} streaming={isGenerating} hasText={!!streamedText} + defaultExpanded={story?.settings.expandThoughtsByDefault ?? true} /> )} diff --git a/src/components/sidebar/SettingsPanel.tsx b/src/components/sidebar/SettingsPanel.tsx index 1143e3ee..69dd36fd 100644 --- a/src/components/sidebar/SettingsPanel.tsx +++ b/src/components/sidebar/SettingsPanel.tsx @@ -642,6 +642,14 @@ export function SettingsPanel({ label="Toggle disable thinking" /> + + updateMutation.mutate({ expandThoughtsByDefault: next })} + disabled={updateMutation.isPending} + label="Toggle expand thinking by default" + /> + diff --git a/src/lib/api/settings.ts b/src/lib/api/settings.ts index 81eab043..8952ab49 100644 --- a/src/lib/api/settings.ts +++ b/src/lib/api/settings.ts @@ -28,6 +28,7 @@ export const settings = { guidedSceneSettingPrompt?: string guidedSuggestPrompt?: string disableThinking?: boolean + expandThoughtsByDefault?: boolean }) => apiFetch(`/stories/${storyId}/settings`, { method: 'PATCH', diff --git a/src/lib/api/types.ts b/src/lib/api/types.ts index 4eb56001..415fddcb 100644 --- a/src/lib/api/types.ts +++ b/src/lib/api/types.ts @@ -56,6 +56,7 @@ export interface StoryMeta { guidedSceneSettingPrompt?: string guidedSuggestPrompt?: string disableThinking?: boolean + expandThoughtsByDefault?: boolean /** erratanet provenance: installed-from pack and/or where this story is published. */ erratanet?: { pack?: string diff --git a/src/server/fragments/schema.ts b/src/server/fragments/schema.ts index 008c0ce8..f31371ef 100644 --- a/src/server/fragments/schema.ts +++ b/src/server/fragments/schema.ts @@ -153,6 +153,7 @@ export const StoryMetaSchema = z.object({ guidedSceneSettingPrompt: z.string().optional(), guidedSuggestPrompt: z.string().optional(), disableThinking: z.boolean().default(false), + expandThoughtsByDefault: z.boolean().default(true), // erratanet provenance. Absent for purely local stories. erratanet: z .object({ @@ -175,7 +176,7 @@ export const StoryMetaSchema = z.object({ }) .optional(), }) - .default({ outputFormat: 'markdown', enabledPlugins: [], summarizationThreshold: 4, maxSteps: 10, modelOverrides: {}, generationMode: 'standard', clarifyBeforeGenerate: false, prewriterReasoning: 'normal', disableLibrarianAutoAnalysis: false, autoApplyLibrarianSuggestions: false, disableLibrarianDirections: false, disableLibrarianSuggestions: false, contextOrderMode: 'simple', fragmentOrder: [], customFragmentTypes: [], enabledBuiltinTools: [], contextCompact: { type: 'proseLimit', value: 10 }, summaryCompact: { maxCharacters: 12000, targetCharacters: 9000 }, enableHierarchicalSummary: false, disableThinking: false }), + .default({ outputFormat: 'markdown', enabledPlugins: [], summarizationThreshold: 4, maxSteps: 10, modelOverrides: {}, generationMode: 'standard', clarifyBeforeGenerate: false, prewriterReasoning: 'normal', disableLibrarianAutoAnalysis: false, autoApplyLibrarianSuggestions: false, disableLibrarianDirections: false, disableLibrarianSuggestions: false, contextOrderMode: 'simple', fragmentOrder: [], customFragmentTypes: [], enabledBuiltinTools: [], contextCompact: { type: 'proseLimit', value: 10 }, summaryCompact: { maxCharacters: 12000, targetCharacters: 9000 }, enableHierarchicalSummary: false, disableThinking: false, expandThoughtsByDefault: true }), }) export type StoryMeta = z.infer diff --git a/src/server/routes/stories.ts b/src/server/routes/stories.ts index 61da1ccf..d0369058 100644 --- a/src/server/routes/stories.ts +++ b/src/server/routes/stories.ts @@ -42,6 +42,7 @@ export function storyRoutes(dataDir: string) { disableLibrarianDirections: false, disableLibrarianSuggestions: false, disableThinking: false, + expandThoughtsByDefault: true, contextOrderMode: 'simple' as const, fragmentOrder: [], customFragmentTypes: [], @@ -170,6 +171,7 @@ export function storyRoutes(dataDir: string) { ...(body.summaryCompact !== undefined ? { summaryCompact: body.summaryCompact } : {}), ...(body.enableHierarchicalSummary !== undefined ? { enableHierarchicalSummary: body.enableHierarchicalSummary } : {}), ...(body.disableThinking !== undefined ? { disableThinking: body.disableThinking } : {}), + ...(body.expandThoughtsByDefault !== undefined ? { expandThoughtsByDefault: body.expandThoughtsByDefault } : {}), } const applyGuidedPrompt = ( @@ -238,6 +240,7 @@ export function storyRoutes(dataDir: string) { guidedSceneSettingPrompt: t.Optional(t.String()), guidedSuggestPrompt: t.Optional(t.String()), disableThinking: t.Optional(t.Boolean()), + expandThoughtsByDefault: t.Optional(t.Boolean()), }), detail: { summary: 'Update story settings' }, }) diff --git a/tests/fragments/schema.test.ts b/tests/fragments/schema.test.ts index ad4ea100..4d1c6112 100644 --- a/tests/fragments/schema.test.ts +++ b/tests/fragments/schema.test.ts @@ -109,6 +109,7 @@ describe('StoryMetaSchema', () => { expect(result.settings.outputFormat).toBe('markdown') expect(result.settings.enabledPlugins).toEqual([]) expect(result.settings.customFragmentTypes).toEqual([]) + expect(result.settings.expandThoughtsByDefault).toBe(true) }) it('accepts full story metadata', () => {