///| AI-assisted rebase helpers.

///|
pub let default_provider : String = "openrouter"

///|
pub let default_model : String = "moonshotai/kimi-k2"

///|
pub struct AiRebaseOptions {
  model : String
  max_auto_rounds : Int
  agent_max_steps : Int
  use_agent_loop : Bool
  verbose : Bool
  provider : @llmlib.BoxedProvider?
}

///|
pub struct AiConflictResolution {
  resolved : Array[String]
  failed : Array[String]
}

///|
pub fn AiRebaseOptions::default() -> AiRebaseOptions {
  {
    model: "",
    max_auto_rounds: 32,
    agent_max_steps: 16,
    use_agent_loop: false,
    verbose: false,
    provider: None,
  }
}

///|
pub fn AiRebaseOptions::new(
  model? : String = "",
  max_auto_rounds? : Int = 32,
  agent_max_steps? : Int = 16,
  use_agent_loop? : Bool = false,
  verbose? : Bool = false,
  provider~ : @llmlib.BoxedProvider?,
) -> AiRebaseOptions {
  { model, max_auto_rounds, agent_max_steps, use_agent_loop, verbose, provider }
}

///|
fn trim_text(s : String) -> String {
  s.trim(chars=" \n\r\t").to_owned()
}

///|
fn effective_model(options : AiRebaseOptions) -> String {
  let explicit = trim_text(options.model)
  if explicit.length() > 0 {
    return explicit
  }
  let env_model = trim_text(@llm_ffi.get_env("BIT_REBASE_AI_MODEL"))
  if env_model.length() > 0 {
    return env_model
  }
  default_model
}

///|
fn env_truthy(name : String) -> Bool {
  let raw = trim_text(@llm_ffi.get_env(name))
  if raw.length() == 0 {
    return false
  }
  match raw.to_string_view().to_lower().to_owned() {
    "1" | "true" | "yes" | "on" => true
    _ => false
  }
}

///|
fn use_agent_loop_mode(options : AiRebaseOptions) -> Bool {
  options.use_agent_loop || env_truthy("BIT_REBASE_AI_AGENT_LOOP")
}

///|
fn normalized_agent_step_limit(options : AiRebaseOptions) -> Int {
  if options.agent_max_steps <= 0 {
    1
  } else {
    options.agent_max_steps
  }
}

///|
fn openrouter_system_prompt(agent_loop_mode : Bool) -> String {
  if agent_loop_mode {
    let prompt =
      #|You are an expert merge conflict resolver.
      #|Use tools to resolve git conflict markers in repository files.
      #|You can only use read_file and write_file.
      #|
      #|Rules:
      #|- Keep all edits inside repository paths.
      #|- Never access or modify `.git` paths.
      #|- Remove all conflict markers.
      #|- After writing final content, stop with a short confirmation.
    return prompt
  }
  let prompt =
    #|You are an expert merge conflict resolver.
    #|You receive one conflicted file content that contains Git conflict markers.
    #|Return ONLY the final resolved file content.
    #|
    #|Rules:
    #|- Do not output Markdown fences.
    #|- Do not explain your reasoning.
    #|- Remove all conflict markers.
    #|- Keep the code/text valid and coherent.
  prompt
}

///|
fn commit_system_prompt(_agent_loop_mode : Bool) -> String {
  (
    #|You are an expert commit message writer.
    #|Write commit messages that follow Conventional Commits.
    #|Return only the commit message text, never explanations or JSON wrappers.
  )
}

///|
fn build_commit_message_prompt(diff_units : String, context : String) -> String {
  let buf = StringBuilder::new()
  buf.write_string(
    "Write one commit message that follows Conventional Commits.\n",
  )
  buf.write_string(
    "Allowed types: feat, fix, chore, docs, style, refactor, perf, test, build, ci, revert, deps.\n",
  )
  buf.write_string(
    "Treat each file block as a logical unit and summarize all units in one message.\n",
  )
  buf.write_string("Output format must be:\n")
  buf.write_string("(): \n\n")
  buf.write_string("- : \n")
  buf.write_string("- ...\n\n")
  if context.length() > 0 {
    buf.write_string("Project/User instructions:\n")
    buf.write_string(context)
    buf.write_string("\n\n")
  }
  buf.write_string("Diff units:\n")
  buf.write_string(diff_units)
  buf.to_string()
}

