main.rs

  1mod example;
  2mod headless;
  3mod source_location;
  4mod syntax_retrieval_stats;
  5mod util;
  6
  7use crate::example::{ExampleFormat, NamedExample};
  8use crate::syntax_retrieval_stats::retrieval_stats;
  9use ::serde::Serialize;
 10use ::util::paths::PathStyle;
 11use ::util::rel_path::RelPath;
 12use anyhow::{Context as _, Result, anyhow};
 13use clap::{Args, Parser, Subcommand};
 14use cloud_llm_client::predict_edits_v3::{self, Excerpt};
 15use cloud_zeta2_prompt::{CURSOR_MARKER, write_codeblock};
 16use edit_prediction_context::{
 17    EditPredictionContextOptions, EditPredictionExcerpt, EditPredictionExcerptOptions,
 18    EditPredictionScoreOptions, Line,
 19};
 20use futures::StreamExt as _;
 21use futures::channel::mpsc;
 22use gpui::{Application, AsyncApp, Entity, prelude::*};
 23use language::{Bias, Buffer, BufferSnapshot, OffsetRangeExt, Point};
 24use language_model::LanguageModelRegistry;
 25use project::{Project, ProjectPath, Worktree};
 26use reqwest_client::ReqwestClient;
 27use serde_json::json;
 28use std::io;
 29use std::{collections::HashSet, path::PathBuf, process::exit, str::FromStr, sync::Arc};
 30use zeta2::{ContextMode, LlmContextOptions, SearchToolQuery};
 31
 32use crate::headless::ZetaCliAppState;
 33use crate::source_location::SourceLocation;
 34use crate::util::{open_buffer, open_buffer_with_language_server};
 35
 36#[derive(Parser, Debug)]
 37#[command(name = "zeta")]
 38struct ZetaCliArgs {
 39    #[command(subcommand)]
 40    command: Command,
 41}
 42
 43#[derive(Subcommand, Debug)]
 44enum Command {
 45    Zeta1 {
 46        #[command(subcommand)]
 47        command: Zeta1Command,
 48    },
 49    Zeta2 {
 50        #[command(subcommand)]
 51        command: Zeta2Command,
 52    },
 53    ConvertExample {
 54        path: PathBuf,
 55        #[arg(long, value_enum, default_value_t = ExampleFormat::Md)]
 56        output_format: ExampleFormat,
 57    },
 58}
 59
 60#[derive(Subcommand, Debug)]
 61enum Zeta1Command {
 62    Context {
 63        #[clap(flatten)]
 64        context_args: ContextArgs,
 65    },
 66}
 67
 68#[derive(Subcommand, Debug)]
 69enum Zeta2Command {
 70    Syntax {
 71        #[clap(flatten)]
 72        args: Zeta2Args,
 73        #[clap(flatten)]
 74        syntax_args: Zeta2SyntaxArgs,
 75        #[command(subcommand)]
 76        command: Zeta2SyntaxCommand,
 77    },
 78    Llm {
 79        #[clap(flatten)]
 80        args: Zeta2Args,
 81        #[command(subcommand)]
 82        command: Zeta2LlmCommand,
 83    },
 84    Predict {
 85        example_path: PathBuf,
 86    },
 87}
 88
 89#[derive(Subcommand, Debug)]
 90enum Zeta2SyntaxCommand {
 91    Context {
 92        #[clap(flatten)]
 93        context_args: ContextArgs,
 94    },
 95    Stats {
 96        #[arg(long)]
 97        worktree: PathBuf,
 98        #[arg(long)]
 99        extension: Option<String>,
100        #[arg(long)]
101        limit: Option<usize>,
102        #[arg(long)]
103        skip: Option<usize>,
104    },
105}
106
107#[derive(Subcommand, Debug)]
108enum Zeta2LlmCommand {
109    Context {
110        #[clap(flatten)]
111        context_args: ContextArgs,
112    },
113}
114
115#[derive(Debug, Args)]
116#[group(requires = "worktree")]
117struct ContextArgs {
118    #[arg(long)]
119    worktree: PathBuf,
120    #[arg(long)]
121    cursor: SourceLocation,
122    #[arg(long)]
123    use_language_server: bool,
124    #[arg(long)]
125    edit_history: Option<FileOrStdin>,
126}
127
128#[derive(Debug, Args)]
129struct Zeta2Args {
130    #[arg(long, default_value_t = 8192)]
131    max_prompt_bytes: usize,
132    #[arg(long, default_value_t = 2048)]
133    max_excerpt_bytes: usize,
134    #[arg(long, default_value_t = 1024)]
135    min_excerpt_bytes: usize,
136    #[arg(long, default_value_t = 0.66)]
137    target_before_cursor_over_total_bytes: f32,
138    #[arg(long, default_value_t = 1024)]
139    max_diagnostic_bytes: usize,
140    #[arg(long, value_enum, default_value_t = PromptFormat::default())]
141    prompt_format: PromptFormat,
142    #[arg(long, value_enum, default_value_t = Default::default())]
143    output_format: OutputFormat,
144    #[arg(long, default_value_t = 42)]
145    file_indexing_parallelism: usize,
146}
147
148#[derive(Debug, Args)]
149struct Zeta2SyntaxArgs {
150    #[arg(long, default_value_t = false)]
151    disable_imports_gathering: bool,
152    #[arg(long, default_value_t = u8::MAX)]
153    max_retrieved_definitions: u8,
154}
155
156fn syntax_args_to_options(
157    zeta2_args: &Zeta2Args,
158    syntax_args: &Zeta2SyntaxArgs,
159    omit_excerpt_overlaps: bool,
160) -> zeta2::ZetaOptions {
161    zeta2::ZetaOptions {
162        context: ContextMode::Syntax(EditPredictionContextOptions {
163            max_retrieved_declarations: syntax_args.max_retrieved_definitions,
164            use_imports: !syntax_args.disable_imports_gathering,
165            excerpt: EditPredictionExcerptOptions {
166                max_bytes: zeta2_args.max_excerpt_bytes,
167                min_bytes: zeta2_args.min_excerpt_bytes,
168                target_before_cursor_over_total_bytes: zeta2_args
169                    .target_before_cursor_over_total_bytes,
170            },
171            score: EditPredictionScoreOptions {
172                omit_excerpt_overlaps,
173            },
174        }),
175        max_diagnostic_bytes: zeta2_args.max_diagnostic_bytes,
176        max_prompt_bytes: zeta2_args.max_prompt_bytes,
177        prompt_format: zeta2_args.prompt_format.clone().into(),
178        file_indexing_parallelism: zeta2_args.file_indexing_parallelism,
179    }
180}
181
182#[derive(clap::ValueEnum, Default, Debug, Clone)]
183enum PromptFormat {
184    MarkedExcerpt,
185    LabeledSections,
186    OnlySnippets,
187    #[default]
188    NumberedLines,
189}
190
191impl Into<predict_edits_v3::PromptFormat> for PromptFormat {
192    fn into(self) -> predict_edits_v3::PromptFormat {
193        match self {
194            Self::MarkedExcerpt => predict_edits_v3::PromptFormat::MarkedExcerpt,
195            Self::LabeledSections => predict_edits_v3::PromptFormat::LabeledSections,
196            Self::OnlySnippets => predict_edits_v3::PromptFormat::OnlySnippets,
197            Self::NumberedLines => predict_edits_v3::PromptFormat::NumLinesUniDiff,
198        }
199    }
200}
201
202#[derive(clap::ValueEnum, Default, Debug, Clone)]
203enum OutputFormat {
204    #[default]
205    Prompt,
206    Request,
207    Full,
208}
209
210#[derive(Debug, Clone)]
211enum FileOrStdin {
212    File(PathBuf),
213    Stdin,
214}
215
216impl FileOrStdin {
217    async fn read_to_string(&self) -> Result<String, std::io::Error> {
218        match self {
219            FileOrStdin::File(path) => smol::fs::read_to_string(path).await,
220            FileOrStdin::Stdin => smol::unblock(|| std::io::read_to_string(std::io::stdin())).await,
221        }
222    }
223}
224
225impl FromStr for FileOrStdin {
226    type Err = <PathBuf as FromStr>::Err;
227
228    fn from_str(s: &str) -> Result<Self, Self::Err> {
229        match s {
230            "-" => Ok(Self::Stdin),
231            _ => Ok(Self::File(PathBuf::from_str(s)?)),
232        }
233    }
234}
235
236struct LoadedContext {
237    full_path_str: String,
238    snapshot: BufferSnapshot,
239    clipped_cursor: Point,
240    worktree: Entity<Worktree>,
241    project: Entity<Project>,
242    buffer: Entity<Buffer>,
243}
244
245async fn load_context(
246    args: &ContextArgs,
247    app_state: &Arc<ZetaCliAppState>,
248    cx: &mut AsyncApp,
249) -> Result<LoadedContext> {
250    let ContextArgs {
251        worktree: worktree_path,
252        cursor,
253        use_language_server,
254        ..
255    } = args;
256
257    let worktree_path = worktree_path.canonicalize()?;
258
259    let project = cx.update(|cx| {
260        Project::local(
261            app_state.client.clone(),
262            app_state.node_runtime.clone(),
263            app_state.user_store.clone(),
264            app_state.languages.clone(),
265            app_state.fs.clone(),
266            None,
267            cx,
268        )
269    })?;
270
271    let worktree = project
272        .update(cx, |project, cx| {
273            project.create_worktree(&worktree_path, true, cx)
274        })?
275        .await?;
276
277    let mut ready_languages = HashSet::default();
278    let (_lsp_open_handle, buffer) = if *use_language_server {
279        let (lsp_open_handle, _, buffer) = open_buffer_with_language_server(
280            project.clone(),
281            worktree.clone(),
282            cursor.path.clone(),
283            &mut ready_languages,
284            cx,
285        )
286        .await?;
287        (Some(lsp_open_handle), buffer)
288    } else {
289        let buffer =
290            open_buffer(project.clone(), worktree.clone(), cursor.path.clone(), cx).await?;
291        (None, buffer)
292    };
293
294    let full_path_str = worktree
295        .read_with(cx, |worktree, _| worktree.root_name().join(&cursor.path))?
296        .display(PathStyle::local())
297        .to_string();
298
299    let snapshot = cx.update(|cx| buffer.read(cx).snapshot())?;
300    let clipped_cursor = snapshot.clip_point(cursor.point, Bias::Left);
301    if clipped_cursor != cursor.point {
302        let max_row = snapshot.max_point().row;
303        if cursor.point.row < max_row {
304            return Err(anyhow!(
305                "Cursor position {:?} is out of bounds (line length is {})",
306                cursor.point,
307                snapshot.line_len(cursor.point.row)
308            ));
309        } else {
310            return Err(anyhow!(
311                "Cursor position {:?} is out of bounds (max row is {})",
312                cursor.point,
313                max_row
314            ));
315        }
316    }
317
318    Ok(LoadedContext {
319        full_path_str,
320        snapshot,
321        clipped_cursor,
322        worktree,
323        project,
324        buffer,
325    })
326}
327
328async fn zeta2_predict(
329    example: NamedExample,
330    app_state: &Arc<ZetaCliAppState>,
331    cx: &mut AsyncApp,
332) -> Result<()> {
333    let worktree_path = example.setup_worktree().await?;
334
335    cx.update(|cx| {
336        LanguageModelRegistry::global(cx).update(cx, |registry, cx| {
337            registry
338                .provider(&zeta2::related_excerpts::MODEL_PROVIDER_ID)
339                .unwrap()
340                .authenticate(cx)
341        })
342    })?
343    .await?;
344
345    app_state
346        .client
347        .sign_in_with_optional_connect(true, cx)
348        .await?;
349
350    let project = cx.update(|cx| {
351        Project::local(
352            app_state.client.clone(),
353            app_state.node_runtime.clone(),
354            app_state.user_store.clone(),
355            app_state.languages.clone(),
356            app_state.fs.clone(),
357            None,
358            cx,
359        )
360    })?;
361
362    let worktree = project
363        .update(cx, |project, cx| {
364            project.create_worktree(&worktree_path, true, cx)
365        })?
366        .await?;
367    worktree
368        .read_with(cx, |worktree, _cx| {
369            worktree.as_local().unwrap().scan_complete()
370        })?
371        .await;
372
373    let cursor_path = RelPath::new(&example.example.cursor_path, PathStyle::Posix)?.into_arc();
374
375    let cursor_buffer = project
376        .update(cx, |project, cx| {
377            project.open_buffer(
378                ProjectPath {
379                    worktree_id: worktree.read(cx).id(),
380                    path: cursor_path,
381                },
382                cx,
383            )
384        })?
385        .await?;
386
387    let cursor_offset_within_excerpt = example
388        .example
389        .cursor_position
390        .find(CURSOR_MARKER)
391        .ok_or_else(|| anyhow!("missing cursor marker"))?;
392    let mut cursor_excerpt = example.example.cursor_position.clone();
393    cursor_excerpt.replace_range(
394        cursor_offset_within_excerpt..(cursor_offset_within_excerpt + CURSOR_MARKER.len()),
395        "",
396    );
397    let excerpt_offset = cursor_buffer.read_with(cx, |buffer, _cx| {
398        let text = buffer.text();
399
400        let mut matches = text.match_indices(&cursor_excerpt);
401        let Some((excerpt_offset, _)) = matches.next() else {
402            anyhow::bail!(
403                "Cursor excerpt did not exist in buffer.\nExcerpt:\n\n{cursor_excerpt}\nBuffer text:\n{text}\n"
404            );
405        };
406        assert!(matches.next().is_none());
407
408        Ok(excerpt_offset)
409    })??;
410    let cursor_offset = excerpt_offset + cursor_offset_within_excerpt;
411
412    let cursor_anchor =
413        cursor_buffer.read_with(cx, |buffer, _| buffer.anchor_after(cursor_offset))?;
414
415    let zeta = cx.update(|cx| zeta2::Zeta::global(&app_state.client, &app_state.user_store, cx))?;
416
417    let (prediction_task, mut debug_rx) = zeta.update(cx, |zeta, cx| {
418        let receiver = zeta.debug_info();
419        let prediction_task = zeta.request_prediction(&project, &cursor_buffer, cursor_anchor, cx);
420        (prediction_task, receiver)
421    })?;
422
423    prediction_task.await.context("No prediction")?;
424    let mut response = None;
425
426    let mut excerpts_text = String::new();
427    while let Some(event) = debug_rx.next().await {
428        match event {
429            zeta2::ZetaDebugInfo::EditPredicted(request) => {
430                response = Some(request.response_rx.await?);
431                for included_file in request.request.included_files {
432                    let insertions = vec![(request.request.cursor_point, CURSOR_MARKER)];
433                    write_codeblock(
434                        &included_file.path,
435                        included_file.excerpts.iter(),
436                        if included_file.path == request.request.excerpt_path {
437                            &insertions
438                        } else {
439                            &[]
440                        },
441                        included_file.max_row,
442                        false,
443                        &mut excerpts_text,
444                    );
445                }
446            }
447            _ => {}
448        }
449    }
450
451    println!("## Excerpts\n");
452    println!("{excerpts_text}");
453
454    println!("## Prediction\n");
455    let response = response
456        .unwrap()
457        .map(|r| r.debug_info.unwrap().model_response)
458        .unwrap_or_else(|s| s);
459    println!("{response}");
460
461    anyhow::Ok(())
462}
463
464async fn zeta2_syntax_context(
465    zeta2_args: Zeta2Args,
466    syntax_args: Zeta2SyntaxArgs,
467    args: ContextArgs,
468    app_state: &Arc<ZetaCliAppState>,
469    cx: &mut AsyncApp,
470) -> Result<String> {
471    let LoadedContext {
472        worktree,
473        project,
474        buffer,
475        clipped_cursor,
476        ..
477    } = load_context(&args, app_state, cx).await?;
478
479    // wait for worktree scan before starting zeta2 so that wait_for_initial_indexing waits for
480    // the whole worktree.
481    worktree
482        .read_with(cx, |worktree, _cx| {
483            worktree.as_local().unwrap().scan_complete()
484        })?
485        .await;
486    let output = cx
487        .update(|cx| {
488            let zeta = cx.new(|cx| {
489                zeta2::Zeta::new(app_state.client.clone(), app_state.user_store.clone(), cx)
490            });
491            let indexing_done_task = zeta.update(cx, |zeta, cx| {
492                zeta.set_options(syntax_args_to_options(&zeta2_args, &syntax_args, true));
493                zeta.register_buffer(&buffer, &project, cx);
494                zeta.wait_for_initial_indexing(&project, cx)
495            });
496            cx.spawn(async move |cx| {
497                indexing_done_task.await?;
498                let request = zeta
499                    .update(cx, |zeta, cx| {
500                        let cursor = buffer.read(cx).snapshot().anchor_before(clipped_cursor);
501                        zeta.cloud_request_for_zeta_cli(&project, &buffer, cursor, cx)
502                    })?
503                    .await?;
504
505                let (prompt_string, section_labels) = cloud_zeta2_prompt::build_prompt(&request)?;
506
507                match zeta2_args.output_format {
508                    OutputFormat::Prompt => anyhow::Ok(prompt_string),
509                    OutputFormat::Request => anyhow::Ok(serde_json::to_string_pretty(&request)?),
510                    OutputFormat::Full => anyhow::Ok(serde_json::to_string_pretty(&json!({
511                        "request": request,
512                        "prompt": prompt_string,
513                        "section_labels": section_labels,
514                    }))?),
515                }
516            })
517        })?
518        .await?;
519
520    Ok(output)
521}
522
523async fn zeta2_llm_context(
524    zeta2_args: Zeta2Args,
525    context_args: ContextArgs,
526    app_state: &Arc<ZetaCliAppState>,
527    cx: &mut AsyncApp,
528) -> Result<String> {
529    let LoadedContext {
530        buffer,
531        clipped_cursor,
532        snapshot: cursor_snapshot,
533        project,
534        ..
535    } = load_context(&context_args, app_state, cx).await?;
536
537    let cursor_position = cursor_snapshot.anchor_after(clipped_cursor);
538
539    cx.update(|cx| {
540        LanguageModelRegistry::global(cx).update(cx, |registry, cx| {
541            registry
542                .provider(&zeta2::related_excerpts::MODEL_PROVIDER_ID)
543                .unwrap()
544                .authenticate(cx)
545        })
546    })?
547    .await?;
548
549    let edit_history_unified_diff = match context_args.edit_history {
550        Some(events) => events.read_to_string().await?,
551        None => String::new(),
552    };
553
554    let (debug_tx, mut debug_rx) = mpsc::unbounded();
555
556    let excerpt_options = EditPredictionExcerptOptions {
557        max_bytes: zeta2_args.max_excerpt_bytes,
558        min_bytes: zeta2_args.min_excerpt_bytes,
559        target_before_cursor_over_total_bytes: zeta2_args.target_before_cursor_over_total_bytes,
560    };
561
562    let related_excerpts = cx
563        .update(|cx| {
564            zeta2::related_excerpts::find_related_excerpts(
565                buffer,
566                cursor_position,
567                &project,
568                edit_history_unified_diff,
569                &LlmContextOptions {
570                    excerpt: excerpt_options.clone(),
571                },
572                Some(debug_tx),
573                cx,
574            )
575        })?
576        .await?;
577
578    let cursor_excerpt = EditPredictionExcerpt::select_from_buffer(
579        clipped_cursor,
580        &cursor_snapshot,
581        &excerpt_options,
582        None,
583    )
584    .context("line didn't fit")?;
585
586    #[derive(Serialize)]
587    struct Output {
588        excerpts: Vec<OutputExcerpt>,
589        formatted_excerpts: String,
590        meta: OutputMeta,
591    }
592
593    #[derive(Default, Serialize)]
594    struct OutputMeta {
595        search_prompt: String,
596        search_queries: Vec<SearchToolQuery>,
597    }
598
599    #[derive(Serialize)]
600    struct OutputExcerpt {
601        path: PathBuf,
602        #[serde(flatten)]
603        excerpt: Excerpt,
604    }
605
606    let mut meta = OutputMeta::default();
607
608    while let Some(debug_info) = debug_rx.next().await {
609        match debug_info {
610            zeta2::ZetaDebugInfo::ContextRetrievalStarted(info) => {
611                meta.search_prompt = info.search_prompt;
612            }
613            zeta2::ZetaDebugInfo::SearchQueriesGenerated(info) => {
614                meta.search_queries = info.queries
615            }
616            _ => {}
617        }
618    }
619
620    cx.update(|cx| {
621        let mut excerpts = Vec::new();
622        let mut formatted_excerpts = String::new();
623
624        let cursor_insertions = [(
625            predict_edits_v3::Point {
626                line: Line(clipped_cursor.row),
627                column: clipped_cursor.column,
628            },
629            CURSOR_MARKER,
630        )];
631
632        let mut cursor_excerpt_added = false;
633
634        for (buffer, ranges) in related_excerpts {
635            let excerpt_snapshot = buffer.read(cx).snapshot();
636
637            let mut line_ranges = ranges
638                .into_iter()
639                .map(|range| {
640                    let point_range = range.to_point(&excerpt_snapshot);
641                    Line(point_range.start.row)..Line(point_range.end.row)
642                })
643                .collect::<Vec<_>>();
644
645            let Some(file) = excerpt_snapshot.file() else {
646                continue;
647            };
648            let path = file.full_path(cx);
649
650            let is_cursor_file = path == cursor_snapshot.file().unwrap().full_path(cx);
651            if is_cursor_file {
652                let insertion_ix = line_ranges
653                    .binary_search_by(|probe| {
654                        probe
655                            .start
656                            .cmp(&cursor_excerpt.line_range.start)
657                            .then(cursor_excerpt.line_range.end.cmp(&probe.end))
658                    })
659                    .unwrap_or_else(|ix| ix);
660                line_ranges.insert(insertion_ix, cursor_excerpt.line_range.clone());
661                cursor_excerpt_added = true;
662            }
663
664            let merged_excerpts =
665                zeta2::merge_excerpts::merge_excerpts(&excerpt_snapshot, line_ranges)
666                    .into_iter()
667                    .map(|excerpt| OutputExcerpt {
668                        path: path.clone(),
669                        excerpt,
670                    });
671
672            let excerpt_start_ix = excerpts.len();
673            excerpts.extend(merged_excerpts);
674
675            write_codeblock(
676                &path,
677                excerpts[excerpt_start_ix..].iter().map(|e| &e.excerpt),
678                if is_cursor_file {
679                    &cursor_insertions
680                } else {
681                    &[]
682                },
683                Line(excerpt_snapshot.max_point().row),
684                true,
685                &mut formatted_excerpts,
686            );
687        }
688
689        if !cursor_excerpt_added {
690            write_codeblock(
691                &cursor_snapshot.file().unwrap().full_path(cx),
692                &[Excerpt {
693                    start_line: cursor_excerpt.line_range.start,
694                    text: cursor_excerpt.text(&cursor_snapshot).body.into(),
695                }],
696                &cursor_insertions,
697                Line(cursor_snapshot.max_point().row),
698                true,
699                &mut formatted_excerpts,
700            );
701        }
702
703        let output = Output {
704            excerpts,
705            formatted_excerpts,
706            meta,
707        };
708
709        Ok(serde_json::to_string_pretty(&output)?)
710    })
711    .unwrap()
712}
713
714async fn zeta1_context(
715    args: ContextArgs,
716    app_state: &Arc<ZetaCliAppState>,
717    cx: &mut AsyncApp,
718) -> Result<zeta::GatherContextOutput> {
719    let LoadedContext {
720        full_path_str,
721        snapshot,
722        clipped_cursor,
723        ..
724    } = load_context(&args, app_state, cx).await?;
725
726    let events = match args.edit_history {
727        Some(events) => events.read_to_string().await?,
728        None => String::new(),
729    };
730
731    let prompt_for_events = move || (events, 0);
732    cx.update(|cx| {
733        zeta::gather_context(
734            full_path_str,
735            &snapshot,
736            clipped_cursor,
737            prompt_for_events,
738            cx,
739        )
740    })?
741    .await
742}
743
744fn main() {
745    zlog::init();
746    zlog::init_output_stderr();
747    let args = ZetaCliArgs::parse();
748    let http_client = Arc::new(ReqwestClient::new());
749    let app = Application::headless().with_http_client(http_client);
750
751    app.run(move |cx| {
752        let app_state = Arc::new(headless::init(cx));
753        cx.spawn(async move |cx| {
754            let result = match args.command {
755                Command::Zeta1 {
756                    command: Zeta1Command::Context { context_args },
757                } => {
758                    let context = zeta1_context(context_args, &app_state, cx).await.unwrap();
759                    serde_json::to_string_pretty(&context.body).map_err(|err| anyhow::anyhow!(err))
760                }
761                Command::Zeta2 { command } => match command {
762                    Zeta2Command::Predict { example_path } => {
763                        let example = NamedExample::load(example_path).unwrap();
764                        zeta2_predict(example, &app_state, cx).await.unwrap();
765                        return;
766                    }
767                    Zeta2Command::Syntax {
768                        args,
769                        syntax_args,
770                        command,
771                    } => match command {
772                        Zeta2SyntaxCommand::Context { context_args } => {
773                            zeta2_syntax_context(args, syntax_args, context_args, &app_state, cx)
774                                .await
775                        }
776                        Zeta2SyntaxCommand::Stats {
777                            worktree,
778                            extension,
779                            limit,
780                            skip,
781                        } => {
782                            retrieval_stats(
783                                worktree,
784                                app_state,
785                                extension,
786                                limit,
787                                skip,
788                                syntax_args_to_options(&args, &syntax_args, false),
789                                cx,
790                            )
791                            .await
792                        }
793                    },
794                    Zeta2Command::Llm { args, command } => match command {
795                        Zeta2LlmCommand::Context { context_args } => {
796                            zeta2_llm_context(args, context_args, &app_state, cx).await
797                        }
798                    },
799                },
800                Command::ConvertExample {
801                    path,
802                    output_format,
803                } => {
804                    let example = NamedExample::load(path).unwrap();
805                    example.write(output_format, io::stdout()).unwrap();
806                    let _ = cx.update(|cx| cx.quit());
807                    return;
808                }
809            };
810
811            match result {
812                Ok(output) => {
813                    println!("{}", output);
814                    let _ = cx.update(|cx| cx.quit());
815                }
816                Err(e) => {
817                    eprintln!("Failed: {:?}", e);
818                    exit(1);
819                }
820            }
821        })
822        .detach();
823    });
824}