feat: rebuild Phase 1 ideation and curation workflow

This commit is contained in:
2026-07-19 07:54:12 -07:00
parent cb36bbc958
commit 4b8dc903e4
13 changed files with 145 additions and 45 deletions
+1 -1
View File
@@ -3980,7 +3980,7 @@ dependencies = [
[[package]]
name = "thinkloom"
version = "0.4.0"
version = "0.5.0"
dependencies = [
"chrono",
"hex",
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "thinkloom"
version = "0.4.0"
version = "0.5.0"
description = "Local-first writing studio with creative provenance"
authors = ["Christopher Chambers"]
license = "AGPL-3.0-only"
+5 -1
View File
@@ -3,7 +3,7 @@
"id": "conversation",
"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."
}
}
+2 -1
View File
@@ -6,9 +6,10 @@
"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."
}
}
+50 -5
View File
@@ -105,6 +105,7 @@ struct DraftingPromptConfig {
system_prompt: String,
draft_prompt_template: String,
editorial_prompt_template: String,
distillation_prompt_template: String,
}
#[derive(Debug, Serialize)]
@@ -189,6 +190,23 @@ fn prompt_config_dir(app: &AppHandle) -> CommandResult<PathBuf> {
})
}
fn merge_missing_prompt_fields(current: &mut Value, defaults: &Value) -> bool {
let (Some(current_object), Some(default_object)) =
(current.as_object_mut(), defaults.as_object())
else {
return false;
};
let mut changed = false;
for (key, default_value) in default_object {
if let Some(current_value) = current_object.get_mut(key) {
changed |= merge_missing_prompt_fields(current_value, default_value);
} else {
current_object.insert(key.clone(), default_value.clone());
changed = true;
}
}
changed
}
fn ensure_prompt_files_at(app: &AppHandle) -> CommandResult<PathBuf> {
let directory = prompt_config_dir(app)?;
fs::create_dir_all(&directory).map_err(|error| {
@@ -202,6 +220,32 @@ fn ensure_prompt_files_at(app: &AppHandle) -> CommandResult<PathBuf> {
let path = directory.join(name);
if !path.exists() {
atomic_write(&path, contents.as_bytes())?;
} else if name.ends_with(".json") {
let existing = fs::read_to_string(&path).map_err(|error| {
CommandError::io("Could not read prompt configuration for migration", error)
})?;
if let (Ok(mut current), Ok(defaults)) = (
serde_json::from_str::<Value>(&existing),
serde_json::from_str::<Value>(contents),
) {
let mut changed = merge_missing_prompt_fields(&mut current, &defaults);
if name == "conversation.json" {
let legacy_system = current
.get("systemPrompt")
.and_then(Value::as_str)
.filter(|prompt| !prompt.contains("{{persona_instruction}}"))
.map(str::to_owned);
if let Some(legacy_system) = legacy_system {
current["systemPrompt"] = Value::String(format!(
"{{{{persona_instruction}}}}\n\n{{{{genre_instruction}}}}\n\nProject lore and context:\n{{{{lore_context}}}}\n\n{{{{web_search_instruction}}}}\n\n{legacy_system}"
));
changed = true;
}
}
if changed {
write_json(&path, &current)?;
}
}
}
}
Ok(directory)
@@ -297,7 +341,7 @@ fn prompts_for_request(
})?;
variables.insert("challenge_guidance".into(), guidance.clone());
Ok((
config.system_prompt,
render_prompt_template(&config.system_prompt, &variables)?,
render_prompt_template(&config.user_prompt_template, &variables)?,
))
}
@@ -308,6 +352,7 @@ fn prompts_for_request(
|| config.system_prompt.trim().is_empty()
|| config.draft_prompt_template.trim().is_empty()
|| config.editorial_prompt_template.trim().is_empty()
|| config.distillation_prompt_template.trim().is_empty()
{
return Err(CommandError::new(
"PROMPT_CONFIG_INVALID",
@@ -318,10 +363,10 @@ fn prompts_for_request(
true,
));
}
let template = if required_prompt_variable(&variables, "action")? == "draft" {
&config.draft_prompt_template
} else {
&config.editorial_prompt_template
let template = match required_prompt_variable(&variables, "action")? {
"draft" => &config.draft_prompt_template,
"distill" => &config.distillation_prompt_template,
_ => &config.editorial_prompt_template,
};
Ok((
config.system_prompt,
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "Thinkloom",
"version": "0.4.0",
"version": "0.5.0",
"identifier": "com.thinkloom.desktop",
"build": {
"beforeDevCommand": "npm run dev",