///|
fn build_resolve_prompt(path : String, conflicted_content : String) -> String {
  let buf = StringBuilder::new()
  buf.write_string("Resolve this conflicted file:\n")
  buf.write_string("path: ")
  buf.write_string(path)
  buf.write_string("\n\n")
  buf.write_string("Return only the resolved file content.\n\n")
  buf.write_string("----- BEGIN CONFLICTED FILE -----\n")
  buf.write_string(conflicted_content)
  buf.write_string("\n----- END CONFLICTED FILE -----\n")
  buf.to_string()
}

///|
fn build_resolve_prompt_with_context(
  path : String,
  conflicted_content : String,
  context : String,
) -> String {
  let trimmed_context = trim_text(context)
  if trimmed_context.length() == 0 {
    return build_resolve_prompt(path, conflicted_content)
  }
  let buf = StringBuilder::new()
  buf.write_string("Project/User constraints:\n")
  buf.write_string(trimmed_context)
  buf.write_string("\n\n")
  buf.write_string(build_resolve_prompt(path, conflicted_content))
  buf.to_string()
}

///|
fn has_conflict_markers(content : String) -> Bool {
  content.contains("<<<<<<<") ||
  content.contains(">>>>>>>") ||
  content.contains("|||||||")
}

///|
fn decode_bytes(data : Bytes) -> String {
  @string_utils.decode_bytes(data)
}

///|
fn collect_text_with_provider(
  provider : @llmlib.BoxedProvider,
  prompt : String,
) -> String {
  let buf = StringBuilder::new()
  let handler : @llmlib.StreamHandler = {
    on_event: fn(event) {
      match event {
        @llmlib.StreamEvent::TextDelta(delta) => buf.write_string(delta)
        _ => ()
      }
    },
  }
  provider.stream([@llmlib.Message::user(prompt)], [], handler)
  buf.to_string()
}

///|
fn extract_fenced_code_block(text : String) -> String? {
  let lines : Array[String] = []
  for line_view in text.split("\n") {
    lines.push(line_view.to_owned())
  }
  let captured : Array[String] = []
  let mut started = false
  let mut in_fence = false
  for line in lines {
    if line.has_prefix("```") {
      if !started {
        started = true
        in_fence = true
        continue
      } else if in_fence {
        return Some(captured.join("\n"))
      }
    }
    if in_fence {
      captured.push(line)
    }
  }
  if started && captured.length() > 0 {
    Some(captured.join("\n"))
  } else {
    None
  }
}

///|
fn normalize_model_output(raw : String) -> String {
  match extract_fenced_code_block(raw) {
    Some(code) => code
    None => trim_text(raw)
  }
}

///|
fn json_get_str(j : Json, field : String) -> String {
  match j {
    Object(m) =>
      match m.get(field) {
        Some(String(s)) => s
        _ => ""
      }
    _ => ""
  }
}

///|
fn normalize_repo_relative_path(path : String) -> String? {
  let trimmed = trim_text(path)
  if trimmed.length() == 0 {
    return None
  }
  let normalized_parts : Array[String] = []
  for part_view in trimmed.split("/") {
    let part = part_view.to_owned()
    if part == "" || part == "." {
      continue
    }
    if part == ".." || part == ".git" {
      return None
    }
    normalized_parts.push(part)
  }
  if normalized_parts.length() == 0 {
    return None
  }
  Some(normalized_parts.join("/"))
}

///|
fn resolve_repo_scoped_path(root : String, path : String) -> String? {
  let trimmed = trim_text(path)
  if trimmed.length() == 0 {
    return None
  }
  if trimmed.has_prefix("/") {
    let prefix = root + "/"
    if trimmed.has_prefix(prefix) {
      let rel = String::unsafe_substring(
        trimmed,
        start=prefix.length(),
        end=trimmed.length(),
      )
      return normalize_repo_relative_path(rel)
    }
    return None
  }
  normalize_repo_relative_path(trimmed)
}

///|
fn push_unique_path(paths : Array[String], path : String) -> Unit {
  for current in paths {
    if current == path {
      return
    }
  }
  paths.push(path)
}

///|
fn build_agent_loop_prompt(rel_path : String) -> String {
  let buf = StringBuilder::new()
  buf.write_string("Resolve git conflict markers using tools.\n")
  buf.write_string("Target path: ")
  buf.write_string(rel_path)
  buf.write_string("\n\n")
  buf.write_string("Requirements:\n")
  buf.write_string("- Use read_file before writing.\n")
  buf.write_string("- Write final merged content with write_file.\n")
  buf.write_string("- Remove all conflict markers.\n")
  buf.write_string("- Keep edits coherent and valid.\n")
  buf.write_string("- Prefer minimal, safe change.\n")
  buf.to_string()
}

