Thinkloom 0.4.0 exposes every instruction sent to a language model as editable JSON. The desktop app creates a prompts folder in its operating-system configuration directory and shows its exact path under Settings → Prompt configuration. Use Open prompt folder to open it.
Thinkloom 0.5.0 exposes every instruction sent to a language model as editable JSON. The desktop app creates a prompts folder in its operating-system configuration directory and shows its exact path under Settings → Prompt configuration. Use Open prompt folder to open it.
Prompt files are loaded immediately before every model request. Save a valid edit, then make the next request; no restart or rebuild is required. Thinkloom never overwrites existing user prompt files during startup or an update.
Prompt files are loaded immediately before every model request. Save a valid edit, then make the next request; no restart or rebuild is required. Thinkloom preserves existing prompt instructions. When a release introduces a required prompt field, it adds only the missing field and wraps legacy conversation instructions with the new modular context variables.
## Files and effects
### conversation.json
Affects replies in Ideation. systemPrompt defines the overall role, userPromptTemplate defines the turn task, and challengeGuidance supplies the Gentle, Balanced, or Rigorous instruction. The {{challenge_guidance}} and {{context}} placeholders are required.
Affects replies in Ideation. systemPrompt defines the overall role, userPromptTemplate defines the turn task, and challengeGuidance supplies the Gentle, Balanced, or Rigorous instruction. The {{persona_instruction}}, {{genre_instruction}}, {{lore_context}}, {{web_search_instruction}}, {{challenge_guidance}}, and {{context}} placeholders are required. Together they form the modular system instruction for each session.
### drafting.json
Affects passage previews in Drafting and editorial previews in Finalization. systemPrompt defines the overall role, draftPromptTemplate is used by Draft a passage, and editorialPromptTemplate is used by the editorial actions. Available placeholders are {{relation}}, {{action}}, and {{context}}.
Affects passage previews in Drafting and editorial previews in Finalization. systemPrompt defines the overall role, draftPromptTemplate is used by Draft a passage, editorialPromptTemplate is used by the editorial actions, and distillationPromptTemplate turns the Phase 1 drafting paper into a token-efficient handoff. Available placeholders are {{relation}}, {{action}}, and {{context}}.
The description, effect, and variables objects document the configuration and are not sent to the model.
- Three-pane Phase 1 workspace for session context, live conversation, and idea distillation
- Persona, genre, lore, and provider-aware web-search guidance assembled into each conversational instruction
- Per-message Append to Ideas controls, editable drafting paper, one-off LLM distillation, and structured session export
- Typed conversation, challenge levels, push-to-talk transcription, persona-linked speech output, and no retained audio
- Suggested ideas with explicit accept/reject, editing, variants, archiving, source links, drafting sets, and merges
- TipTap/ProseMirror structured manuscript editor with canonical Markdown round-tripping, undo/redo, headings, lists, selection replacement, and cursor insertion
- Persisted preview-first generation states with retry-safe provider errors and partial acceptance
@@ -88,7 +91,7 @@ See [PROMPTS.md](PROMPTS.md) for each file's effect, variables, editing workflow
The approved Thinkloom 1.0 provenance architecture is documented in the [Stage 1 normative provenance specification](docs/provenance/README.md). It defines the future single-writer subsystem, canonical records, segmented ledger, recovery protocol, retention policy, native verification, and release bindings.
Thinkloom 0.4.0 extends the formal Stage 2 package with canonical provenance assertions, point-in-time evaluations, versioned semantic registries, and deterministic invalidation vectors. The current native provenance writer must not be represented as conforming until the later implementation and fault-injection stages are complete.
Thinkloom 0.5.0 extends the formal Stage 2 package with canonical provenance assertions, point-in-time evaluations, versioned semantic registries, and deterministic invalidation vectors. The current native provenance writer must not be represented as conforming until the later implementation and fault-injection stages are complete.
"description":"Controls Thinkloom replies during the Ideation conversation.",
"effect":"Changes how the assistant reflects on the writer's latest message and which single follow-up question it asks.",
"systemPrompt":"You are Thinkloom, a focused writing collaborator in an ideation conversation. Respond naturally to the writer's latest message, briefly reflect what is useful, and ask exactly one focused question. Do not force a stock interpretation, fabricate details, or draft prose unless asked.",
"systemPrompt":"{{persona_instruction}}\n\n{{genre_instruction}}\n\nProject lore and context:\n{{lore_context}}\n\n{{web_search_instruction}}\n\nYou are Thinkloom, a focused writing collaborator in an ideation conversation. Respond naturally to the writer's latest message, briefly reflect what is useful, and ask exactly one focused question. Do not force a stock interpretation, fabricate details, or draft prose unless asked.",
"userPromptTemplate":"Continue this ideation conversation. Respond directly and naturally to the writer's latest message, then ask exactly one focused question that helps develop their writing. {{challenge_guidance}}\n\nConversation so far:\n{{context}}",
"challengeGuidance":{
"Gentle":"Be warm and exploratory; help the writer locate a concrete personal example.",
@@ -12,6 +12,10 @@
},
"variables":{
"challenge_guidance":"Selected from challengeGuidance using the current Gentle, Balanced, or Rigorous setting.",
"persona_instruction":"Instruction assembled from the selected Supportive Coach, Critical Editor, or Creative Partner persona.",
"genre_instruction":"Instruction assembled from the selected writing genre.",
"lore_context":"The session-specific lore and background supplied by the writer.",
"web_search_instruction":"Requires web search for verification and current-data requests when the active provider supports it.",
"context":"The ten most recent Writer and Thinkloom conversation turns."
"systemPrompt":"You are Thinkloom, a focused writing collaborator. Return only proposed prose; it will be staged for review. Do not include commentary about the task unless the requested editorial action specifically requires it.",
"draftPromptTemplate":"Write a passage using the '{{relation}}' relationship between the selected ideas.\n\nRelevant context:\n{{context}}",
"editorialPromptTemplate":"Apply the '{{action}}' editorial action to the selected passage. Return only the proposed result.\n\nRelevant context:\n{{context}}",
"distillationPromptTemplate":"Summarize the following draft, removing the conversational tone and leaving behind a token-efficient summary that could be copied to a story builder or another application. Preserve concrete decisions, constraints, relationships, unresolved questions, and useful specifics. Do not add a conversational lead-in. Provide only the raw summary.\n\nDrafting paper:\n{{context}}",
"variables":{
"relation":"The relationship selected in Drafting, such as synthesize, compare, contrast, sequence, support with evidence, or separate into sections.",
"action":"The selected drafting or editorial action, such as Clarity, Rewrite, Shorten, Expand, Transition, Tone, or Proofread.",
"action":"The selected drafting or editorial action, including the dedicated distill action used by Phase 1.",
"context":"The selected ideas or current passage supplied by Thinkloom."
"{{{{persona_instruction}}}}\n\n{{{{genre_instruction}}}}\n\nProject lore and context:\n{{{{lore_context}}}}\n\n{{{{web_search_instruction}}}}\n\n{legacy_system}"
"Supportive Coach":"Be an encouraging writing coach. Ask clarifying questions and help the writer expand promising ideas without taking control of them.",
"Critical Editor":"Be a rigorous critical editor. Focus on structure, logic, and clarity; respectfully identify contradictions, weak assumptions, and plot holes.",
"Creative Partner":"Be an inventive creative partner. Explore what-if scenarios, world-building possibilities, surprising connections, and unexpected but relevant turns.",
};
constgenreInstructions: Record<string,string>={
Fiction:"Support fiction development with attention to character, conflict, setting, causality, and narrative momentum.",
"Non-Fiction":"Support non-fiction development with attention to thesis, evidence, reader value, structure, and factual precision.",
Academic:"Support academic development with attention to research questions, argument structure, evidence, counterarguments, and precise claims.",
Poetry:"Support poetry development with attention to image, sound, compression, line, voice, and emotional resonance.",
Screenplay:"Support screenplay development with attention to scene objectives, visual action, dialogue, pacing, and dramatic turns.",
id:"project_attention_commons",title:"The Attention Commons",subtitle:"An essay on reclaiming shared focus",phase:"ideation",privacy:"Local",cloudApproved: false,challenge:"Balanced",spokenReplies: false,
id:"project_attention_commons",title:"The Attention Commons",subtitle:"An essay on reclaiming shared focus",phase:"ideation",privacy:"Local",cloudApproved: false,challenge:"Balanced",spokenReplies: false,persona:"Supportive Coach",genre:"Non-Fiction",lore:"A long-form essay exploring attention as a shared civic condition rather than only a matter of personal discipline.",workspace:"Attention may be a form of civic infrastructure.\n\nInstitutions shape whether sustained focus is possible.\n\nPersonal digital-minimalism advice cannot repair systems designed around extraction.",summary:"Attention should be framed as civic infrastructure: a shared condition shaped by institutional norms, incentives, architecture, and technology. The essay will contrast individual self-management with systemic responsibility, using classrooms, meetings, and public debate as examples of spaces where degraded attention creates collective costs.",activeSessionId:"session_1",sessionTitle:"Attention as civic infrastructure",sessionCreatedAt:"2026-07-16T17:42:00.000Z",sessions:[],
turns:[
{id:"turn_1",speaker:"user",text:"I keep returning to the idea that attention is treated as a private resource, even though its erosion changes public life.",createdAt:"2026-07-16T17:42:00.000Z"},
{id:"turn_2",speaker:"assistant",text:"That tension feels central: we experience distraction individually, but the consequences accumulate socially. Where do you see that shared cost most clearly?",createdAt:"2026-07-16T17:43:00.000Z"},
const[project,setProject]=useState<State>(initial);const[hydrated,setHydrated]=useState(false);const[view,setView]=useState<View>("ideation");const[message,setMessage]=useState("");const[notice,setNotice]=useState("Project ready. History is being recorded.");const[busy,setBusy]=useState(false);const[listening,setListening]=useState(false);const[showRejected,setShowRejected]=useState(false);const[editingIdea,setEditingIdea]=useState<string|null>(null);const[newHabit,setNewHabit]=useState("");const[sanitized,setSanitized]=useState(true);const[credential,setCredential]=useState("");const[projectPath,setProjectPath]=useState("");const[promptPath,setPromptPath]=useState("");consteditor=useRef<StructuredEditorHandle>(null);constfinalEditor=useRef<HTMLTextAreaElement>(null);constconversationEnd=useRef<HTMLDivElement>(null);
useEffect(()=>{queueMicrotask(()=>{try{constraw=localStorage.getItem("thinkloom-project-v1");if(raw)setProject({...initial,...JSON.parse(raw)asState});}catch{/* Ignore invalid local recovery state. */}setHydrated(true);});},[]);
const[project,setProject]=useState<State>(initial);const[hydrated,setHydrated]=useState(false);const[view,setView]=useState<View>("ideation");const[message,setMessage]=useState("");const[notice,setNotice]=useState("Project ready. History is being recorded.");const[busy,setBusy]=useState(false);const[listening,setListening]=useState(false);const[newHabit,setNewHabit]=useState("");const[sanitized,setSanitized]=useState(true);const[credential,setCredential]=useState("");const[projectPath,setProjectPath]=useState("");const[promptPath,setPromptPath]=useState("");consteditor=useRef<StructuredEditorHandle>(null);constfinalEditor=useRef<HTMLTextAreaElement>(null);constconversationEnd=useRef<HTMLDivElement>(null);
useEffect(()=>{queueMicrotask(()=>{try{constraw=localStorage.getItem("thinkloom-project-v1");if(raw)setProject(normalizeState(JSON.parse(raw)asPartial<State>));}catch{/* Ignore invalid local recovery state. */}setHydrated(true);});},[]);
useEffect(()=>{voidinvokeNative<{directory: string}>("ensure_prompt_files").then((info)=>{if(info?.directory)setPromptPath(info.directory);}).catch((error)=>setNotice(`Prompt configuration could not be prepared: ${nativeError(error)}`));},[]);
constnavigate=(next: View)=>{setView(next);if(["ideation","drafting","finalization"].includes(next)&&project.phase!==next)mutate("PHASE_CHANGED",`Moved to ${next}`,(current)=>({...current,phase: nextasPhase}));};
constappendToIdeas=(turn: Turn)=>mutate("TRANSCRIPT_APPENDED_TO_DRAFT",`Appended ${turn.speaker==="user"?"your":"Thinkloom's"} message to the drafting paper`,(current)=>({...current,workspace:`${current.workspace.trimEnd()}${current.workspace.trim()?"\n\n":""}${turn.text}`}));
constsummarizeDraft=async()=>{if(!project.workspace.trim()||busy)return;setBusy(true);setNotice("Distilling the drafting paper…");try{constgenerated=awaitinvokeNative<string>("generate_text",{profile: project.provider,promptVariables:{action:"distill",relation:"synthesize",context: project.workspace},cloudApproved: project.cloudApproved,purpose:"drafting"});if(!generated?.trim())thrownewError("The provider returned an empty response.");mutate("DRAFT_DISTILLED","Token-efficient summary ready",(current)=>({...current,summary: generated.trim()}),"assistant");}catch(error){setNotice(`Summary could not be generated: ${nativeError(error)} Check Settings → Model provider, then retry.`);}finally{setBusy(false);}};
constgenerated=awaitinvokeNative<string>("generate_text",{profile: project.provider,promptVariables:{challenge: project.challenge,persona_instruction: personaInstructions[project.persona],genre_instruction: genreInstructions[project.genre]??genreInstructions.Fiction,lore_context: project.lore.trim()||"No additional lore or context was provided.",web_search_instruction:"When the writer asks to verify facts, search the internet, or check current data, use an available web search tool and summarize findings naturally. Clearly say when the active provider cannot search the web.",context: conversation},cloudApproved: project.cloudApproved,purpose:"conversation"});
if(!generated?.trim())thrownewError("The provider returned an empty response.");
constsuggestion: Idea={id: uid("idea"),title: focus.length>46?`${focus.slice(0,43)}…`:focus,summary: text,detail:"Explore how this supports or complicates the central argument.",status:"suggested",sourceTurnIds:[userTurn.id],parentIdeaIds:[],tags:["from conversation"],pinned: false,selected: false,createdBy:"mixed"};
mutate("ASSISTANT_RESPONSE_GENERATED",`Response generated by ${project.provider.name}`,(current)=>({...current,turns:[...current.turns,assistantTurn],ideas:[...current.ideas,suggestion]}),"assistant");
mutate("ASSISTANT_RESPONSE_GENERATED",`Response generated by ${project.provider.name}`,(current)=>({...current,turns:[...current.turns,assistantTurn]}),"assistant");
constsaveLastTurn=()=>{constturn=[...project.turns].reverse().find((item)=>item.speaker==="user");if(!turn)return;constidea: Idea={id: uid("idea"),title: turn.text.slice(0,58),summary: turn.text,detail:"Saved directly from conversation.",status:"accepted",sourceTurnIds:[turn.id],parentIdeaIds:[],tags:["saved directly"],pinned: false,selected: true,createdBy:"user"};mutate("IDEA_ACCEPTED",`Saved “${idea.title}” as an idea`,(current)=>({...current,ideas:[...current.ideas,idea]}));};
constcreateVariant=(source: Idea)=>{constvariant: Idea={...source,id: uid("idea"),title:`${source.title} — another angle`,detail:`A deliberate counter-reading of: ${source.detail}`,parentIdeaIds:[source.id],status:"suggested",selected: false,pinned: false,createdBy:"mixed"};mutate("IDEA_VARIANT_CREATED",`Created a variant of “${source.title}”`,(current)=>({...current,ideas:[...current.ideas,variant]}));};
constmergeSelected=()=>{if(selectedIdeas.length<2){setNotice("Select at least two ideas to merge.");return;}constmerged: Idea={id: uid("idea"),title: selectedIdeas.map((idea)=>idea.title).join(" + ").slice(0,70),summary: selectedIdeas.map((idea)=>idea.summary).join(" "),detail:"A synthesis retaining both parent ideas.",status:"accepted",sourceTurnIds:[...newSet(selectedIdeas.flatMap((idea)=>idea.sourceTurnIds))],parentIdeaIds: selectedIdeas.map((idea)=>idea.id),tags:[...newSet(selectedIdeas.flatMap((idea)=>idea.tags))],pinned: true,selected: true,createdBy:"mixed"};mutate("IDEAS_MERGED",`Merged ${selectedIdeas.length} ideas`,(current)=>({...current,ideas:[...current.ideas.map((idea)=>selectedIdeas.some((selected)=>selected.id===idea.id)?{...idea,selected: false}:idea),merged]}));};
conststartGeneration=async(action="draft")=>{
@@ -169,14 +187,14 @@ export default function Thinkloom() {
setNotice("New empty project created. Give it a title when you are ready.");
}catch(error){setNotice(`New project could not be created: ${String(error)}`);}
};
constcreateNativeProject=async()=>{try{constfolder=awaitinvokeNative<string>("choose_project_folder");if(!folder){setNotice("Choose a folder when you are ready to create the desktop project.");return;}constresult=awaitinvokeNative<{path: string}>("create_project",{path: folder,title: project.title});if(!result){setNotice("Project folders are available in the installed desktop app.");return;}setProjectPath(result.path);awaitinvokeNative("persist_state",{appState: project});setNotice("Self-contained project created and ready.");}catch(error){setNotice(`Project could not be created: ${String(error)}`);}};
constopenNativeProject=async()=>{try{constfolder=awaitinvokeNative<string>("choose_project_folder");if(!folder)return;constresult=awaitinvokeNative<{path: string}>("open_project",{path: folder});if(!result){setNotice("Opening project folders is available in the installed desktop app.");return;}setProjectPath(result.path);constrestored=awaitinvokeNative<State>("load_project_state");if(restored)setProject(restored);setNotice("Project reopened and its history verified.");}catch(error){setNotice(`Project could not be opened: ${String(error)}`);}};
constopenNativeProject=async()=>{try{constfolder=awaitinvokeNative<string>("choose_project_folder");if(!folder)return;constresult=awaitinvokeNative<{path: string}>("open_project",{path: folder});if(!result){setNotice("Opening project folders is available in the installed desktop app.");return;}setProjectPath(result.path);constrestored=awaitinvokeNative<State>("load_project_state");if(restored)setProject(normalizeState(restored));setNotice("Project reopened and its history verified.");}catch(error){setNotice(`Project could not be opened: ${String(error)}`);}};
constopenPromptFolder=async()=>{try{constpath=awaitinvokeNative<string>("open_prompt_folder");if(path){setPromptPath(path);setNotice("Prompt configuration folder opened.");}}catch(error){setNotice(`Prompt configuration folder could not be opened: ${nativeError(error)}`);}};
<sectionclassName="config-section lore-section"><divclassName="section-label"><span>03</span><labelhtmlFor="lore">Lore&context</label></div><textareaid="lore"value={project.lore}onChange={(event)=>setProject((current)=>({...current,lore: event.target.value}))}placeholder="Paste character sheets, world rules, research notes, or earlier story context…"/><small>{countWords(project.lore)}words·includedintheAIinstruction</small></section>
<divclassName={`composer ${listening?"listening":""}`}>{listening&&<divclassName="voice-state"><spanclassName="pulse"/>Listening—audiostaysinmemoryandwillbediscarded</div>}<labelhtmlFor="message"className="sr-only">Continuetheconversation</label><textareaid="message"value={message}onChange={(event)=>setMessage(event.target.value)}onKeyDown={(event)=>{if(event.key==="Enter"&&!event.shiftKey){event.preventDefault();voidsendMessage();}}}placeholder="Speak or type the next thread…"rows={3}/><divclassName="composer-actions"><div><buttonclassName={`record-button ${listening?"recording":""}`}onClick={startVoice}aria-label={listening?"Stop voice input":"Start voice input"}><span>{listening?"■":"●"}</span>{listening?"Stop":"Talk"}</button><spanclassName="privacy-note">Noaudioretained</span></div><buttonclassName="send-button"onClick={()=>voidsendMessage()}disabled={!message.trim()||busy}>Send<span>↗</span></button></div></div>
</section>
<asideclassName="curation-panel panel"aria-label="Idea curation and distillation">
<sectionclassName="draft-paper"><divclassName="paper-heading"><div><spanclassName="step-number">1</span><div><strong>Draftingpaper</strong><small>Editablescratchpad</small></div></div><buttononClick={()=>{if(window.confirm("Clear the drafting paper and its summary?"))setProject((current)=>({...current,workspace:"",summary:""}));}}disabled={!project.workspace}>Clear</button></div><textareavalue={project.workspace}onChange={(event)=>setProject((current)=>({...current,workspace: event.target.value}))}placeholder="Append transcript moments or write freely here. This is your raw material…"aria-label="Ideas and drafting workspace"/></section>
<sectionclassName={`summary-paper ${project.summary?"has-summary":""}`}><divclassName="paper-heading"><div><spanclassName="step-number">3</span><div><strong>Finalsummary</strong><small>Phase2ready</small></div></div>{project.summary&&<buttononClick={()=>voidnavigator.clipboard.writeText(project.summary).then(()=>setNotice("Summary copied to clipboard.")).catch(()=>setNotice("Clipboard access was unavailable. Select the summary text to copy it."))}>Copy</button>}</div>{project.summary?<p>{project.summary}</p>:<divclassName="summary-empty"><span>◇</span><p>Yourtoken-efficientsummarywillappearhere,readyforthestorybuilder.</p></div>}</section>
for(constphraseof["Insert at cursor","Replace selection","New section","Discard","History recorded","No audio retained","Approve for this project","Relationships, not percentages"]){
for(constphraseof["Append to ideas","Summarize draft","Lore & context","Insert at cursor","Replace selection","New section","Discard","History recorded","No audio retained","Approve for this project","Relationships, not percentages"]){
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.