feat: Add 'New Scene' button to file list menu for creating linked planning+draft files

- Added create_scene_pair() method to workspace.rs
- Added '+ New Scene' button in file_list.rs UI
- Button creates both planning file (05-Plot/act_N/scene_XX.md) and draft file (06-First Draft/Act N/sceneXX.md)
- Files are automatically linked via YAML frontmatter (draft_file and planning_template fields)
- Auto-detects current act from folder context
- Auto-generates next scene number
- Selects the draft file after creation (ready to edit)
- Includes hover text explaining the feature

Generated by Mistral Vibe.
Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
This commit is contained in:
landon
2026-09-15 14:27:30 -05:00
parent b183237d59
commit 921f3e74c5
2 changed files with 327 additions and 0 deletions
+10
View File
@@ -539,6 +539,16 @@ impl App {
if ui.button("+ New").clicked() { if ui.button("+ New").clicked() {
self.create_file(); self.create_file();
} }
if ui
.button("+ New Scene")
.on_hover_text(
"Create a planning file in 05-Plot and a linked draft file \
in 06-First Draft with automatic cross-references",
)
.clicked()
{
self.create_scene_pair();
}
}); });
if ui if ui
.button("+ New from template") .button("+ New from template")
+317
View File
@@ -600,6 +600,323 @@ impl App {
self.rebuild_field_names(); self.rebuild_field_names();
} }
/// Create a scene pair: both a planning file (in 05-Plot) and a draft file
/// (in 06-First Draft), linked to each other. Used for the "New Scene" button.
pub(super) fn create_scene_pair(&mut self) {
use super::today_utc;
// Determine which folder we're in
let current_dir = self
.selected
.and_then(|i| self.files.get(i))
.map(|path| parent_dir(path).to_string())
.unwrap_or_default();
// Check if we're in a First Draft folder (Act 1, Act 2, etc.)
let in_draft_folder = current_dir.starts_with("06-First Draft/Act")
|| current_dir.starts_with("06-First Draft/");
// Check if we're in a Plot folder (act_1, act_2, etc.)
let in_plot_folder = current_dir.starts_with("05-Plot/act_")
|| current_dir.starts_with("05-Plot/");
// Extract act number from current directory
let act_num = if in_draft_folder {
// Parse "06-First Draft/Act N" or "06-First Draft/Act N/"
current_dir.split('/').nth(1).and_then(|s| {
s.strip_prefix("Act ").and_then(|n| n.parse::<u32>().ok())
})
} else if in_plot_folder {
// Parse "05-Plot/act_N" or "05-Plot/act_N/"
current_dir.split('/').nth(1).and_then(|s| {
s.strip_prefix("act_").and_then(|n| n.parse::<u32>().ok())
})
} else {
// Default to Act 1
Some(1)
};
let act = act_num.unwrap_or(1);
let today = today_utc();
// Find next available scene number in this act
let existing_scenes: Vec<u32> = self.files.iter()
.filter(|f| {
let path_lower = f.to_lowercase();
if in_draft_folder || in_plot_folder {
// Check files in current act
path_lower.contains(&format!("act_{}/", act))
|| path_lower.contains(&format!("Act {}/", act))
} else {
// Check both folders
path_lower.contains(&format!("act_{}/scene_", act))
|| path_lower.contains(&format!("Act {}/scene", act))
}
})
.filter_map(|f| {
// Extract scene number from filename
let filename = Path::new(f).file_name().and_then(|n| n.to_str()).unwrap_or("");
let num_str = filename.trim_start_matches("scene").trim_start_matches("_").trim_end_matches(".md");
num_str.parse::<u32>().ok()
})
.collect();
let scene_num = (1..=1000).find(|n| !existing_scenes.contains(n)).unwrap_or(1);
// Default chapter is 1
let chapter = 1;
// Generate scene title
let title = format!("Chapter {}: Scene {}", chapter, scene_num);
let slug = format!("chapter-{}--scene-{}", chapter, scene_num);
// Determine where to create files
let draft_dir = format!("06-First Draft/Act {}", act);
let plot_dir = format!("05-Plot/act_{:02}", act);
// Create planning file
let planning_name = format!("{}/scene_{:02}.md", plot_dir, scene_num);
let draft_name = format!("{}/scene{:02}.md", draft_dir, scene_num);
// Planning file content
let planning_id = format!("scene_{:02d}_{:02d}_{:02d}", act, chapter, scene_num);
let planning_content = format!(r#"---
id: {}
title: "{}"
slug: {}
act: {}
chapter: {}
scene: {}
type: scene
status: planning
word_count_target: 2000
word_count_min: 1800
word_count_max: 2200
pov: ""
characters: []
setting: ""
date: ""
time: ""
conflict: ""
theme: ""
tone: ""
draft_file: "../../06-First Draft/Act {}/scene{:02}.md"
order: {}
created: "{}"
updated: "{}"
---
# {}
**Planning Template** | **ID:** {} | **Status:** Planning
---
## Planning Metadata
| Attribute | Value |
|-----------|-------|
| **Act** | {} |
| **Chapter** | {} |
| **Scene** | {} |
| **Type** | scene |
| **Order** | {} |
| **Draft File** | [Open Draft](../../06-First Draft/Act {}/scene{:02}.md) |
---
## Creative Elements
### POV
>
### Characters
### Setting
>
### Date & Time
>
---
## Story Structure
### Conflict
>
### Theme
>
### Tone
>
---
## Word Count Goals
| Metric | Value |
|--------|-------|
| **Target** | 2000 |
| **Minimum** | 1800 |
| **Maximum** | 2200 |
---
## Scene Planning
### The Setup
>
### The Reveal
>
### Things to Note
-
-
---
## Status Tracking
- [ ] Scene outline complete
- [ ] Character arcs aligned
- [ ] Location details verified
- [ ] Conflict established
- [ ] Word count target set
- [ ] Ready for drafting
---
**Draft File:** [Open Draft](../../06-First Draft/Act {}/scene{:02}.md) | **Created:** {} | **Updated:** {}
"#,
planning_id, title, slug, act, chapter, scene_num,
act, scene_num, scene_num,
today, today,
title, planning_id,
act, chapter, scene_num, scene_num,
act, scene_num,
act, chapter, scene_num,
today, today
);
// Draft file content
let draft_id = format!("draft_scene_{:02d}_{:02d}_{:02d}", act, chapter, scene_num);
let draft_content = format!(r#"---
id: {}
title: "{}"
slug: {}
planning_template: "../../05-Plot/act_{:02}/scene_{:02}.md"
act: {}
chapter: {}
scene: {}
type: draft
status: not_started
word_count: 0
word_count_updated: ""
session_word_count: 0
last_session: ""
total_sessions: 0
pov: ""
version: 1
created: "{}"
updated: "{}"
---
# {}
**Draft File** | **ID:** {} | **Status:** Not Started
---
## Planning Reference
> **Linked to:** [Planning Template](../../05-Plot/act_{:02}/scene_{:02}.md)
---
## Scene Metadata
| Attribute | Value |
|-----------|-------|
| **Act** | {} |
| **Chapter** | {} |
| **Scene** | {} |
| **Type** | draft |
| **POV** | Not set |
| **Version** | 1 |
---
## Word Count Tracking
| Metric | Value |
|--------|-------|
| **Target** | 2000 (from planning) |
| **Current** | 0 |
| **This Session** | 0 |
| **Last Updated** | Never |
---
## Draft Content
### Rough Draft:
[Write your scene content here...]
---
## Writing Notes
### Session Log
| Date | Words Added | Notes |
|------|--------------|-------|
### Revision Notes
-
-
---
## Status Checklist
- [ ] First draft complete
- [ ] Word count target met
- [ ] POV consistent
- [ ] Character voices distinct
- [ ] Setting details accurate
- [ ] Conflict resolved/advanced
- [ ] Ready for revision
---
**Planning Template:** [Open Planning](../../05-Plot/act_{:02}/scene_{:02}.md) | **Created:** {} | **Updated:** {}
"#,
draft_id, title, slug,
act, scene_num,
act, chapter, scene_num,
today, today,
title, draft_id,
act, scene_num,
act, chapter, scene_num,
act, scene_num,
today, today
);
// Create both files
self.insert_new_file(planning_name, planning_content);
self.insert_new_file(draft_name, draft_content);
// Select the draft file (more likely what user wants to edit)
let draft_idx = self.files.iter().position(|f| *f == draft_name).unwrap_or(0);
self.selected = Some(draft_idx);
self.buffer.clear();
self.dirty = false;
}
/// Write `contents` to a new file, add it to the manuscript order and select /// Write `contents` to a new file, add it to the manuscript order and select
/// it. Shared by the plain and template-backed create paths. /// it. Shared by the plain and template-backed create paths.
fn insert_new_file(&mut self, name: String, contents: String) { fn insert_new_file(&mut self, name: String, contents: String) {