///|
fn build_repo_scoped_rw_registry(
  fs : &@bit.FileSystem,
  rfs : &@bit.RepoFileSystem,
  root : String,
) -> (@llmlib.ToolRegistry, Ref[Array[String]]) {
  let registry = @llmlib.ToolRegistry::new()
  let touched_paths : Ref[Array[String]] = { val: [] }
  registry.register(
    "read_file",
    "Read file contents from repository scope. Path must stay inside repository.",
    @llmlib.SchemaBuilder::new()
    .string("path", "Repository-relative file path", required=true)
    .build(),
    fn(input) {
      let raw_path = json_get_str(input, "path")
      guard resolve_repo_scoped_path(root, raw_path) is Some(rel_path) else {
        return "error: invalid path (repository scope only)"
      }
      let abs_path = root + "/" + rel_path
      if !rfs.is_file(abs_path) {
        return "error: file not found"
      }
      let bytes = rfs.read_file(abs_path) catch {
        _ => return "error: read failed"
      }
      decode_bytes(bytes)
    },
  )
  registry.register(
    "write_file",
    "Write complete file content to repository scope. Path must stay inside repository.",
    @llmlib.SchemaBuilder::new()
    .string("path", "Repository-relative file path", required=true)
    .string("content", "Complete file content", required=true)
    .build(),
    fn(input) {
      let raw_path = json_get_str(input, "path")
      guard resolve_repo_scoped_path(root, raw_path) is Some(rel_path) else {
        return "error: invalid path (repository scope only)"
      }
      let content = json_get_str(input, "content")
      let abs_path = root + "/" + rel_path
      fs.write_string(abs_path, content) catch {
        _ => return "error: write failed"
      }
      push_unique_path(touched_paths.val, rel_path)
      "ok"
    },
  )
  (registry, touched_paths)
}

///|
fn build_repo_scoped_read_tools(
  rfs : &@bit.RepoFileSystem,
  root : String,
) -> @llmlib.ToolRegistry {
  let registry = @llmlib.ToolRegistry::new()
  registry.register(
    "read_file",
    "Read file content from repository scope.",
    @llmlib.SchemaBuilder::new()
    .string("path", "Repository-relative file path", required=true)
    .build(),
    fn(input) {
      let raw_path = json_get_str(input, "path")
      guard resolve_repo_scoped_path(root, raw_path) is Some(path) else {
        return "error: invalid path"
      }
      if !rfs.is_file(root + "/" + path) {
        return "error: file not found"
      }
      let abs_path = root + "/" + path
      let bytes = rfs.read_file(abs_path) catch {
        _ => return "error: read failed"
      }
      decode_bytes(bytes)
    },
  )
  registry.register(
    "readdir",
    "List repository-relative directory entries (non-recursive).",
    @llmlib.SchemaBuilder::new()
    .string("path", "Repository-relative directory path", required=false)
    .build(),
    fn(input) {
      let raw_path = json_get_str(input, "path")
      let rel_path = trim_text(raw_path)
      let mut target_path = ""
      if rel_path.length() > 0 {
        guard resolve_repo_scoped_path(root, rel_path) is Some(path) else {
          return "error: invalid path"
        }
        target_path = path
      }
      let abs_path = if target_path.length() == 0 {
        root
      } else {
        root + "/" + target_path
      }
      if !rfs.is_dir(abs_path) {
        return "error: invalid path"
      }
      let entries = rfs.readdir(abs_path) catch {
        _ => return "error: readdir failed"
      }
      entries.sort_by((a, b) => String::lexical_compare(a, b))
      let out : Array[String] = []
      for entry in entries {
        if entry == "." || entry == ".." || entry == ".git" {
          continue
        }
        let rel_entry = if target_path.length() == 0 {
          entry
        } else {
          target_path + "/" + entry
        }
        if rfs.is_dir(root + "/" + rel_entry) {
          out.push(rel_entry + "/")
        } else {
          out.push(rel_entry)
        }
      }
      if out.length() == 0 {
        "(empty)"
      } else {
        out.join("\n")
      }
    },
  )
  registry
}

