Fill in only the missing acts when a proposal is part-drafted
The beat generator had one brief, which assumed the proposal was a prose premise and asked for a whole three-act sheet at "roughly four to eight beats per act". Handed a proposal that already had acts beaten out past that, it would regenerate them — compressing the author's own work into a shorter paraphrase, losing the specific detail that made it worth keeping, and burying the one act they actually wanted among two they did not. `mistral::analyse` now reads a proposal's act headings and counts the numbered beats under each, in whatever notation the author used. An act with three or more beats is drafted; fewer, or an act never mentioned, is still to write. Two stray numbered lines are a note to self, not an act. When some acts are drafted and some are not, a second briefing is sent. That briefing asks for the missing acts only, and asks for them under the author's own heading text and depth, so what comes back drops into their document. It drops the per-act cap in favour of matching the density of the drafted acts, and states which acts are context and which to write. The drafted acts are canon: not to be rewritten, reordered, summarised, condensed or reproduced, and — since a reveal often lands in a later act — the model is told to work backwards from those reveals and plant what they need using setups already placed. The model is asked not to reproduce the drafted acts at all, rather than to echo them unchanged. Reproducing twenty-odd beats verbatim is exactly the drift being avoided; not asking makes it impossible. Because the output is then partial, it is labelled: the status line names the acts written, the file is saved as `<proposal> — beats Two.md`, and its title says what it holds, so a partial sheet is not later mistaken for a whole one. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011ZGoPiDuZ7vmryNCJWjYSD
This commit is contained in:
+38
-5
@@ -68,8 +68,16 @@ impl App {
|
||||
self.beats_rx = None;
|
||||
match result {
|
||||
Ok(beats) => {
|
||||
self.beats_status = "Done — review, then save or copy.".to_string();
|
||||
self.beats_output = Some(beats);
|
||||
self.beats_status = match beats.filled.as_slice() {
|
||||
// A proposal with no beats of its own gets a full sheet.
|
||||
[] => "Done — review, then save or copy.".to_string(),
|
||||
acts => format!(
|
||||
"Done — wrote {}. Your drafted acts were left alone.",
|
||||
join_names(acts)
|
||||
),
|
||||
};
|
||||
self.beats_filled = beats.filled;
|
||||
self.beats_output = Some(beats.markdown);
|
||||
}
|
||||
Err(e) => {
|
||||
self.beats_status = format!("✖ {e}");
|
||||
@@ -87,17 +95,31 @@ impl App {
|
||||
.beats_source_stem
|
||||
.clone()
|
||||
.unwrap_or_else(|| "plot".to_string());
|
||||
let mut name = format!("{stem} — beats.md");
|
||||
let suffix = match self.beats_filled.as_slice() {
|
||||
[] => "beats".to_string(),
|
||||
acts => format!(
|
||||
"beats {}",
|
||||
acts.iter()
|
||||
.map(|a| a.name().trim_start_matches("Act ").to_string())
|
||||
.collect::<Vec<_>>()
|
||||
.join("+")
|
||||
),
|
||||
};
|
||||
let mut name = format!("{stem} — {suffix}.md");
|
||||
let mut n = 2;
|
||||
while self.path_for(&name).exists() {
|
||||
name = format!("{stem} — beats-{n}.md");
|
||||
name = format!("{stem} — {suffix}-{n}.md");
|
||||
n += 1;
|
||||
}
|
||||
let path = self.path_for(&name);
|
||||
if let Some(parent) = path.parent() {
|
||||
let _ = std::fs::create_dir_all(parent);
|
||||
}
|
||||
let contents = format!("# Plot Beats — {stem}\n\n{}\n", beats.trim());
|
||||
let heading = match self.beats_filled.as_slice() {
|
||||
[] => format!("# Plot Beats — {stem}"),
|
||||
acts => format!("# Plot Beats — {stem} ({})", join_names(acts)),
|
||||
};
|
||||
let contents = format!("{heading}\n\n{}\n", beats.trim());
|
||||
match std::fs::write(&path, contents) {
|
||||
Ok(_) => {
|
||||
if !self.files.contains(&name) {
|
||||
@@ -258,3 +280,14 @@ impl App {
|
||||
self.show_mistral_settings = now_open;
|
||||
}
|
||||
}
|
||||
|
||||
/// Join act names for a status line or title: "Act Two", "Act Two and Act
|
||||
/// Three", "Act One, Act Two and Act Three".
|
||||
fn join_names(acts: &[crate::mistral::Act]) -> String {
|
||||
let names: Vec<&str> = acts.iter().map(|a| a.name()).collect();
|
||||
match names.as_slice() {
|
||||
[] => String::new(),
|
||||
[one] => one.to_string(),
|
||||
[rest @ .., last] => format!("{} and {last}", rest.join(", ")),
|
||||
}
|
||||
}
|
||||
|
||||
+6
-1
@@ -276,7 +276,11 @@ pub struct App {
|
||||
/// Whether the new-file template settings window is open.
|
||||
show_template_settings: bool,
|
||||
/// In-flight background plot-beat generation, if any.
|
||||
beats_rx: Option<std::sync::mpsc::Receiver<Result<String, String>>>,
|
||||
beats_rx: Option<std::sync::mpsc::Receiver<Result<crate::mistral::Beats, String>>>,
|
||||
/// Acts the last generation was asked to fill. Empty means it wrote a whole
|
||||
/// three-act sheet; non-empty means the proposal was already partly beaten
|
||||
/// out and only these acts came back.
|
||||
beats_filled: Vec<crate::mistral::Act>,
|
||||
/// One-line status for the plot-beat generator.
|
||||
beats_status: String,
|
||||
/// The generated (and user-editable) plot beats; `Some` opens the results
|
||||
@@ -361,6 +365,7 @@ impl App {
|
||||
show_template_settings: false,
|
||||
beats_rx: None,
|
||||
beats_status: String::new(),
|
||||
beats_filled: Vec::new(),
|
||||
beats_output: None,
|
||||
beats_source_stem: None,
|
||||
show_new_project: false,
|
||||
|
||||
Reference in New Issue
Block a user