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', () => {