///|
fn stage_paths_with_storage_runtime(
  fs : &@bit.FileSystem,
  rfs : &@bit.RepoFileSystem,
  root : String,
  paths : Array[String],
) -> Unit raise @bit.GitError {
  @runtime.run_storage_command(
    fs,
    rfs,
    root,
    @runtime.StorageCommand::Add({ add_all: false, paths }),
  ) catch {
    err =>
      raise @bit.GitError::InvalidObject(
        "rebase-ai: storage runtime add failed: \{err}",
      )
  }
}

///|
fn resolve_conflict_file_with_agent_loop(
  fs : &@bit.FileSystem,
  rfs : &@bit.RepoFileSystem,
  root : String,
  rel_path : String,
  provider : @llmlib.BoxedProvider,
  options : AiRebaseOptions,
  context : String,
) -> Bool raise @bit.GitError {
  let abs_path = root + "/" + rel_path
  let bytes = rfs.read_file(abs_path) catch { _ => return false }
  let conflicted = decode_bytes(bytes)
  if !has_conflict_markers(conflicted) {
    return false
  }
  let (registry, touched_paths) = build_repo_scoped_rw_registry(fs, rfs, root)
  let user_prompt = if context.length() == 0 {
    build_agent_loop_prompt(rel_path)
  } else {
    build_agent_loop_prompt(rel_path) +
    "\n\n" +
    build_resolve_prompt_with_context(rel_path, conflicted, context)
  }
  let messages : Array[@llmlib.Message] = [@llmlib.Message::user(user_prompt)]
  @llmlib.run_agent_cancellable(
    provider,
    registry,
    messages,
    @llmlib.StopCondition::MaxSteps(normalized_agent_step_limit(options)),
    fn() { false },
    fn(_event) {  },
  )
  let after_bytes = rfs.read_file(abs_path) catch { _ => return false }
  let resolved = decode_bytes(after_bytes)
  if trim_text(resolved).length() == 0 {
    return false
  }
  if has_conflict_markers(resolved) {
    return false
  }
  let stage_paths = if touched_paths.val.length() > 0 {
    touched_paths.val
  } else {
    [rel_path]
  }
  stage_paths_with_storage_runtime(fs, rfs, root, stage_paths)
  true
}

///|
fn resolve_conflict_file_with_provider(
  fs : &@bit.FileSystem,
  rfs : &@bit.RepoFileSystem,
  root : String,
  rel_path : String,
  provider : @llmlib.BoxedProvider,
  context : String,
) -> Bool raise @bit.GitError {
  let abs_path = root + "/" + rel_path
  let bytes = rfs.read_file(abs_path) catch { _ => return false }
  let conflicted = decode_bytes(bytes)
  if !has_conflict_markers(conflicted) {
    return false
  }
  let prompt = build_resolve_prompt_with_context(rel_path, conflicted, context)
  let raw = collect_text_with_provider(provider, prompt)
  let resolved = normalize_model_output(raw)
  if trim_text(resolved).length() == 0 {
    return false
  }
  if has_conflict_markers(resolved) {
    return false
  }
  fs.write_string(abs_path, resolved)
  stage_paths_with_storage_runtime(fs, rfs, root, [rel_path])
  true
}

///|
fn collect_conflict_marker_paths_recursive(
  rfs : &@bit.RepoFileSystem,
  root : String,
  rel : String,
  out : Array[String],
) -> Unit {
  let dir_path = if rel.length() == 0 { root } else { root + "/" + rel }
  let entries = rfs.readdir(dir_path) catch { _ => [] }
  entries.sort_by((a, b) => String::lexical_compare(a, b))
  for entry in entries {
    if entry == "." || entry == ".." || entry == ".git" {
      continue
    }
    let child_rel = if rel.length() == 0 { entry } else { rel + "/" + entry }
    let child_path = root + "/" + child_rel
    if rfs.is_dir(child_path) {
      collect_conflict_marker_paths_recursive(rfs, root, child_rel, out)
    } else if rfs.is_file(child_path) {
      let data = rfs.read_file(child_path) catch { _ => Default::default() }
      let text = decode_bytes(data)
      if has_conflict_markers(text) {
        out.push(child_rel)
      }
    }
  }
}

///|
pub fn find_conflict_marker_paths(
  rfs : &@bit.RepoFileSystem,
  root : String,
) -> Array[String] {
  let out : Array[String] = []
  collect_conflict_marker_paths_recursive(rfs, root, "", out)
  out
}

