From 3f6ab58e5419c98ab14f4a6ec9955d6758db2b7b Mon Sep 17 00:00:00 2001 From: Denis Olehov Date: Thu, 18 Jun 2026 01:43:05 +0200 Subject: [PATCH 1/2] refactor: remove the terraform feature Removes the natural-prose directives feature end to end: the terraform Rust module, IPC commands, output rendering and snapshots; the palette UI, composables, icons, types, keyboard/undo wiring, context plumbing and styles; plus the dev-doc references. Terraform was session-only, so there is no persisted data to migrate. --- CLAUDE.md | 8 +- src-tauri/src/commands.rs | 49 -- src-tauri/src/lib.rs | 13 +- src-tauri/src/output/formatters.rs | 97 --- src-tauri/src/output/mod.rs | 103 +-- src-tauri/src/output/snapshot_tests.rs | 417 ------------ ...napshot_tests__terraform_kitchen_sink.snap | 101 --- ...hot_tests__terraform_multiple_regions.snap | 23 - ...hot_tests__terraform_pin_and_dissolve.snap | 20 - ...ut__snapshot_tests__terraform_reframe.snap | 13 - ...put__snapshot_tests__terraform_remove.snap | 13 - ...apshot_tests__terraform_single_region.snap | 13 - ...hot_tests__terraform_with_annotations.snap | 21 - src-tauri/src/review.rs | 29 - src-tauri/src/terraform.rs | 591 ------------------ src/lib/HelpOverlay.svelte | 19 - src/lib/components/ChoiceButtons.svelte | 11 +- src/lib/components/DiscreteSlider.svelte | 98 --- src/lib/components/TerraformPalette.svelte | 538 ---------------- src/lib/components/embedded/LineRow.svelte | 114 +--- src/lib/composables/useHistory.svelte.ts | 9 - src/lib/composables/useHistory.test.ts | 1 - src/lib/composables/useInteraction.svelte.ts | 38 +- src/lib/composables/useKeyboard.svelte.ts | 28 +- src/lib/composables/useTerraform.svelte.ts | 365 ----------- .../composables/useTerraformRegions.svelte.ts | 143 ----- src/lib/context/AnnotProvider.svelte | 9 - src/lib/context/annot-context.svelte.ts | 2 - src/lib/icons/DirectionIcon.svelte | 12 - src/lib/icons/FormIcon.svelte | 10 - src/lib/icons/GravityIcon.svelte | 11 - src/lib/icons/MassIcon.svelte | 12 - src/lib/icons/TerraformIcon.svelte | 10 - src/lib/icons/index.ts | 5 - src/lib/types.ts | 75 --- src/routes/+page.svelte | 26 +- src/routes/page.test.ts | 4 - src/styles/components/code-viewer.css | 43 +- src/styles/components/terraform.css | 274 -------- src/styles/index.css | 1 - src/styles/tokens.css | 19 - 41 files changed, 28 insertions(+), 3360 deletions(-) delete mode 100644 src-tauri/src/output/snapshots/annot_lib__output__snapshot_tests__terraform_kitchen_sink.snap delete mode 100644 src-tauri/src/output/snapshots/annot_lib__output__snapshot_tests__terraform_multiple_regions.snap delete mode 100644 src-tauri/src/output/snapshots/annot_lib__output__snapshot_tests__terraform_pin_and_dissolve.snap delete mode 100644 src-tauri/src/output/snapshots/annot_lib__output__snapshot_tests__terraform_reframe.snap delete mode 100644 src-tauri/src/output/snapshots/annot_lib__output__snapshot_tests__terraform_remove.snap delete mode 100644 src-tauri/src/output/snapshots/annot_lib__output__snapshot_tests__terraform_single_region.snap delete mode 100644 src-tauri/src/output/snapshots/annot_lib__output__snapshot_tests__terraform_with_annotations.snap delete mode 100644 src-tauri/src/terraform.rs delete mode 100644 src/lib/components/DiscreteSlider.svelte delete mode 100644 src/lib/components/TerraformPalette.svelte delete mode 100644 src/lib/composables/useTerraform.svelte.ts delete mode 100644 src/lib/composables/useTerraformRegions.svelte.ts delete mode 100644 src/lib/icons/DirectionIcon.svelte delete mode 100644 src/lib/icons/FormIcon.svelte delete mode 100644 src/lib/icons/GravityIcon.svelte delete mode 100644 src/lib/icons/MassIcon.svelte delete mode 100644 src/lib/icons/TerraformIcon.svelte delete mode 100644 src/styles/components/terraform.css diff --git a/CLAUDE.md b/CLAUDE.md index fe7228d..0cb9b2a 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -122,10 +122,9 @@ process before rebuilding or you'll get `Access is denied (os error 5)`. - `output/` — Structured output rendering for LLM consumption - `mcp/` — Model Context Protocol server - `config.rs` — Persistent user settings (tags, exit modes, bookmarks) -- `terraform.rs` — Natural prose directives feature **Frontend** (`src/lib/`): -- `composables/` — Svelte 5 runes-based state (useAnnotations, useTerraformRegions, etc.) +- `composables/` — Svelte 5 runes-based state (useAnnotations, etc.) - `components/` — UI components (LineRow, CodeViewer, etc.) - `CommandPalette/` — `:` command palette with namespaces - `tiptap.ts` — Rich text editor configuration @@ -184,7 +183,7 @@ Kill the stray `annot.exe` before rebuilding. ## UI Patterns ### Line Actions (right-side icons) -Add buttons inside the `{#if trailing || showBookmarkIcon || terraformRegionStart}` block in `LineRow.svelte`. Use `.line-action` class. +Add buttons inside the `{#if trailing || showBookmarkIcon}` block in `LineRow.svelte`. Use `.line-action` class. ### Left Border Indicators Use `::before` pseudo-elements with `position: absolute; left: 0; width: 3px;`. For overlapping indicators, use `repeating-linear-gradient`. @@ -194,6 +193,3 @@ Use `::before` pseudo-elements with `position: absolute; left: 0; width: 3px;`. - Source lines: `line.origin.line` (file) or `line.origin.new_line`/`old_line` (diff) Use `getLineNumber(line)` and `getFilePath(line)` from `line-utils.ts`. - -### Terraform Regions -Persist to backend with source line numbers. Use `useTerraformRegions` composable for state. diff --git a/src-tauri/src/commands.rs b/src-tauri/src/commands.rs index 0cb4791..f71edae 100644 --- a/src-tauri/src/commands.rs +++ b/src-tauri/src/commands.rs @@ -88,55 +88,6 @@ pub fn delete_annotation( }) } -// ========== Terraform Commands ========== - -use crate::terraform::TerraformRegion; - -#[tauri::command] -pub fn upsert_terraform( - review_state: State, - path: String, - region: TerraformRegion, -) -> Result<(), String> { - with_review!(review_state, |review| { - let target = review.resolve_target_mut(&path)?; - target.upsert_terraform(region); - Ok(()) - }) -} - -#[tauri::command] -pub fn delete_terraform( - review_state: State, - path: String, - start_line: u32, - end_line: u32, -) -> Result<(), String> { - with_review!(review_state, |review| { - let target = review.resolve_target_mut(&path)?; - target.delete_terraform(start_line, end_line); - Ok(()) - }) -} - -#[tauri::command] -pub fn get_terraform_regions( - review_state: State, - path: String, -) -> Result, String> { - with_review!(review_state, |review| { - let target = review.resolve_target_mut(&path)?; - Ok(target.terraform_regions().to_vec()) - }) -} - -/// Get the natural language phrase for a terraform region. -/// Used for live preview in the terraform palette. -#[tauri::command] -pub fn get_terraform_phrase(region: TerraformRegion) -> String { - region.to_prose() -} - /// Unified finish command - handles both CLI and MCP modes. #[tauri::command] pub fn finish_review( diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 203f60a..6ac87ed 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -22,17 +22,16 @@ pub mod output; pub mod portal; pub mod review; pub mod state; -pub mod terraform; pub mod window_state; use commands::{ compute_replace_diff, copy_section, copy_to_clipboard, create_bookmark, create_selection_bookmark, cycle_exit_mode, delete_annotation, delete_bookmark, - delete_exit_mode, delete_tag, delete_terraform, export_to_obsidian, finish_review, - get_bookmarks, get_config, get_content, get_exit_modes, get_tags, get_terraform_phrase, - get_terraform_regions, get_theme, reload_config, reorder_exit_modes, save_config, + delete_exit_mode, delete_tag, export_to_obsidian, finish_review, + get_bookmarks, get_config, get_content, get_exit_modes, get_tags, + get_theme, reload_config, reorder_exit_modes, save_config, save_content, set_exit_mode, set_session_comment, set_theme, update_bookmark, - upsert_annotation, upsert_exit_mode, upsert_tag, upsert_terraform, + upsert_annotation, upsert_exit_mode, upsert_tag, }; use excalidraw_window::{ close_excalidraw_by_placeholder, excalidraw_cancel, excalidraw_save, get_excalidraw_context, @@ -50,10 +49,6 @@ macro_rules! all_commands { get_content, upsert_annotation, delete_annotation, - upsert_terraform, - delete_terraform, - get_terraform_regions, - get_terraform_phrase, finish_review, set_exit_mode, cycle_exit_mode, diff --git a/src-tauri/src/output/formatters.rs b/src-tauri/src/output/formatters.rs index a908bb3..a1020b2 100644 --- a/src-tauri/src/output/formatters.rs +++ b/src-tauri/src/output/formatters.rs @@ -9,7 +9,6 @@ use crate::mcp::tools::SessionImage; use crate::state::{ Annotation, Bookmark, ContentMetadata, ContentModel, LineOrigin, }; -use crate::terraform::TerraformRegion; use super::builder::{BuilderMode, OutputBuilder}; use super::render::render_content; @@ -223,99 +222,3 @@ pub fn calculate_builder_mode( BuilderMode::File { line_num_width } } } - -/// Format a single terraform region block with context lines and prose. -pub fn format_terraform_region( - out: &mut OutputBuilder, - content_model: &ContentModel, - region: &TerraformRegion, - file_path: &str, -) { - let is_diff = matches!(content_model.metadata, ContentMetadata::Diff(_)); - - // File header - if is_diff { - format_diff_header_for_terraform(out, content_model, region, file_path); - } else if region.start_line == region.end_line { - out.raw_line(&format!("{}:{}", file_path, region.start_line)); - } else { - out.raw_line(&format!( - "{}:{}-{}", - file_path, region.start_line, region.end_line - )); - } - - // Context line (1 line before, if exists and non-empty) - if region.start_line > 1 { - let context_line_num = region.start_line - 1; - if let Some(line) = content_model.find_line(file_path, context_line_num) { - if !line.content.trim().is_empty() { - if is_diff { - format_diff_context_line(out, content_model, file_path, context_line_num, &line.content); - } else { - out.code_line(context_line_num, &line.content); - } - } - } - } - - // Selected lines - for line_num in region.start_line..=region.end_line { - if let Some(line) = content_model.find_line(file_path, line_num) { - if is_diff { - format_diff_selected_line(out, content_model, file_path, line_num, &line.content); - } else { - out.selected_code_line(line_num, &line.content); - } - } - } - - // Terraform prose with arrow - let prose = region.to_prose(); - if !prose.is_empty() { - let mut lines = prose.lines(); - if let Some(first) = lines.next() { - out.arrow(first); - for continuation in lines { - out.arrow_continuation(continuation); - } - } - } -} - -/// Format diff header for terraform region (mirrors annotation format). -fn format_diff_header_for_terraform( - out: &mut OutputBuilder, - content: &ContentModel, - region: &TerraformRegion, - file_path: &str, -) { - // Collect old/new line ranges from the terraform lines - let mut old_lines: Vec = Vec::new(); - let mut new_lines: Vec = Vec::new(); - - for line_num in region.start_line..=region.end_line { - if let Some(line) = content.find_line(file_path, line_num) { - if let LineOrigin::Diff { old_line, new_line, .. } = &line.origin { - if let Some(old) = old_line { - old_lines.push(*old); - } - if let Some(new) = new_line { - new_lines.push(*new); - } - } - } - } - - // Format header with available line info - let old_range = format_line_range(&old_lines); - let new_range = format_line_range(&new_lines); - - let header = match (old_range.as_str(), new_range.as_str()) { - ("", "") => format!("{}:", file_path), - (old, "") => format!("{} (old:{}):", file_path, old), - ("", new) => format!("{} (new:{}):", file_path, new), - (old, new) => format!("{} (old:{} new:{}):", file_path, old, new), - }; - out.raw_line(&header); -} diff --git a/src-tauri/src/output/mod.rs b/src-tauri/src/output/mod.rs index f467cd8..10dcc35 100644 --- a/src-tauri/src/output/mod.rs +++ b/src-tauri/src/output/mod.rs @@ -25,7 +25,7 @@ use crate::state::{ pub use builder::{BuilderMode, OutputBuilder, SECTION_DIVIDER, SEPARATOR}; pub use render::render_content; -use formatters::{calculate_builder_mode, format_annotation, format_bookmark, format_legend, format_terraform_region}; +use formatters::{calculate_builder_mode, format_annotation, format_bookmark, format_legend}; /// Output mode determines how content is formatted. #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] @@ -54,7 +54,6 @@ pub struct FormatMetadata { pub general_comment: Option, pub exit_mode: Option, pub general_comment_count: usize, - pub terraform_count: usize, pub bookmark_count: usize, } @@ -71,7 +70,6 @@ pub fn format_json(result: &FormatResult) -> String { "general_comment": result.metadata.general_comment, "exit_mode": result.metadata.exit_mode, "general_comment_count": result.metadata.general_comment_count, - "terraform_count": result.metadata.terraform_count, "bookmark_count": result.metadata.bookmark_count, }); serde_json::to_string(&json).expect("FormatResult JSON serialization should not fail") @@ -363,17 +361,12 @@ pub fn format_output(review: &Review, mode: OutputMode) -> FormatResult { // Get content from root_view let content = review.root_view.content(); - // Check if ANY file has annotations or terraform regions + // Check if ANY file has annotations let has_annotations = review .files .values() .any(|target| !target.annotations.is_empty()); - let has_terraform = review - .files - .values() - .any(|target| !target.terraform_regions.is_empty()); - let has_exit_mode = review.selected_exit_mode_id.is_some(); let has_session_comment = review .session_comment @@ -382,7 +375,7 @@ pub fn format_output(review: &Review, mode: OutputMode) -> FormatResult { .unwrap_or(false); let has_saved_to = review.saved_to.is_some(); - if !has_exit_mode && !has_annotations && !has_terraform && !has_session_comment && !has_saved_to { + if !has_exit_mode && !has_annotations && !has_session_comment && !has_saved_to { return FormatResult { text: String::new(), images: Vec::new(), @@ -483,42 +476,12 @@ pub fn format_output(review: &Review, mode: OutputMode) -> FormatResult { } } - // Add divider before terraform/annotations if we had any header content + // Add divider before annotations if we had any header content let has_header_content = has_context || has_session_comment || has_exit_mode; - if (has_terraform || has_annotations) && has_header_content { + if has_annotations && has_header_content { out.raw("---\n\n"); } - // Build terraform blocks (if any) - if has_terraform { - out.raw_line("TERRAFORM:"); - out.blank_line(); - - let files_with_terraform = collect_files_with_terraform(review); - let mut first_block = true; - - for (display_path, target) in &files_with_terraform { - // Sort terraform regions by start line - let mut sorted_regions: Vec<&crate::terraform::TerraformRegion> = - target.terraform_regions.iter().collect(); - sorted_regions.sort_by_key(|r| r.start_line); - - for region in sorted_regions { - if !first_block { - out.divider(); - } - first_block = false; - format_terraform_region(&mut out, content, region, display_path); - } - } - - // Add divider between terraform and annotations - if has_annotations { - out.blank_line(); - out.raw("---\n\n"); - } - } - // Build annotation blocks (if any) if has_annotations { let files_with_annotations = collect_files_with_annotations(review); @@ -562,12 +525,6 @@ pub fn format_output(review: &Review, mode: OutputMode) -> FormatResult { .map(|target| target.annotations.len()) .sum(); - let terraform_count = review - .files - .values() - .map(|target| target.terraform_regions.len()) - .sum(); - let general_comment_count = if review.session_comment.as_ref() .map(|c| !c.is_empty()) .unwrap_or(false) { 1 } else { 0 }; @@ -599,31 +556,20 @@ pub fn format_output(review: &Review, mode: OutputMode) -> FormatResult { general_comment, exit_mode, general_comment_count, - terraform_count, bookmark_count, }, } } -/// Calculate max line number across all annotations and terraform regions. +/// Calculate max line number across all annotations. fn calculate_max_line(review: &Review) -> u32 { - let annotation_max = review + review .files .values() .flat_map(|target| target.annotations.values()) .map(|a| a.end_line) .max() - .unwrap_or(0); - - let terraform_max = review - .files - .values() - .flat_map(|target| target.terraform_regions.iter()) - .map(|r| r.end_line) - .max() - .unwrap_or(0); - - annotation_max.max(terraform_max) + .unwrap_or(0) } /// Collect files with annotations in display order. @@ -659,39 +605,6 @@ fn collect_files_with_annotations(review: &Review) -> Vec<(String, &crate::revie } } -/// Collect files with terraform regions in display order. -fn collect_files_with_terraform(review: &Review) -> Vec<(String, &crate::review::AnnotationTarget)> { - if let Some(diff_files) = review.root_view.diff_files() { - // Diff mode: use DiffFileView for display paths, enumerate for index - diff_files - .iter() - .enumerate() - .filter_map(|(index, df)| { - let key = FileKey::diff_file(index); - review.files.get(&key).and_then(|target| { - if target.terraform_regions.is_empty() { - None - } else { - Some((df.path.display().to_string(), target)) - } - }) - }) - .collect() - } else { - // File mode: extract display string from FileKey - review - .files - .iter() - .filter(|(_, target)| !target.terraform_regions.is_empty()) - .filter_map(|(key, target)| match key { - FileKey::Path(p) => Some((p.display().to_string(), target)), - FileKey::Ephemeral { label } => Some((label.clone(), target)), - FileKey::DiffFile { .. } => None, // Should not happen in file mode - }) - .collect() - } -} - #[cfg(test)] mod tests { use super::*; diff --git a/src-tauri/src/output/snapshot_tests.rs b/src-tauri/src/output/snapshot_tests.rs index 282f161..9da204b 100644 --- a/src-tauri/src/output/snapshot_tests.rs +++ b/src-tauri/src/output/snapshot_tests.rs @@ -963,423 +963,6 @@ description: "Create a well-structured git commit" insta::assert_snapshot!(stable_output); } -// ========== Terraform Regions ========== - -#[test] -fn terraform_single_region() { - use crate::terraform::{FormType, Intensity, MassChange, TerraformIntent, TerraformRegion}; - - let source = ContentSource::Cli(CliSource::File { - path: PathBuf::from("plan.md"), - }); - let content = ContentModel { - label: "plan.md".to_string(), - lines: make_lines("plan.md", 1, 20), - source, - metadata: ContentMetadata::Plain, - portals: Vec::new(), - }; - let config = UserConfig::empty(); - let mut review = Review::cli(content, config, "main".to_string()); - - // Add a terraform region - if let Some(file) = review.files.values_mut().next() { - file.terraform_regions.push(TerraformRegion { - start_line: 5, - end_line: 8, - intent: TerraformIntent::Transform { - form: vec![FormType::Table], - mass: Some(MassChange::Expand { - intensity: Intensity::Moderately, - }), - gravity: None, - direction: None, - }, - }); - } - - let output = format_output(&review, OutputMode::Cli).text; - insta::assert_snapshot!(output); -} - -#[test] -fn terraform_multiple_regions() { - use crate::terraform::{ - DirectionDirective, FormType, GravityChange, Intensity, MassChange, TerraformIntent, - TerraformRegion, - }; - - let source = ContentSource::Cli(CliSource::File { - path: PathBuf::from("content.md"), - }); - let content = ContentModel { - label: "content.md".to_string(), - lines: make_lines("content.md", 1, 30), - source, - metadata: ContentMetadata::Plain, - portals: Vec::new(), - }; - let config = UserConfig::empty(); - let mut review = Review::cli(content, config, "main".to_string()); - - if let Some(file) = review.files.values_mut().next() { - // First region: expand into table and prose - file.terraform_regions.push(TerraformRegion { - start_line: 5, - end_line: 8, - intent: TerraformIntent::Transform { - form: vec![FormType::Table, FormType::Prose], - mass: Some(MassChange::Expand { - intensity: Intensity::Moderately, - }), - gravity: Some(GravityChange::Focus { - intensity: Intensity::Slightly, - }), - direction: None, - }, - }); - - // Second region: condense with direction - file.terraform_regions.push(TerraformRegion { - start_line: 15, - end_line: 18, - intent: TerraformIntent::Transform { - form: vec![], - mass: Some(MassChange::Condense { - intensity: Intensity::Significantly, - }), - gravity: None, - direction: Some(DirectionDirective::MoveAway { - intensity: Intensity::Moderately, - }), - }, - }); - } - - let output = format_output(&review, OutputMode::Cli).text; - insta::assert_snapshot!(output); -} - -#[test] -fn terraform_with_annotations() { - use crate::terraform::{FormType, Intensity, MassChange, TerraformIntent, TerraformRegion}; - - let source = ContentSource::Cli(CliSource::File { - path: PathBuf::from("mixed.md"), - }); - let content = ContentModel { - label: "mixed.md".to_string(), - lines: make_lines("mixed.md", 1, 30), - source, - metadata: ContentMetadata::Plain, - portals: Vec::new(), - }; - let config = UserConfig::empty(); - let mut review = Review::cli(content, config, "main".to_string()); - - if let Some(file) = review.files.values_mut().next() { - // Terraform region - file.terraform_regions.push(TerraformRegion { - start_line: 5, - end_line: 8, - intent: TerraformIntent::Transform { - form: vec![FormType::List], - mass: Some(MassChange::Expand { - intensity: Intensity::Moderately, - }), - gravity: None, - direction: None, - }, - }); - - // Annotation - file.annotations.insert( - LineRange::new(15, 16), - Annotation { - start_line: 15, - end_line: 16, - content: vec![ContentNode::Text { - text: "This needs clarification".to_string(), - }], - }, - ); - } - - let output = format_output(&review, OutputMode::Cli).text; - insta::assert_snapshot!(output); -} - -#[test] -fn terraform_pin_and_dissolve() { - use crate::terraform::{TerraformIntent, TerraformRegion}; - - let source = ContentSource::Cli(CliSource::File { - path: PathBuf::from("gravity.md"), - }); - let content = ContentModel { - label: "gravity.md".to_string(), - lines: make_lines("gravity.md", 1, 20), - source, - metadata: ContentMetadata::Plain, - portals: Vec::new(), - }; - let config = UserConfig::empty(); - let mut review = Review::cli(content, config, "main".to_string()); - - if let Some(file) = review.files.values_mut().next() { - // Pin region - file.terraform_regions.push(TerraformRegion { - start_line: 3, - end_line: 4, - intent: TerraformIntent::Pin, - }); - - // Dissolve region - file.terraform_regions.push(TerraformRegion { - start_line: 12, - end_line: 14, - intent: TerraformIntent::Dissolve { direction: None }, - }); - } - - let output = format_output(&review, OutputMode::Cli).text; - insta::assert_snapshot!(output); -} - -#[test] -fn terraform_reframe() { - use crate::terraform::{DirectionDirective, TerraformIntent, TerraformRegion}; - - let source = ContentSource::Cli(CliSource::File { - path: PathBuf::from("reframe.md"), - }); - let content = ContentModel { - label: "reframe.md".to_string(), - lines: make_lines("reframe.md", 1, 15), - source, - metadata: ContentMetadata::Plain, - portals: Vec::new(), - }; - let config = UserConfig::empty(); - let mut review = Review::cli(content, config, "main".to_string()); - - if let Some(file) = review.files.values_mut().next() { - file.terraform_regions.push(TerraformRegion { - start_line: 5, - end_line: 8, - intent: TerraformIntent::Transform { - form: vec![], - mass: None, - gravity: None, - direction: Some(DirectionDirective::Reframe), - }, - }); - } - - let output = format_output(&review, OutputMode::Cli).text; - insta::assert_snapshot!(output); -} - -#[test] -fn terraform_remove() { - use crate::terraform::{TerraformIntent, TerraformRegion}; - - let source = ContentSource::Cli(CliSource::File { - path: PathBuf::from("remove.md"), - }); - let content = ContentModel { - label: "remove.md".to_string(), - lines: make_lines("remove.md", 1, 15), - source, - metadata: ContentMetadata::Plain, - portals: Vec::new(), - }; - let config = UserConfig::empty(); - let mut review = Review::cli(content, config, "main".to_string()); - - if let Some(file) = review.files.values_mut().next() { - file.terraform_regions.push(TerraformRegion { - start_line: 5, - end_line: 8, - intent: TerraformIntent::Remove, - }); - } - - let output = format_output(&review, OutputMode::Cli).text; - insta::assert_snapshot!(output); -} - -/// Comprehensive terraform test that exercises every feature: -/// - Multiple terraform regions with all intents -/// - All form types (single, double, multi-select) -/// - All mass changes (expand, condense) -/// - All gravity changes (focus, blur) -/// - Terminal states (remove, pin, dissolve) -/// - All direction directives (lean-in, move-away, reframe) -/// - All intensity levels -/// - Combined with annotations -/// - Combined with session comment and exit mode -#[test] -fn terraform_kitchen_sink() { - use crate::terraform::{ - DirectionDirective, FormType, GravityChange, Intensity, MassChange, TerraformIntent, - TerraformRegion, - }; - - let config = UserConfig::with_data( - vec![], - vec![ExitMode { - id: "iterate".to_string(), - name: "Iterate".to_string(), - color: "#8b5cf6".to_string(), - instruction: "Apply terraform directives and regenerate content".to_string(), - order: 0, - source: ExitModeSource::Persisted, - }], - ); - - let source = ContentSource::Cli(CliSource::File { - path: PathBuf::from("design-doc.md"), - }); - let content = ContentModel { - label: "design-doc.md".to_string(), - lines: make_lines("design-doc.md", 1, 80), - source, - metadata: ContentMetadata::Plain, - portals: Vec::new(), - }; - let mut review = Review::cli(content, config, "main".to_string()); - - if let Some(file) = review.files.values_mut().next() { - // Region 1: Single form + expand slightly + focus moderately + lean-in completely - file.terraform_regions.push(TerraformRegion { - start_line: 5, - end_line: 8, - intent: TerraformIntent::Transform { - form: vec![FormType::Table], - mass: Some(MassChange::Expand { - intensity: Intensity::Slightly, - }), - gravity: Some(GravityChange::Focus { - intensity: Intensity::Moderately, - }), - direction: Some(DirectionDirective::LeanIn { - intensity: Intensity::Significantly, - }), - }, - }); - - // Region 2: Two forms + condense significantly - file.terraform_regions.push(TerraformRegion { - start_line: 15, - end_line: 18, - intent: TerraformIntent::Transform { - form: vec![FormType::List, FormType::Diagram], - mass: Some(MassChange::Condense { - intensity: Intensity::Significantly, - }), - gravity: None, - direction: None, - }, - }); - - // Region 3: Multiple forms + blur a bit + move-away moderately - file.terraform_regions.push(TerraformRegion { - start_line: 25, - end_line: 28, - intent: TerraformIntent::Transform { - form: vec![FormType::Prose, FormType::Code, FormType::Table], - mass: None, - gravity: Some(GravityChange::Blur { - intensity: Intensity::Moderately, - }), - direction: Some(DirectionDirective::MoveAway { - intensity: Intensity::Moderately, - }), - }, - }); - - // Region 4: Pin only (preserve exactly) - file.terraform_regions.push(TerraformRegion { - start_line: 35, - end_line: 36, - intent: TerraformIntent::Pin, - }); - - // Region 5: Dissolve only (integrate into surroundings) - file.terraform_regions.push(TerraformRegion { - start_line: 42, - end_line: 45, - intent: TerraformIntent::Dissolve { direction: None }, - }); - - // Region 6: Remove entirely - file.terraform_regions.push(TerraformRegion { - start_line: 52, - end_line: 55, - intent: TerraformIntent::Remove, - }); - - // Region 7: Reframe only - file.terraform_regions.push(TerraformRegion { - start_line: 62, - end_line: 65, - intent: TerraformIntent::Transform { - form: vec![FormType::Prose], - mass: None, - gravity: None, - direction: Some(DirectionDirective::Reframe), - }, - }); - - // Add some annotations alongside terraform regions - file.annotations.insert( - LineRange::new(10, 12), - Annotation { - start_line: 10, - end_line: 12, - content: vec![ - ContentNode::Tag { - id: "clarity001".to_string(), - name: "CLARITY".to_string(), - instruction: "Needs clearer explanation".to_string(), - }, - ContentNode::Text { - text: " This section is confusing".to_string(), - }, - ], - }, - ); - - file.annotations.insert( - LineRange::new(70, 72), - Annotation { - start_line: 70, - end_line: 72, - content: vec![ContentNode::Text { - text: "Good example, keep this".to_string(), - }], - }, - ); - } - - // Set session comment - review.session_comment = Some(vec![ - ContentNode::Text { - text: "Overall feedback on the design document.\n\n".to_string(), - }, - ContentNode::Text { - text: "The structure needs work - use terraform directives to reshape.".to_string(), - }, - ]); - - // Set exit mode - review.selected_exit_mode_id = Some("iterate".to_string()); - - let output = format_output(&review, OutputMode::Cli).text; - insta::assert_snapshot!(output); -} - // ========== JSON Output Tests ========== #[test] diff --git a/src-tauri/src/output/snapshots/annot_lib__output__snapshot_tests__terraform_kitchen_sink.snap b/src-tauri/src/output/snapshots/annot_lib__output__snapshot_tests__terraform_kitchen_sink.snap deleted file mode 100644 index 208a67c..0000000 --- a/src-tauri/src/output/snapshots/annot_lib__output__snapshot_tests__terraform_kitchen_sink.snap +++ /dev/null @@ -1,101 +0,0 @@ ---- -source: src/output/snapshot_tests.rs -expression: output ---- -TAGS: - [# CLARITY] Needs clearer explanation - -GENERAL: - Overall feedback on the design document. - - The structure needs work - use terraform directives to reshape. - -NEXT: Iterate — Apply terraform directives and regenerate content - ---- - -TERRAFORM: - -design-doc.md:5-8 - 4 | line 4 content -> 5 | line 5 content -> 6 | line 6 content -> 7 | line 7 content -> 8 | line 8 content - └──> Present as a fuller table. Emphasize the key points. Double down on this approach. - ---- - -design-doc.md:15-18 - 14 | line 14 content -> 15 | line 15 content -> 16 | line 16 content -> 17 | line 17 content -> 18 | line 18 content - └──> Express via minimal list and diagram. - ---- - -design-doc.md:25-28 - 24 | line 24 content -> 25 | line 25 content -> 26 | line 26 content -> 27 | line 27 content -> 28 | line 28 content - └──> Express via passage, code block, and table. Treat as supporting context. This needs a different direction. - ---- - -design-doc.md:35-36 - 34 | line 34 content -> 35 | line 35 content -> 36 | line 36 content - └──> Preserve this exactly as written. - ---- - -design-doc.md:42-45 - 41 | line 41 content -> 42 | line 42 content -> 43 | line 43 content -> 44 | line 44 content -> 45 | line 45 content - └──> Dissolve this as a unit, integrating its essence into surroundings. - ---- - -design-doc.md:52-55 - 51 | line 51 content -> 52 | line 52 content -> 53 | line 53 content -> 54 | line 54 content -> 55 | line 55 content - └──> Remove this entirely. - ---- - -design-doc.md:62-65 - 61 | line 61 content -> 62 | line 62 content -> 63 | line 63 content -> 64 | line 64 content -> 65 | line 65 content - └──> Present as a passage. Same content, reframed from a different angle. - ---- - -design-doc.md:10-12 - 9 | line 9 content -> 10 | line 10 content -> 11 | line 11 content -> 12 | line 12 content - └──> [# CLARITY] This section is confusing - ---- - -design-doc.md:70-72 - 69 | line 69 content -> 70 | line 70 content -> 71 | line 71 content -> 72 | line 72 content - └──> Good example, keep this diff --git a/src-tauri/src/output/snapshots/annot_lib__output__snapshot_tests__terraform_multiple_regions.snap b/src-tauri/src/output/snapshots/annot_lib__output__snapshot_tests__terraform_multiple_regions.snap deleted file mode 100644 index 63d3cf8..0000000 --- a/src-tauri/src/output/snapshots/annot_lib__output__snapshot_tests__terraform_multiple_regions.snap +++ /dev/null @@ -1,23 +0,0 @@ ---- -source: src/output/snapshot_tests.rs -expression: output ---- -TERRAFORM: - -content.md:5-8 - 4 | line 4 content -> 5 | line 5 content -> 6 | line 6 content -> 7 | line 7 content -> 8 | line 8 content - └──> Express via detailed table and passage. Give this slightly more weight. - ---- - -content.md:15-18 - 14 | line 14 content -> 15 | line 15 content -> 16 | line 16 content -> 17 | line 17 content -> 18 | line 18 content - └──> Make this minimal. This needs a different direction. diff --git a/src-tauri/src/output/snapshots/annot_lib__output__snapshot_tests__terraform_pin_and_dissolve.snap b/src-tauri/src/output/snapshots/annot_lib__output__snapshot_tests__terraform_pin_and_dissolve.snap deleted file mode 100644 index 22b0af6..0000000 --- a/src-tauri/src/output/snapshots/annot_lib__output__snapshot_tests__terraform_pin_and_dissolve.snap +++ /dev/null @@ -1,20 +0,0 @@ ---- -source: src/output/snapshot_tests.rs -expression: output ---- -TERRAFORM: - -gravity.md:3-4 - 2 | line 2 content -> 3 | line 3 content -> 4 | line 4 content - └──> Preserve this exactly as written. - ---- - -gravity.md:12-14 - 11 | line 11 content -> 12 | line 12 content -> 13 | line 13 content -> 14 | line 14 content - └──> Dissolve this as a unit, integrating its essence into surroundings. diff --git a/src-tauri/src/output/snapshots/annot_lib__output__snapshot_tests__terraform_reframe.snap b/src-tauri/src/output/snapshots/annot_lib__output__snapshot_tests__terraform_reframe.snap deleted file mode 100644 index 49cab1c..0000000 --- a/src-tauri/src/output/snapshots/annot_lib__output__snapshot_tests__terraform_reframe.snap +++ /dev/null @@ -1,13 +0,0 @@ ---- -source: src/output/snapshot_tests.rs -expression: output ---- -TERRAFORM: - -reframe.md:5-8 - 4 | line 4 content -> 5 | line 5 content -> 6 | line 6 content -> 7 | line 7 content -> 8 | line 8 content - └──> Same content, reframed from a different angle. diff --git a/src-tauri/src/output/snapshots/annot_lib__output__snapshot_tests__terraform_remove.snap b/src-tauri/src/output/snapshots/annot_lib__output__snapshot_tests__terraform_remove.snap deleted file mode 100644 index 33f5342..0000000 --- a/src-tauri/src/output/snapshots/annot_lib__output__snapshot_tests__terraform_remove.snap +++ /dev/null @@ -1,13 +0,0 @@ ---- -source: src/output/snapshot_tests.rs -expression: output ---- -TERRAFORM: - -remove.md:5-8 - 4 | line 4 content -> 5 | line 5 content -> 6 | line 6 content -> 7 | line 7 content -> 8 | line 8 content - └──> Remove this entirely. diff --git a/src-tauri/src/output/snapshots/annot_lib__output__snapshot_tests__terraform_single_region.snap b/src-tauri/src/output/snapshots/annot_lib__output__snapshot_tests__terraform_single_region.snap deleted file mode 100644 index 5372a09..0000000 --- a/src-tauri/src/output/snapshots/annot_lib__output__snapshot_tests__terraform_single_region.snap +++ /dev/null @@ -1,13 +0,0 @@ ---- -source: src/output/snapshot_tests.rs -expression: output ---- -TERRAFORM: - -plan.md:5-8 - 4 | line 4 content -> 5 | line 5 content -> 6 | line 6 content -> 7 | line 7 content -> 8 | line 8 content - └──> Present as a detailed table. diff --git a/src-tauri/src/output/snapshots/annot_lib__output__snapshot_tests__terraform_with_annotations.snap b/src-tauri/src/output/snapshots/annot_lib__output__snapshot_tests__terraform_with_annotations.snap deleted file mode 100644 index 31b0117..0000000 --- a/src-tauri/src/output/snapshots/annot_lib__output__snapshot_tests__terraform_with_annotations.snap +++ /dev/null @@ -1,21 +0,0 @@ ---- -source: src/output/snapshot_tests.rs -expression: output ---- -TERRAFORM: - -mixed.md:5-8 - 4 | line 4 content -> 5 | line 5 content -> 6 | line 6 content -> 7 | line 7 content -> 8 | line 8 content - └──> Present as a detailed list. - ---- - -mixed.md:15-16 - 14 | line 14 content -> 15 | line 15 content -> 16 | line 16 content - └──> This needs clarification diff --git a/src-tauri/src/review.rs b/src-tauri/src/review.rs index 88c3cf6..0a43e01 100644 --- a/src-tauri/src/review.rs +++ b/src-tauri/src/review.rs @@ -109,16 +109,12 @@ pub struct Review { pub saved_to: Option, } -use crate::terraform::TerraformRegion; - /// Annotation target — a file that can receive annotations. /// Contains annotations and file-specific metadata, but NOT content. /// Content lives in `View` (the root_view field on Review). pub struct AnnotationTarget { /// Annotations keyed by normalized line range. pub annotations: HashMap, - /// Terraform regions for structured transformation directives. - pub terraform_regions: Vec, /// File-specific metadata (language, etc.). pub metadata: FileMetadata, } @@ -128,7 +124,6 @@ impl AnnotationTarget { pub fn new() -> Self { Self { annotations: HashMap::new(), - terraform_regions: Vec::new(), metadata: FileMetadata::default(), } } @@ -514,30 +509,6 @@ impl AnnotationTarget { pub fn delete_annotation(&mut self, start_line: u32, end_line: u32) { self.annotations.remove(&LineRange::new(start_line, end_line)); } - - /// Insert or update a terraform region. - /// Replaces any overlapping regions with the new one. - pub fn upsert_terraform(&mut self, region: TerraformRegion) { - // Remove any regions that overlap with the new one - self.terraform_regions.retain(|r| { - !(r.start_line <= region.end_line && r.end_line >= region.start_line) - }); - // Add the new region - self.terraform_regions.push(region); - // Keep sorted by start_line - self.terraform_regions.sort_by_key(|r| r.start_line); - } - - /// Delete a terraform region by exact range match. - pub fn delete_terraform(&mut self, start_line: u32, end_line: u32) { - self.terraform_regions - .retain(|r| r.start_line != start_line || r.end_line != end_line); - } - - /// Get all terraform regions. - pub fn terraform_regions(&self) -> &[TerraformRegion] { - &self.terraform_regions - } } impl ContentModel { diff --git a/src-tauri/src/terraform.rs b/src-tauri/src/terraform.rs deleted file mode 100644 index 8ce036e..0000000 --- a/src-tauri/src/terraform.rs +++ /dev/null @@ -1,591 +0,0 @@ -//! Terraform regions — structured controls for guiding AI content transformation. -//! -//! The type system enforces valid combinations: -//! - `Remove` and `Pin` are terminal states — no other axes apply -//! - `Dissolve` only allows direction — form/mass are irrelevant -//! - `Transform` allows all axes: form, mass, gravity (focus/blur), direction -//! -//! This makes invalid states unrepresentable at compile time. - -use serde::{Deserialize, Serialize}; - -/// A terraform region attached to a line range. -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] -pub struct TerraformRegion { - pub start_line: u32, - pub end_line: u32, - /// The transformation intent — type-safe combinations only. - pub intent: TerraformIntent, -} - -/// Type-safe terraform intent — invalid combinations are unrepresentable. -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] -#[serde(tag = "kind", rename_all = "snake_case")] -pub enum TerraformIntent { - /// Remove this content entirely. Terminal state. - Remove, - - /// Preserve exactly as written. Terminal state. - Pin, - - /// Dissolve into surroundings — only direction applies. - Dissolve { - direction: Option, - }, - - /// Full transformation — all axes available. - Transform { - form: Vec, - mass: Option, - gravity: Option, - direction: Option, - }, -} - -impl Default for TerraformIntent { - fn default() -> Self { - TerraformIntent::Transform { - form: vec![], - mass: None, - gravity: None, - direction: None, - } - } -} - -/// Target format for restructuring. -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] -#[serde(rename_all = "lowercase")] -pub enum FormType { - Table, - List, - Prose, - Diagram, - Code, -} - -impl FormType { - /// Tag value for FORMAT directive. - /// Note: "prose" becomes "passage" to work with articles ("a passage" not "a prose"). - fn as_tag(&self) -> &'static str { - match self { - FormType::Table => "table", - FormType::List => "list", - FormType::Prose => "passage", - FormType::Diagram => "diagram", - FormType::Code => "code block", - } - } -} - -/// Mass change: expand or condense (Remove is a separate intent). -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] -#[serde(tag = "type", rename_all = "lowercase")] -pub enum MassChange { - Expand { intensity: Intensity }, - Condense { intensity: Intensity }, -} - -/// Gravity change: focus or blur (Pin/Dissolve are separate intents). -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] -#[serde(tag = "type", rename_all = "lowercase")] -pub enum GravityChange { - Focus { intensity: Intensity }, - Blur { intensity: Intensity }, -} - -/// Correctness signal: lean-in, move-away, or reframe. -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] -#[serde(tag = "type", rename_all = "lowercase")] -pub enum DirectionDirective { - LeanIn { intensity: Intensity }, - MoveAway { intensity: Intensity }, - Reframe, -} - -/// Intensity level for graduated controls. -#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)] -#[serde(rename_all = "snake_case")] -pub enum Intensity { - Slightly, // level 1 (gentlest) - Moderately, // level 2 - Significantly, // level 3 (strongest) -} - -impl Intensity { - /// Convert to adverb for natural language output. - pub fn as_adverb(&self) -> &'static str { - match self { - Intensity::Slightly => "slightly", - Intensity::Moderately => "moderately", - Intensity::Significantly => "significantly", - } - } -} - -impl TerraformRegion { - /// Convert terraform region to natural language prose. - /// - /// Uses exhaustive match on `TerraformIntent` — the compiler ensures - /// all valid combinations are handled, and invalid ones are impossible. - pub fn to_prose(&self) -> String { - self.intent.to_prose() - } -} - -impl TerraformIntent { - /// Convert intent to natural language prose. - pub fn to_prose(&self) -> String { - match self { - // Terminal states — single output, nothing else applies - TerraformIntent::Remove => "Remove this entirely.".to_string(), - TerraformIntent::Pin => "Preserve this exactly as written.".to_string(), - - // Dissolve — only direction applies - TerraformIntent::Dissolve { direction } => { - let mut clauses = vec![ - "Dissolve this as a unit, integrating its essence into surroundings.".to_string() - ]; - if let Some(dir) = direction { - clauses.push(format!("{}.", dir.to_prose_clause())); - } - clauses.join(" ") - } - - // Full transform — all axes available - TerraformIntent::Transform { - form, - mass, - gravity, - direction, - } => { - let mut clauses = Vec::new(); - - // Form + Mass combined clause - if !form.is_empty() { - clauses.push(form_mass_clause(form, mass.as_ref())); - } else if let Some(m) = mass { - // Mass-only when no form - clauses.push(format!("Make this {}.", m.as_verbosity())); - } - - // Gravity clause - if let Some(g) = gravity { - clauses.push(format!("{}.", g.to_prose_clause())); - } - - // Direction clause - if let Some(d) = direction { - clauses.push(format!("{}.", d.to_prose_clause())); - } - - clauses.join(" ") - } - } - } - - /// Check if this intent is empty (no transformation requested). - pub fn is_empty(&self) -> bool { - matches!( - self, - TerraformIntent::Transform { - form, - mass: None, - gravity: None, - direction: None, - } if form.is_empty() - ) - } -} - -/// Build combined Form + Mass clause for natural output. -fn form_mass_clause(form: &[FormType], mass: Option<&MassChange>) -> String { - let verbosity = mass.map(|m| m.as_verbosity()); - - match form.len() { - 0 => String::new(), - 1 => { - let f = form[0].as_tag(); - match verbosity { - Some(v) => format!("Present as a {} {}.", v, f), - None => format!("Present as a {}.", f), - } - } - _ => { - let forms: Vec<_> = form.iter().map(|f| f.as_tag()).collect(); - let joined = join_with_and(&forms); - match verbosity { - Some(v) => format!("Express via {} {}.", v, joined), - None => format!("Express via {}.", joined), - } - } - } -} - -/// Join items with commas and "and" for the last item. -/// ["a"] -> "a" -/// ["a", "b"] -> "a and b" -/// ["a", "b", "c"] -> "a, b, and c" -fn join_with_and(items: &[&str]) -> String { - match items.len() { - 0 => String::new(), - 1 => items[0].to_string(), - 2 => format!("{} and {}", items[0], items[1]), - _ => { - let (last, rest) = items.split_last().unwrap(); - format!("{}, and {}", rest.join(", "), last) - } - } -} - -impl MassChange { - /// Convert to verbosity descriptor for natural language output. - fn as_verbosity(&self) -> &'static str { - match self { - MassChange::Expand { intensity } => match intensity { - Intensity::Slightly => "fuller", - Intensity::Moderately => "detailed", - Intensity::Significantly => "comprehensive", - }, - MassChange::Condense { intensity } => match intensity { - Intensity::Slightly => "tighter", - Intensity::Moderately => "concise", - Intensity::Significantly => "minimal", - }, - } - } -} - -impl GravityChange { - /// Convert to natural language prose clause. - fn to_prose_clause(&self) -> &'static str { - match self { - GravityChange::Focus { intensity } => match intensity { - Intensity::Slightly => "Give this slightly more weight", - Intensity::Moderately => "Emphasize the key points", - Intensity::Significantly => "Make this the centerpiece", - }, - GravityChange::Blur { intensity } => match intensity { - Intensity::Slightly => "Soften the emphasis slightly", - Intensity::Moderately => "Treat as supporting context", - Intensity::Significantly => "Push this to the background", - }, - } - } -} - -impl DirectionDirective { - /// Convert to natural language prose clause. - fn to_prose_clause(&self) -> &'static str { - match self { - DirectionDirective::LeanIn { intensity } => match intensity { - Intensity::Slightly => "You're on the right track", - Intensity::Moderately => "This direction is working — keep going", - Intensity::Significantly => "Double down on this approach", - }, - DirectionDirective::MoveAway { intensity } => match intensity { - Intensity::Slightly => "Consider adjusting the angle slightly", - Intensity::Moderately => "This needs a different direction", - Intensity::Significantly => "This is off-target — overhaul the approach", - }, - DirectionDirective::Reframe => "Same content, reframed from a different angle", - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - - /// Helper to create a region with given intent. - fn region(intent: TerraformIntent) -> TerraformRegion { - TerraformRegion { - start_line: 1, - end_line: 10, - intent, - } - } - - /// Helper to create a Transform intent. - fn transform( - form: Vec, - mass: Option, - gravity: Option, - direction: Option, - ) -> TerraformIntent { - TerraformIntent::Transform { - form, - mass, - gravity, - direction, - } - } - - // ========== Terminal States ========== - - #[test] - fn remove() { - let r = region(TerraformIntent::Remove); - assert_eq!(r.to_prose(), "Remove this entirely."); - } - - #[test] - fn pin() { - let r = region(TerraformIntent::Pin); - assert_eq!(r.to_prose(), "Preserve this exactly as written."); - } - - // ========== Dissolve (only direction applies) ========== - - #[test] - fn dissolve_only() { - let r = region(TerraformIntent::Dissolve { direction: None }); - assert_eq!( - r.to_prose(), - "Dissolve this as a unit, integrating its essence into surroundings." - ); - } - - #[test] - fn dissolve_with_direction() { - let r = region(TerraformIntent::Dissolve { - direction: Some(DirectionDirective::MoveAway { - intensity: Intensity::Moderately, - }), - }); - assert_eq!( - r.to_prose(), - "Dissolve this as a unit, integrating its essence into surroundings. \ - This needs a different direction." - ); - } - - // ========== Transform: Form ========== - - #[test] - fn form_single() { - let r = region(transform(vec![FormType::Table], None, None, None)); - assert_eq!(r.to_prose(), "Present as a table."); - } - - #[test] - fn form_two() { - let r = region(transform( - vec![FormType::Table, FormType::Prose], - None, - None, - None, - )); - assert_eq!(r.to_prose(), "Express via table and passage."); - } - - #[test] - fn form_multiple() { - let r = region(transform( - vec![FormType::Table, FormType::List, FormType::Diagram], - None, - None, - None, - )); - assert_eq!(r.to_prose(), "Express via table, list, and diagram."); - } - - // ========== Transform: Mass ========== - - #[test] - fn mass_expand() { - let r = region(transform( - vec![], - Some(MassChange::Expand { - intensity: Intensity::Moderately, - }), - None, - None, - )); - assert_eq!(r.to_prose(), "Make this detailed."); - } - - #[test] - fn mass_condense() { - let r = region(transform( - vec![], - Some(MassChange::Condense { - intensity: Intensity::Significantly, - }), - None, - None, - )); - assert_eq!(r.to_prose(), "Make this minimal."); - } - - // ========== Transform: Gravity ========== - - #[test] - fn gravity_focus() { - let r = region(transform( - vec![], - None, - Some(GravityChange::Focus { - intensity: Intensity::Moderately, - }), - None, - )); - assert_eq!(r.to_prose(), "Emphasize the key points."); - } - - #[test] - fn gravity_blur() { - let r = region(transform( - vec![], - None, - Some(GravityChange::Blur { - intensity: Intensity::Slightly, - }), - None, - )); - assert_eq!(r.to_prose(), "Soften the emphasis slightly."); - } - - // ========== Transform: Direction ========== - - #[test] - fn direction_lean_in() { - let r = region(transform( - vec![], - None, - None, - Some(DirectionDirective::LeanIn { - intensity: Intensity::Significantly, - }), - )); - assert_eq!(r.to_prose(), "Double down on this approach."); - } - - #[test] - fn direction_move_away() { - let r = region(transform( - vec![], - None, - None, - Some(DirectionDirective::MoveAway { - intensity: Intensity::Moderately, - }), - )); - assert_eq!(r.to_prose(), "This needs a different direction."); - } - - #[test] - fn direction_reframe() { - let r = region(transform( - vec![], - None, - None, - Some(DirectionDirective::Reframe), - )); - assert_eq!(r.to_prose(), "Same content, reframed from a different angle."); - } - - // ========== Transform: Combined ========== - - #[test] - fn combined_all_axes() { - let r = region(transform( - vec![FormType::Table, FormType::Prose], - Some(MassChange::Expand { - intensity: Intensity::Moderately, - }), - Some(GravityChange::Focus { - intensity: Intensity::Moderately, - }), - Some(DirectionDirective::LeanIn { - intensity: Intensity::Slightly, - }), - )); - assert_eq!( - r.to_prose(), - "Express via detailed table and passage. Emphasize the key points. You're on the right track." - ); - } - - #[test] - fn form_with_mass() { - let r = region(transform( - vec![FormType::List], - Some(MassChange::Expand { - intensity: Intensity::Slightly, - }), - Some(GravityChange::Focus { - intensity: Intensity::Moderately, - }), - Some(DirectionDirective::LeanIn { - intensity: Intensity::Significantly, - }), - )); - assert_eq!( - r.to_prose(), - "Present as a fuller list. Emphasize the key points. Double down on this approach." - ); - } - - // ========== Empty / Default ========== - - #[test] - fn empty_transform() { - let r = region(transform(vec![], None, None, None)); - assert_eq!(r.to_prose(), ""); - } - - #[test] - fn intent_is_empty() { - assert!(TerraformIntent::default().is_empty()); - assert!(!TerraformIntent::Remove.is_empty()); - assert!(!TerraformIntent::Pin.is_empty()); - assert!(!TerraformIntent::Dissolve { direction: None }.is_empty()); - } - - // ========== Serialization ========== - - #[test] - fn serialization_roundtrip_remove() { - let r = region(TerraformIntent::Remove); - let json = serde_json::to_string(&r).unwrap(); - let deserialized: TerraformRegion = serde_json::from_str(&json).unwrap(); - assert_eq!(r, deserialized); - } - - #[test] - fn serialization_roundtrip_transform() { - let r = region(transform( - vec![FormType::Code, FormType::Diagram], - Some(MassChange::Condense { - intensity: Intensity::Significantly, - }), - Some(GravityChange::Focus { - intensity: Intensity::Moderately, - }), - Some(DirectionDirective::Reframe), - )); - let json = serde_json::to_string(&r).unwrap(); - let deserialized: TerraformRegion = serde_json::from_str(&json).unwrap(); - assert_eq!(r, deserialized); - } - - #[test] - fn serialization_roundtrip_dissolve() { - let r = region(TerraformIntent::Dissolve { - direction: Some(DirectionDirective::LeanIn { - intensity: Intensity::Slightly, - }), - }); - let json = serde_json::to_string(&r).unwrap(); - let deserialized: TerraformRegion = serde_json::from_str(&json).unwrap(); - assert_eq!(r, deserialized); - } - - #[test] - fn intensity_adverbs() { - assert_eq!(Intensity::Slightly.as_adverb(), "slightly"); - assert_eq!(Intensity::Moderately.as_adverb(), "moderately"); - assert_eq!(Intensity::Significantly.as_adverb(), "significantly"); - } -} diff --git a/src/lib/HelpOverlay.svelte b/src/lib/HelpOverlay.svelte index 1955785..cf4db09 100644 --- a/src/lib/HelpOverlay.svelte +++ b/src/lib/HelpOverlay.svelte @@ -44,25 +44,6 @@ { keys: ['e'], description: 'Edit last created bookmark' }, ] }, - { - category: 'Terraform', - items: [ - { keys: ['t'], description: 'Terraform hovered line or selection' }, - { keys: ['1-5'], description: 'Toggle form (table/list/prose/diagram/code)' }, - { keys: ['+'], description: 'Expand (mass)' }, - { keys: ['-'], description: 'Condense (mass)' }, - { keys: ['x'], description: 'Toggle remove (mass)' }, - { keys: ['f'], description: 'Focus (gravity)' }, - { keys: ['b'], description: 'Blur (gravity)' }, - { keys: ['p'], description: 'Pin (gravity)' }, - { keys: ['d'], description: 'Dissolve (gravity)' }, - { keys: ['<'], description: 'Lean in (direction)' }, - { keys: ['>'], description: 'Move away (direction)' }, - { keys: ['r'], description: 'Toggle reframe (direction)' }, - { keys: [keys.cmd, '↵'], description: 'Save terraform' }, - { keys: [keys.cmd, 'D'], description: 'Delete terraform' }, - ] - }, { category: 'Navigation', items: [ diff --git a/src/lib/components/ChoiceButtons.svelte b/src/lib/components/ChoiceButtons.svelte index 471f86d..82354a8 100644 --- a/src/lib/components/ChoiceButtons.svelte +++ b/src/lib/components/ChoiceButtons.svelte @@ -1,15 +1,14 @@
@@ -21,10 +20,4 @@ b bookmark - {#if onTerraform} - - {/if}
diff --git a/src/lib/components/DiscreteSlider.svelte b/src/lib/components/DiscreteSlider.svelte deleted file mode 100644 index 37bdf87..0000000 --- a/src/lib/components/DiscreteSlider.svelte +++ /dev/null @@ -1,98 +0,0 @@ - - -
- {#if leftLabel} - {@render leftLabel()} - {/if} -
- {#each Array(steps) as _, idx} - - {/each} -
- {#if rightLabel} - {@render rightLabel()} - {/if} -
- - diff --git a/src/lib/components/TerraformPalette.svelte b/src/lib/components/TerraformPalette.svelte deleted file mode 100644 index 4f6bc25..0000000 --- a/src/lib/components/TerraformPalette.svelte +++ /dev/null @@ -1,538 +0,0 @@ - - - diff --git a/src/lib/components/embedded/LineRow.svelte b/src/lib/components/embedded/LineRow.svelte index 46cf10e..477cac2 100644 --- a/src/lib/components/embedded/LineRow.svelte +++ b/src/lib/components/embedded/LineRow.svelte @@ -16,12 +16,8 @@ import type { Snippet } from 'svelte'; import type { Line } from '$lib/types'; import { getAnnotContext } from '$lib/context'; - import { BookmarkIcon, TerraformIcon } from '$lib/icons'; - import { getLineNumber, getFilePath } from '$lib/line-utils'; - import { tooltip } from '$lib/actions/tooltip'; + import { BookmarkIcon } from '$lib/icons'; import ChoiceButtons from '$lib/components/ChoiceButtons.svelte'; - import TerraformPalette from '$lib/components/TerraformPalette.svelte'; - import { isIntentEmpty, type TerraformRegion } from '$lib/types'; interface Props { line: Line; @@ -59,30 +55,6 @@ const annotated = $derived(ctx.annotations.hasAnnotation(displayIndex)); const markdownMetadata = $derived(ctx.markdownMetadata); - // Terraform region indicator (shows on first line of region) - const terraformRegionStart = $derived.by(() => { - const lineNum = getLineNumber(line); - if (lineNum === null) return undefined; - return ctx.terraform.isRegionStart(lineNum); - }); - - // Terraform border (shows on all lines in region) - const inTerraformRegion = $derived.by(() => { - const lineNum = getLineNumber(line); - if (lineNum === null) return false; - return ctx.terraform.isInRegion(lineNum); - }); - - // Terraform phrase for tooltip (fetched lazily) - let terraformPhrase = $state(''); - $effect(() => { - if (terraformRegionStart) { - ctx.terraform.getPhrase(terraformRegionStart).then(p => terraformPhrase = p); - } else { - terraformPhrase = ''; - } - }); - // Show choice buttons on the last line of selection when pending choice const showChoiceButtons = $derived( ctx.interaction.pendingChoice && @@ -90,13 +62,6 @@ displayIndex === Math.max(ctx.interaction.range.start, ctx.interaction.range.end) ); - // Show terraform palette on last line of selection when terraforming - const showTerraformPalette = $derived( - ctx.interaction.phase === 'terraforming' && - ctx.interaction.range !== null && - displayIndex === Math.max(ctx.interaction.range.start, ctx.interaction.range.end) - ); - // Convert additionalClasses object to class string const extraClasses = $derived( Object.entries(additionalClasses) @@ -112,52 +77,6 @@ function handleChooseBookmark() { ctx.interaction.confirmChoice('bookmark'); } - - function handleChooseTerraform() { - ctx.interaction.confirmChoice('terraform'); - } - - async function handleTerraformConfirm(region: TerraformRegion) { - const range = ctx.interaction.range; - if (!range) return; - - if (isIntentEmpty(region.intent)) { - await ctx.terraform.remove(range); - } else { - await ctx.terraform.upsert(range, region); - // Show toast with line range and phrase - const phrase = await ctx.terraform.getPhrase(region); - const startLine = Math.min(range.start, range.end); - const endLine = Math.max(range.start, range.end); - const lineLabel = startLine === endLine ? `Line ${startLine}` : `Lines ${startLine}-${endLine}`; - ctx.showToast(`${lineLabel}: ${phrase}`); - } - - ctx.interaction.closeTerraform(); - } - - function handleTerraformCancel() { - ctx.interaction.closeTerraform(); - } - - // Auto-save terraform changes as user edits - async function handleTerraformChange(region: TerraformRegion) { - const range = ctx.interaction.range; - if (!range) return; - - if (isIntentEmpty(region.intent)) { - await ctx.terraform.remove(range); - } else { - await ctx.terraform.upsert(range, region); - } - } - - // Load existing terraform region when palette opens - function loadTerraformRegion() { - const range = ctx.interaction.range; - if (!range) return Promise.resolve(undefined); - return ctx.terraform.load(range); - }
ctx.interaction.handleLineEnter(displayIndex)} onmouseleave={() => ctx.interaction.handleLineLeave()} @@ -194,7 +112,7 @@ {@render code()} {/if} - {#if trailing || showBookmarkIcon || terraformRegionStart} + {#if trailing || showBookmarkIcon} {#if trailing} {@render trailing()} @@ -204,21 +122,6 @@ {/if} - {#if terraformRegionStart} - - {/if} {/if}
@@ -226,18 +129,5 @@ {/if} -{#if showTerraformPalette && ctx.interaction.range} - {#await loadTerraformRegion() then existingRegion} - - {/await} -{/if} diff --git a/src/lib/composables/useHistory.svelte.ts b/src/lib/composables/useHistory.svelte.ts index 2d02a73..2ea90e9 100644 --- a/src/lib/composables/useHistory.svelte.ts +++ b/src/lib/composables/useHistory.svelte.ts @@ -1,5 +1,4 @@ import type { JSONContent } from '@tiptap/core'; -import type { TerraformRegion } from '$lib/types'; import type { AnnotationEntry } from './useAnnotations.svelte'; /** @@ -9,8 +8,6 @@ import type { AnnotationEntry } from './useAnnotations.svelte'; export interface SessionData { /** Annotations keyed by range string (e.g., "10-15") */ annotations: Record; - /** Terraform regions for the current file */ - terraform: TerraformRegion[]; /** Session-level comment (TipTap JSON) */ sessionComment: JSONContent | null; /** Selected exit mode ID */ @@ -59,11 +56,6 @@ function cloneSessionData(data: SessionData): SessionData { }, ]) ), - terraform: data.terraform.map(r => ({ - start_line: r.start_line, - end_line: r.end_line, - intent: JSON.parse(JSON.stringify(r.intent)), - })), sessionComment: data.sessionComment ? JSON.parse(JSON.stringify(data.sessionComment)) : null, selectedExitMode: data.selectedExitMode, }; @@ -75,7 +67,6 @@ function cloneSessionData(data: SessionData): SessionData { export function emptySessionData(): SessionData { return { annotations: {}, - terraform: [], sessionComment: null, selectedExitMode: null, }; diff --git a/src/lib/composables/useHistory.test.ts b/src/lib/composables/useHistory.test.ts index 6a5aa99..f413d10 100644 --- a/src/lib/composables/useHistory.test.ts +++ b/src/lib/composables/useHistory.test.ts @@ -5,7 +5,6 @@ describe('useHistory', () => { it('starts with empty session data', () => { const history = useHistory(); expect(history.current.annotations).toEqual({}); - expect(history.current.terraform).toEqual([]); expect(history.current.sessionComment).toBeNull(); expect(history.current.selectedExitMode).toBeNull(); }); diff --git a/src/lib/composables/useInteraction.svelte.ts b/src/lib/composables/useInteraction.svelte.ts index 70c2595..ae74dcb 100644 --- a/src/lib/composables/useInteraction.svelte.ts +++ b/src/lib/composables/useInteraction.svelte.ts @@ -24,8 +24,7 @@ export type UiState = | { phase: 'idle' } | { phase: 'selecting'; anchor: number; current: number } | { phase: 'committed'; range: Range; pendingChoice: boolean } - | { phase: 'editing'; editor: EditorKind } - | { phase: 'terraforming'; range: Range }; + | { phase: 'editing'; editor: EditorKind }; /** Derived type for phase names (for backwards compatibility) */ export type Phase = UiState['phase']; @@ -36,10 +35,8 @@ export type UiAction = | { type: 'COMMIT_SELECT'; pendingChoice: boolean } | { type: 'OPEN_EDITOR'; editor: EditorKind } | { type: 'CLOSE_EDITOR' } - | { type: 'OPEN_TERRAFORM' } - | { type: 'CLOSE_TERRAFORM' } | { type: 'SET_SELECTION'; range: Range } - | { type: 'CONFIRM_CHOICE'; action: 'annotate' | 'bookmark' | 'terraform' } + | { type: 'CONFIRM_CHOICE'; action: 'annotate' | 'bookmark' } | { type: 'CANCEL_CHOICE' } | { type: 'RESET' }; @@ -76,22 +73,12 @@ export function uiReducer(state: UiState, action: UiAction): UiState { if (state.phase !== 'editing') return state; return { phase: 'idle' }; - case 'OPEN_TERRAFORM': - if (state.phase !== 'committed') return state; - return { phase: 'terraforming', range: state.range }; - - case 'CLOSE_TERRAFORM': - if (state.phase !== 'terraforming') return state; - return { phase: 'idle' }; - case 'SET_SELECTION': return { phase: 'committed', range: action.range, pendingChoice: false }; case 'CONFIRM_CHOICE': if (state.phase !== 'committed' || !state.pendingChoice) return state; - if (action.action === 'terraform') { - return { phase: 'terraforming', range: state.range }; - } else if (action.action === 'annotate') { + if (action.action === 'annotate') { // Transition to editing phase with the annotation editor const rangeKey = `${state.range.start}-${state.range.end}`; return { phase: 'editing', editor: { kind: 'annotation', rangeKey } }; @@ -169,7 +156,6 @@ export function useInteraction(options: UseInteractionOptions) { case 'selecting': return normalizeRange(state.anchor, state.current); case 'committed': - case 'terraforming': return state.range; case 'editing': // Editing phase: derive range from editor kind @@ -195,7 +181,7 @@ export function useInteraction(options: UseInteractionOptions) { */ function isLineHighlighted(displayIdx: number): boolean { const range = getRange(); - if (range && (state.phase === 'selecting' || state.phase === 'committed' || state.phase === 'editing' || state.phase === 'terraforming')) { + if (range && (state.phase === 'selecting' || state.phase === 'committed' || state.phase === 'editing')) { return isLineInRange(displayIdx, range); } return false; @@ -374,7 +360,7 @@ export function useInteraction(options: UseInteractionOptions) { } } - function confirmChoice(action: 'annotate' | 'bookmark' | 'terraform') { + function confirmChoice(action: 'annotate' | 'bookmark') { if (state.phase !== 'committed' || !state.pendingChoice) return; if (action === 'bookmark') { @@ -390,16 +376,6 @@ export function useInteraction(options: UseInteractionOptions) { dispatch({ type: 'CANCEL_CHOICE' }); } - // --- Terraform transitions --- - - function openTerraform() { - dispatch({ type: 'OPEN_TERRAFORM' }); - } - - function closeTerraform() { - dispatch({ type: 'CLOSE_TERRAFORM' }); - } - // --- Shift key handlers --- function handleShiftKeyDown() { @@ -472,10 +448,6 @@ export function useInteraction(options: UseInteractionOptions) { setDragModifier, confirmChoice, cancelChoice, - - // Terraform transitions - openTerraform, - closeTerraform, }; } diff --git a/src/lib/composables/useKeyboard.svelte.ts b/src/lib/composables/useKeyboard.svelte.ts index 00a7ac7..8bd3813 100644 --- a/src/lib/composables/useKeyboard.svelte.ts +++ b/src/lib/composables/useKeyboard.svelte.ts @@ -21,12 +21,11 @@ export interface KeyboardHandlers { onZoomOut?: () => void; onZoomReset?: () => void; onCommentHoveredLine?: () => void; - onTerraformHoveredLine?: () => void; onSelectAllContent?: () => void; /** Called when 'c' or 'b' is pressed during 'selecting' phase (drag in progress) */ onDragModifierPress?: (key: 'c' | 'b') => void; - /** Called when 'c', 'b', or 't' is pressed to confirm pending choice (after shift-drag-release) */ - onConfirmChoice?: (action: 'annotate' | 'bookmark' | 'terraform') => void; + /** Called when 'c' or 'b' is pressed to confirm pending choice (after shift-drag-release) */ + onConfirmChoice?: (action: 'annotate' | 'bookmark') => void; /** Called when Escape is pressed to cancel pending choice */ onCancelChoice?: () => void; } @@ -212,29 +211,6 @@ export function useKeyboard(handlers: KeyboardHandlers, state: KeyboardState) { return; } - // 't' for terraform - if (e.key === 't' && !e.metaKey && !e.ctrlKey) { - if (isInEditorOrInput()) return; - - // Pending choice: confirm terraform - if (state.isPendingChoice()) { - e.preventDefault(); - handlers.onConfirmChoice?.('terraform'); - return; - } - - // Block if editor is active - if (state.isEditorActive()) return; - - // Default: terraform hovered line - if (state.hasHoveredLine() && state.isHoveredLineSelectable()) { - e.preventDefault(); - window.getSelection()?.removeAllRanges(); - handlers.onTerraformHoveredLine?.(); - return; - } - } - // 'e' to edit last created bookmark if (e.key === 'e' && !e.metaKey && !e.ctrlKey && state.hasLastCreatedBookmark() && !state.isEditorActive() && !state.isCommandPaletteOpen()) { if (isInEditorOrInput()) return; diff --git a/src/lib/composables/useTerraform.svelte.ts b/src/lib/composables/useTerraform.svelte.ts deleted file mode 100644 index 140187e..0000000 --- a/src/lib/composables/useTerraform.svelte.ts +++ /dev/null @@ -1,365 +0,0 @@ -import { invoke } from '@tauri-apps/api/core'; -import type { - FormType, - Intensity, - MassChange, - GravityChange, - DirectionDirective, - TerraformRegion, - TerraformIntent, -} from '$lib/types'; -import { INTENSITY_LEVELS, FORM_TYPES, emptyTransformIntent, isIntentEmpty } from '$lib/types'; - -/** Intensity display labels for UI. */ -export const INTENSITY_LABELS: Record = { - slightly: 'slightly', - moderately: 'moderately', - significantly: 'significantly', -}; - -/** Form type display labels for UI. */ -export const FORM_LABELS: Record = { - table: 'table', - list: 'list', - prose: 'prose', - diagram: 'diagram', - code: 'code', -}; - -/** Default intensity (gentlest level). */ -const DEFAULT_INTENSITY: Intensity = 'slightly'; - -/** Debounce delay for phrase IPC calls (ms). */ -const PHRASE_DEBOUNCE_MS = 20; - -/** Create terraform state composable for palette. */ -export function useTerraform(initialRegion?: TerraformRegion) { - // Single intent state - type-safe combinations only - let intent: TerraformIntent = $state(initialRegion?.intent ?? emptyTransformIntent()); - - // Store previous transform state for recovery when exiting terminal modes - let previousTransform: (TerraformIntent & { kind: 'transform' }) | null = $state(null); - - // Phrase from backend (updated via debounced IPC) - let phrase = $state(''); - let debounceTimer: ReturnType | null = null; - - // Check if state is empty (nothing configured) - const isEmpty = $derived(isIntentEmpty(intent)); - - // --- Derived state for UI convenience --- - // These expose the inner fields when in transform mode - const form = $derived(intent.kind === 'transform' ? intent.form : []); - const mass = $derived(intent.kind === 'transform' ? intent.mass : null); - const gravity = $derived(intent.kind === 'transform' ? intent.gravity : null); - const direction = $derived( - intent.kind === 'transform' ? intent.direction : - intent.kind === 'dissolve' ? intent.direction : - null - ); - - // --- Override status for visual dimming --- - const isRemoveActive = $derived(intent.kind === 'remove'); - const isPinActive = $derived(intent.kind === 'pin'); - const isDissolveActive = $derived(intent.kind === 'dissolve'); - - const formOverridden = $derived(isRemoveActive || isPinActive || isDissolveActive); - const massOverridden = $derived(isRemoveActive || isPinActive || isDissolveActive); - const gravityOverridden = $derived(isRemoveActive); - const directionOverridden = $derived(isRemoveActive || isPinActive); - - // Human-readable override reasons for tooltips - const formOverrideReason = $derived( - isRemoveActive ? 'Overridden by Remove' : - isPinActive ? 'Overridden by Pin' : - isDissolveActive ? 'Overridden by Dissolve' : null - ); - const massOverrideReason = $derived( - isRemoveActive ? 'Overridden by Remove' : - isPinActive ? 'Overridden by Pin' : - isDissolveActive ? 'Overridden by Dissolve' : null - ); - const gravityOverrideReason = $derived( - isRemoveActive ? 'Overridden by Remove' : null - ); - const directionOverrideReason = $derived( - isRemoveActive ? 'Overridden by Remove' : - isPinActive ? 'Overridden by Pin' : null - ); - - // Update phrase from backend when state changes - function updatePhrase() { - if (debounceTimer) clearTimeout(debounceTimer); - debounceTimer = setTimeout(async () => { - // Build a region with dummy line numbers (phrase doesn't use them) - const region: TerraformRegion = { - start_line: 0, - end_line: 0, - intent, - }; - try { - phrase = await invoke('get_terraform_phrase', { region }); - } catch (e) { - console.error('Failed to get terraform phrase:', e); - } - }, PHRASE_DEBOUNCE_MS); - } - - // Trigger phrase update on any state change - $effect(() => { - void intent; - updatePhrase(); - }); - - // --- Helper: ensure we're in transform mode --- - function ensureTransform(): TerraformIntent & { kind: 'transform' } { - if (intent.kind === 'transform') { - return intent; - } - // Transition to transform, preserving direction if coming from dissolve - const newIntent: TerraformIntent = { - kind: 'transform', - form: [], - mass: null, - gravity: null, - direction: intent.kind === 'dissolve' ? intent.direction : null, - }; - intent = newIntent; - return newIntent; - } - - // --- Helper: save current transform state before entering terminal mode --- - function saveTransformState(): void { - if (intent.kind === 'transform') { - previousTransform = intent; - } - } - - // --- Terminal state setters --- - function setRemove(): void { - saveTransformState(); - intent = { kind: 'remove' }; - } - - function clearRemove(): void { - if (intent.kind === 'remove') { - intent = previousTransform ?? emptyTransformIntent(); - previousTransform = null; - } - } - - function setPin(): void { - saveTransformState(); - intent = { kind: 'pin' }; - } - - function clearPin(): void { - if (intent.kind === 'pin') { - intent = previousTransform ?? emptyTransformIntent(); - previousTransform = null; - } - } - - function setDissolve(): void { - saveTransformState(); - // Preserve direction when transitioning to dissolve - const currentDirection = intent.kind === 'transform' ? intent.direction : - intent.kind === 'dissolve' ? intent.direction : null; - intent = { kind: 'dissolve', direction: currentDirection }; - } - - function clearDissolve(): void { - if (intent.kind === 'dissolve') { - // Restore previous transform, merging in any direction changes made in dissolve mode - const prev = previousTransform; - intent = { - kind: 'transform', - form: prev?.form ?? [], - mass: prev?.mass ?? null, - gravity: prev?.gravity ?? null, - direction: intent.direction, // Keep direction from dissolve mode - }; - previousTransform = null; - } - } - - // --- Form mutations (auto-switch to transform) --- - function toggleForm(type: FormType): void { - const t = ensureTransform(); - const idx = t.form.indexOf(type); - if (idx >= 0) { - intent = { ...t, form: t.form.filter((_, i) => i !== idx) }; - } else { - intent = { ...t, form: [...t.form, type] }; - } - } - - // --- Mass mutations (auto-switch to transform) --- - function setMassExpand(intensity: Intensity = DEFAULT_INTENSITY): void { - const t = ensureTransform(); - intent = { ...t, mass: { type: 'expand', intensity } }; - } - - function setMassCondense(intensity: Intensity = DEFAULT_INTENSITY): void { - const t = ensureTransform(); - intent = { ...t, mass: { type: 'condense', intensity } }; - } - - function clearMass(): void { - if (intent.kind === 'transform') { - intent = { ...intent, mass: null }; - } - } - - function adjustMassIntensity(delta: number): void { - if (intent.kind !== 'transform' || !intent.mass) return; - const currentIdx = INTENSITY_LEVELS.indexOf(intent.mass.intensity); - const newIdx = Math.max(0, Math.min(INTENSITY_LEVELS.length - 1, currentIdx + delta)); - intent = { ...intent, mass: { ...intent.mass, intensity: INTENSITY_LEVELS[newIdx] } }; - } - - // --- Gravity mutations (auto-switch to transform) --- - function setGravityFocus(intensity: Intensity = DEFAULT_INTENSITY): void { - const t = ensureTransform(); - intent = { ...t, gravity: { type: 'focus', intensity } }; - } - - function setGravityBlur(intensity: Intensity = DEFAULT_INTENSITY): void { - const t = ensureTransform(); - intent = { ...t, gravity: { type: 'blur', intensity } }; - } - - function clearGravity(): void { - if (intent.kind === 'transform') { - intent = { ...intent, gravity: null }; - } - } - - function adjustGravityIntensity(delta: number): void { - if (intent.kind !== 'transform' || !intent.gravity) return; - const currentIdx = INTENSITY_LEVELS.indexOf(intent.gravity.intensity); - const newIdx = Math.max(0, Math.min(INTENSITY_LEVELS.length - 1, currentIdx + delta)); - intent = { ...intent, gravity: { ...intent.gravity, intensity: INTENSITY_LEVELS[newIdx] } }; - } - - // --- Direction mutations (works in transform or dissolve) --- - function setDirectionReframe(): void { - if (intent.kind === 'transform') { - intent = { ...intent, direction: { type: 'reframe' } }; - } else if (intent.kind === 'dissolve') { - intent = { ...intent, direction: { type: 'reframe' } }; - } else { - // Auto-switch to transform if in terminal state - intent = { kind: 'transform', form: [], mass: null, gravity: null, direction: { type: 'reframe' } }; - } - } - - function setDirectionLeanIn(intensity: Intensity = DEFAULT_INTENSITY): void { - if (intent.kind === 'transform') { - intent = { ...intent, direction: { type: 'leanin', intensity } }; - } else if (intent.kind === 'dissolve') { - intent = { ...intent, direction: { type: 'leanin', intensity } }; - } else { - intent = { kind: 'transform', form: [], mass: null, gravity: null, direction: { type: 'leanin', intensity } }; - } - } - - function setDirectionMoveAway(intensity: Intensity = DEFAULT_INTENSITY): void { - if (intent.kind === 'transform') { - intent = { ...intent, direction: { type: 'moveaway', intensity } }; - } else if (intent.kind === 'dissolve') { - intent = { ...intent, direction: { type: 'moveaway', intensity } }; - } else { - intent = { kind: 'transform', form: [], mass: null, gravity: null, direction: { type: 'moveaway', intensity } }; - } - } - - function clearDirection(): void { - if (intent.kind === 'transform') { - intent = { ...intent, direction: null }; - } else if (intent.kind === 'dissolve') { - intent = { ...intent, direction: null }; - } - } - - function adjustDirectionIntensity(delta: number): void { - const dir = intent.kind === 'transform' ? intent.direction : - intent.kind === 'dissolve' ? intent.direction : null; - if (!dir || dir.type === 'reframe') return; - - const currentIdx = INTENSITY_LEVELS.indexOf(dir.intensity); - const newIdx = Math.max(0, Math.min(INTENSITY_LEVELS.length - 1, currentIdx + delta)); - const newDirection = { ...dir, intensity: INTENSITY_LEVELS[newIdx] }; - - if (intent.kind === 'transform') { - intent = { ...intent, direction: newDirection }; - } else if (intent.kind === 'dissolve') { - intent = { ...intent, direction: newDirection }; - } - } - - // --- Serialization --- - function toRegion(startLine: number, endLine: number): TerraformRegion { - return { - start_line: startLine, - end_line: endLine, - intent, - }; - } - - return { - // Intent (full access) - get intent() { return intent; }, - - // Derived fields for UI convenience - get form() { return form; }, - get mass() { return mass; }, - get gravity() { return gravity; }, - get direction() { return direction; }, - get phrase() { return phrase; }, - get isEmpty() { return isEmpty; }, - - // Override status (for visual dimming based on precedence) - get formOverridden() { return formOverridden; }, - get massOverridden() { return massOverridden; }, - get gravityOverridden() { return gravityOverridden; }, - get directionOverridden() { return directionOverridden; }, - get formOverrideReason() { return formOverrideReason; }, - get massOverrideReason() { return massOverrideReason; }, - get gravityOverrideReason() { return gravityOverrideReason; }, - get directionOverrideReason() { return directionOverrideReason; }, - - // Terminal state setters - setRemove, - clearRemove, - setPin, - clearPin, - setDissolve, - clearDissolve, - - // Form - toggleForm, - - // Mass - setMassExpand, - setMassCondense, - clearMass, - adjustMassIntensity, - - // Gravity - setGravityFocus, - setGravityBlur, - clearGravity, - adjustGravityIntensity, - - // Direction - setDirectionReframe, - setDirectionLeanIn, - setDirectionMoveAway, - clearDirection, - adjustDirectionIntensity, - - // Serialization - toRegion, - }; -} diff --git a/src/lib/composables/useTerraformRegions.svelte.ts b/src/lib/composables/useTerraformRegions.svelte.ts deleted file mode 100644 index 09b233d..0000000 --- a/src/lib/composables/useTerraformRegions.svelte.ts +++ /dev/null @@ -1,143 +0,0 @@ -import { invoke } from '@tauri-apps/api/core'; -import type { TerraformRegion } from '$lib/types'; -import type { Line } from '$lib/types'; -import { validateRange } from '$lib/range'; -import type { Range } from '$lib/range'; - -export interface UseTerraformRegionsOptions { - /** Lines array for validating ranges and resolving paths */ - getLines: () => Line[]; -} - -/** - * Composable for managing terraform regions with backend persistence. - * Follows the useAnnotations pattern. - */ -export function useTerraformRegions(options: UseTerraformRegionsOptions) { - let regions: TerraformRegion[] = $state([]); - - /** - * Load terraform regions for a path and find one matching the given range. - * Returns the matching region if found, undefined otherwise. - */ - async function load(range: Range): Promise { - const coords = validateRange(range, options.getLines()); - if (!coords) return undefined; - - const all = await invoke('get_terraform_regions', { path: coords.path }); - regions = all; - return all.find(r => r.start_line === coords.startLine && r.end_line === coords.endLine); - } - - /** - * Upsert a terraform region to the backend. - * Also updates the local regions list for visual indicators. - */ - async function upsert(range: Range, region: TerraformRegion): Promise { - const coords = validateRange(range, options.getLines()); - if (!coords) return; - await invoke('upsert_terraform', { path: coords.path, region }); - // Refresh regions to keep visual indicators in sync - await loadAll(coords.path); - } - - /** - * Remove a terraform region from the backend. - * Also updates the local regions list for visual indicators. - */ - async function remove(range: Range): Promise { - const coords = validateRange(range, options.getLines()); - if (!coords) return; - await invoke('delete_terraform', { - path: coords.path, - startLine: coords.startLine, - endLine: coords.endLine - }); - // Refresh regions to keep visual indicators in sync - await loadAll(coords.path); - } - - /** - * Load all terraform regions for a file path. - * Used on mount to populate visual indicators. - */ - async function loadAll(path: string): Promise { - regions = await invoke('get_terraform_regions', { path }); - } - - /** - * Check if a source line number starts a terraform region. - * Returns the region if found, undefined otherwise. - */ - function isRegionStart(sourceLineNum: number): TerraformRegion | undefined { - return regions.find(r => r.start_line === sourceLineNum); - } - - /** - * Check if a source line number is within any terraform region. - */ - function isInRegion(sourceLineNum: number): boolean { - return regions.some(r => sourceLineNum >= r.start_line && sourceLineNum <= r.end_line); - } - - /** - * Get display index range for a terraform region. - * Returns {start, end} display indices (1-indexed). - */ - function getDisplayRange(region: TerraformRegion): { start: number; end: number } | null { - const lines = options.getLines(); - let start: number | null = null; - let end: number | null = null; - - for (let i = 0; i < lines.length; i++) { - const line = lines[i]; - const lineNum = line.origin.type === 'source' ? line.origin.line - : line.origin.type === 'diff' ? (line.origin.new_line ?? line.origin.old_line) - : null; - - if (lineNum === region.start_line && start === null) { - start = i + 1; // Convert to 1-indexed display index - } - if (lineNum === region.end_line) { - end = i + 1; - } - } - - return start !== null && end !== null ? { start, end } : null; - } - - /** - * Get the natural language phrase for a terraform region. - */ - async function getPhrase(region: TerraformRegion): Promise { - return invoke('get_terraform_phrase', { region }); - } - - /** - * Replace all regions with new data (used for undo/redo). - * Does NOT sync to backend - caller is responsible for that. - */ - function replaceAll(newRegions: TerraformRegion[]): void { - regions = newRegions.map(r => ({ - start_line: r.start_line, - end_line: r.end_line, - intent: JSON.parse(JSON.stringify(r.intent)), - })); - } - - return { - /** All loaded regions (reactive) */ - get regions() { return regions; }, - /** Alias for regions getter (for history system) */ - get all() { return regions; }, - load, - upsert, - remove, - loadAll, - isRegionStart, - isInRegion, - getDisplayRange, - getPhrase, - replaceAll, - }; -} diff --git a/src/lib/context/AnnotProvider.svelte b/src/lib/context/AnnotProvider.svelte index b217aa2..25d1b26 100644 --- a/src/lib/context/AnnotProvider.svelte +++ b/src/lib/context/AnnotProvider.svelte @@ -19,7 +19,6 @@ import type { useSearch } from '$lib/composables/useSearch.svelte'; import type { useMermaid } from '$lib/composables/useMermaid.svelte'; import type { useBookmarks } from '$lib/composables/useBookmarks.svelte'; - import type { useTerraformRegions } from '$lib/composables/useTerraformRegions.svelte'; interface Props { // Reactive data @@ -36,7 +35,6 @@ search: ReturnType; mermaid: ReturnType; bookmarks: ReturnType; - terraform: ReturnType; // Utilities showToast: (message: string, duration?: number) => void; @@ -58,7 +56,6 @@ search, mermaid, bookmarks, - terraform, showToast, isLineSelectable, getOriginalLinesForRange, @@ -108,11 +105,6 @@ return null; } - // Don't show new editor during terraforming (terraform palette is open) - if (interaction.phase === 'terraforming') { - return null; - } - const isLast = displayIndex === lastSelectedLine && selection && !isDragging; if (isLast && selection) { return rangeToKey(selection); @@ -129,7 +121,6 @@ get search() { return search; }, get mermaid() { return mermaid; }, get bookmarks() { return bookmarks; }, - get terraform() { return terraform; }, get selection() { return selection; }, get isDragging() { return isDragging; }, diff --git a/src/lib/context/annot-context.svelte.ts b/src/lib/context/annot-context.svelte.ts index 54d2eed..20ecb80 100644 --- a/src/lib/context/annot-context.svelte.ts +++ b/src/lib/context/annot-context.svelte.ts @@ -7,7 +7,6 @@ import type { useExitModes } from '$lib/composables/useExitModes.svelte'; import type { useSearch } from '$lib/composables/useSearch.svelte'; import type { useMermaid } from '$lib/composables/useMermaid.svelte'; import type { useBookmarks } from '$lib/composables/useBookmarks.svelte'; -import type { useTerraformRegions } from '$lib/composables/useTerraformRegions.svelte'; /** * AnnotContext - Shared state and utilities for annot components. @@ -23,7 +22,6 @@ export interface AnnotContext { search: ReturnType; mermaid: ReturnType; bookmarks: ReturnType; - terraform: ReturnType; // Derived values (computed once in provider) readonly selection: Range | null; diff --git a/src/lib/icons/DirectionIcon.svelte b/src/lib/icons/DirectionIcon.svelte deleted file mode 100644 index 88f0908..0000000 --- a/src/lib/icons/DirectionIcon.svelte +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - diff --git a/src/lib/icons/FormIcon.svelte b/src/lib/icons/FormIcon.svelte deleted file mode 100644 index 227cbce..0000000 --- a/src/lib/icons/FormIcon.svelte +++ /dev/null @@ -1,10 +0,0 @@ - - - - - diff --git a/src/lib/icons/GravityIcon.svelte b/src/lib/icons/GravityIcon.svelte deleted file mode 100644 index 7127c8e..0000000 --- a/src/lib/icons/GravityIcon.svelte +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - diff --git a/src/lib/icons/MassIcon.svelte b/src/lib/icons/MassIcon.svelte deleted file mode 100644 index 708e147..0000000 --- a/src/lib/icons/MassIcon.svelte +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - diff --git a/src/lib/icons/TerraformIcon.svelte b/src/lib/icons/TerraformIcon.svelte deleted file mode 100644 index b6e4e38..0000000 --- a/src/lib/icons/TerraformIcon.svelte +++ /dev/null @@ -1,10 +0,0 @@ - - - - - diff --git a/src/lib/icons/index.ts b/src/lib/icons/index.ts index 083f0f6..421fad6 100644 --- a/src/lib/icons/index.ts +++ b/src/lib/icons/index.ts @@ -21,11 +21,6 @@ export { default as XMarkIcon } from './XMarkIcon.svelte'; export { default as ImageIcon } from './ImageIcon.svelte'; export { default as ClipboardIcon } from './ClipboardIcon.svelte'; export { default as GlobeIcon } from './GlobeIcon.svelte'; -export { default as TerraformIcon } from './TerraformIcon.svelte'; -export { default as FormIcon } from './FormIcon.svelte'; -export { default as MassIcon } from './MassIcon.svelte'; -export { default as GravityIcon } from './GravityIcon.svelte'; -export { default as DirectionIcon } from './DirectionIcon.svelte'; export { default as PaperclipIcon } from './PaperclipIcon.svelte'; export { default as ChatBubbleIcon } from './ChatBubbleIcon.svelte'; export { default as HeadingH1Icon } from './HeadingH1Icon.svelte'; diff --git a/src/lib/types.ts b/src/lib/types.ts index 7760440..fdfdd2c 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -304,78 +304,3 @@ export interface Bookmark { /** The captured content snapshot. */ snapshot: BookmarkSnapshot; } - -// ============================================================================= -// Terraform — structured controls for guiding AI content transformation -// ============================================================================= - -/** Target format for restructuring. */ -export type FormType = 'table' | 'list' | 'prose' | 'diagram' | 'code'; - -/** Intensity level for graduated controls. */ -export type Intensity = 'slightly' | 'moderately' | 'significantly'; - -/** All intensity levels in order (for slider). */ -export const INTENSITY_LEVELS: Intensity[] = ['slightly', 'moderately', 'significantly']; - -/** All form types in order (for buttons). */ -export const FORM_TYPES: FormType[] = ['table', 'list', 'prose', 'diagram', 'code']; - -/** Mass change: expand or condense (remove is a separate intent). */ -export type MassChange = - | { type: 'expand'; intensity: Intensity } - | { type: 'condense'; intensity: Intensity }; - -/** Gravity change: focus or blur (pin/dissolve are separate intents). */ -export type GravityChange = - | { type: 'focus'; intensity: Intensity } - | { type: 'blur'; intensity: Intensity }; - -/** Correctness signal: lean-in, move-away, or reframe. */ -export type DirectionDirective = - | { type: 'leanin'; intensity: Intensity } - | { type: 'moveaway'; intensity: Intensity } - | { type: 'reframe' }; - -/** Type-safe terraform intent — invalid combinations are unrepresentable. */ -export type TerraformIntent = - | { kind: 'remove' } - | { kind: 'pin' } - | { kind: 'dissolve'; direction: DirectionDirective | null } - | { - kind: 'transform'; - form: FormType[]; - mass: MassChange | null; - gravity: GravityChange | null; - direction: DirectionDirective | null; - }; - -/** A terraform region attached to a line range. */ -export interface TerraformRegion { - start_line: number; - end_line: number; - /** The transformation intent — type-safe combinations only. */ - intent: TerraformIntent; -} - -/** Create a default empty transform intent. */ -export function emptyTransformIntent(): TerraformIntent { - return { - kind: 'transform', - form: [], - mass: null, - gravity: null, - direction: null - }; -} - -/** Check if an intent is effectively empty (no transformation requested). */ -export function isIntentEmpty(intent: TerraformIntent): boolean { - if (intent.kind !== 'transform') return false; - return ( - intent.form.length === 0 && - intent.mass === null && - intent.gravity === null && - intent.direction === null - ); -} diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index 7782673..4d5a403 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -4,7 +4,7 @@ import { getCurrentWindow } from "@tauri-apps/api/window"; import { onMount } from "svelte"; import type { ContentResponse, ContentNode, ContentMetadata, Line, JSONContent, ExitMode, Tag, DiffMetadata, HunkInfo, MarkdownMetadata, SectionInfo, ConfigSnapshot } from "$lib/types"; - import { getLineNumber, getDiffKind, isSelectable, isPortalLine, isCodeBlockLine, isCodeBlockFence, isTableLine, isHorizontalRule, getFilePath } from "$lib/line-utils"; + import { getLineNumber, getDiffKind, isSelectable, isPortalLine, isCodeBlockLine, isCodeBlockFence, isTableLine, isHorizontalRule } from "$lib/line-utils"; import { rangeToKey, keyToRange, isLineInRange, validateRange, type Range } from "$lib/range"; import { extractContentNodes, isContentEmpty, contentNodesToTipTap, findExcalidrawChip } from "$lib/tiptap"; import { ContentTracker, type HunkPayload, type SectionPayload } from "$lib/content-tracker"; @@ -28,7 +28,6 @@ import { useLineSegments } from "$lib/composables/useLineSegments.svelte"; import { useSearch } from "$lib/composables/useSearch.svelte"; import { useBookmarks } from "$lib/composables/useBookmarks.svelte"; - import { useTerraformRegions } from "$lib/composables/useTerraformRegions.svelte"; import { useOverlay } from "$lib/composables/useOverlay.svelte"; import { useHistory, emptySessionData, type SessionData } from "$lib/composables/useHistory.svelte"; import SearchBar from "$lib/components/SearchBar.svelte"; @@ -202,11 +201,6 @@ getMarkdownMetadata: () => markdownMetadata, }); - // Terraform regions (composable) - const terraform = useTerraformRegions({ - getLines: () => lines, - }); - // Line segmentation (composable) const lineSegmentation = useLineSegments(() => lines); @@ -254,7 +248,6 @@ function captureSessionData(): SessionData { return { annotations: { ...annotationState.all }, - terraform: [...terraform.all], sessionComment: sessionComment ? JSON.parse(JSON.stringify(sessionComment)) : null, selectedExitMode: exitModeState.selectedId, }; @@ -268,9 +261,6 @@ // Restore annotations annotationState.replaceAll(data.annotations); - // Restore terraform regions - terraform.replaceAll(data.terraform); - // Restore session comment sessionComment = data.sessionComment ? JSON.parse(JSON.stringify(data.sessionComment)) : undefined; @@ -688,12 +678,6 @@ interaction.openEditor({ kind: 'annotation', rangeKey }); } }, - onTerraformHoveredLine: () => { - if (interaction.hoverLine !== null) { - interaction.selectLine(interaction.hoverLine); - interaction.openTerraform(); - } - }, onDragModifierPress: (key) => interaction.setDragModifier(key), onConfirmChoice: (action) => interaction.confirmChoice(action), onCancelChoice: () => interaction.cancelChoice(), @@ -744,13 +728,6 @@ sessionComment = contentNodesToTipTap(res.session_comment); } - // Load terraform regions for visual indicators - const firstSourceLine = lines.find(l => l.origin.type === 'source' || l.origin.type === 'diff'); - const firstPath = firstSourceLine ? getFilePath(firstSourceLine) : null; - if (firstPath) { - terraform.loadAll(firstPath); - } - // Listen for window close - this triggers output and exit const unlisten = await window.onCloseRequested(async (event) => { event.preventDefault(); @@ -846,7 +823,6 @@ {search} {mermaid} bookmarks={bookmarkState} - {terraform} {showToast} {isLineSelectable} {getOriginalLinesForRange} diff --git a/src/routes/page.test.ts b/src/routes/page.test.ts index 219918d..a0e5bd6 100644 --- a/src/routes/page.test.ts +++ b/src/routes/page.test.ts @@ -83,7 +83,6 @@ describe("+page.svelte", () => { it("renders file content with line numbers", async () => { vi.mocked(invoke).mockImplementation((cmd: string) => { - if (cmd === 'get_terraform_regions') return Promise.resolve([]); return Promise.resolve(createMockResponse({ label: "test.rs", lines: [ @@ -107,7 +106,6 @@ describe("+page.svelte", () => { it("displays filename in header", async () => { vi.mocked(invoke).mockImplementation((cmd: string) => { - if (cmd === 'get_terraform_regions') return Promise.resolve([]); return Promise.resolve(createMockResponse({ label: "my_module.rs", lines: [makeLine(1, "// comment")], @@ -145,7 +143,6 @@ describe("+page.svelte", () => { it("does not open editor when Cmd+C is pressed (allows copy)", async () => { vi.mocked(invoke).mockImplementation((cmd: string) => { - if (cmd === 'get_terraform_regions') return Promise.resolve([]); return Promise.resolve(createMockResponse({ label: "test.rs", lines: [ @@ -184,7 +181,6 @@ describe("+page.svelte", () => { it("opens editor when 'c' is pressed alone on hovered line", async () => { vi.mocked(invoke).mockImplementation((cmd: string) => { - if (cmd === 'get_terraform_regions') return Promise.resolve([]); return Promise.resolve(createMockResponse({ label: "test.rs", lines: [ diff --git a/src/styles/components/code-viewer.css b/src/styles/components/code-viewer.css index cfda00a..9cb0b77 100644 --- a/src/styles/components/code-viewer.css +++ b/src/styles/components/code-viewer.css @@ -137,8 +137,8 @@ div.line { } /* Left accent bar on hover — only for plain lines; annotated/bookmarked/ - terraform/selected keep their own ::before border. */ -.content.phase-idle div.line:not(.selected):not(.annotated):not(.bookmarked):not(.has-terraform):hover::before { + selected keep their own ::before border. */ +.content.phase-idle div.line:not(.selected):not(.annotated):not(.bookmarked):hover::before { content: ""; position: absolute; left: 0; @@ -202,30 +202,6 @@ div.line.bookmarked::before { z-index: 1; } -/* Terraform region indicator - purple left border on first line */ -div.line.has-terraform::before { - content: ""; - position: absolute; - left: 0; - top: 0; - bottom: 0; - width: 3px; - background: var(--terraform-border); - z-index: 1; -} - -/* Both bookmark and terraform - diagonal stripes (continuous across lines) */ -div.line.bookmarked.has-terraform::before { - background: repeating-linear-gradient( - -45deg, - var(--bookmark-color, #ef4444), - var(--bookmark-color, #ef4444) 3px, - var(--terraform-border) 3px, - var(--terraform-border) 6px - ); - background-attachment: fixed; -} - /* Line actions container - holds trailing icons with consistent spacing */ .line-actions { display: inline-flex; @@ -262,16 +238,6 @@ div.line.bookmarked.has-terraform::before { font-size: 16px; } -/* Terraform indicator */ -.terraform-indicator { - color: var(--terraform-icon); -} - -.terraform-indicator svg { - width: 14px; - height: 14px; -} - /* Shift+drag cursor */ .content.shift-held .code { cursor: pointer; @@ -775,11 +741,6 @@ div.line.separator-line:hover { outline: none; } -.choice-button.terraform:hover, -.choice-button.terraform:focus { - border-color: var(--terraform-border); -} - .choice-button.bookmark:hover, .choice-button.bookmark:focus { border-color: var(--danger); diff --git a/src/styles/components/terraform.css b/src/styles/components/terraform.css deleted file mode 100644 index 78e8953..0000000 --- a/src/styles/components/terraform.css +++ /dev/null @@ -1,274 +0,0 @@ -/* ============================================================================= - TERRAFORM PALETTE - Electric purple accent, keyboard-first controls - ============================================================================= */ - -.terraform-palette { - display: flex; - flex-direction: column; - gap: 10px; - width: 450px; - padding: 8px 12px; - margin: 8px 8px 8px calc(var(--gutter-width) + 8px); - background: var(--bg-window); - border: 1px solid var(--terraform-border); - border-radius: var(--radius-md); - animation: terraform-fade-in 0.15s ease; - outline: none; -} - -@keyframes terraform-fade-in { - from { - opacity: 0; - transform: translateY(-4px); - } - to { - opacity: 1; - transform: translateY(0); - } -} - -/* Header */ -.terraform-header { - display: flex; - align-items: center; - gap: 6px; - font-family: var(--font-ui); - font-size: 10px; - font-weight: 600; - text-transform: uppercase; - letter-spacing: 0.05em; - color: var(--terraform-icon); -} - -/* Inline row: label + controls */ -.terraform-row { - display: flex; - align-items: center; - gap: 6px; - flex-wrap: wrap; -} - -.terraform-row-label { - display: inline-flex; - align-items: center; - gap: 6px; - font-family: var(--font-ui); - font-size: 11px; - font-weight: 500; - color: var(--text-muted); - width: 74px; - flex-shrink: 0; -} - -.terraform-axis-icon { - width: 12px; - height: 12px; - flex-shrink: 0; -} - -.terraform-warning-icon { - color: var(--warning); - cursor: help; -} - -/* Toggle button (compact) */ -.terraform-toggle { - display: inline-flex; - align-items: center; - gap: 3px; - padding: 2px 6px; - background: var(--bg-window); - border: 1px solid var(--border-strong); - border-radius: var(--radius-sm); - font-family: var(--font-ui); - font-size: 11px; - color: var(--text-primary); - cursor: pointer; - transition: border-color var(--transition-fast), background var(--transition-fast); -} - -.terraform-toggle:hover { - background: var(--bg-hover); - border-color: var(--terraform-border); -} - -.terraform-toggle.active { - background: var(--terraform-bg); - border-color: var(--terraform-border); - color: var(--terraform-icon); -} - -.terraform-toggle kbd { - display: inline-flex; - align-items: center; - justify-content: center; - min-width: 14px; - height: 14px; - padding: 0 3px; - background: var(--bg-panel); - border: 1px solid var(--border-subtle); - border-radius: 2px; - font-family: var(--font-mono); - font-size: 9px; - color: var(--text-muted); -} - -.terraform-toggle.active kbd { - background: var(--terraform-bg); - border-color: var(--terraform-border); - color: var(--terraform-icon); -} - -/* Phrase preview (only shows when content exists) */ -.terraform-phrase { - padding-top: 6px; - border-top: 1px solid var(--border-subtle); - font-family: var(--font-ui); - font-size: 11px; - font-style: italic; - color: var(--text-secondary); - line-height: 1.4; -} - -/* Keyboard hints */ -.terraform-hints { - display: flex; - gap: 8px; - font-family: var(--font-ui); - font-size: 10px; - color: var(--text-muted); -} - -.terraform-hint kbd { - display: inline; - padding: 1px 3px; - margin-right: 2px; - background: var(--bg-panel); - border: 1px solid var(--border-subtle); - border-radius: 2px; - font-family: var(--font-mono); - font-size: 9px; -} - -/* Discrete slider */ -.terraform-slider { - display: flex; - align-items: center; - gap: 4px; -} - -.terraform-slider.disabled { - opacity: 0.4; - pointer-events: none; -} - -.terraform-slider-track { - display: flex; - gap: 2px; -} - -.terraform-slider-step { - height: 6px; - background: var(--border-subtle); - border: none; - border-radius: 2px; - cursor: pointer; - padding: 0; - transition: background var(--transition-fast); -} - -/* Varying widths: edge > middle > adjacent > center */ -.terraform-slider-step.size-edge { - width: 20px; -} - -.terraform-slider-step.size-middle { - width: 16px; -} - -.terraform-slider-step.size-adjacent { - width: 12px; -} - -.terraform-slider-step.size-center { - width: 8px; -} - -.terraform-slider-step:hover { - background: var(--border-hover); -} - -.terraform-slider-step.active { - background: var(--terraform-border); -} - -/* Neutral center uses desaturated purple */ -.terraform-slider-step.size-center.active { - background: color-mix(in srgb, var(--terraform-border) 50%, var(--border-subtle)); -} - -.terraform-slider-step.filled { - background: color-mix(in srgb, var(--terraform-border) 50%, var(--border-subtle)); -} - -/* Graduated glow based on intensity (distance from center) */ -.terraform-slider-step.glow-1 { - box-shadow: 0 0 3px var(--terraform-border); -} - -.terraform-slider-step.glow-2 { - box-shadow: 0 0 6px var(--terraform-border); -} - -.terraform-slider-step.glow-3 { - box-shadow: 0 0 10px var(--terraform-border); -} - -.terraform-slider-label { - font-family: var(--font-ui); - font-size: 9px; - color: var(--text-muted); -} - -/* Clickable kbd buttons in slider labels */ -.terraform-slider-label .kbd-btn { - display: inline-flex; - align-items: center; - justify-content: center; - padding: 0; - background: none; - border: none; - cursor: pointer; -} - -.terraform-slider-label .kbd-btn kbd { - display: inline-flex; - align-items: center; - justify-content: center; - min-width: 14px; - height: 14px; - padding: 0 3px; - background: var(--bg-panel); - border: 1px solid var(--border-subtle); - border-radius: 2px; - font-family: var(--font-mono); - font-size: 9px; - color: var(--text-muted); - transition: border-color var(--transition-fast), color var(--transition-fast); -} - -.terraform-slider-label .kbd-btn:hover kbd { - border-color: var(--terraform-border); - color: var(--terraform-icon); -} - -/* Override dimming (for precedence: Remove > Pin > Dissolve) */ -.terraform-row.overridden, -.terraform-slider-wrap.overridden { - opacity: 0.4; -} - -.terraform-slider-wrap { - display: contents; -} diff --git a/src/styles/index.css b/src/styles/index.css index 5cbcd56..4dba677 100644 --- a/src/styles/index.css +++ b/src/styles/index.css @@ -24,5 +24,4 @@ @import './components/chips.css'; @import './components/annotation-editor.css'; @import './components/command-palette.css'; -@import './components/terraform.css'; @import './components/color-preview.css'; diff --git a/src/styles/tokens.css b/src/styles/tokens.css index f54c61d..70b085b 100644 --- a/src/styles/tokens.css +++ b/src/styles/tokens.css @@ -58,11 +58,6 @@ /* Green family - success */ --light-green-600: #22863a; - /* Purple family - terraform */ - --light-purple-500: #7c3aed; - --light-purple-600: #6d28d9; - --light-purple-700: #5b21b6; - /* Code syntax (GitHub Light) */ --light-code-keyword: #d73a49; --light-code-func: #6f42c1; @@ -101,10 +96,6 @@ --dark-red-400: #f87171; /* Error text */ --dark-blue-500: #60a5fa; /* Action blue */ - /* Dark purple family - terraform */ - --dark-purple-400: #a78bfa; - --dark-purple-500: #8b5cf6; - /* Dark code syntax (GitHub Dark Dimmed style) */ --dark-code-keyword: #ff7b72; /* Soft Red */ --dark-code-func: #d2a8ff; /* Soft Purple */ @@ -222,11 +213,6 @@ /* Editor hover border */ --border-editor-hover: var(--accent-primary); - /* Terraform - deep purple */ - --terraform-border: var(--light-purple-600); - --terraform-bg: color-mix(in srgb, var(--light-purple-600) 12%, transparent); - --terraform-icon: var(--light-purple-700); - /* Overlays */ --backdrop-light: rgba(250, 250, 249, 0.4); --backdrop-dark: rgba(0, 0, 0, 0.6); @@ -397,11 +383,6 @@ /* Editor hover border - light in dark mode for visibility */ --border-editor-hover: var(--text-primary); - /* --- TERRAFORM --- */ - --terraform-border: var(--dark-purple-400); - --terraform-bg: color-mix(in srgb, var(--dark-purple-500) 15%, transparent); - --terraform-icon: var(--dark-purple-400); - /* --- OVERLAYS --- */ --backdrop-light: rgba(19, 18, 15, 0.4); --backdrop-dark: rgba(10, 9, 7, 0.85); From 32982118fe341ca476df08c96f948a656bb9c5c0 Mon Sep 17 00:00:00 2001 From: Denis Olehov Date: Sat, 20 Jun 2026 16:49:52 +0200 Subject: [PATCH 2/2] refactor: remove bookmark feature --- CLAUDE.md | 4 +- README.md | 22 +- docs/features.md | 28 +- src-tauri/src/commands.rs | 155 +--------- src-tauri/src/config.rs | 27 +- src-tauri/src/id.rs | 2 +- src-tauri/src/lib.rs | 17 +- src-tauri/src/main.rs | 275 ------------------ src-tauri/src/mcp/mod.rs | 210 +------------ src-tauri/src/mcp/tools.rs | 27 -- src-tauri/src/output/formatters.rs | 34 +-- src-tauri/src/output/mod.rs | 72 +---- src-tauri/src/output/render.rs | 8 - src-tauri/src/output/snapshot_tests.rs | 75 +---- ...apshot_tests__kitchen_sink_everything.snap | 19 +- src-tauri/src/review.rs | 6 +- src-tauri/src/state.rs | 228 +-------------- src/lib/AnnotationEditor.svelte | 23 +- .../CommandPalette/BookmarkEditView.svelte | 100 ------- src/lib/CommandPalette/CommandPalette.svelte | 44 +-- src/lib/CommandPalette/Icon.svelte | 2 - .../engine/capabilities.test.ts | 6 +- src/lib/CommandPalette/engine/reducer.test.ts | 102 +++---- src/lib/CommandPalette/engine/types.ts | 2 +- .../CommandPalette/items/BookmarkItem.svelte | 117 -------- src/lib/CommandPalette/items/index.ts | 1 - .../CommandPalette/namespaces/bookmarks.ts | 104 ------- src/lib/CommandPalette/namespaces/index.ts | 6 +- src/lib/HelpOverlay.svelte | 11 +- src/lib/components/AnnotationSlot.svelte | 3 +- src/lib/components/ChoiceButtons.svelte | 23 -- src/lib/components/Header.svelte | 11 - src/lib/components/SessionEditor.svelte | 3 +- src/lib/components/StatusBar.svelte | 1 - src/lib/components/embedded/CodeBlock.svelte | 9 - src/lib/components/embedded/LineRow.svelte | 46 +-- src/lib/components/embedded/Portal.svelte | 8 +- .../components/embedded/RegularLines.svelte | 9 - src/lib/components/embedded/Table.svelte | 65 +---- .../composables/useAnnotationEditor.svelte.ts | 56 +--- .../composables/useAnnotationEditor.test.ts | 1 - src/lib/composables/useBookmarks.svelte.ts | 190 ------------ src/lib/composables/useInteraction.svelte.ts | 98 +------ src/lib/composables/useKeyboard.svelte.ts | 98 +------ src/lib/composables/useKeyboard.test.ts | 55 ---- src/lib/context/AnnotProvider.svelte | 13 +- src/lib/context/annot-context.svelte.ts | 2 - src/lib/icons/BookmarkIcon.svelte | 17 -- src/lib/icons/index.ts | 1 - src/lib/tiptap/extensions/RefChip.ts | 14 +- .../tiptap/nodeviews/BookmarkRefChip.svelte | 39 --- src/lib/tiptap/nodeviews/RefChipView.svelte | 7 - src/lib/tiptap/serialize.ts | 24 +- src/lib/types.ts | 60 +--- src/routes/+page.svelte | 124 +------- src/styles/components/annotation-editor.css | 74 ----- src/styles/components/chips.css | 62 ---- src/styles/components/code-viewer.css | 76 +---- src/styles/components/command-palette.css | 82 ------ 59 files changed, 161 insertions(+), 2837 deletions(-) delete mode 100644 src/lib/CommandPalette/BookmarkEditView.svelte delete mode 100644 src/lib/CommandPalette/items/BookmarkItem.svelte delete mode 100644 src/lib/CommandPalette/namespaces/bookmarks.ts delete mode 100644 src/lib/components/ChoiceButtons.svelte delete mode 100644 src/lib/composables/useBookmarks.svelte.ts delete mode 100644 src/lib/icons/BookmarkIcon.svelte delete mode 100644 src/lib/tiptap/nodeviews/BookmarkRefChip.svelte diff --git a/CLAUDE.md b/CLAUDE.md index 0cb9b2a..b7ade25 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -121,7 +121,7 @@ process before rebuilding or you'll get `Access is denied (os error 5)`. - `commands.rs` — All Tauri IPC handlers - `output/` — Structured output rendering for LLM consumption - `mcp/` — Model Context Protocol server -- `config.rs` — Persistent user settings (tags, exit modes, bookmarks) +- `config.rs` — Persistent user settings (tags, exit modes) **Frontend** (`src/lib/`): - `composables/` — Svelte 5 runes-based state (useAnnotations, etc.) @@ -183,7 +183,7 @@ Kill the stray `annot.exe` before rebuilding. ## UI Patterns ### Line Actions (right-side icons) -Add buttons inside the `{#if trailing || showBookmarkIcon}` block in `LineRow.svelte`. Use `.line-action` class. +Add buttons inside the `{#if trailing}` block in `LineRow.svelte`. Use `.line-action` class. ### Left Border Indicators Use `::before` pseudo-elements with `position: absolute; left: 0; width: 3px;`. For overlapping indicators, use `repeating-linear-gradient`. diff --git a/README.md b/README.md index de7ae05..514f033 100644 --- a/README.md +++ b/README.md @@ -136,7 +136,7 @@ claude mcp add --scope user annot annot mcp Make sure `annot.exe` is on your `PATH` first (see build instructions above), or pass the full path: `claude mcp add --scope user annot "C:\path\to\annot.exe" mcp`. -Claude now has review tools (`review_file`, `review_diff`, `review_content`) and bookmark tools (`get_bookmark`, `list_bookmarks`). Ask it to review something and a window opens for your feedback. +Claude now has review tools (`review_file`, `review_diff`, `review_content`). Ask it to review something and a window opens for your feedback. ### Standalone @@ -193,7 +193,6 @@ Press `Shift+C` to add comments that apply to the entire review — framing cont - **Syntax highlighting** for 50+ languages - **Mermaid diagrams** rendered inline - **Portal links** — embed live code from other files -- **Bookmarks** — save and recall annotations across sessions - **`/excalidraw`** — draw diagrams inside annotations - **`/replace`** — propose inline code changes @@ -225,21 +224,6 @@ Press `Shift+C` to add comments that apply to the entire review — framing cont | `label` | string | yes | Display name with .md extension | | `exit_modes` | array | no | Ephemeral exit modes for this session | -### `get_bookmark` - -| Parameter | Type | Required | Description | -|-----------|------|----------|-------------| -| `id` | string | yes | Full or prefix bookmark ID | - -### `list_bookmarks` - -| Parameter | Type | Required | Description | -|-----------|------|----------|-------------| -| `limit` | number | no | Maximum number of bookmarks to return | -| `search` | string | no | Filter by label, selected text, or context | -| `project` | string | no | Filter by project path | -| `sort` | string | no | Sort order: "asc" (default) or "desc" | - ## Keyboard shortcuts | Shortcut | Function | @@ -248,8 +232,6 @@ Press `Shift+C` to add comments that apply to the entire review — framing cont | Shift+Drag | Select range | | c | Comment hovered line | | Shift+C | Session context (global comment) | -| b | Bookmark hovered line or selection | -| Shift+B | Bookmark entire session | | Tab / Shift+Tab | Cycle exit modes | | Alt+Tab | Exit mode picker | | : | Command palette | @@ -262,7 +244,7 @@ Press `Shift+C` to add comments that apply to the entire review — framing cont | Shortcut | Function | |---|---| | # | Insert tag | -| @ | Reference (annotations, bookmarks, sections) | +| @ | Reference (annotations, sections, files) | | / | Slash commands (/replace, /excalidraw) | ## License diff --git a/docs/features.md b/docs/features.md index 3d67c9d..6d2749d 100644 --- a/docs/features.md +++ b/docs/features.md @@ -43,12 +43,12 @@ Review agent-generated content — plans, drafts, analysis. Markdown rendering w - **Images**: Paste screenshots directly into annotations - **Excalidraw diagrams**: Sketch ideas visually (can also convert Mermaid diagrams to Excalidraw) - **Replace blocks**: Propose code replacements with before/after display -- **Bookmark references**: Link to previously captured moments +- **References**: Link to annotations, sections, and project files ### Slash Commands in Annotation Editor - `/` triggers command menu (Excalidraw, Replace block, etc.) - `#` triggers tag autocomplete -- `@` triggers bookmark reference autocomplete +- `@` triggers reference autocomplete (annotations, sections, files) ### Tag System - Create custom tags with names and LLM instructions @@ -84,17 +84,6 @@ This fetches and displays the actual code inline, syntax-highlighted. Max 50 por --- -## Bookmarks - -Capture moments for future reference: - -- **Session bookmarks**: Snapshot the entire current review with an optional label -- **Selection bookmarks**: Bookmark specific line ranges with optional labels -- Reference bookmarks in annotations with `@` syntax -- Bookmarks are **detached** — if deleted, references keep their content - ---- - ## Content Shaping annot isn't a document editor — humans shape content through **reaction**, not direct authorship: @@ -149,12 +138,6 @@ Press `:` (colon) to open. Seven namespaces: - Press `s` to set as active - Press `r` to reorder (drag with arrow keys) -### Bookmarks -- Browse, edit labels, delete bookmarks -- Search by label or content -- Shows project context -- Sorted by creation date (newest first) - ### Copy - Copy content only - Copy annotations only @@ -180,7 +163,7 @@ Press `:` (colon) to open. Seven namespaces: | Shift+Drag | Select range | | `/` | Slash command menu (Excalidraw, Replace) | | `#` | Tag autocomplete | -| `@` | Bookmark reference autocomplete | +| `@` | Reference autocomplete (annotations, sections, files) | | Tab/Shift+Tab | Cycle exit modes | | `Shift+C` | Session context editor | | `:` | Command palette | @@ -201,9 +184,6 @@ TAGS: [# SECURITY] Review for security vulnerabilities [# TODO] Items needing follow-up -BOOKMARKS: - [BOOKMARK abc] auth-flow (this session) - CONTEXT: plan.md [embeds: src/lib.rs, src/main.rs] GENERAL: @@ -225,7 +205,6 @@ Saved to /path/to/file.md ### Section Meanings - **TAGS**: Tag definitions used in annotations (only if tags are present) -- **BOOKMARKS**: Referenced bookmarks with their snapshots - **CONTEXT**: What's being reviewed, with any embedded portal files - **GENERAL**: High-level comment about the entire review (not line-specific) - **NEXT**: What action the human wants (exit mode name + instruction) @@ -264,7 +243,6 @@ All block until window closes, returning structured output with annotations, exi Persisted in the OS config directory (`~/.config/annot/` on Linux, `~/Library/Application Support/annot/` on macOS, `%APPDATA%\annot\` on Windows): - Tags and exit modes -- Bookmarks with snapshots - Usage statistics - Theme preference - Obsidian vault paths diff --git a/src-tauri/src/commands.rs b/src-tauri/src/commands.rs index f71edae..2f611a3 100644 --- a/src-tauri/src/commands.rs +++ b/src-tauri/src/commands.rs @@ -9,18 +9,13 @@ use crate::config::{self, Config, Theme}; use crate::lang::extension_to_fence_language; use crate::output::{export_content, export_section, format_json, format_output, OutputMode}; use crate::review::ActiveReview; -use crate::input::{ContentSource, McpSource}; -use crate::state::{ - Bookmark, BookmarkSnapshot, ContentMetadata, ContentNode, ContentResponse, ExitMode, - SessionType, Tag, TagUsageStats, -}; +use crate::state::{ContentNode, ContentResponse, ExitMode, Tag, TagUsageStats}; /// Snapshot of config data for reload_config command. #[derive(Serialize)] pub struct ConfigSnapshot { pub tags: Vec, pub exit_modes: Vec, - pub bookmarks: Vec, } use crate::ShouldExit; @@ -298,15 +293,6 @@ pub fn reorder_exit_modes( }) } -// --- Bookmark commands --- - -#[tauri::command] -pub fn get_bookmarks(review_state: State) -> Result, String> { - let guard = review_state.lock(); - let review = guard.as_ref().ok_or("No active review")?; - Ok(review.config.bookmarks().to_vec()) -} - /// Reload config from disk, merging new items while preserving in-session edits. /// Called on window focus to pick up changes from other annot windows. #[tauri::command] @@ -319,145 +305,6 @@ pub fn reload_config(review_state: State) -> Result, - label: Option, -) -> Result { - with_review!(review_state, |review| { - // Determine session type from the content source - let source = &review.root_view.content().source; - let source_type = match source { - ContentSource::Mcp(McpSource::Diff { .. }) => SessionType::Diff, - ContentSource::Mcp(McpSource::Content { .. }) => SessionType::Content, - _ => SessionType::File, - }; - - // Get source title (label) and full content - let source_title = review.root_view.content().label.clone(); - let context = review - .root_view - .content() - .lines - .iter() - .map(|l| l.content.as_str()) - .collect::>() - .join("\n"); - - let snapshot = BookmarkSnapshot::Session { - source_type, - source_title, - context, - }; - - // Auto-derive label from H1 heading for markdown content if not provided - let label = label.or_else(|| { - if let ContentMetadata::Markdown(md) = &review.root_view.content().metadata { - md.sections.iter().find(|s| s.level == 1).map(|s| s.title.clone()) - } else { - None - } - }); - - // Get current working directory as project path - let project_path = std::env::current_dir().ok(); - - let bookmark = Bookmark::new(label, project_path, snapshot); - review.config.upsert_bookmark(bookmark.clone()); - review.session_created_bookmarks.insert(bookmark.id.clone()); - - Ok(bookmark) - }) -} - -#[tauri::command] -pub fn create_selection_bookmark( - review_state: State, - start_line: usize, - end_line: usize, - label: Option, -) -> Result { - with_review!(review_state, |review| { - // Determine session type from the content source - let source = &review.root_view.content().source; - let source_type = match source { - ContentSource::Mcp(McpSource::Diff { .. }) => SessionType::Diff, - ContentSource::Mcp(McpSource::Content { .. }) => SessionType::Content, - _ => SessionType::File, - }; - - let lines = &review.root_view.content().lines; - - // Extract selected text (display indices are 1-indexed) - let selected_text = lines - .iter() - .enumerate() - .filter(|(i, _)| *i + 1 >= start_line && *i + 1 <= end_line) - .map(|(_, l)| l.content.as_str()) - .collect::>() - .join("\n"); - - // Get source title and full context - let source_title = review.root_view.content().label.clone(); - let context = lines - .iter() - .map(|l| l.content.as_str()) - .collect::>() - .join("\n"); - - let snapshot = BookmarkSnapshot::Selection { - source_type, - source_title, - context, - selected_text, - }; - - let project_path = std::env::current_dir().ok(); - let bookmark = Bookmark::new(label, project_path, snapshot); - review.config.upsert_bookmark(bookmark.clone()); - review.session_created_bookmarks.insert(bookmark.id.clone()); - - Ok(bookmark) - }) -} - -#[tauri::command] -pub fn update_bookmark( - review_state: State, - id: String, - label: String, -) -> Result { - with_review!(review_state, |review| { - let bookmark = review - .config - .get_bookmark(&id) - .ok_or_else(|| format!("Bookmark not found: {}", id))? - .clone(); - - let updated = Bookmark { - label: Some(label), - ..bookmark - }; - review.config.upsert_bookmark(updated.clone()); - - Ok(updated) - }) -} - -#[tauri::command] -pub fn delete_bookmark( - review_state: State, - id: String, -) -> Result, String> { - with_review!(review_state, |review| { - if !review.config.delete_bookmark(&id) { - return Err(format!("Bookmark not found: {}", id)); - } - Ok(review.config.bookmarks().to_vec()) }) } diff --git a/src-tauri/src/config.rs b/src-tauri/src/config.rs index 5de47f9..9a418dc 100644 --- a/src-tauri/src/config.rs +++ b/src-tauri/src/config.rs @@ -7,7 +7,7 @@ use std::path::{Path, PathBuf}; use fs4::FileExt; use serde::{de::DeserializeOwned, Deserialize, Serialize}; -use crate::state::{Bookmark, ExitMode, Tag, TagUsageStats}; +use crate::state::{ExitMode, Tag, TagUsageStats}; /// Current config version. Bump when making breaking changes. pub const CONFIG_VERSION: u32 = 1; @@ -83,13 +83,6 @@ impl Mergeable for ExitMode { } } -impl Mergeable for Bookmark { - type Id = String; - fn id(&self) -> String { - self.id.clone() - } -} - /// Merge two collections, respecting deletions and preferring memory on conflicts. fn merge_collections( memory: Vec, @@ -208,24 +201,6 @@ pub fn save_exit_modes(modes: &[ExitMode], deleted_ids: &HashSet) -> io: save_merged("exit-modes.json", modes, deleted_ids) } -/// Loads bookmarks from ~/.config/annot/bookmarks.json. Returns empty vec if file doesn't exist. -pub fn load_bookmarks() -> Vec { - let Some(dir) = config_dir() else { - return vec![]; - }; - - let path = dir.join("bookmarks.json"); - match fs::read_to_string(&path) { - Ok(content) => serde_json::from_str(&content).unwrap_or_else(|_| vec![]), - Err(_) => vec![], - } -} - -/// Saves bookmarks to ~/.config/annot/bookmarks.json with locking and merge. -pub fn save_bookmarks(bookmarks: &[Bookmark], deleted_ids: &HashSet) -> io::Result<()> { - save_merged("bookmarks.json", bookmarks, deleted_ids) -} - /// Loads tag usage stats from ~/.config/annot/tag-usage.json. Returns default if file doesn't exist. pub fn load_tag_usage() -> TagUsageStats { let Some(dir) = config_dir() else { diff --git a/src-tauri/src/id.rs b/src-tauri/src/id.rs index bd718c4..bc21c75 100644 --- a/src-tauri/src/id.rs +++ b/src-tauri/src/id.rs @@ -1,4 +1,4 @@ -//! ID generation for persistent entities (bookmarks, tags, exit modes). +//! ID generation for persistent entities (tags, exit modes). //! //! Follows jj's approach: random bytes encoded as "reverse hex" using k-z instead of 0-9a-f. //! This produces IDs like `kmwvzxyqstnp` — always lowercase, always 12 chars. diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 6ac87ed..81768fe 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -25,13 +25,11 @@ pub mod state; pub mod window_state; use commands::{ - compute_replace_diff, copy_section, copy_to_clipboard, create_bookmark, - create_selection_bookmark, cycle_exit_mode, delete_annotation, delete_bookmark, - delete_exit_mode, delete_tag, export_to_obsidian, finish_review, - get_bookmarks, get_config, get_content, get_exit_modes, get_tags, - get_theme, reload_config, reorder_exit_modes, save_config, - save_content, set_exit_mode, set_session_comment, set_theme, update_bookmark, - upsert_annotation, upsert_exit_mode, upsert_tag, + compute_replace_diff, copy_section, copy_to_clipboard, cycle_exit_mode, delete_annotation, + delete_exit_mode, delete_tag, export_to_obsidian, finish_review, get_config, get_content, + get_exit_modes, get_tags, get_theme, reload_config, reorder_exit_modes, save_config, + save_content, set_exit_mode, set_session_comment, set_theme, upsert_annotation, + upsert_exit_mode, upsert_tag, }; use excalidraw_window::{ close_excalidraw_by_placeholder, excalidraw_cancel, excalidraw_save, get_excalidraw_context, @@ -60,12 +58,7 @@ macro_rules! all_commands { upsert_exit_mode, delete_exit_mode, reorder_exit_modes, - get_bookmarks, reload_config, - create_bookmark, - create_selection_bookmark, - update_bookmark, - delete_bookmark, copy_to_clipboard, copy_section, save_content, diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 26740bd..9fec2e4 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -50,42 +50,10 @@ struct Cli { enum Command { /// Run as MCP server (Model Context Protocol) Mcp, - /// Manage bookmarks - #[command(subcommand)] - Bookmark(BookmarksCommand), /// Print version information Version, } -#[derive(clap::Subcommand)] -enum BookmarksCommand { - /// List all bookmarks - List { - /// Output as JSON - #[arg(long)] - json: bool, - - /// Sort order by creation date: "asc" (oldest first) or "desc" (newest first, default) - #[arg(long, default_value = "desc")] - sort: String, - }, - /// Show a bookmark's full snapshot - Show { - /// Bookmark ID or prefix - id: String, - }, - /// Delete a bookmark - Delete { - /// Bookmark ID or prefix - id: String, - }, - /// Open a bookmark for annotation - Open { - /// Bookmark ID or prefix - id: String, - }, -} - fn main() { // Suppress macOS system logs (XPC, CoreAnalytics, etc.) in release builds #[cfg(all(target_os = "macos", not(debug_assertions)))] @@ -93,14 +61,6 @@ fn main() { let cli = Cli::parse(); - // Handle bookmark subcommands that don't need Tauri (list/show/delete) - if let Some(Command::Bookmark(cmd)) = &cli.command { - if !matches!(cmd, BookmarksCommand::Open { .. }) { - handle_bookmarks_command(cmd); - return; - } - } - // Handle version subcommand (doesn't need Tauri) if let Some(Command::Version) = cli.command { println!("annot {}", env!("CARGO_PKG_VERSION")); @@ -110,12 +70,6 @@ fn main() { // Generate context once (avoids duplicate symbol errors) let context = tauri::generate_context!(); - // Handle bookmark open (needs Tauri window) - if let Some(Command::Bookmark(BookmarksCommand::Open { id })) = &cli.command { - handle_bookmark_open(id, context); - return; - } - // Handle MCP subcommand if let Some(Command::Mcp) = cli.command { annot_lib::run_mcp(context); @@ -193,232 +147,3 @@ fn main() { annot_lib::run(state, context, cli.json); } - -// ════════════════════════════════════════════════════════════════════════════ -// BOOKMARK CLI HANDLERS -// ════════════════════════════════════════════════════════════════════════════ - -fn handle_bookmarks_command(cmd: &BookmarksCommand) { - use annot_lib::state::UserConfig; - - let mut config = UserConfig::load(); - - match cmd { - BookmarksCommand::List { json, sort } => { - let bookmarks = config.bookmarks(); - - if bookmarks.is_empty() { - if *json { - println!("[]"); - } else { - println!("*No bookmarks.*"); - } - return; - } - - // Sort bookmarks by creation date - let mut sorted: Vec<_> = bookmarks.iter().collect(); - let descending = sort == "desc"; - sorted.sort_by(|a, b| { - if descending { - b.created_at.cmp(&a.created_at) - } else { - a.created_at.cmp(&b.created_at) - } - }); - - if *json { - // Collect sorted refs into owned values for JSON serialization - let sorted_owned: Vec<_> = sorted.into_iter().cloned().collect(); - println!("{}", serde_json::to_string_pretty(&sorted_owned).unwrap()); - } else { - // Markdown table - println!("| ID | Label | Source | Project |"); - println!("|-----|-------|--------|---------|"); - - for bookmark in sorted { - let label = bookmark.display_label().replace('|', "\\|"); - let label_display = if label.len() > 38 { - format!("{}…", &label[..37]) - } else { - label - }; - - let source = bookmark.snapshot.source_title().replace('|', "\\|"); - let source_display = if source.len() > 18 { - format!("{}…", &source[..17]) - } else { - source - }; - - let project = bookmark - .project_path - .as_ref() - .and_then(|p| p.file_name()) - .map(|n| n.to_string_lossy().to_string()) - .unwrap_or_else(|| "—".to_string()); - - println!( - "| {} | {} | {} | {} |", - &bookmark.id[..12.min(bookmark.id.len())], - label_display, - source_display, - project - ); - } - } - } - - BookmarksCommand::Show { id } => { - match find_bookmark(&config, id) { - Ok(bookmark) => print_bookmark_markdown(bookmark), - Err(e) => { - eprintln!("{}", e); - process::exit(1); - } - } - } - - BookmarksCommand::Delete { id } => { - // First resolve the full ID - let full_id = match find_bookmark(&config, id) { - Ok(bookmark) => bookmark.id.clone(), - Err(e) => { - eprintln!("{}", e); - process::exit(1); - } - }; - - if config.delete_bookmark(&full_id) { - println!("Deleted bookmark {}", full_id); - } else { - eprintln!("Failed to delete bookmark {}", full_id); - process::exit(1); - } - } - - // Handled separately in main() since it needs Tauri context - BookmarksCommand::Open { .. } => unreachable!(), - } -} - -fn find_bookmark<'a>( - config: &'a annot_lib::state::UserConfig, - id_prefix: &str, -) -> Result<&'a annot_lib::state::Bookmark, String> { - let bookmarks = config.bookmarks(); - let matches: Vec<_> = bookmarks - .iter() - .filter(|b| b.id.starts_with(id_prefix)) - .collect(); - - match matches.len() { - 0 => Err(format!("No bookmark found with ID prefix '{}'", id_prefix)), - 1 => Ok(matches[0]), - _ => { - let candidates: Vec = matches - .iter() - .map(|b| format!(" {} — {}", &b.id[..6], b.display_label())) - .collect(); - Err(format!( - "Ambiguous ID prefix '{}'. Candidates:\n{}", - id_prefix, - candidates.join("\n") - )) - } - } -} - -fn print_bookmark_markdown(bookmark: &annot_lib::state::Bookmark) { - use annot_lib::state::BookmarkSnapshot; - - let project = bookmark - .project_path - .as_ref() - .and_then(|p| p.file_name()) - .map(|n| n.to_string_lossy().to_string()) - .unwrap_or_else(|| "(none)".to_string()); - - println!("# {}", bookmark.display_label()); - println!(); - println!("**Source**: {}", bookmark.snapshot.source_title()); - println!("**Project**: {}", project); - println!( - "**Created**: {}", - bookmark.created_at.format("%B %d, %Y") - ); - println!(); - println!("---"); - println!(); - - // For selection bookmarks, show selected text - if let BookmarkSnapshot::Selection { selected_text, .. } = &bookmark.snapshot { - println!("```"); - println!("{}", selected_text); - println!("```"); - } else { - // For session bookmarks, show context - println!("{}", bookmark.snapshot.content()); - } -} - -fn handle_bookmark_open(id: &str, context: tauri::Context) { - use annot_lib::input::{CliSource, ContentSource}; - use annot_lib::state::{BookmarkSnapshot, ContentModel, SessionType, UserConfig}; - - let config = UserConfig::load(); - - // Find bookmark with prefix matching (reuse existing helper) - let bookmark = match find_bookmark(&config, id) { - Ok(b) => b.clone(), - Err(e) => { - eprintln!("{}", e); - process::exit(1); - } - }; - - // Extract content and type from snapshot - let (source_type, source_title, content_str) = match &bookmark.snapshot { - BookmarkSnapshot::Session { - source_type, - source_title, - context, - } => (source_type, source_title.clone(), context.clone()), - BookmarkSnapshot::Selection { - source_type, - source_title, - context, - .. - } => { - // Show full context (per requirements) - (source_type, source_title.clone(), context.clone()) - } - }; - - // Create ContentSource with original label (for syntax highlighting) - let content_source = ContentSource::Cli(CliSource::Stdin { - label: source_title.clone(), - }); - - // Build ContentModel based on source_type - let content = match source_type { - SessionType::Diff => match ContentModel::from_diff(&content_str, content_source) { - Ok(c) => c, - Err(e) => { - eprintln!("Error parsing bookmark diff: {}", e); - process::exit(1); - } - }, - SessionType::Content | SessionType::File => { - // Check if markdown by label extension - if source_title.ends_with(".md") { - ContentModel::from_markdown(&content_str, content_source) - } else { - ContentModel::from_file(&content_str, content_source) - } - } - }; - - let state = AppState::new(content, config); - annot_lib::run(state, context, false); -} diff --git a/src-tauri/src/mcp/mod.rs b/src-tauri/src/mcp/mod.rs index 5860b05..ba84029 100644 --- a/src-tauri/src/mcp/mod.rs +++ b/src-tauri/src/mcp/mod.rs @@ -18,8 +18,7 @@ use crate::state::AppState; use crate::window_state::{self, WindowType}; use crate::SessionLock; use tools::{ - GetBookmarkInput, ListBookmarksInput, ReviewContentInput, ReviewDiffInput, ReviewFileInput, - SessionImage, SessionOutput, + ReviewContentInput, ReviewDiffInput, ReviewFileInput, SessionImage, SessionOutput, }; /// Instructions for AI agents using the MCP server. @@ -125,119 +124,6 @@ impl AnnotServer { Ok(build_mcp_response(output)) } - - #[tool(description = "Retrieves a single bookmark by ID or ID prefix. Returns the full bookmark including its snapshot content. If the prefix matches multiple bookmarks, returns an error with candidates.")] - async fn get_bookmark( - &self, - params: Parameters, - ) -> Result { - let input = params.0; - - let result = tokio::task::spawn_blocking(move || { - let config = crate::state::UserConfig::load(); - let bookmarks = config.bookmarks(); - - // Find all bookmarks matching the prefix - let matches: Vec<_> = bookmarks - .iter() - .filter(|b| b.id.starts_with(&input.id)) - .collect(); - - match matches.len() { - 0 => Err(format!("No bookmark found with ID prefix '{}'", input.id)), - 1 => { - let bookmark = matches[0]; - Ok(format_bookmark_full(bookmark)) - } - _ => { - // Ambiguous - list candidates - let candidates: Vec = matches - .iter() - .map(|b| format!(" {} — {}", &b.id[..6], b.display_label())) - .collect(); - Err(format!( - "Ambiguous ID prefix '{}'. Candidates:\n{}", - input.id, - candidates.join("\n") - )) - } - } - }) - .await - .map_err(|e| McpError::internal_error(format!("Task join error: {}", e), None))?; - - match result { - Ok(text) => Ok(CallToolResult::success(vec![Content::text(text)])), - Err(e) => Err(McpError::invalid_params(e, None)), - } - } - - #[tool(description = "List all bookmarks, optionally filtered by search query, project path, or limited to a maximum count. Returns a summary list with IDs, labels, creation dates, sources, and projects. Sorted by creation date ascending (oldest first) by default; use sort=\"desc\" for newest first.")] - async fn list_bookmarks( - &self, - params: Parameters, - ) -> Result { - let input = params.0; - - let result = tokio::task::spawn_blocking(move || { - let config = crate::state::UserConfig::load(); - let bookmarks = config.bookmarks(); - - // Apply filters - let mut filtered: Vec<_> = bookmarks - .iter() - .filter(|b| { - // Project filter - if let Some(ref project) = input.project { - if let Some(ref path) = b.project_path { - if !path.to_string_lossy().contains(project) { - return false; - } - } else { - return false; - } - } - - // Search filter (label, selected text, context) - if let Some(ref query) = input.search { - let query_lower = query.to_lowercase(); - let label_match = b.display_label().to_lowercase().contains(&query_lower); - let content_match = b.snapshot.content().to_lowercase().contains(&query_lower); - if !label_match && !content_match { - return false; - } - } - - true - }) - .collect(); - - // Sort by created_at (ascending/oldest first by default) - let descending = input.sort.as_deref() == Some("desc"); - filtered.sort_by(|a, b| { - if descending { - b.created_at.cmp(&a.created_at) - } else { - a.created_at.cmp(&b.created_at) - } - }); - - // Apply limit - if let Some(limit) = input.limit { - filtered.truncate(limit); - } - - if filtered.is_empty() { - "No bookmarks found.".to_string() - } else { - format_bookmark_list(&filtered) - } - }) - .await - .map_err(|e| McpError::internal_error(format!("Task join error: {}", e), None))?; - - Ok(CallToolResult::success(vec![Content::text(result)])) - } } #[tool_handler] @@ -488,100 +374,6 @@ fn run_session_with_state( }) } -// ════════════════════════════════════════════════════════════════════════════ -// BOOKMARK FORMATTING -// ════════════════════════════════════════════════════════════════════════════ - -/// Format a single bookmark with full details for get_bookmark output. -fn format_bookmark_full(bookmark: &crate::state::Bookmark) -> String { - use crate::state::BookmarkSnapshot; - - let source_type = match &bookmark.snapshot { - BookmarkSnapshot::Session { source_type, .. } - | BookmarkSnapshot::Selection { source_type, .. } => match source_type { - crate::state::SessionType::File => "file", - crate::state::SessionType::Diff => "diff", - crate::state::SessionType::Content => "content", - }, - }; - - let project = bookmark - .project_path - .as_ref() - .map(|p| p.to_string_lossy().to_string()) - .unwrap_or_else(|| "(none)".to_string()); - - let mut lines = vec![ - format!("Bookmark: {}", bookmark.id), - format!("Label: {}", bookmark.display_label()), - format!("Source: {} ({})", bookmark.snapshot.source_title(), source_type), - format!("Project: {}", project), - format!("Created: {}", bookmark.created_at.format("%Y-%m-%d %H:%M:%S UTC")), - String::new(), - ]; - - // Add selected text for selection bookmarks - if let BookmarkSnapshot::Selection { selected_text, .. } = &bookmark.snapshot { - lines.push("─── Selected Text ──────────────────────────────────────".to_string()); - lines.push(selected_text.clone()); - lines.push("─────────────────────────────────────────────────────────".to_string()); - lines.push(String::new()); - } - - lines.push("─── Snapshot ───────────────────────────────────────────".to_string()); - lines.push(bookmark.snapshot.content().to_string()); - lines.push("─────────────────────────────────────────────────────────".to_string()); - - lines.join("\n") -} - -/// Format a list of bookmarks as a summary table for list_bookmarks output. -fn format_bookmark_list(bookmarks: &[&crate::state::Bookmark]) -> String { - let mut lines = Vec::new(); - - lines.push(format!( - "{:<12} {:<40} {:<12} {:<20} {}", - "ID", "LABEL", "CREATED", "SOURCE", "PROJECT" - )); - lines.push("─".repeat(100)); - - for bookmark in bookmarks { - let label = bookmark.display_label(); - let label_display = if label.len() > 38 { - format!("{}…", &label[..37]) - } else { - label - }; - - let created = bookmark.created_at.format("%Y-%m-%d").to_string(); - - let source = bookmark.snapshot.source_title(); - let source_display = if source.len() > 18 { - format!("{}…", &source[..17]) - } else { - source.to_string() - }; - - let project = bookmark - .project_path - .as_ref() - .and_then(|p| p.file_name()) - .map(|n| n.to_string_lossy().to_string()) - .unwrap_or_else(|| "—".to_string()); - - lines.push(format!( - "{:<12} {:<40} {:<12} {:<20} {}", - &bookmark.id[..12.min(bookmark.id.len())], - label_display, - created, - source_display, - project - )); - } - - lines.join("\n") -} - // ════════════════════════════════════════════════════════════════════════════ // RESPONSE BUILDERS // ════════════════════════════════════════════════════════════════════════════ diff --git a/src-tauri/src/mcp/tools.rs b/src-tauri/src/mcp/tools.rs index 528653f..10db101 100644 --- a/src-tauri/src/mcp/tools.rs +++ b/src-tauri/src/mcp/tools.rs @@ -101,30 +101,3 @@ impl ExitModeInput { } } } - -// ════════════════════════════════════════════════════════════════════════════ -// BOOKMARK TOOLS -// ════════════════════════════════════════════════════════════════════════════ - -/// Input for the get_bookmark tool. -#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] -pub struct GetBookmarkInput { - #[schemars(description = "Full or prefix ID")] - pub id: String, -} - -/// Input for the list_bookmarks tool. -#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] -pub struct ListBookmarksInput { - #[schemars(description = "Maximum number of bookmarks to return")] - pub limit: Option, - - #[schemars(description = "Search query to filter by label, selected text, or context")] - pub search: Option, - - #[schemars(description = "Filter by project path")] - pub project: Option, - - #[schemars(description = "Sort order by creation date: \"asc\" (oldest first, default) or \"desc\" (newest first)")] - pub sort: Option, -} diff --git a/src-tauri/src/output/formatters.rs b/src-tauri/src/output/formatters.rs index a1020b2..006af04 100644 --- a/src-tauri/src/output/formatters.rs +++ b/src-tauri/src/output/formatters.rs @@ -6,9 +6,7 @@ use std::collections::BTreeMap; use crate::mcp::tools::SessionImage; -use crate::state::{ - Annotation, Bookmark, ContentMetadata, ContentModel, LineOrigin, -}; +use crate::state::{Annotation, ContentMetadata, ContentModel, LineOrigin}; use super::builder::{BuilderMode, OutputBuilder}; use super::render::render_content; @@ -21,36 +19,6 @@ pub fn format_legend(out: &mut OutputBuilder, tags: &BTreeMap) { } } -/// Format a single bookmark entry. -pub fn format_bookmark(out: &mut OutputBuilder, bookmark: &Bookmark, created_this_session: bool) { - let short_id = &bookmark.id[..bookmark.id.len().min(3)]; - let display_label = bookmark.display_label(); - - if created_this_session { - // Condensed: created this session, agent already has context - out.line(&format!( - "[BOOKMARK {}] {} (this session)", - short_id, display_label - )); - } else { - // Full: pre-existing bookmark, emit full context - out.line(&format!("[BOOKMARK {}] {}", short_id, display_label)); - out.indented(|b| { - b.field("Source", &bookmark.snapshot.source_title()); - if let Some(ref project) = bookmark.project_path { - b.field("Project", &project.display().to_string()); - } - b.field("Created", &bookmark.created_at.format("%Y-%m-%d").to_string()); - b.separator(); - for line in bookmark.snapshot.content().lines() { - b.line(line); - } - b.separator(); - }); - out.blank_line(); - } -} - /// Format a single annotation block with context lines and content. pub fn format_annotation( out: &mut OutputBuilder, diff --git a/src-tauri/src/output/mod.rs b/src-tauri/src/output/mod.rs index 10dcc35..6b982e0 100644 --- a/src-tauri/src/output/mod.rs +++ b/src-tauri/src/output/mod.rs @@ -17,15 +17,12 @@ use crate::lang; use crate::mcp::tools::SessionImage; use crate::portal::LoadedPortal; use crate::review::{FileKey, Review}; -use crate::state::{ - Annotation, Bookmark, ContentModel, ContentNode, LineSemantics, - PortalSemantics, -}; +use crate::state::{Annotation, ContentModel, ContentNode, LineSemantics, PortalSemantics}; pub use builder::{BuilderMode, OutputBuilder, SECTION_DIVIDER, SEPARATOR}; pub use render::render_content; -use formatters::{calculate_builder_mode, format_annotation, format_bookmark, format_legend}; +use formatters::{calculate_builder_mode, format_annotation, format_legend}; /// Output mode determines how content is formatted. #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] @@ -54,7 +51,6 @@ pub struct FormatMetadata { pub general_comment: Option, pub exit_mode: Option, pub general_comment_count: usize, - pub bookmark_count: usize, } /// Serialize a FormatResult as JSON for `--json` CLI output. @@ -70,7 +66,6 @@ pub fn format_json(result: &FormatResult) -> String { "general_comment": result.metadata.general_comment, "exit_mode": result.metadata.exit_mode, "general_comment_count": result.metadata.general_comment_count, - "bookmark_count": result.metadata.bookmark_count, }); serde_json::to_string(&json).expect("FormatResult JSON serialization should not fail") } @@ -307,55 +302,6 @@ fn collect_unique_tags(review: &Review) -> BTreeMap { tags } -/// Collect unique bookmarks referenced from all content nodes (session comment + annotations). -/// Returns embedded Bookmark data in order of first occurrence (keyed by ID). -fn collect_unique_bookmarks(review: &Review) -> Vec { - use indexmap::IndexMap; - let mut bookmarks: IndexMap = IndexMap::new(); - - let mut process_nodes = |nodes: &[ContentNode]| { - for node in nodes { - match node { - ContentNode::BookmarkRef { id, bookmark, .. } => { - bookmarks - .entry(id.clone()) - .or_insert_with(|| bookmark.clone()); - } - ContentNode::Ref { snapshot, .. } => { - if let crate::state::RefSnapshot::Bookmark { bookmark } = snapshot { - bookmarks - .entry(bookmark.id.clone()) - .or_insert_with(|| bookmark.clone()); - } - } - // Other node types don't contain bookmark references - ContentNode::Text { .. } - | ContentNode::Tag { .. } - | ContentNode::Media { .. } - | ContentNode::Excalidraw { .. } - | ContentNode::Replace { .. } - | ContentNode::Error { .. } - | ContentNode::Paste { .. } - | ContentNode::File { .. } => {} - } - } - }; - - // Collect from session comment - if let Some(ref comment) = review.session_comment { - process_nodes(comment); - } - - // Collect from all file annotations - for file in review.files.values() { - for annotation in file.annotations.values() { - process_nodes(&annotation.content); - } - } - - bookmarks.into_values().collect() -} - /// Format all annotations as structured output for LLM consumption. pub fn format_output(review: &Review, mode: OutputMode) -> FormatResult { // Get content from root_view @@ -399,17 +345,6 @@ pub fn format_output(review: &Review, mode: OutputMode) -> FormatResult { }); } - // BOOKMARKS block (if any bookmarks are referenced) - let unique_bookmarks = collect_unique_bookmarks(review); - if !unique_bookmarks.is_empty() { - out.section("BOOKMARKS", |b| { - for bookmark in &unique_bookmarks { - let created_this_session = review.session_created_bookmarks.contains(&bookmark.id); - format_bookmark(b, bookmark, created_this_session); - } - }); - } - // CONTEXT block (if reviewing content with portals) let has_context = !content.portals.is_empty(); if has_context { @@ -529,8 +464,6 @@ pub fn format_output(review: &Review, mode: OutputMode) -> FormatResult { .map(|c| !c.is_empty()) .unwrap_or(false) { 1 } else { 0 }; - let bookmark_count = collect_unique_bookmarks(review).len(); - let general_comment = review.session_comment.as_ref().and_then(|comment| { if comment.is_empty() { None @@ -556,7 +489,6 @@ pub fn format_output(review: &Review, mode: OutputMode) -> FormatResult { general_comment, exit_mode, general_comment_count, - bookmark_count, }, } } diff --git a/src-tauri/src/output/render.rs b/src-tauri/src/output/render.rs index 258df8a..92f7abd 100644 --- a/src-tauri/src/output/render.rs +++ b/src-tauri/src/output/render.rs @@ -96,19 +96,11 @@ fn render_node( // Output pasted content as plain text content.clone() } - ContentNode::BookmarkRef { id, .. } => { - let short_id = &id[..id.len().min(3)]; - format!("[BOOKMARK {}]", short_id) - } ContentNode::Ref { snapshot, .. } => { match snapshot { RefSnapshot::Annotation(snap) => { format!("[ANNOTATION L{}]", snap.source_key) } - RefSnapshot::Bookmark { bookmark } => { - let short_id = &bookmark.id[..bookmark.id.len().min(3)]; - format!("[BOOKMARK {}]", short_id) - } RefSnapshot::Heading(snap) => { format!("[H{} {}]", snap.level, snap.title) } diff --git a/src-tauri/src/output/snapshot_tests.rs b/src-tauri/src/output/snapshot_tests.rs index 9da204b..05350cf 100644 --- a/src-tauri/src/output/snapshot_tests.rs +++ b/src-tauri/src/output/snapshot_tests.rs @@ -10,15 +10,13 @@ use std::collections::HashMap; use std::io::Write; use std::path::PathBuf; -use chrono::{DateTime, Utc}; use tempfile::NamedTempFile; use crate::input::{CliSource, ContentSource, DiffSource, McpSource}; use crate::review::Review; use crate::state::{ - AnnotationRefSnapshot, Annotation, Bookmark, BookmarkSnapshot, ContentMetadata, - ContentModel, ContentNode, ExitMode, ExitModeSource, Line, LineOrigin, LineRange, - LineSemantics, RefSnapshot, SessionType, UserConfig, + AnnotationRefSnapshot, Annotation, ContentMetadata, ContentModel, ContentNode, ExitMode, + ExitModeSource, Line, LineOrigin, LineRange, LineSemantics, RefSnapshot, UserConfig, }; use super::{format_output, OutputMode}; @@ -616,13 +614,11 @@ fn context_line_whitespace_only() { /// Comprehensive test that exercises every output feature: /// - TAGS section (multiple tags) -/// - BOOKMARKS section (both "this session" and pre-existing) /// - CONTEXT (would need portals, skipped for simplicity) /// - GENERAL block (session comment with tags and refs) /// - NEXT block (exit mode) /// - Multiple annotations with: /// - Tags -/// - Bookmark references (BookmarkRef and Ref variants) /// - Annotation references /// - File references /// - Replace blocks @@ -633,9 +629,6 @@ fn context_line_whitespace_only() { /// - Saved to path #[test] fn kitchen_sink_everything() { - // Fixed timestamp so the rendered "Created:" date is deterministic. - let fixed_created_at: DateTime = "2026-02-16T00:00:00Z".parse().unwrap(); - let config = UserConfig::with_data( vec![], // tags loaded from annotations vec![ExitMode { @@ -648,36 +641,9 @@ fn kitchen_sink_everything() { }], ); - // Create a pre-existing bookmark (not created this session) - let old_bookmark = Bookmark { - id: "oldbookmark1".to_string(), - label: Some("auth-validation".to_string()), - created_at: fixed_created_at, - project_path: Some(PathBuf::from("/projects/myapp")), - snapshot: BookmarkSnapshot::Selection { - source_type: SessionType::File, - source_title: "src/auth.rs".to_string(), - context: "fn validate_token(token: &str) -> Result {\n // validation logic\n}".to_string(), - selected_text: "validate_token".to_string(), - }, - }; - - // Create a bookmark from "this session" - let new_bookmark = Bookmark { - id: "newbookmark2".to_string(), - label: Some("error-handler".to_string()), - created_at: fixed_created_at, - project_path: None, - snapshot: BookmarkSnapshot::Session { - source_type: SessionType::Content, - source_title: "error-handling-plan.md".to_string(), - context: "# Error Handling Plan\n\nHandle errors gracefully.".to_string(), - }, - }; - let mut annotations = HashMap::new(); - // Annotation 1: Tags + text + bookmark ref (legacy format) + // Annotation 1: Tags + text annotations.insert( LineRange::new(10, 12), Annotation { @@ -690,15 +656,7 @@ fn kitchen_sink_everything() { instruction: "Review for security vulnerabilities".to_string(), }, ContentNode::Text { - text: " This authentication logic needs review. See ".to_string(), - }, - ContentNode::BookmarkRef { - id: "oldbookmark1".to_string(), - label: "auth-validation".to_string(), - bookmark: old_bookmark.clone(), - }, - ContentNode::Text { - text: " for context.".to_string(), + text: " This authentication logic needs review.".to_string(), }, ], }, @@ -727,7 +685,7 @@ fn kitchen_sink_everything() { }, ); - // Annotation 3: Annotation ref + file ref + new-style bookmark ref + // Annotation 3: Annotation ref + file ref annotations.insert( LineRange::new(40, 42), Annotation { @@ -760,13 +718,7 @@ fn kitchen_sink_everything() { path: "src/handlers/api.rs".to_string(), }, ContentNode::Text { - text: ". Also related to ".to_string(), - }, - ContentNode::Ref { - ref_type: "bookmark".to_string(), - snapshot: RefSnapshot::Bookmark { - bookmark: new_bookmark.clone(), - }, + text: ".".to_string(), }, ], }, @@ -826,7 +778,7 @@ fn kitchen_sink_everything() { config, ); - // Set session comment with tags and bookmark ref + // Set session comment with tags review.session_comment = Some(vec![ ContentNode::Text { text: "Overall code review feedback.\n\n".to_string(), @@ -837,21 +789,10 @@ fn kitchen_sink_everything() { instruction: "Performance consideration".to_string(), }, ContentNode::Text { - text: " Watch for N+1 queries in the data layer.\n\nSee also: ".to_string(), - }, - ContentNode::BookmarkRef { - id: "newbookmark2".to_string(), - label: "error-handler".to_string(), - bookmark: new_bookmark.clone(), - }, - ContentNode::Text { - text: " for error handling patterns.".to_string(), + text: " Watch for N+1 queries in the data layer.".to_string(), }, ]); - // Mark new_bookmark as created this session - review.session_created_bookmarks.insert("newbookmark2".to_string()); - // Set exit mode review.selected_exit_mode_id = Some("apply-with-changes".to_string()); diff --git a/src-tauri/src/output/snapshots/annot_lib__output__snapshot_tests__kitchen_sink_everything.snap b/src-tauri/src/output/snapshots/annot_lib__output__snapshot_tests__kitchen_sink_everything.snap index d7112cd..2d5bd9c 100644 --- a/src-tauri/src/output/snapshots/annot_lib__output__snapshot_tests__kitchen_sink_everything.snap +++ b/src-tauri/src/output/snapshots/annot_lib__output__snapshot_tests__kitchen_sink_everything.snap @@ -8,25 +8,10 @@ TAGS: [# SECURITY] Review for security vulnerabilities [# TODO] Action item for follow-up -BOOKMARKS: - [BOOKMARK new] error-handler (this session) - [BOOKMARK old] auth-validation - Source: src/auth.rs - Project: /projects/myapp - Created: 2026-02-16 -──────────────────────────────────── - fn validate_token(token: &str) -> Result { - // validation logic - } -──────────────────────────────────── - - GENERAL: Overall code review feedback. [# PERF] Watch for N+1 queries in the data layer. - - See also: [BOOKMARK new] for error handling patterns. NEXT: Apply with Changes — Apply the code but incorporate the suggested modifications @@ -37,7 +22,7 @@ src/main.rs:10-12 > 10 | line 10 content > 11 | line 11 content > 12 | line 12 content - └──> [# SECURITY] This authentication logic needs review. See [BOOKMARK old] for context. + └──> [# SECURITY] This authentication logic needs review. --- @@ -57,7 +42,7 @@ src/main.rs:40-42 > 40 | line 40 content > 41 | line 41 content > 42 | line 42 content - └──> [# TODO] Cross-reference: see [ANNOTATION L10-12] and @ref:file:src/handlers/api.rs. Also related to [BOOKMARK new] + └──> [# TODO] Cross-reference: see [ANNOTATION L10-12] and @ref:file:src/handlers/api.rs. --- diff --git a/src-tauri/src/review.rs b/src-tauri/src/review.rs index 0a43e01..6633071 100644 --- a/src-tauri/src/review.rs +++ b/src-tauri/src/review.rs @@ -12,7 +12,7 @@ //! - A window is a viewport that can display content //! - Two windows showing the same file share annotations -use std::collections::{HashMap, HashSet}; +use std::collections::HashMap; use std::fmt; use std::path::PathBuf; use std::sync::mpsc::Sender; @@ -97,8 +97,6 @@ pub struct Review { pub selected_exit_mode_id: Option, /// User configuration (tags, exit modes). pub config: UserConfig, - /// Bookmark IDs created during this session (context omitted in output). - pub session_created_bookmarks: HashSet, //--- Result delivery --- /// Channel to send result when review ends. `None` for CLI mode. @@ -245,7 +243,6 @@ impl Review { session_comment: None, selected_exit_mode_id: None, config, - session_created_bookmarks: HashSet::new(), result_channel, saved_to: None, } @@ -483,7 +480,6 @@ impl Review { session_comment: self.session_comment.clone(), metadata: content.metadata.clone(), allows_image_paste: content.source.allows_image_paste(), - bookmarks: self.config.bookmarks().to_vec(), }) } WindowView::Mermaid { .. } => None, // Mermaid windows don't use ContentResponse diff --git a/src-tauri/src/state.rs b/src-tauri/src/state.rs index f982aed..b77f773 100644 --- a/src-tauri/src/state.rs +++ b/src-tauri/src/state.rs @@ -198,20 +198,10 @@ pub enum ContentNode { Error { source: String, message: String }, /// Pasted text content collapsed into a chip (large paste). Paste { content: String }, - /// Reference to a bookmark (captured moment of attention). - /// Embeds full bookmark data at insertion time for "detachment" — - /// if the bookmark is later deleted, the reference still renders fully. - BookmarkRef { - id: String, - label: String, - /// Full bookmark data captured at insertion time. - /// Used for output if the bookmark no longer exists (detached). - bookmark: Bookmark, - }, - /// Unified reference (annotation or bookmark). - /// New format that supports referencing other annotations within the session. + /// Unified reference (annotation or heading). + /// Supports referencing other annotations within the session. Ref { - /// Discriminator: "annotation" or "bookmark" + /// Discriminator: "annotation" or "heading" ref_type: String, /// Self-contained snapshot (survives source deletion) snapshot: RefSnapshot, @@ -224,7 +214,7 @@ pub enum ContentNode { } // ════════════════════════════════════════════════════════════════════════════ -// UNIFIED REFERENCE SYSTEM — @ mentions for annotations and bookmarks +// UNIFIED REFERENCE SYSTEM — @ mentions for annotations and headings // ════════════════════════════════════════════════════════════════════════════ /// Snapshot for annotation references (self-contained). @@ -251,12 +241,11 @@ pub struct HeadingRefSnapshot { pub title: String, } -/// Unified reference snapshot — annotation, bookmark, or heading. +/// Unified reference snapshot — annotation or heading. #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] #[serde(tag = "type", rename_all = "lowercase")] pub enum RefSnapshot { Annotation(AnnotationRefSnapshot), - Bookmark { bookmark: Bookmark }, Heading(HeadingRefSnapshot), } @@ -368,152 +357,6 @@ impl ExitMode { } } -// ════════════════════════════════════════════════════════════════════════════ -// BOOKMARKS — capture moments of attention for later reference -// ════════════════════════════════════════════════════════════════════════════ - -/// A bookmark capturing a moment of attention during an annot session. -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] -pub struct Bookmark { - /// Unique 12-character base32 ID (prefix-matchable). - pub id: String, - /// User-provided or auto-derived label. - pub label: Option, - /// When this bookmark was created. - pub created_at: DateTime, - /// Project context (cwd at creation time). - pub project_path: Option, - /// The captured content snapshot. - pub snapshot: BookmarkSnapshot, -} - -impl HasId for Bookmark { - fn id(&self) -> &str { - &self.id - } -} - -/// The content snapshot captured by a bookmark. -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] -#[serde(tag = "type", rename_all = "snake_case")] -pub enum BookmarkSnapshot { - /// Entire session content. - Session { - source_type: SessionType, - source_title: String, - /// Full document snapshot. - context: String, - }, - /// Inline selection within session. - Selection { - source_type: SessionType, - source_title: String, - /// Full document snapshot. - context: String, - /// The text the user selected. - selected_text: String, - }, -} - -/// Type of session where the bookmark was created. -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] -#[serde(rename_all = "snake_case")] -pub enum SessionType { - File, - Diff, - Content, -} - -impl BookmarkSnapshot { - /// Get the source title for this snapshot. - pub fn source_title(&self) -> &str { - match self { - BookmarkSnapshot::Session { source_title, .. } - | BookmarkSnapshot::Selection { source_title, .. } => source_title, - } - } - - /// Get a preview of the content (first N lines). - pub fn preview(&self, max_lines: usize) -> String { - self.content() - .lines() - .take(max_lines) - .collect::>() - .join("\n") - } - - /// Get the full content of this snapshot. - pub fn content(&self) -> &str { - match self { - BookmarkSnapshot::Session { context, .. } - | BookmarkSnapshot::Selection { context, .. } => context, - } - } -} - -impl Bookmark { - /// Creates a new bookmark. - /// Label is user-provided only; display_label() derives from content for output. - pub fn new( - label: Option, - project_path: Option, - snapshot: BookmarkSnapshot, - ) -> Self { - Self { - id: crate::id::generate(), - label, // User-provided only, no auto-derivation - created_at: Utc::now(), - project_path, - snapshot, - } - } - - /// Get display label for output: user label if set, otherwise derived from content. - /// - Selection bookmarks: first ~50 chars of selected_text - /// - Session bookmarks: first heading (for .md) or source_title - pub fn display_label(&self) -> String { - if let Some(ref label) = self.label { - return label.clone(); - } - match &self.snapshot { - BookmarkSnapshot::Selection { selected_text, .. } => { - Self::truncate(selected_text.lines().next().unwrap_or(selected_text), 50) - } - BookmarkSnapshot::Session { - source_title, - context, - .. - } => { - // For markdown: extract first # heading - if source_title.ends_with(".md") { - if let Some(heading) = Self::extract_first_heading(context) { - return Self::truncate(&heading, 50); - } - } - source_title.clone() - } - } - } - - /// Extract the first markdown heading from content. - fn extract_first_heading(content: &str) -> Option { - content - .lines() - .find(|line| line.starts_with('#')) - .map(|line| line.trim_start_matches('#').trim().to_string()) - } - - /// Truncate a string to max_len, adding ellipsis if needed. - fn truncate(s: &str, max_len: usize) -> String { - if s.len() <= max_len { - s.to_string() - } else { - let truncated: String = s.chars().take(max_len - 1).collect(); - format!("{}…", truncated) - } - } -} - // ════════════════════════════════════════════════════════════════════════════ // CONTENT MODEL — immutable after construction // ════════════════════════════════════════════════════════════════════════════ @@ -567,15 +410,13 @@ pub struct SessionState { // USER CONFIG — encapsulates deletion tracking // ════════════════════════════════════════════════════════════════════════════ -/// User configuration for tags, exit modes, and bookmarks. +/// User configuration for tags and exit modes. /// Encapsulates deletion tracking for safe concurrent writes. pub struct UserConfig { tags: Vec, exit_modes: Vec, - bookmarks: Vec, deleted_tags: HashSet, deleted_exit_modes: HashSet, - deleted_bookmarks: HashSet, /// Tag usage statistics (global + per-language). usage_stats: TagUsageStats, } @@ -594,10 +435,8 @@ impl UserConfig { Self { tags: config::load_tags(), exit_modes, - bookmarks: config::load_bookmarks(), deleted_tags: HashSet::new(), deleted_exit_modes: HashSet::new(), - deleted_bookmarks: HashSet::new(), usage_stats: config::load_tag_usage(), } } @@ -614,14 +453,11 @@ impl UserConfig { let mut disk_exit_modes = config::load_exit_modes(); let disk_commands = config::discover_commands(); disk_exit_modes.extend(disk_commands); - let disk_bookmarks = config::load_bookmarks(); // Merge: add new items from disk that aren't already in memory and weren't deleted self.tags = Self::merge_for_reload(&self.tags, disk_tags, &self.deleted_tags); self.exit_modes = Self::merge_for_reload(&self.exit_modes, disk_exit_modes, &self.deleted_exit_modes); - self.bookmarks = - Self::merge_for_reload(&self.bookmarks, disk_bookmarks, &self.deleted_bookmarks); } /// Merge memory and disk collections: memory wins, new disk items added, deleted items excluded. @@ -651,10 +487,8 @@ impl UserConfig { Self { tags: Vec::new(), exit_modes: Vec::new(), - bookmarks: Vec::new(), deleted_tags: HashSet::new(), deleted_exit_modes: HashSet::new(), - deleted_bookmarks: HashSet::new(), usage_stats: TagUsageStats::default(), } } @@ -757,61 +591,14 @@ impl UserConfig { self.exit_modes.extend(modes); } - /// Get all bookmarks. - pub fn bookmarks(&self) -> &[Bookmark] { - &self.bookmarks - } - - /// Get a bookmark by ID or prefix. - /// - /// Returns `Some(bookmark)` if exactly one bookmark matches the prefix, - /// `None` if no match or ambiguous (multiple matches). - pub fn get_bookmark(&self, id_prefix: &str) -> Option<&Bookmark> { - let matches: Vec<_> = self - .bookmarks - .iter() - .filter(|b| b.id.starts_with(id_prefix)) - .collect(); - - match matches.len() { - 1 => Some(matches[0]), - _ => None, // Ambiguous or not found - } - } - - /// Insert or update a bookmark, then save to disk. - pub fn upsert_bookmark(&mut self, bookmark: Bookmark) { - if let Some(existing) = self.bookmarks.iter_mut().find(|b| b.id == bookmark.id) { - *existing = bookmark; - } else { - self.bookmarks.push(bookmark); - } - let _ = config::save_bookmarks(&self.bookmarks, &self.deleted_bookmarks); - } - - /// Delete a bookmark by ID, then save to disk. - pub fn delete_bookmark(&mut self, id: &str) -> bool { - let len_before = self.bookmarks.len(); - self.bookmarks.retain(|b| b.id != id); - if self.bookmarks.len() < len_before { - self.deleted_bookmarks.insert(id.to_string()); - let _ = config::save_bookmarks(&self.bookmarks, &self.deleted_bookmarks); - true - } else { - false - } - } - /// Create config with specific tags and exit modes (for testing). #[cfg(test)] pub fn with_data(tags: Vec, exit_modes: Vec) -> Self { Self { tags, exit_modes, - bookmarks: Vec::new(), deleted_tags: HashSet::new(), deleted_exit_modes: HashSet::new(), - deleted_bookmarks: HashSet::new(), usage_stats: TagUsageStats::default(), } } @@ -851,8 +638,6 @@ pub struct ContentResponse { pub metadata: ContentMetadata, /// Whether image paste is allowed (MCP mode only). pub allows_image_paste: bool, - /// All bookmarks for @ autocomplete. - pub bookmarks: Vec, } // Render functions moved to crate::markdown (render_line, render_inline) @@ -1258,7 +1043,6 @@ impl AppState { session_comment: self.session.comment.clone(), metadata: self.content.metadata.clone(), allows_image_paste: self.content.source.allows_image_paste(), - bookmarks: self.config.bookmarks().to_vec(), } } diff --git a/src/lib/AnnotationEditor.svelte b/src/lib/AnnotationEditor.svelte index 8f67149..7988051 100644 --- a/src/lib/AnnotationEditor.svelte +++ b/src/lib/AnnotationEditor.svelte @@ -8,9 +8,8 @@ import { useAnnotationEditor, type AnnotationEntry } from './composables'; import { trimContent, isContentEmpty } from './tiptap'; import type { RefSuggestionItem } from './tiptap/extensions'; - import type { Tag, Bookmark } from './types'; + import type { Tag } from './types'; import Icon from './CommandPalette/Icon.svelte'; - import { BookmarkIcon } from './icons'; import { getAnnotContext } from './context/annot-context.svelte'; interface NodeRef { @@ -106,7 +105,6 @@ onUnseal?: () => void; onDismiss?: () => void; tags?: Tag[]; - bookmarks?: Bookmark[]; annotationEntries?: Record; allowsImagePaste?: boolean; onImagePasteBlocked?: () => void; @@ -116,7 +114,7 @@ getOriginalLines?: () => string; // Returns original lines content for /replace } - let { content, onUpdate, sealed = false, onUnseal, onDismiss, tags = [], bookmarks = [], annotationEntries = {}, allowsImagePaste = false, onImagePasteBlocked, onRequestCreateTag, pendingTagInsertion, rangeKey = '', getOriginalLines }: Props = $props(); + let { content, onUpdate, sealed = false, onUnseal, onDismiss, tags = [], annotationEntries = {}, allowsImagePaste = false, onImagePasteBlocked, onRequestCreateTag, pendingTagInsertion, rangeKey = '', getOriginalLines }: Props = $props(); // Get zoom level from context for floating elements const ctx = getAnnotContext(); @@ -135,7 +133,6 @@ getContent: () => content, getSealed: () => sealed, getTags: () => tags, - getBookmarks: () => bookmarks, getAnnotationEntries: () => annotationEntries, getCurrentRangeKey: () => rangeKey, getAllowsImagePaste: () => allowsImagePaste, @@ -591,22 +588,6 @@ L{item.key} {item.preview || '(empty)'} - {:else if item.type === 'bookmark'} - {@const displayLabel = item.bookmark.label ?? (item.bookmark.snapshot.type === 'selection' ? item.bookmark.snapshot.selected_text : item.bookmark.snapshot.source_title)} - {@const dateStr = new Date(item.bookmark.created_at).toLocaleDateString('en-US', { month: 'short', day: 'numeric' })} - {:else if item.type === 'heading'} {@const headingIcon = item.section.level === 1 ? 'heading-h1' : item.section.level === 2 ? 'heading-h2' : 'heading-h3'} - - diff --git a/src/lib/components/Header.svelte b/src/lib/components/Header.svelte index 45461c1..4de3c8b 100644 --- a/src/lib/components/Header.svelte +++ b/src/lib/components/Header.svelte @@ -1,7 +1,6 @@
ctx.interaction.handleLineEnter(displayIndex)} onmouseleave={() => ctx.interaction.handleLineLeave()} @@ -112,22 +87,9 @@ {@render code()} {/if} - {#if trailing || showBookmarkIcon} + {#if trailing} - {#if trailing} - {@render trailing()} - {/if} - {#if showBookmarkIcon} - - {/if} + {@render trailing()} {/if}
-{#if showChoiceButtons} - -{/if} diff --git a/src/lib/components/embedded/Portal.svelte b/src/lib/components/embedded/Portal.svelte index ff61088..fb8cc00 100644 --- a/src/lib/components/embedded/Portal.svelte +++ b/src/lib/components/embedded/Portal.svelte @@ -12,13 +12,10 @@ interface Props { lines: Array<{ line: Line; displayIndex: number }>; - isLineBookmarked?: (displayIdx: number) => boolean; - isFirstLineOfBookmark?: (displayIdx: number) => boolean; - deleteBookmarkAtLine?: (displayIdx: number) => void; annotationSlot: Snippet<[displayIndex: number, rangeKey: string | null]>; } - let { lines, isLineBookmarked, isFirstLineOfBookmark, deleteBookmarkAtLine, annotationSlot }: Props = $props(); + let { lines, annotationSlot }: Props = $props(); const ctx = getAnnotContext(); @@ -72,9 +69,6 @@ deleteBookmarkAtLine(displayIndex) : undefined} additionalClasses={{ 'portal-header': portalSemantics?.kind === 'header', 'portal-content': portalSemantics?.kind === 'content', diff --git a/src/lib/components/embedded/RegularLines.svelte b/src/lib/components/embedded/RegularLines.svelte index d1d7219..0058669 100644 --- a/src/lib/components/embedded/RegularLines.svelte +++ b/src/lib/components/embedded/RegularLines.svelte @@ -22,17 +22,11 @@ interface Props { lines: DisplayLine[]; - isLineBookmarked: (displayIdx: number) => boolean; - isFirstLineOfBookmark: (displayIdx: number) => boolean; - deleteBookmarkAtLine: (displayIdx: number) => void; annotationSlotProps: Omit; } let { lines, - isLineBookmarked, - isFirstLineOfBookmark, - deleteBookmarkAtLine, annotationSlotProps, }: Props = $props(); @@ -115,9 +109,6 @@ deleteBookmarkAtLine(displayIndex)} additionalClasses={{ 'diff-added': diffKind === 'added', 'diff-deleted': diffKind === 'deleted', diff --git a/src/lib/components/embedded/Table.svelte b/src/lib/components/embedded/Table.svelte index cb03f51..af102ac 100644 --- a/src/lib/components/embedded/Table.svelte +++ b/src/lib/components/embedded/Table.svelte @@ -5,8 +5,8 @@ * * ⚠️ SYNC WARNING: This component uses / structure instead of
/, * so it cannot use LineRow.svelte. When LineRow is modified (especially for: - * selection state, bookmark support, event handlers, new CSS classes), check if - * equivalent changes are needed here. + * selection state, event handlers, new CSS classes), check if equivalent + * changes are needed here. */ import type { Snippet } from 'svelte'; import type { Line } from '$lib/types'; @@ -17,18 +17,14 @@ } from '$lib/utils/tableParser'; import { getAnnotContext } from '$lib/context'; import { highlightMatches, clearHighlights } from '$lib/search-highlight'; - import ChoiceButtons from '$lib/components/ChoiceButtons.svelte'; import Icon from '$lib/CommandPalette/Icon.svelte'; interface Props { lines: Array<{ line: Line; displayIndex: number }>; - isLineBookmarked?: (displayIdx: number) => boolean; - isFirstLineOfBookmark?: (displayIdx: number) => boolean; - deleteBookmarkAtLine?: (displayIdx: number) => void; annotationSlot: Snippet<[displayIndex: number, rangeKey: string | null]>; } - let { lines, isLineBookmarked, isFirstLineOfBookmark, deleteBookmarkAtLine, annotationSlot }: Props = $props(); + let { lines, annotationSlot }: Props = $props(); const ctx = getAnnotContext(); @@ -173,21 +169,6 @@ let firstDisplayIndex = $derived(visibleLines[0]?.displayIndex ?? -1); let lastDisplayIndex = $derived(visibleLines[visibleLines.length - 1]?.displayIndex ?? -1); - // Check if choice buttons should show for a given display index - function shouldShowChoiceButtons(displayIdx: number): boolean { - if (!ctx.interaction.pendingChoice || ctx.interaction.range === null) return false; - const rangeEnd = Math.max(ctx.interaction.range.start, ctx.interaction.range.end); - return displayIdx === rangeEnd; - } - - function handleChooseAnnotate() { - ctx.interaction.confirmChoice('annotate'); - } - - function handleChooseBookmark() { - ctx.interaction.confirmChoice('bookmark'); - } - let copied = $state(false); async function handleCopyTable() { @@ -221,7 +202,6 @@ {@const isHeader = isHeaderRow(lineIndex)} {@const isFirst = displayIndex === firstDisplayIndex} {@const isLast = displayIndex === lastDisplayIndex} - {@const bookmarked = isLineBookmarked?.(displayIndex)} {@const isPreview = ctx.interaction.isLinePreview(displayIndex)} {/if} - - {#if shouldShowChoiceButtons(displayIndex)} - - - - - - - {/if} {/each} @@ -498,35 +468,6 @@ display: flex; } - /* Bookmarked rows - border + background tint (no icon, tables are dense enough) */ - .content-row.bookmarked .gutter-cell::before { - content: ""; - position: absolute; - left: 0; - top: 0; - bottom: 0; - width: 3px; - background: var(--bookmark-color, #ef4444); - z-index: 5; - } - - .content-row.bookmarked .gutter-cell, - .content-row.bookmarked .table-cell { - background-color: color-mix(in srgb, var(--bookmark-color, #ef4444) 8%, transparent); - } - - /* Choice buttons row */ - .choice-buttons-row { - background: - var(--chip-pattern-bg), - var(--bg-code-block); - background-size: var(--chip-pattern-size), auto; - } - - .choice-buttons-cell { - padding: 4px 0 4px 8px; - } - /* Table copy button - top right, always visible */ .table-copy-btn { position: absolute; diff --git a/src/lib/composables/useAnnotationEditor.svelte.ts b/src/lib/composables/useAnnotationEditor.svelte.ts index 8d3306e..88b604e 100644 --- a/src/lib/composables/useAnnotationEditor.svelte.ts +++ b/src/lib/composables/useAnnotationEditor.svelte.ts @@ -44,7 +44,7 @@ import { ExcalidrawChip, ExcalidrawPlaceholder, } from '../tiptap/extensions'; -import type { Tag, Bookmark, RefSnapshot, AnnotationRefSnapshot, ContentNode, SectionInfo } from '../types'; +import type { Tag, RefSnapshot, AnnotationRefSnapshot, ContentNode, SectionInfo } from '../types'; import type { AnnotationEntry } from './useAnnotations.svelte'; import { fuzzySearch } from '../fuzzy'; @@ -57,8 +57,6 @@ export interface AnnotationEditorOptions { getSealed: () => boolean; /** Returns available tags for autocomplete (reactive) */ getTags: () => Tag[]; - /** Returns available bookmarks for @ autocomplete (reactive) */ - getBookmarks: () => Bookmark[]; /** Returns all annotation entries for @ autocomplete (reactive) */ getAnnotationEntries: () => Record; /** Returns the current annotation's range key (to exclude from suggestions) */ @@ -195,7 +193,7 @@ export function useAnnotationEditor(options: AnnotationEditorOptions) { const el = options.element(); if (!el) return; - const { getSealed, getTags, getBookmarks, getAnnotationEntries, getCurrentRangeKey, getOnUpdate, getOnDismiss, getSections } = options; + const { getSealed, getTags, getAnnotationEntries, getCurrentRangeKey, getOnUpdate, getOnDismiss, getSections } = options; editor = new Editor({ element: el, @@ -248,14 +246,13 @@ export function useAnnotationEditor(options: AnnotationEditorOptions) { }, }, }), - // Unified RefChip with @ trigger for annotations, bookmarks, and files + // Unified RefChip with @ trigger for annotations, headings, and files RefChip.configure({ suggestion: { char: '@', items: async ({ query }: { query: string }): Promise => { const currentKey = getCurrentRangeKey(); const annotations = getAnnotationEntries(); - const bookmarks = getBookmarks(); const sections = getSections?.() ?? null; // Build annotation items (exclude current annotation) @@ -272,12 +269,6 @@ export function useAnnotationEditor(options: AnnotationEditorOptions) { }; }); - // Build bookmark items - const bookmarkItems: RefSuggestionItem[] = bookmarks.map((b) => ({ - type: 'bookmark' as const, - bookmark: b, - })); - // Build heading items (only in markdown mode) const headingItems: RefSuggestionItem[] = sections ? sections.map((s) => ({ @@ -306,12 +297,6 @@ export function useAnnotationEditor(options: AnnotationEditorOptions) { item, searchText: item.type === 'annotation' ? `${item.key} ${item.preview}` : '', })), - ...bookmarkItems.map((item) => ({ - item, - searchText: item.type === 'bookmark' - ? `${item.bookmark.id} ${item.bookmark.label || item.bookmark.snapshot.source_title || ''}` - : '', - })), ...headingItems.map((item) => ({ item, searchText: item.type === 'heading' ? item.section.title : '', @@ -326,10 +311,9 @@ export function useAnnotationEditor(options: AnnotationEditorOptions) { const filtered = fuzzySearch(allItems, query, [{ name: 'searchText', weight: 1 }]); const items = filtered.map((f) => f.item); - // Re-sort by priority: current doc (headings, annotations) → bookmarks → files + // Re-sort by priority: current doc (headings, annotations) → files // This ensures contextually relevant items appear first const currentDocItems = items.filter((i) => i.type === 'heading' || i.type === 'annotation'); - const bookmarkResults = items.filter((i) => i.type === 'bookmark'); let fileResults = items.filter((i) => i.type === 'file'); // Soft limit files to 5 for short queries (< 4 chars) to reduce noise @@ -339,7 +323,7 @@ export function useAnnotationEditor(options: AnnotationEditorOptions) { fileResults = fileResults.slice(0, FILE_SOFT_LIMIT); } - return [...currentDocItems, ...bookmarkResults, ...fileResults]; + return [...currentDocItems, ...fileResults]; }, render: createSuggestionRender(...suggestionBridge('ref')), command: ({ editor, range, props }: { editor: Editor; range: Range; props: RefSuggestionItem }) => { @@ -380,25 +364,15 @@ export function useAnnotationEditor(options: AnnotationEditorOptions) { return; } - let snapshot: RefSnapshot; - let refType: 'annotation' | 'bookmark'; - - if (props.type === 'annotation') { - refType = 'annotation'; - snapshot = { - type: 'annotation', - source_key: props.key, - source_file: null, // Same file - preview: props.preview, - content: props.content, - } as AnnotationRefSnapshot; - } else { - refType = 'bookmark'; - snapshot = { - type: 'bookmark', - bookmark: props.bookmark, - }; - } + if (props.type !== 'annotation') return; + + const snapshot: RefSnapshot = { + type: 'annotation', + source_key: props.key, + source_file: null, // Same file + preview: props.preview, + content: props.content, + } as AnnotationRefSnapshot; editor .chain() @@ -406,7 +380,7 @@ export function useAnnotationEditor(options: AnnotationEditorOptions) { .insertContentAt(range, [ { type: 'refChip', - attrs: { refType, snapshot }, + attrs: { refType: 'annotation', snapshot }, }, { type: 'text', text: ' ' }, ]) diff --git a/src/lib/composables/useAnnotationEditor.test.ts b/src/lib/composables/useAnnotationEditor.test.ts index 9be4ef3..2347da9 100644 --- a/src/lib/composables/useAnnotationEditor.test.ts +++ b/src/lib/composables/useAnnotationEditor.test.ts @@ -78,7 +78,6 @@ describe('useAnnotationEditor', () => { getContent: () => undefined, getSealed: () => false, getTags: () => [], - getBookmarks: () => [], getAnnotationEntries: () => ({}), getCurrentRangeKey: () => '', getAllowsImagePaste: () => true, diff --git a/src/lib/composables/useBookmarks.svelte.ts b/src/lib/composables/useBookmarks.svelte.ts deleted file mode 100644 index 64344e9..0000000 --- a/src/lib/composables/useBookmarks.svelte.ts +++ /dev/null @@ -1,190 +0,0 @@ -import { invoke } from '@tauri-apps/api/core'; -import type { Bookmark } from '$lib/types'; - -export interface LineRange { - start: number; - end: number; - id: string; -} - -export function useBookmarks(initialBookmarks: Bookmark[] = []) { - let bookmarks = $state(initialBookmarks); - - // Session bookmark tracking - let sessionBookmarkId = $state(null); - let lastCreatedId = $state(null); - - // Line ranges for selection bookmarks (tracked client-side since backend doesn't store them) - // This is ephemeral — only populated for bookmarks created in this session - let lineRangesMap = $state>(new Map()); - - // Derived: lookup map - const byId = $derived(new Map(bookmarks.map((b) => [b.id, b]))); - - // Derived: selection bookmark line ranges as array - const lineRanges = $derived.by(() => { - const ranges: LineRange[] = []; - for (const [id, range] of lineRangesMap) { - ranges.push({ ...range, id }); - } - return ranges; - }); - - // Derived: is session bookmarked - const isSessionBookmarked = $derived(sessionBookmarkId !== null); - - // CRUD - async function createSession(label?: string): Promise { - const bookmark = await invoke('create_bookmark', { - label: label ?? null, - }); - bookmarks = [...bookmarks, bookmark]; - sessionBookmarkId = bookmark.id; - lastCreatedId = bookmark.id; - return bookmark; - } - - async function createSelection( - start: number, - end: number, - label?: string - ): Promise { - const normalizedStart = Math.min(start, end); - const normalizedEnd = Math.max(start, end); - - const bookmark = await invoke('create_selection_bookmark', { - startLine: normalizedStart, - endLine: normalizedEnd, - label: label ?? null, - }); - bookmarks = [...bookmarks, bookmark]; - lastCreatedId = bookmark.id; - - // Track line range for visual indicator - lineRangesMap = new Map(lineRangesMap).set(bookmark.id, { - start: normalizedStart, - end: normalizedEnd, - }); - - return bookmark; - } - - async function update(id: string, label: string): Promise { - await invoke('update_bookmark', { id, label }); - bookmarks = bookmarks.map((b) => (b.id === id ? { ...b, label } : b)); - } - - async function deleteBookmark(id: string): Promise { - await invoke('delete_bookmark', { id }); - bookmarks = bookmarks.filter((b) => b.id !== id); - if (sessionBookmarkId === id) { - sessionBookmarkId = null; - } - if (lastCreatedId === id) { - lastCreatedId = null; - } - // Remove from line ranges if it was a selection bookmark - if (lineRangesMap.has(id)) { - const newMap = new Map(lineRangesMap); - newMap.delete(id); - lineRangesMap = newMap; - } - } - - // Toggle session bookmark - async function toggleSession(): Promise { - if (sessionBookmarkId) { - await deleteBookmark(sessionBookmarkId); - } else { - await createSession(); - } - } - - // Toggle selection bookmark (delete if same range exists) - async function toggleSelection(start: number, end: number): Promise { - const normalizedStart = Math.min(start, end); - const normalizedEnd = Math.max(start, end); - - const existing = lineRanges.find( - (r) => r.start === normalizedStart && r.end === normalizedEnd - ); - - if (existing) { - await deleteBookmark(existing.id); - } else { - await createSelection(normalizedStart, normalizedEnd); - } - } - - // Query - function findByLineRange(start: number, end: number): Bookmark | undefined { - const normalizedStart = Math.min(start, end); - const normalizedEnd = Math.max(start, end); - - const range = lineRanges.find( - (r) => r.start === normalizedStart && r.end === normalizedEnd - ); - return range ? byId.get(range.id) : undefined; - } - - function isLineInBookmarkedRange(displayIdx: number): boolean { - return lineRanges.some( - (range) => displayIdx >= range.start && displayIdx <= range.end - ); - } - - function isFirstLineOfBookmark(displayIdx: number): boolean { - return lineRanges.some((range) => displayIdx === range.start); - } - - function getBookmarkIdAtStart(displayIdx: number): string | undefined { - return lineRanges.find((range) => displayIdx === range.start)?.id; - } - - function clearLastCreated(): void { - lastCreatedId = null; - } - - /** Merge bookmarks from disk reload (adds new items, preserves local state). */ - function reloadFromSnapshot(diskBookmarks: Bookmark[]): void { - const existingIds = new Set(bookmarks.map((b) => b.id)); - const newBookmarks = diskBookmarks.filter((b) => !existingIds.has(b.id)); - if (newBookmarks.length > 0) { - bookmarks = [...bookmarks, ...newBookmarks]; - } - } - - return { - get all() { - return bookmarks; - }, - get byId() { - return byId; - }, - get lineRanges() { - return lineRanges; - }, - get isSessionBookmarked() { - return isSessionBookmarked; - }, - get sessionBookmarkId() { - return sessionBookmarkId; - }, - get lastCreatedId() { - return lastCreatedId; - }, - - createSession, - createSelection, - update, - delete: deleteBookmark, - toggleSession, - toggleSelection, - findByLineRange, - isLineInBookmarkedRange, - isFirstLineOfBookmark, - getBookmarkIdAtStart, - clearLastCreated, - reloadFromSnapshot, - }; -} diff --git a/src/lib/composables/useInteraction.svelte.ts b/src/lib/composables/useInteraction.svelte.ts index ae74dcb..696fb74 100644 --- a/src/lib/composables/useInteraction.svelte.ts +++ b/src/lib/composables/useInteraction.svelte.ts @@ -23,7 +23,7 @@ export type ModalLock = export type UiState = | { phase: 'idle' } | { phase: 'selecting'; anchor: number; current: number } - | { phase: 'committed'; range: Range; pendingChoice: boolean } + | { phase: 'committed'; range: Range } | { phase: 'editing'; editor: EditorKind }; /** Derived type for phase names (for backwards compatibility) */ @@ -32,12 +32,10 @@ export type Phase = UiState['phase']; export type UiAction = | { type: 'START_SELECT'; anchor: number } | { type: 'EXTEND_SELECT'; to: number } - | { type: 'COMMIT_SELECT'; pendingChoice: boolean } + | { type: 'COMMIT_SELECT' } | { type: 'OPEN_EDITOR'; editor: EditorKind } | { type: 'CLOSE_EDITOR' } | { type: 'SET_SELECTION'; range: Range } - | { type: 'CONFIRM_CHOICE'; action: 'annotate' | 'bookmark' } - | { type: 'CANCEL_CHOICE' } | { type: 'RESET' }; /** Actions that are blocked when a modal lock is active */ @@ -60,7 +58,7 @@ export function uiReducer(state: UiState, action: UiAction): UiState { case 'COMMIT_SELECT': if (state.phase !== 'selecting') return state; const range = normalizeRange(state.anchor, state.current); - return { phase: 'committed', range, pendingChoice: action.pendingChoice }; + return { phase: 'committed', range }; case 'OPEN_EDITOR': // Can open from committed, idle, or editing (to switch editors) @@ -74,23 +72,7 @@ export function uiReducer(state: UiState, action: UiAction): UiState { return { phase: 'idle' }; case 'SET_SELECTION': - return { phase: 'committed', range: action.range, pendingChoice: false }; - - case 'CONFIRM_CHOICE': - if (state.phase !== 'committed' || !state.pendingChoice) return state; - if (action.action === 'annotate') { - // Transition to editing phase with the annotation editor - const rangeKey = `${state.range.start}-${state.range.end}`; - return { phase: 'editing', editor: { kind: 'annotation', rangeKey } }; - } - // bookmark is handled externally (callback), then reset - return { phase: 'idle' }; - - case 'CANCEL_CHOICE': - if (state.phase === 'committed' && state.pendingChoice) { - return { phase: 'idle' }; - } - return state; + return { phase: 'committed', range: action.range }; case 'RESET': return { phase: 'idle' }; @@ -112,8 +94,6 @@ export interface UseInteractionOptions { isLineSelectable: (displayIdx: number) => boolean; /** Constrain selection to bounds (e.g., hunk bounds in diff mode) */ constrainToBounds: (displayIdx: number, anchorIdx: number) => number; - /** Called when 'b' was held during drag — create bookmark immediately */ - onImmediateBookmark?: (context: { start: number; end: number }) => void; } export function useInteraction(options: UseInteractionOptions) { @@ -125,13 +105,10 @@ export function useInteraction(options: UseInteractionOptions) { // Shift key tracking (for cursor styling) - separate from phase state let isShiftHeld = $state(false); - // Drag modifier tracking (c or b key during drag) - ephemeral, not part of UiState - let dragModifier = $state<'c' | 'b' | null>(null); - // Hovered line — deliberately NOT part of the reducer state. Hover changes on // every mouse-move; if 10k LineRows derived from it they'd all re-run each move. // The hover *visual* is pure CSS (:hover); this value only feeds keyboard actions - // that need "the line under the cursor" (bookmark/annotate without a selection). + // that need "the line under the cursor" (annotate without a selection). let hoverLine = $state(null); // Dispatch action through reducer, respecting modal lock @@ -172,10 +149,6 @@ export function useInteraction(options: UseInteractionOptions) { return hoverLine; } - function getPendingChoice(): boolean { - return state.phase === 'committed' && state.pendingChoice; - } - /** * Check if a line should show selection highlight. */ @@ -210,7 +183,6 @@ export function useInteraction(options: UseInteractionOptions) { clearNativeSelection(); (e.currentTarget as HTMLElement).setPointerCapture(e.pointerId); - dragModifier = null; dispatch({ type: 'START_SELECT', anchor: displayIdx }); } @@ -245,18 +217,11 @@ export function useInteraction(options: UseInteractionOptions) { if (state.phase !== 'selecting') return; const range = normalizeRange(state.anchor, state.current); - const rangeContext = { start: range.start, end: range.end }; + const rangeKey = `${range.start}-${range.end}`; - if (dragModifier === 'b') { - options.onImmediateBookmark?.(rangeContext); - dispatch({ type: 'RESET' }); - } else if (dragModifier === 'c') { - dispatch({ type: 'COMMIT_SELECT', pendingChoice: false }); - } else { - dispatch({ type: 'COMMIT_SELECT', pendingChoice: true }); - } - - dragModifier = null; + // Releasing a selection opens the annotation editor directly. + dispatch({ type: 'COMMIT_SELECT' }); + dispatch({ type: 'OPEN_EDITOR', editor: { kind: 'annotation', rangeKey } }); } function handleContentPointerDown(e: PointerEvent) { @@ -271,7 +236,6 @@ export function useInteraction(options: UseInteractionOptions) { e.preventDefault(); clearNativeSelection(); - dragModifier = null; dispatch({ type: 'START_SELECT', anchor: displayIdx }); } @@ -352,30 +316,6 @@ export function useInteraction(options: UseInteractionOptions) { } } - // --- Drag modifier methods --- - - function setDragModifier(key: 'c' | 'b') { - if (state.phase === 'selecting') { - dragModifier = key; - } - } - - function confirmChoice(action: 'annotate' | 'bookmark') { - if (state.phase !== 'committed' || !state.pendingChoice) return; - - if (action === 'bookmark') { - const range = state.range; - options.onImmediateBookmark?.({ start: range.start, end: range.end }); - dispatch({ type: 'RESET' }); - } else { - dispatch({ type: 'CONFIRM_CHOICE', action }); - } - } - - function cancelChoice() { - dispatch({ type: 'CANCEL_CHOICE' }); - } - // --- Shift key handlers --- function handleShiftKeyDown() { @@ -386,17 +326,6 @@ export function useInteraction(options: UseInteractionOptions) { isShiftHeld = false; } - /** Get context for bookmark creation: committed selection or hovered line. */ - function getBookmarkContext(): { start: number; end: number } | null { - if (state.phase === 'committed') { - return { start: state.range.start, end: state.range.end }; - } - if (state.phase === 'idle' && hoverLine !== null) { - return { start: hoverLine, end: hoverLine }; - } - return null; - } - return { // State getters get phase() { return state.phase; }, @@ -404,7 +333,6 @@ export function useInteraction(options: UseInteractionOptions) { get range() { return getRange(); }, get hoverLine() { return getHoverLine(); }, get isShiftHeld() { return isShiftHeld; }, - get pendingChoice() { return getPendingChoice(); }, get modalLock() { return modalLock; }, // Query functions @@ -440,14 +368,6 @@ export function useInteraction(options: UseInteractionOptions) { // Keyboard handleShiftKeyDown, handleShiftKeyUp, - - // Bookmark context - getBookmarkContext, - - // Drag modifier / choice methods - setDragModifier, - confirmChoice, - cancelChoice, }; } diff --git a/src/lib/composables/useKeyboard.svelte.ts b/src/lib/composables/useKeyboard.svelte.ts index 8bd3813..0267a9c 100644 --- a/src/lib/composables/useKeyboard.svelte.ts +++ b/src/lib/composables/useKeyboard.svelte.ts @@ -1,8 +1,5 @@ declare const __IS_MACOS__: boolean; -/** Context for creating a selection bookmark (start === end for single line). */ -export type BookmarkContext = { start: number; end: number }; - export interface KeyboardHandlers { onShiftDown?: () => void; onShiftUp?: () => void; @@ -14,20 +11,11 @@ export interface KeyboardHandlers { onCloseWindow?: () => void; onOpenSearch?: () => void; onOpenHelp?: () => void; - onCreateSessionBookmark?: () => void; - onCreateSelectionBookmark?: (context: BookmarkContext) => void; - onEditLastBookmark?: () => void; onZoomIn?: () => void; onZoomOut?: () => void; onZoomReset?: () => void; onCommentHoveredLine?: () => void; onSelectAllContent?: () => void; - /** Called when 'c' or 'b' is pressed during 'selecting' phase (drag in progress) */ - onDragModifierPress?: (key: 'c' | 'b') => void; - /** Called when 'c' or 'b' is pressed to confirm pending choice (after shift-drag-release) */ - onConfirmChoice?: (action: 'annotate' | 'bookmark') => void; - /** Called when Escape is pressed to cancel pending choice */ - onCancelChoice?: () => void; } export interface KeyboardState { @@ -47,16 +35,6 @@ export interface KeyboardState { hasExitModes: () => boolean; /** Whether the hovered line is selectable */ isHoveredLineSelectable: () => boolean; - /** Whether there's a last created bookmark that can be edited */ - hasLastCreatedBookmark: () => boolean; - /** Get bookmark context (hover or selection), null if neither */ - getBookmarkContext: () => BookmarkContext | null; - /** Get current interaction phase */ - getPhase: () => string; - /** Whether shift key is currently held */ - isShiftHeld: () => boolean; - /** Whether choice buttons are pending (after shift-drag-release) */ - isPendingChoice: () => boolean; } export function useKeyboard(handlers: KeyboardHandlers, state: KeyboardState) { @@ -77,15 +55,6 @@ export function useKeyboard(handlers: KeyboardHandlers, state: KeyboardState) { // Block all shortcuts when help overlay is open (it handles its own Escape) if (state.isHelpOverlayOpen()) return; - // Escape to cancel pending choice - if (e.key === 'Escape') { - if (state.isPendingChoice()) { - e.preventDefault(); - handlers.onCancelChoice?.(); - return; - } - } - if (e.key === 'Tab') { e.preventDefault(); if (state.isEditorActive() || state.isCommandPaletteOpen()) return; @@ -98,24 +67,10 @@ export function useKeyboard(handlers: KeyboardHandlers, state: KeyboardState) { return; } - // 'c' key handling + // 'c' to comment the hovered line if (e.key === 'c' && !e.metaKey && !e.ctrlKey) { if (isInEditorOrInput()) return; - // During drag (selecting phase): set as drag modifier - if (state.getPhase() === 'selecting') { - e.preventDefault(); - handlers.onDragModifierPress?.('c'); - return; - } - - // Pending choice: confirm annotate (check BEFORE isEditorActive since range is still set) - if (state.isPendingChoice()) { - e.preventDefault(); - handlers.onConfirmChoice?.('annotate'); - return; - } - // Block if editor is active if (state.isEditorActive()) return; @@ -168,57 +123,6 @@ export function useKeyboard(handlers: KeyboardHandlers, state: KeyboardState) { return; } - // Shift+B for session bookmark (full document) - if (e.key === 'B' && !e.metaKey && !e.ctrlKey && !state.isEditorActive()) { - if (isInEditorOrInput()) return; - e.preventDefault(); - handlers.onCreateSessionBookmark?.(); - return; - } - - // 'b' key handling - if (e.key === 'b' && !e.metaKey && !e.ctrlKey) { - if (isInEditorOrInput()) return; - - // During drag (selecting phase): set as drag modifier - if (state.getPhase() === 'selecting') { - e.preventDefault(); - handlers.onDragModifierPress?.('b'); - return; - } - - // Pending choice: confirm bookmark (check BEFORE isEditorActive since range is still set) - if (state.isPendingChoice()) { - e.preventDefault(); - handlers.onConfirmChoice?.('bookmark'); - return; - } - - // Block if editor is active - if (state.isEditorActive()) return; - - // Guard: Don't fire if shift is held and we're not in committed state - // (Trap #2 from D-Mail: prevents Shift+B+drag from creating bookmark before drag completes) - if (state.isShiftHeld() && state.getPhase() !== 'committed') { - return; - } - - // Default: selection bookmark for hover/selection context - const context = state.getBookmarkContext(); - if (!context) return; - e.preventDefault(); - handlers.onCreateSelectionBookmark?.(context); - return; - } - - // 'e' to edit last created bookmark - if (e.key === 'e' && !e.metaKey && !e.ctrlKey && state.hasLastCreatedBookmark() && !state.isEditorActive() && !state.isCommandPaletteOpen()) { - if (isInEditorOrInput()) return; - e.preventDefault(); - handlers.onEditLastBookmark?.(); - return; - } - // Cmd+F for search (blocked in editor or command palette) if (e.key === 'f' && (e.metaKey || e.ctrlKey) && !state.isSearchOpen() && !state.isEditorActive() && !state.isCommandPaletteOpen()) { e.preventDefault(); diff --git a/src/lib/composables/useKeyboard.test.ts b/src/lib/composables/useKeyboard.test.ts index a3bd21f..5affef6 100644 --- a/src/lib/composables/useKeyboard.test.ts +++ b/src/lib/composables/useKeyboard.test.ts @@ -16,11 +16,6 @@ describe('useKeyboard', () => { hasHoveredLine: () => false, hasExitModes: () => true, isHoveredLineSelectable: () => true, - hasLastCreatedBookmark: () => false, - getBookmarkContext: () => null, - getPhase: () => 'idle', - isShiftHeld: () => false, - isPendingChoice: () => false, }; function createKeyboardEvent(key: string, options: Partial = {}): KeyboardEvent { @@ -221,54 +216,4 @@ describe('useKeyboard', () => { expect(onCommentHoveredLine).not.toHaveBeenCalled(); }); - - it('calls onEditLastBookmark when "e" is pressed and hasLastCreatedBookmark', () => { - const onEditLastBookmark = vi.fn(); - const keyboard = useKeyboard({ onEditLastBookmark }, { - ...defaultState, - hasLastCreatedBookmark: () => true, - }); - - keyboard.handleKeyDown(createKeyboardEvent('e')); - - expect(onEditLastBookmark).toHaveBeenCalled(); - }); - - it('does not edit bookmark when hasLastCreatedBookmark is false', () => { - const onEditLastBookmark = vi.fn(); - const keyboard = useKeyboard({ onEditLastBookmark }, { - ...defaultState, - hasLastCreatedBookmark: () => false, - }); - - keyboard.handleKeyDown(createKeyboardEvent('e')); - - expect(onEditLastBookmark).not.toHaveBeenCalled(); - }); - - it('does not edit bookmark when command palette is open', () => { - const onEditLastBookmark = vi.fn(); - const keyboard = useKeyboard({ onEditLastBookmark }, { - ...defaultState, - hasLastCreatedBookmark: () => true, - isCommandPaletteOpen: () => true, - }); - - keyboard.handleKeyDown(createKeyboardEvent('e')); - - expect(onEditLastBookmark).not.toHaveBeenCalled(); - }); - - it('does not edit bookmark when editor is active', () => { - const onEditLastBookmark = vi.fn(); - const keyboard = useKeyboard({ onEditLastBookmark }, { - ...defaultState, - hasLastCreatedBookmark: () => true, - isEditorActive: () => true, - }); - - keyboard.handleKeyDown(createKeyboardEvent('e')); - - expect(onEditLastBookmark).not.toHaveBeenCalled(); - }); }); diff --git a/src/lib/context/AnnotProvider.svelte b/src/lib/context/AnnotProvider.svelte index 25d1b26..4a20d28 100644 --- a/src/lib/context/AnnotProvider.svelte +++ b/src/lib/context/AnnotProvider.svelte @@ -18,7 +18,6 @@ import type { useExitModes } from '$lib/composables/useExitModes.svelte'; import type { useSearch } from '$lib/composables/useSearch.svelte'; import type { useMermaid } from '$lib/composables/useMermaid.svelte'; - import type { useBookmarks } from '$lib/composables/useBookmarks.svelte'; interface Props { // Reactive data @@ -34,7 +33,6 @@ exitModes: ReturnType; search: ReturnType; mermaid: ReturnType; - bookmarks: ReturnType; // Utilities showToast: (message: string, duration?: number) => void; @@ -55,7 +53,6 @@ exitModes, search, mermaid, - bookmarks, showToast, isLineSelectable, getOriginalLinesForRange, @@ -90,8 +87,8 @@ * Get the range key for a line. Used by embedded components to connect * annotation slots to their content. * - * Always returns existing annotation keys. Only returns new selection key - * when not in pendingChoice mode (waiting for user to choose annotate/bookmark). + * Always returns existing annotation keys, plus the new selection key for + * the last selected line once a selection is committed. */ function getRangeKeyForLine(displayIndex: number): string | null { // Always show existing annotations @@ -100,11 +97,6 @@ return annotationAtLine.key; } - // Don't show new editor during pendingChoice (user choosing annotate vs bookmark) - if (interaction.pendingChoice) { - return null; - } - const isLast = displayIndex === lastSelectedLine && selection && !isDragging; if (isLast && selection) { return rangeToKey(selection); @@ -120,7 +112,6 @@ get exitModes() { return exitModes; }, get search() { return search; }, get mermaid() { return mermaid; }, - get bookmarks() { return bookmarks; }, get selection() { return selection; }, get isDragging() { return isDragging; }, diff --git a/src/lib/context/annot-context.svelte.ts b/src/lib/context/annot-context.svelte.ts index 20ecb80..6ab0ee7 100644 --- a/src/lib/context/annot-context.svelte.ts +++ b/src/lib/context/annot-context.svelte.ts @@ -6,7 +6,6 @@ import type { useAnnotations } from '$lib/composables/useAnnotations.svelte'; import type { useExitModes } from '$lib/composables/useExitModes.svelte'; import type { useSearch } from '$lib/composables/useSearch.svelte'; import type { useMermaid } from '$lib/composables/useMermaid.svelte'; -import type { useBookmarks } from '$lib/composables/useBookmarks.svelte'; /** * AnnotContext - Shared state and utilities for annot components. @@ -21,7 +20,6 @@ export interface AnnotContext { exitModes: ReturnType; search: ReturnType; mermaid: ReturnType; - bookmarks: ReturnType; // Derived values (computed once in provider) readonly selection: Range | null; diff --git a/src/lib/icons/BookmarkIcon.svelte b/src/lib/icons/BookmarkIcon.svelte deleted file mode 100644 index 728e936..0000000 --- a/src/lib/icons/BookmarkIcon.svelte +++ /dev/null @@ -1,17 +0,0 @@ - - -{#if filled} - - - -{:else} - - - -{/if} diff --git a/src/lib/icons/index.ts b/src/lib/icons/index.ts index 421fad6..9469ed5 100644 --- a/src/lib/icons/index.ts +++ b/src/lib/icons/index.ts @@ -1,6 +1,5 @@ export { default as SearchIcon } from './SearchIcon.svelte'; export { default as HashtagIcon } from './HashtagIcon.svelte'; -export { default as BookmarkIcon } from './BookmarkIcon.svelte'; export { default as ExitIcon } from './ExitIcon.svelte'; export { default as CopyIcon } from './CopyIcon.svelte'; export { default as SaveIcon } from './SaveIcon.svelte'; diff --git a/src/lib/tiptap/extensions/RefChip.ts b/src/lib/tiptap/extensions/RefChip.ts index ffeb8f7..b6065b8 100644 --- a/src/lib/tiptap/extensions/RefChip.ts +++ b/src/lib/tiptap/extensions/RefChip.ts @@ -3,14 +3,13 @@ import { SvelteNodeViewRenderer } from 'svelte-tiptap'; import Suggestion, { type SuggestionOptions } from '@tiptap/suggestion'; import { PluginKey } from '@tiptap/pm/state'; import RefChipView from '../nodeviews/RefChipView.svelte'; -import type { RefSnapshot, Bookmark, AnnotationRefSnapshot, SectionInfo } from '$lib/types'; +import type { RefSnapshot, AnnotationRefSnapshot, SectionInfo } from '$lib/types'; const RefSuggestionPluginKey = new PluginKey('refSuggestion'); -/** Unified suggestion item for @ menu - annotation, bookmark, file, or heading section. */ +/** Unified suggestion item for @ menu - annotation, file, or heading section. */ export type RefSuggestionItem = | { type: 'annotation'; key: string; preview: string; content: import('$lib/types').ContentNode[] } - | { type: 'bookmark'; bookmark: Bookmark } | { type: 'file'; path: string } | { type: 'heading'; section: SectionInfo }; @@ -26,8 +25,8 @@ export const RefChip = Node.create({ addAttributes() { return { - refType: { default: null }, // 'annotation' | 'bookmark' | 'file' | 'heading' - snapshot: { default: null }, // RefSnapshot (for annotation/bookmark) + refType: { default: null }, // 'annotation' | 'file' | 'heading' + snapshot: { default: null }, // RefSnapshot (for annotation) path: { default: null }, // string (for file refs) // Heading section attributes sectionLine: { default: null }, // number (source line of heading) @@ -78,11 +77,6 @@ export const RefChip = Node.create({ const annSnap = snapshot as AnnotationRefSnapshot; const preview = annSnap.preview?.slice(0, 20) || ''; displayText = `[@L${annSnap.source_key}${preview ? ' · ' + preview : ''}...]`; - } else if (refType === 'bookmark' && snapshot.type === 'bookmark') { - const shortId = snapshot.bookmark.id?.slice(0, 3) || ''; - const label = snapshot.bookmark.label || snapshot.bookmark.snapshot.source_title || ''; - const truncLabel = label.length > 20 ? label.slice(0, 20) + '...' : label; - displayText = `[@${shortId}${truncLabel ? ' · ' + truncLabel : ''}]`; } } diff --git a/src/lib/tiptap/nodeviews/BookmarkRefChip.svelte b/src/lib/tiptap/nodeviews/BookmarkRefChip.svelte deleted file mode 100644 index 0ea7b3a..0000000 --- a/src/lib/tiptap/nodeviews/BookmarkRefChip.svelte +++ /dev/null @@ -1,39 +0,0 @@ - - - - - - - - - {shortId} - {#if displayLabel} - · - {displayLabel} - {/if} - diff --git a/src/lib/tiptap/nodeviews/RefChipView.svelte b/src/lib/tiptap/nodeviews/RefChipView.svelte index 27cf3dd..ed7aab7 100644 --- a/src/lib/tiptap/nodeviews/RefChipView.svelte +++ b/src/lib/tiptap/nodeviews/RefChipView.svelte @@ -3,7 +3,6 @@ import type { NodeViewProps } from '@tiptap/core'; import type { RefSnapshot } from '$lib/types'; import AnnotationRefChip from './AnnotationRefChip.svelte'; - import BookmarkRefChip from './BookmarkRefChip.svelte'; import FileRefChip from './FileRefChip.svelte'; import HeadingRefChip from './HeadingRefChip.svelte'; @@ -13,7 +12,6 @@ | { kind: 'heading'; level: number; title: string; line: number } | { kind: 'file'; path: string } | { kind: 'annotation'; snapshot: RefSnapshot & { type: 'annotation' } } - | { kind: 'bookmark'; snapshot: RefSnapshot & { type: 'bookmark' } } | { kind: 'unknown' }; const variant = $derived.by((): RefVariant => { @@ -28,9 +26,6 @@ if (refType === 'annotation' && snapshot?.type === 'annotation') { return { kind: 'annotation', snapshot }; } - if (refType === 'bookmark' && snapshot?.type === 'bookmark') { - return { kind: 'bookmark', snapshot }; - } return { kind: 'unknown' }; }); @@ -49,7 +44,5 @@ path={variant.path} />{:else if variant.kind === 'annotation'}{:else if variant.kind === 'bookmark'}{:else}@?{/if} diff --git a/src/lib/tiptap/serialize.ts b/src/lib/tiptap/serialize.ts index 37a128a..f96000a 100644 --- a/src/lib/tiptap/serialize.ts +++ b/src/lib/tiptap/serialize.ts @@ -1,5 +1,5 @@ import type { JSONContent } from '@tiptap/core'; -import type { ContentNode, Bookmark, RefSnapshot } from '../types'; +import type { ContentNode, RefSnapshot } from '../types'; // ============================================================================ // extractContentNodes: Transform TipTap JSON to ContentNode[] @@ -69,12 +69,6 @@ const CHIP_EXTRACTORS: Record = { type: 'paste', content: attrs.content as string, }), - bookmarkChip: (attrs) => ({ - type: 'bookmarkref', - id: attrs.id as string, - label: attrs.label as string, - bookmark: attrs.bookmark as Bookmark, - }), refChip: (attrs) => { // File refs have a path attribute instead of snapshot if (attrs.refType === 'file' && attrs.path) { @@ -96,10 +90,10 @@ const CHIP_EXTRACTORS: Record = { }, }; } - // Annotation/bookmark refs have snapshot + // Annotation refs have snapshot return { type: 'ref', - ref_type: attrs.refType as 'annotation' | 'bookmark', + ref_type: attrs.refType as 'annotation', snapshot: attrs.snapshot as RefSnapshot, }; }, @@ -389,18 +383,8 @@ export function contentNodesToTipTap(nodes: ContentNode[] | null): JSONContent | lineCount, }, }); - } else if (node.type === 'bookmarkref') { - // Legacy: Insert bookmark chip inline with full embedded bookmark data - currentParagraph.push({ - type: 'bookmarkChip', - attrs: { - id: node.id, - label: node.label, - bookmark: node.bookmark, - }, - }); } else if (node.type === 'ref') { - // Unified ref chip inline - handles both annotation and bookmark refs + // Unified ref chip inline - handles annotation and heading refs currentParagraph.push({ type: 'refChip', attrs: { diff --git a/src/lib/types.ts b/src/lib/types.ts index fdfdd2c..d8c2015 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -84,15 +84,12 @@ export interface ContentResponse { metadata: ContentMetadata; /** Whether image paste is allowed (MCP content mode). */ allows_image_paste: boolean; - /** All bookmarks for @ autocomplete. */ - bookmarks: Bookmark[]; } /** Config snapshot returned by reload_config command. */ export interface ConfigSnapshot { tags: Tag[]; exit_modes: ExitMode[]; - bookmarks: Bookmark[]; } // Diff types @@ -161,7 +158,7 @@ export interface Tag { } // Content node types for structured annotation content (output format) -export type ContentNode = TextNode | TagNode | MediaNode | ExcalidrawNode | ReplaceNode | ErrorNode | PasteNode | BookmarkRefNode | RefNode | FileNode; +export type ContentNode = TextNode | TagNode | MediaNode | ExcalidrawNode | ReplaceNode | ErrorNode | PasteNode | RefNode | FileNode; export interface TextNode { type: 'text'; @@ -204,14 +201,6 @@ export interface PasteNode { content: string; // Full pasted text } -export interface BookmarkRefNode { - type: 'bookmarkref'; - id: string; // Full resolved bookmark ID - label: string; // Cached label for display - /** Full bookmark data captured at insertion time (for detachment). */ - bookmark: Bookmark; -} - // ============================================================================= // Unified Reference System (@ mentions) // ============================================================================= @@ -240,14 +229,14 @@ export interface HeadingRefSnapshot { title: string; } -/** Unified reference snapshot - annotation, bookmark, or heading. */ -export type RefSnapshot = AnnotationRefSnapshot | { type: 'bookmark'; bookmark: Bookmark } | HeadingRefSnapshot; +/** Unified reference snapshot - annotation or heading. */ +export type RefSnapshot = AnnotationRefSnapshot | HeadingRefSnapshot; -/** Unified reference node - replaces BookmarkRefNode for new references. */ +/** Unified reference node for @ mentions. */ export interface RefNode { type: 'ref'; - /** Discriminator for ref type: 'annotation', 'bookmark', or 'heading' */ - ref_type: 'annotation' | 'bookmark' | 'heading'; + /** Discriminator for ref type: 'annotation' or 'heading' */ + ref_type: 'annotation' | 'heading'; /** Self-contained snapshot (survives source deletion) */ snapshot: RefSnapshot; } @@ -267,40 +256,3 @@ export interface SaveContentResponse { saved_path: string; new_label: string; } - -// ============================================================================= -// Bookmarks — capture moments of attention for later reference -// ============================================================================= - -/** Type of session where the bookmark was created. */ -export type SessionType = 'file' | 'diff' | 'content'; - -/** The content snapshot captured by a bookmark. */ -export type BookmarkSnapshot = - | { - type: 'session'; - source_type: SessionType; - source_title: string; - context: string; - } - | { - type: 'selection'; - source_type: SessionType; - source_title: string; - context: string; - selected_text: string; - }; - -/** A bookmark capturing a moment of attention during an annot session. */ -export interface Bookmark { - /** Unique 12-character base32 ID (prefix-matchable). */ - id: string; - /** User-provided or auto-derived label. */ - label: string | null; - /** When this bookmark was created (ISO 8601). */ - created_at: string; - /** Project context (cwd at creation time). */ - project_path: string | null; - /** The captured content snapshot. */ - snapshot: BookmarkSnapshot; -} diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index 4d5a403..a0efd10 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -27,7 +27,6 @@ import { useMermaid } from "$lib/composables/useMermaid.svelte"; import { useLineSegments } from "$lib/composables/useLineSegments.svelte"; import { useSearch } from "$lib/composables/useSearch.svelte"; - import { useBookmarks } from "$lib/composables/useBookmarks.svelte"; import { useOverlay } from "$lib/composables/useOverlay.svelte"; import { useHistory, emptySessionData, type SessionData } from "$lib/composables/useHistory.svelte"; import SearchBar from "$lib/components/SearchBar.svelte"; @@ -63,12 +62,6 @@ let toastExiting = $state(false); let toastTimeout: ReturnType | null = null; - // Bookmark ID to edit when opening command palette (set by e key, cleared after use) - let editBookmarkId = $state(null); - - // Bookmarks composable (initialized in onMount after data is loaded) - let bookmarkState: ReturnType | null = $state(null); - function showToast(message: string, duration = 3000) { if (toastTimeout) clearTimeout(toastTimeout); toastMessage = message; @@ -177,13 +170,6 @@ const interaction = useInteraction({ isLineSelectable, constrainToBounds: selectionBounds.constrainToSelectionBounds, - onImmediateBookmark: async (context) => { - // Called when 'b' was held during drag — create bookmark immediately - if (!bookmarkState) return; - await bookmarkState.toggleSelection(context.start, context.end); - const shortId = bookmarkState.lastCreatedId?.slice(0, 3) ?? ''; - showToast(`Bookmarked as ${shortId} · [e] edit`); - }, }); // Annotation state (composable) @@ -377,88 +363,14 @@ showToast(`Saved to ${response.saved_path}`); } - // Bookmark toggle handler - async function handleToggleBookmark() { - if (!bookmarkState) return; - const wasBookmarked = bookmarkState.isSessionBookmarked; - await bookmarkState.toggleSession(); - if (wasBookmarked) { - showToast('Bookmark removed'); - } else { - const shortId = bookmarkState.lastCreatedId?.slice(0, 3) ?? ''; - showToast(`Bookmarked as ${shortId} · [e] edit`); - } - } - - // Create or toggle selection bookmark handler - async function handleCreateSelectionBookmark(context: { start: number; end: number }) { - if (!bookmarkState) return; - const existing = bookmarkState.findByLineRange(context.start, context.end); - await bookmarkState.toggleSelection(context.start, context.end); - if (existing) { - showToast('Bookmark removed'); - } else { - const shortId = bookmarkState.lastCreatedId?.slice(0, 3) ?? ''; - showToast(`Bookmarked as ${shortId} · [e] edit`); - } - } - - // Check if a display index is in any bookmarked range - function isLineBookmarked(displayIdx: number): boolean { - return bookmarkState?.isLineInBookmarkedRange(displayIdx) ?? false; - } - - // Check if a display index is the first line of any bookmark - function isFirstLineOfBookmark(displayIdx: number): boolean { - return bookmarkState?.isFirstLineOfBookmark(displayIdx) ?? false; - } - - // Delete bookmark by display index (for inline delete button) - function deleteBookmarkAtLine(displayIdx: number): void { - const id = bookmarkState?.getBookmarkIdAtStart(displayIdx); - if (id) { - bookmarkState?.delete(id); - showToast('Bookmark removed'); - } - } - - // Edit last created bookmark handler - function handleEditLastBookmark() { - if (bookmarkState?.lastCreatedId) { - editBookmarkId = bookmarkState.lastCreatedId; - overlay.openCommandPalette(); - } - } - // CommandPalette handlers function handleCommandPaletteClose() { overlay.close(); // Clear pending states pendingTagCreation = null; - editBookmarkId = null; commandPaletteInitialState = undefined; } - async function handleBookmarkDeleted(id: string) { - // Composable handles all state updates - await bookmarkState?.delete(id); - } - - async function handleBookmarkUpdated(id: string, label: string) { - // Capture before await (onClose may clear editBookmarkId while we await) - const wasEditTriggered = editBookmarkId === id; - - // Composable handles state update - await bookmarkState?.update(id, label); - - // Show toast if edit was triggered via 'e' key - if (wasEditTriggered) { - const shortId = id.slice(0, 3); - const displayLabel = label ? `"${label}"` : '(no label)'; - showToast(`${shortId} → ${displayLabel}`); - } - } - // Handle events from CommandPalette (e.g., theme change) function handleCommandPaletteEvent(event: string, payload: unknown) { if (event === 'SET_THEME') { @@ -663,9 +575,6 @@ onCloseWindow: () => getCurrentWindow().close(), onOpenSearch: () => search.open(), onOpenHelp: () => overlay.openHelp(), - onCreateSessionBookmark: handleToggleBookmark, - onCreateSelectionBookmark: handleCreateSelectionBookmark, - onEditLastBookmark: handleEditLastBookmark, onZoomIn: () => contentZoom = Math.min(contentZoom + 0.1, 3.0), onZoomOut: () => contentZoom = Math.max(contentZoom - 0.1, 0.5), onZoomReset: () => contentZoom = 1.0, @@ -678,9 +587,6 @@ interaction.openEditor({ kind: 'annotation', rangeKey }); } }, - onDragModifierPress: (key) => interaction.setDragModifier(key), - onConfirmChoice: (action) => interaction.confirmChoice(action), - onCancelChoice: () => interaction.cancelChoice(), }, { isEditorActive: () => interaction.phase === 'editing', @@ -691,11 +597,6 @@ hasHoveredLine: () => interaction.hoverLine !== null, hasExitModes: () => exitModeState.modes.length > 0, isHoveredLineSelectable: () => interaction.hoverLine !== null && isLineSelectable(interaction.hoverLine), - hasLastCreatedBookmark: () => !!bookmarkState?.lastCreatedId, - getBookmarkContext: () => interaction.getBookmarkContext(), - getPhase: () => interaction.phase, - isShiftHeld: () => interaction.isShiftHeld, - isPendingChoice: () => interaction.pendingChoice, } ); @@ -710,7 +611,6 @@ label = res.label; lines = res.lines; tags = res.tags; - bookmarkState = useBookmarks(res.bookmarks); exitModeState.initialize(res.exit_modes, res.selected_exit_mode_id); metadata = res.metadata; allowsImagePaste = res.allows_image_paste; @@ -793,7 +693,6 @@ const snapshot = await invoke('reload_config'); tags = snapshot.tags; exitModeState.setModes(snapshot.exit_modes); - bookmarkState?.reloadFromSnapshot(snapshot.bookmarks); } catch { // Ignore errors - reload is best-effort } @@ -808,7 +707,7 @@
{#if error}
{error}
- {:else if !bookmarkState || lines.length === 0} + {:else if lines.length === 0}
Loading...
{:else}
@@ -877,7 +774,7 @@ > {#each lineSegmentation.segments as segment} {#if segment.type === 'portal'} - + {#snippet annotationSlot(displayIndex, rangeKey)} {/snippet} @@ -896,9 +793,6 @@ lines={segment.lines} language={segment.language} color={segment.color} - {isLineBookmarked} - {isFirstLineOfBookmark} - {deleteBookmarkAtLine} onMermaidOpen={mermaidBlock && !mermaidError ? () => mermaid.openMermaidWindow(mermaidBlock) : undefined} onExcalidrawOpen={mermaidBlock ? () => openExcalidrawFromMermaid( mermaidBlock, // source block for content extraction @@ -913,7 +807,7 @@ {/snippet} {:else if segment.type === 'table'} - +
{#snippet annotationSlot(displayIndex, rangeKey)} {/snippet} @@ -926,9 +820,6 @@ {:else} {/if} @@ -948,25 +839,20 @@ -{#if overlay.isCommandPaletteOpen() && bookmarkState} +{#if overlay.isCommandPaletteOpen()} diff --git a/src/styles/components/annotation-editor.css b/src/styles/components/annotation-editor.css index 2e1b9f8..e0cc470 100644 --- a/src/styles/components/annotation-editor.css +++ b/src/styles/components/annotation-editor.css @@ -269,76 +269,6 @@ color: var(--text-muted); } -/* =========================================== - BOOKMARK AUTOCOMPLETE POPUP (@) - =========================================== */ -.bookmark-suggestions { - position: fixed; - background: var(--bg-window); - border: 1px solid var(--border-strong); - border-radius: 6px; - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); - z-index: 100; - min-width: 280px; - max-width: 400px; - max-height: 280px; - overflow-y: auto; - scroll-padding-block: 40px; - padding: 4px; - font-family: var(--font-ui); -} - -.bookmark-suggestion { - display: flex; - align-items: flex-start; - gap: 10px; - width: 100%; - padding: 8px 10px; - border: none; - border-radius: 4px; - background: none; - cursor: pointer; - text-align: left; -} - -.bookmark-suggestion:hover, -.bookmark-suggestion.selected { - background: var(--bg-panel); -} - -.bookmark-suggestion .bookmark-id { - font-family: var(--font-mono); - font-size: 11px; - font-weight: 600; - color: #8b5cf6; - flex-shrink: 0; - padding-top: 1px; -} - -.bookmark-suggestion .bookmark-info { - display: flex; - flex-direction: column; - gap: 2px; - min-width: 0; -} - -.bookmark-suggestion .bookmark-label { - font-size: 13px; - font-weight: 500; - color: var(--text-primary); - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -} - -.bookmark-suggestion .bookmark-meta { - font-size: 11px; - color: var(--text-muted); - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -} - /* =========================================== UNIFIED REFERENCE SUGGESTIONS (@ menu) =========================================== */ @@ -383,10 +313,6 @@ color: var(--text-secondary); } -.ref-suggestion svg.ref-icon-bookmark { - color: var(--danger); -} - .ref-suggestion .ref-primary { font-family: var(--font-mono); font-size: 12px; diff --git a/src/styles/components/chips.css b/src/styles/components/chips.css index 37ece18..079e4c2 100644 --- a/src/styles/components/chips.css +++ b/src/styles/components/chips.css @@ -112,52 +112,6 @@ border-color: #06b6d4; } -/* Color variant for bookmark references - subtle purple tint */ -.bookmark-chip { - background: - var(--chip-pattern-bg), - color-mix(in srgb, var(--bg-code-block) 95%, #8b5cf6 5%); - background-size: var(--chip-pattern-size), auto; - color: var(--text-secondary); - cursor: default; -} - -.bookmark-chip:hover { - border-color: #8b5cf6; -} - -.bookmark-chip .bookmark-icon { - display: inline-flex; - align-items: center; - color: #ef4444; -} - -.bookmark-chip .bookmark-icon svg { - width: 12px; - height: 12px; -} - -.bookmark-chip .bookmark-id { - font-family: var(--font-mono); - font-size: 10px; - font-weight: 600; - opacity: 0.7; - margin-left: 4px; -} - -.bookmark-chip .bookmark-divider { - margin: 0 4px; - opacity: 0.4; -} - -.bookmark-chip .bookmark-label { - font-weight: 500; - white-space: nowrap; - max-width: 120px; - overflow: hidden; - text-overflow: ellipsis; -} - /* Color variant for unified references (@ mentions) */ .ref-chip { background: @@ -184,18 +138,6 @@ border-color: #10b981; } -/* Bookmark ref variant - purple tint (matches legacy bookmark-chip) */ -.ref-chip.ref-bookmark { - background: - var(--chip-pattern-bg), - color-mix(in srgb, var(--bg-code-block) 95%, #8b5cf6 5%); - background-size: var(--chip-pattern-size), auto; -} - -.ref-chip.ref-bookmark:hover { - border-color: #8b5cf6; -} - .ref-chip .ref-icon { display: inline-flex; align-items: center; @@ -205,10 +147,6 @@ color: #10b981; } -.ref-chip .ref-icon.bookmark-icon { - color: #ef4444; -} - .ref-chip .ref-icon svg { width: 12px; height: 12px; diff --git a/src/styles/components/code-viewer.css b/src/styles/components/code-viewer.css index 9cb0b77..446e495 100644 --- a/src/styles/components/code-viewer.css +++ b/src/styles/components/code-viewer.css @@ -136,9 +136,9 @@ div.line { background-color: var(--selection-bg); } -/* Left accent bar on hover — only for plain lines; annotated/bookmarked/ - selected keep their own ::before border. */ -.content.phase-idle div.line:not(.selected):not(.annotated):not(.bookmarked):hover::before { +/* Left accent bar on hover — only for plain lines; annotated/selected + keep their own ::before border. */ +.content.phase-idle div.line:not(.selected):not(.annotated):hover::before { content: ""; position: absolute; left: 0; @@ -190,18 +190,6 @@ div.line.annotated .gutter { color: var(--text-secondary); } -/* Bookmarked lines - left border dominates annotated/selected ::before */ -div.line.bookmarked::before { - content: ""; - position: absolute; - left: 0; - top: 0; - bottom: 0; - width: 3px; - background: var(--bookmark-color, #ef4444); - z-index: 1; -} - /* Line actions container - holds trailing icons with consistent spacing */ .line-actions { display: inline-flex; @@ -232,12 +220,6 @@ div.line.bookmarked::before { outline-offset: 2px; } -/* Bookmark indicator */ -.bookmark-indicator { - color: var(--bookmark-color, #ef4444); - font-size: 16px; -} - /* Shift+drag cursor */ .content.shift-held .code { cursor: pointer; @@ -704,55 +686,3 @@ div.line.separator-line:hover { var(--hl-mid) ); } - -/* =========================================== - CHOICE BUTTONS - Annotate/Bookmark choice after shift-drag - =========================================== */ - -.choice-buttons { - display: flex; - gap: 8px; - padding: 6px 0 6px calc(var(--gutter-width) + 8px); - animation: choice-fade-in 0.15s ease; -} - -.choice-button { - display: inline-flex; - align-items: center; - gap: 4px; - padding: 4px 8px; - background: var(--bg-window); - border: 1px solid var(--border-strong); - border-radius: 4px; - font-size: 12px; - font-family: var(--font-ui); - color: var(--text-primary); - cursor: pointer; - transition: border-color 0.1s ease, background 0.1s ease; -} - -.choice-button:hover, -.choice-button:focus { - background: var(--bg-main); - border-color: var(--selection-border); -} - -.choice-button:focus { - outline: none; -} - -.choice-button.bookmark:hover, -.choice-button.bookmark:focus { - border-color: var(--danger); -} - -@keyframes choice-fade-in { - from { - opacity: 0; - transform: translateY(-4px); - } - to { - opacity: 1; - transform: translateY(0); - } -} diff --git a/src/styles/components/command-palette.css b/src/styles/components/command-palette.css index 7f275ba..c189a1d 100644 --- a/src/styles/components/command-palette.css +++ b/src/styles/components/command-palette.css @@ -323,85 +323,3 @@ .form-view::-webkit-scrollbar-thumb:hover { background: var(--border-hover); } - -/* ========================================= - BOOKMARK EDIT VIEW - Metadata and snapshot preview for bookmark editing - ========================================= */ - -/* Metadata section - source, created, project */ -.metadata-section { - display: flex; - flex-direction: column; - gap: 6px; - padding-top: 12px; - border-top: 1px solid var(--border-subtle); - margin-top: 4px; -} - -.metadata-row { - display: flex; - gap: 8px; - font-size: 12px; - line-height: 1.4; -} - -.metadata-label { - color: var(--text-muted); - min-width: 56px; - flex-shrink: 0; -} - -.metadata-value { - color: var(--text-primary); - word-break: break-word; -} - -.metadata-value.project-path { - font-family: var(--font-mono); - font-size: 11px; -} - -/* Snapshot boxes - selection and context previews */ -.snapshot-box { - border: 1px solid var(--border-subtle); - border-radius: 6px; - overflow: hidden; - margin-top: 8px; -} - -.snapshot-header { - padding: 6px 10px; - background: var(--bg-panel); - font-size: 10px; - font-weight: 600; - color: var(--text-muted); - text-transform: uppercase; - letter-spacing: 0.5px; - border-bottom: 1px solid var(--border-subtle); -} - -.snapshot-content { - padding: 10px; - font-family: var(--font-mono); - font-size: 11px; - line-height: 1.5; - overflow-x: auto; - max-height: 300px; - overflow-y: auto; - background: var(--bg-code-block); -} - -.snapshot-line { - white-space: pre; - color: var(--text-primary); -} - -.snapshot-footer { - padding: 6px 10px; - background: var(--bg-panel); - font-size: 11px; - color: var(--text-muted); - border-top: 1px solid var(--border-subtle); - text-align: center; -}