///|
fn ensure_openrouter_api_key() -> Unit raise @bit.GitError {
  let api_key = trim_text(@llm_ffi.get_env("OPENROUTER_API_KEY"))
  if api_key.length() == 0 {
    raise @bit.GitError::InvalidObject("rebase-ai requires OPENROUTER_API_KEY")
  }
}

///|
fn build_provider(
  options : AiRebaseOptions,
  provider_ref : Ref[@llmlib.BoxedProvider?],
) -> @llmlib.BoxedProvider raise @bit.GitError {
  build_provider_with_system_prompt(
    options,
    provider_ref,
    openrouter_system_prompt(use_agent_loop_mode(options)),
  )
}

///|
fn build_provider_with_system_prompt(
  options : AiRebaseOptions,
  provider_ref : Ref[@llmlib.BoxedProvider?],
  system_prompt : String,
) -> @llmlib.BoxedProvider raise @bit.GitError {
  match provider_ref.val {
    Some(provider) => provider
    None => {
      ensure_openrouter_api_key()
      let api_key = trim_text(@llm_ffi.get_env("OPENROUTER_API_KEY"))
      let model = effective_model(options)
      let provider = @llmlib.BoxedProvider::new(
        @openai.OpenAIProvider::new(
          api_key,
          endpoint=OpenRouter,
          model~,
          system_prompt~,
          max_tokens=8192,
        ),
      )
      match system_prompt {
        _ => {
          provider_ref.val = Some(provider)
          provider
        }
      }
    }
  }
}

///|
pub fn ai_generate_commit_message(
  fs : &@bit.RepoFileSystem,
  root : String,
  diff_units : String,
  context : String,
  options? : AiRebaseOptions = AiRebaseOptions::default(),
) -> String raise @bit.GitError {
  let trimmed_diff = trim_text(diff_units)
  if trimmed_diff.length() == 0 {
    raise @bit.GitError::InvalidObject("ai: no diff content provided")
  }
  let mut prompt = build_commit_message_prompt(trimmed_diff, trim_text(context))
  if use_agent_loop_mode(options) {
    prompt += "\n\nYou can call the provided read_file and readdir tools if context is insufficient."
  }
  let provider_ref : Ref[@llmlib.BoxedProvider?] = { val: options.provider }
  let provider = build_provider_with_system_prompt(
    options,
    provider_ref,
    commit_system_prompt(use_agent_loop_mode(options)),
  )
  if use_agent_loop_mode(options) {
    let registry = build_repo_scoped_read_tools(fs, root)
    let out = StringBuilder::new()
    @llmlib.run_agent_cancellable(
      provider,
      registry,
      [@llmlib.Message::user(prompt)],
      @llmlib.StopCondition::MaxSteps(normalized_agent_step_limit(options)),
      fn() { false },
      fn(event) {
        match event {
          Stream(stream_event) =>
            match stream_event {
              TextDelta(delta) => out.write_string(delta)
              _ => ()
            }
          _ => ()
        }
      },
    )
    normalize_model_output(out.to_string())
  } else {
    normalize_model_output(collect_text_with_provider(provider, prompt))
  }
}

///|
fn raise_unresolved_conflicts(
  conflicts : Array[String],
) -> Unit raise @bit.GitError {
  if conflicts.length() == 0 {
    raise @bit.GitError::InvalidObject("rebase-ai: unresolved conflicts remain")
  }
  raise @bit.GitError::InvalidObject(
    "rebase-ai failed to resolve conflicts: " + conflicts.join(", "),
  )
}

///|
fn resolve_conflict_paths(
  fs : &@bit.FileSystem,
  rfs : &@bit.RepoFileSystem,
  root : String,
  paths : Array[String],
  options : AiRebaseOptions,
  provider_ref : Ref[@llmlib.BoxedProvider?],
  context : String,
) -> AiConflictResolution raise @bit.GitError {
  if paths.length() == 0 {
    return { resolved: [], failed: [] }
  }
  let provider = build_provider(options, provider_ref)
  let agent_loop_mode = use_agent_loop_mode(options)
  let resolved : Array[String] = []
  let failed : Array[String] = []
  for path in paths {
    let ok = if agent_loop_mode {
      resolve_conflict_file_with_agent_loop(
        fs, rfs, root, path, provider, options, context,
      ) catch {
        _ => false
      }
    } else {
      resolve_conflict_file_with_provider(
        fs, rfs, root, path, provider, context,
      ) catch {
        _ => false
      }
    }
    if ok {
      resolved.push(path)
    } else {
      failed.push(path)
    }
  }
  { resolved, failed }
}

///|
pub fn ai_resolve_conflict_paths(
  fs : &@bit.FileSystem,
  rfs : &@bit.RepoFileSystem,
  root : String,
  paths : Array[String],
  options : AiRebaseOptions,
  context : String,
) -> AiConflictResolution raise @bit.GitError {
  let provider_ref : Ref[@llmlib.BoxedProvider?] = { val: options.provider }
  resolve_conflict_paths(fs, rfs, root, paths, options, provider_ref, context)
}

///|
fn normalized_round_limit(options : AiRebaseOptions) -> Int {
  if options.max_auto_rounds <= 0 {
    1
  } else {
    options.max_auto_rounds
  }
}

///|
async fn continue_until_non_conflict(
  fs : &@bit.FileSystem,
  rfs : &@bit.RepoFileSystem,
  root : String,
  initial : @bitlib.RebaseResult,
  options : AiRebaseOptions,
  provider_ref : Ref[@llmlib.BoxedProvider?],
) -> @bitlib.RebaseResult raise @bit.GitError {
  let mut current = initial
  let mut rounds = 0
  let round_limit = normalized_round_limit(options)
  while rounds <= round_limit {
    match current.status {
      @bitlib.RebaseStatus::Conflict => ()
      _ => return current
    }
    if rounds >= round_limit {
      return current
    }
    let conflict_paths = if current.conflicts.length() > 0 {
      current.conflicts
    } else {
      find_conflict_marker_paths(rfs, root)
    }
    if conflict_paths.length() == 0 {
      return current
    }
    let resolved = resolve_conflict_paths(
      fs, rfs, root, conflict_paths, options, provider_ref, "",
    )
    if resolved.failed.length() > 0 {
      raise_unresolved_conflicts(resolved.failed)
    }
    current = @bitlib.rebase_continue(fs, rfs, root)
    rounds += 1
  }
  current
}

///|
pub async fn ai_rebase_start(
  fs : &@bit.FileSystem,
  rfs : &@bit.RepoFileSystem,
  root : String,
  upstream : @bit.ObjectId,
  options? : AiRebaseOptions = AiRebaseOptions::default(),
) -> @bitlib.RebaseResult raise @bit.GitError {
  let provider_ref : Ref[@llmlib.BoxedProvider?] = { val: options.provider }
  let start_result = @bitlib.rebase_start(fs, rfs, root, upstream)
  continue_until_non_conflict(
    fs, rfs, root, start_result, options, provider_ref,
  )
}

///|
pub async fn ai_rebase_continue(
  fs : &@bit.FileSystem,
  rfs : &@bit.RepoFileSystem,
  root : String,
  options? : AiRebaseOptions = AiRebaseOptions::default(),
) -> @bitlib.RebaseResult raise @bit.GitError {
  let provider_ref : Ref[@llmlib.BoxedProvider?] = { val: options.provider }
  let marker_paths = find_conflict_marker_paths(rfs, root)
  if marker_paths.length() > 0 {
    let resolved = resolve_conflict_paths(
      fs, rfs, root, marker_paths, options, provider_ref, "",
    )
    if resolved.failed.length() > 0 {
      raise_unresolved_conflicts(resolved.failed)
    }
  }
  let continue_result = @bitlib.rebase_continue(fs, rfs, root)
  continue_until_non_conflict(
    fs, rfs, root, continue_result, options, provider_ref,
  )
}

///|
pub async fn ai_rebase_skip(
  fs : &@bit.FileSystem,
  rfs : &@bit.RepoFileSystem,
  root : String,
  options? : AiRebaseOptions = AiRebaseOptions::default(),
) -> @bitlib.RebaseResult raise @bit.GitError {
  let provider_ref : Ref[@llmlib.BoxedProvider?] = { val: options.provider }
  let skip_result = @bitlib.rebase_skip(fs, rfs, root)
  continue_until_non_conflict(fs, rfs, root, skip_result, options, provider_ref)
}

///|
pub async fn ai_rebase_abort(
  fs : &@bit.FileSystem,
  rfs : &@bit.RepoFileSystem,
  root : String,
) -> Unit raise @bit.GitError {
  @bitlib.rebase_abort(fs, rfs, root)
}