@pheem49/mint 1.8.0 → 1.8.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (200) hide show
  1. package/.github/workflows/release.yml +1 -1
  2. package/Cargo.lock +34 -3
  3. package/Cargo.toml +2 -2
  4. package/README.md +20 -6
  5. package/Release_Note.md +129 -0
  6. package/assets/Mint_CLI.png +0 -0
  7. package/bin/mint +0 -0
  8. package/crates/mint-cli/Cargo.toml +1 -0
  9. package/crates/mint-cli/src/agent.rs +242 -54
  10. package/crates/mint-cli/src/main.rs +397 -74
  11. package/crates/mint-core/src/agent_loop.rs +1 -0
  12. package/crates/mint-core/src/api_server.rs +64 -2
  13. package/crates/mint-core/src/browser.rs +456 -0
  14. package/crates/mint-core/src/channels.rs +1 -0
  15. package/crates/mint-core/src/chat.rs +3 -0
  16. package/crates/mint-core/src/config.rs +130 -1
  17. package/crates/mint-core/src/lib.rs +19 -0
  18. package/crates/mint-core/src/orchestration.rs +317 -12
  19. package/crates/mint-core/src/skills.rs +113 -4
  20. package/crates/mint-core/src/tts.rs +3 -1
  21. package/crates/mint-core/src/workflows.rs +1 -22
  22. package/flatpak/com.pheem49.mint.desktop +9 -0
  23. package/flatpak/com.pheem49.mint.metainfo.xml +35 -0
  24. package/flatpak/com.pheem49.mint.yaml +52 -0
  25. package/package.json +1 -1
  26. package/shared-modules/.github/workflows/ci.yml +120 -0
  27. package/shared-modules/.github/workflows/update.yaml +28 -0
  28. package/shared-modules/CODEOWNERS +23 -0
  29. package/shared-modules/README.md +79 -0
  30. package/shared-modules/SDL/README.md +3 -0
  31. package/shared-modules/SDL/SDL-1.2.15.json +51 -0
  32. package/shared-modules/SDL/SDL_Pango-0.1.2-API-adds.patch +118 -0
  33. package/shared-modules/SDL/SDL_image-1.2.12.json +25 -0
  34. package/shared-modules/SDL/SDL_mixer-1.2.12.json +27 -0
  35. package/shared-modules/SDL/SDL_net-1.2.8.json +19 -0
  36. package/shared-modules/SDL/SDL_pango-0.1.2.json +26 -0
  37. package/shared-modules/SDL/SDL_ttf-2.0.11.json +28 -0
  38. package/shared-modules/SDL/libsdl-1.2.15-strict-prototypes.patch +316 -0
  39. package/shared-modules/SDL/sdl-check-for-SDL_VIDEO_X11_BACKINGSTORE.patch +24 -0
  40. package/shared-modules/SDL/sdl-libx11-build.patch +59 -0
  41. package/shared-modules/SDL/sdl-sysloadso-buffer-length.patch +27 -0
  42. package/shared-modules/SDL/sdl12-compat.json +22 -0
  43. package/shared-modules/SDL2/SDL2_image.json +27 -0
  44. package/shared-modules/SDL2/SDL2_ttf.json +29 -0
  45. package/shared-modules/cld2/CMakeLists.txt +157 -0
  46. package/shared-modules/cld2/cld2.json +24 -0
  47. package/shared-modules/clutter/clutter.json +72 -0
  48. package/shared-modules/dbus-glib/dbus-glib.json +29 -0
  49. package/shared-modules/glew/glew.json +36 -0
  50. package/shared-modules/glu/glu-9.json +12 -0
  51. package/shared-modules/gtk2/arc-gtk2-theme-Replace-Inkscape-dependency-with-rsvg-convert.patch +180 -0
  52. package/shared-modules/gtk2/gtk2-common-themes.json +360 -0
  53. package/shared-modules/gtk2/gtk2-fix-crash-in-show-uri.patch +30 -0
  54. package/shared-modules/gtk2/gtk2-gcc14.patch +281 -0
  55. package/shared-modules/gtk2/gtk2-gcc15.patch +250 -0
  56. package/shared-modules/gtk2/gtk2-murrine-c99.patch +34 -0
  57. package/shared-modules/gtk2/gtk2-use-adwaita-theme.patch +80 -0
  58. package/shared-modules/gtk2/gtk2.json +120 -0
  59. package/shared-modules/gtk2/ibus-gcc15.patch +13 -0
  60. package/shared-modules/gtk2/murrine-engine-fix-crash.patch +12 -0
  61. package/shared-modules/gtk2/readme.md +10 -0
  62. package/shared-modules/intltool/intltool-0.51.json +20 -0
  63. package/shared-modules/intltool/intltool-perl5.26-regex-fixes.patch +59 -0
  64. package/shared-modules/lame/0001-Build-xmm_quantize_sub.c-with-msse.patch +34 -0
  65. package/shared-modules/lame/0002-Include-GTK-1-autoconf-directives-in-build-system.patch +222 -0
  66. package/shared-modules/lame/0003-Patch-out-remaining-ansi2knr.patch +70 -0
  67. package/shared-modules/lame/0004-initscr-is-not-used-anywhere-in-lame-sourcetree.patch +41 -0
  68. package/shared-modules/lame/0005-Update-autoconf.patch +81 -0
  69. package/shared-modules/lame/lame-3.99.5.json +32 -0
  70. package/shared-modules/libappindicator/libappindicator-gtk2-12.10.json +140 -0
  71. package/shared-modules/libappindicator/libappindicator-gtk3-12.10.json +140 -0
  72. package/shared-modules/libappindicator/libappindicator-gtk3-introspection-12.10.json +140 -0
  73. package/shared-modules/libappindicator/patches/0001-libappindicator-ftbfs.patch +20 -0
  74. package/shared-modules/libappindicator/patches/0002-libappindicator-no-python.patch +888 -0
  75. package/shared-modules/libappindicator/patches/0003-libappindicator-fix-crash-from-incorrect-signal-emission.patch +74 -0
  76. package/shared-modules/libappindicator/patches/0004-libappindicator-fix-crash-iterating-icon-themes.patch +11 -0
  77. package/shared-modules/libayatana-appindicator/0001-Fix-HAVE_VALGRIND-AM_CONDITIONAL.patch +51 -0
  78. package/shared-modules/libayatana-appindicator/0001-Make-introspection-configurable.patch +44 -0
  79. package/shared-modules/libayatana-appindicator/libayatana-appindicator-gtk3.json +121 -0
  80. package/shared-modules/libcanberra/0001-gtk-Don-t-assume-all-GdkDisplays-are-GdkX11Displays-.patch +123 -0
  81. package/shared-modules/libcanberra/libcanberra.json +44 -0
  82. package/shared-modules/libmad/libmad.json +19 -0
  83. package/shared-modules/libsecret/libsecret.json +23 -0
  84. package/shared-modules/libsoup/libsoup-2.4.json +14 -0
  85. package/shared-modules/libusb/libusb.json +27 -0
  86. package/shared-modules/linux-audio/dssi.json +31 -0
  87. package/shared-modules/linux-audio/fftw3f-static.json +44 -0
  88. package/shared-modules/linux-audio/fftw3f.json +42 -0
  89. package/shared-modules/linux-audio/fluidsynth2-static.json +30 -0
  90. package/shared-modules/linux-audio/fluidsynth2.json +19 -0
  91. package/shared-modules/linux-audio/ladspa.json +26 -0
  92. package/shared-modules/linux-audio/lash.json +35 -0
  93. package/shared-modules/linux-audio/libinstpatch.json +16 -0
  94. package/shared-modules/linux-audio/liblo-static.json +22 -0
  95. package/shared-modules/linux-audio/liblo.json +17 -0
  96. package/shared-modules/linux-audio/lilv-static.json +94 -0
  97. package/shared-modules/linux-audio/lilv.json +82 -0
  98. package/shared-modules/linux-audio/lrdf.json +57 -0
  99. package/shared-modules/linux-audio/lv2.json +21 -0
  100. package/shared-modules/linux-audio/patches/lash/0001-configure-remove-aclocal-m4.patch +29 -0
  101. package/shared-modules/linux-audio/patches/lash/0002-Fix-build-with-recent-gcc.patch +73 -0
  102. package/shared-modules/linux-audio/patches/lash/0003-Fix-linking.patch +136 -0
  103. package/shared-modules/linux-audio/patches/raptor-libxml.patch +31 -0
  104. package/shared-modules/linux-audio/patches/stk-install.sh +54 -0
  105. package/shared-modules/linux-audio/stk.json +28 -0
  106. package/shared-modules/lua5.1/0004-Fix-stack-overflow-in-vararg-functions.patch +21 -0
  107. package/shared-modules/lua5.1/lua-5.1.5-so.patch +43 -0
  108. package/shared-modules/lua5.1/lua-5.1.5.json +42 -0
  109. package/shared-modules/lua5.3/lua-5.3.5.json +41 -0
  110. package/shared-modules/lua5.3/lua.pc.in +8 -0
  111. package/shared-modules/lua5.3/luaconf.patch +47 -0
  112. package/shared-modules/lua5.3/makefile-add-liblua.so-rule.patch +47 -0
  113. package/shared-modules/lua5.4/lua-5.4.json +41 -0
  114. package/shared-modules/lua5.4/lua.pc.in +8 -0
  115. package/shared-modules/lua5.4/luaconf.patch +34 -0
  116. package/shared-modules/lua5.4/makefile-add-liblua.so-rule.patch +42 -0
  117. package/shared-modules/luajit/luajit.json +30 -0
  118. package/shared-modules/lzo/lzo.json +27 -0
  119. package/shared-modules/mac/mac.json +18 -0
  120. package/shared-modules/physfs/physfs.json +26 -0
  121. package/shared-modules/pygame/audiofile-gcc6.patch +21 -0
  122. package/shared-modules/pygame/cython-fixes.patch +34 -0
  123. package/shared-modules/pygame/fluidsynth-no-rawmidi.patch +69 -0
  124. package/shared-modules/pygame/glib.patch +25 -0
  125. package/shared-modules/pygame/pygame1-python2.json +157 -0
  126. package/shared-modules/pygtk/pygtk-c99.patch +15 -0
  127. package/shared-modules/pygtk/pygtk.json +117 -0
  128. package/shared-modules/pygtk/pygtk.patch +39 -0
  129. package/shared-modules/python2.7/python-2.7.json +48 -0
  130. package/shared-modules/smpeg/smpeg-0.4.5.json +50 -0
  131. package/shared-modules/smpeg/smpeg-am-prog-as.patch +12 -0
  132. package/shared-modules/smpeg/smpeg-export-mpegaudio-class.patch +17 -0
  133. package/shared-modules/smpeg/smpeg-gcc6.patch +40 -0
  134. package/shared-modules/smpeg/smpeg-no-gtk.patch +37 -0
  135. package/shared-modules/squashfs-tools/squashfs-tools.json +25 -0
  136. package/shared-modules/update-checker.sh +50 -0
  137. package/shared-modules/vorbisgain/0001-temp_files.patch +75 -0
  138. package/shared-modules/vorbisgain/0002-errno.patch +33 -0
  139. package/shared-modules/vorbisgain/0003-manpage.patch +248 -0
  140. package/shared-modules/vorbisgain/0004-vorbisgain_mtime.patch +183 -0
  141. package/shared-modules/vorbisgain/0005-double_fclose.patch +21 -0
  142. package/shared-modules/vorbisgain/0006-manpage_hyphens.patch +56 -0
  143. package/shared-modules/vorbisgain/0007-recursively_spelling.patch +14 -0
  144. package/shared-modules/vorbisgain/0008-manpage_recursion_mistake.patch +23 -0
  145. package/shared-modules/vorbisgain/0009-hardening.patch +13 -0
  146. package/shared-modules/vorbisgain/0010-fclose.patch +13 -0
  147. package/shared-modules/vorbisgain/README.md +23 -0
  148. package/shared-modules/vorbisgain/vorbisgain_0.37-2.json +29 -0
  149. package/src/renderer/shared/agentProgress.ts +145 -0
  150. package/src/renderer/shared/calculator.ts +52 -0
  151. package/src/renderer/shared/components/AgentActivityDrawer.tsx +72 -0
  152. package/src/renderer/shared/components/AgentActivityTable.tsx +66 -0
  153. package/src/renderer/shared/components/ApprovalCard.tsx +93 -0
  154. package/src/renderer/shared/components/ChatCodeBlock.tsx +75 -0
  155. package/src/renderer/shared/components/ThinkingBlock.tsx +101 -0
  156. package/src/renderer/shared/constants/models.ts +91 -0
  157. package/src/renderer/shared/platform.ts +82 -0
  158. package/src/renderer/shared/types.ts +140 -0
  159. package/src/renderer/shared/utils/agentActivity.ts +187 -0
  160. package/src/renderer/shared/utils/approval.ts +1 -0
  161. package/src/renderer/shared/utils/approval.tsx +64 -0
  162. package/src/renderer/shared/utils/markdown.tsx +347 -0
  163. package/src/renderer/shared/utils/providers.ts +36 -0
  164. package/src/renderer/shared/utils/speech.ts +216 -0
  165. package/src/renderer/shared/utils/ui.ts +147 -0
  166. package/src/renderer/src/agentProgress.ts +11 -39
  167. package/src/renderer/src/calculator.ts +3 -46
  168. package/src/renderer/src/components/ChatPanel.tsx +230 -1202
  169. package/src/renderer/src/components/ImageStudioPanel.tsx +11 -1
  170. package/src/renderer/src/components/MintDashboard.tsx +63 -171
  171. package/src/renderer/src/components/PicturesLibrary.tsx +32 -8
  172. package/src/renderer/src/components/Settings/AgentsTab.tsx +362 -0
  173. package/src/renderer/src/components/Settings/AutomationTab.tsx +0 -22
  174. package/src/renderer/src/components/Settings/PluginsTab.tsx +221 -41
  175. package/src/renderer/src/components/SettingsWindow.tsx +54 -12
  176. package/src/renderer/src/components/WorkspacePanel.tsx +66 -9
  177. package/src/renderer/src/css/image-studio.css +6 -0
  178. package/src/renderer/src/css/styles.css +148 -11
  179. package/src/renderer/src/index.css +12 -1
  180. package/src/renderer/src/tauri.ts +169 -143
  181. package/src/renderer/src-web/agentProgress.ts +11 -39
  182. package/src/renderer/src-web/calculator.ts +3 -46
  183. package/src/renderer/src-web/components/ChatPanel.tsx +223 -1199
  184. package/src/renderer/src-web/components/ImageStudioPanel.tsx +11 -1
  185. package/src/renderer/src-web/components/MintDashboard.tsx +31 -172
  186. package/src/renderer/src-web/components/PicturesLibrary.tsx +23 -29
  187. package/src/renderer/src-web/components/Settings/AgentsTab.tsx +362 -0
  188. package/src/renderer/src-web/components/Settings/AutomationTab.tsx +0 -22
  189. package/src/renderer/src-web/components/Settings/PluginsTab.tsx +221 -41
  190. package/src/renderer/src-web/components/SettingsWindow.tsx +54 -12
  191. package/src/renderer/src-web/css/image-studio.css +6 -0
  192. package/src/renderer/src-web/css/styles.css +184 -12
  193. package/src/renderer/src-web/index.css +12 -1
  194. package/src/renderer/src-web/tauri.ts +204 -104
  195. package/src-tauri/src/browser.rs +1 -141
  196. package/src-tauri/src/headless.rs +3 -2
  197. package/src-tauri/src/lib.rs +254 -59
  198. package/src-tauri/src/webhooks.rs +1 -0
  199. package/src-tauri/src/workflows.rs +57 -4
  200. package/src-tauri/tauri.conf.json +1 -1
@@ -184,13 +184,19 @@ pub async fn run_code_agent_with_options(
184
184
  }
185
185
  let progress_live_status = Arc::clone(&live_status);
186
186
  let progress_cb = |progress: AgentProgress| match progress {
187
- AgentProgress::Thinking { elapsed_secs } => {
187
+ AgentProgress::Thinking {
188
+ elapsed_secs,
189
+ agent_name,
190
+ model_name,
191
+ } => {
188
192
  if !options.fast_mode {
189
193
  if let Ok(mut status) = progress_live_status.lock() {
190
- status.thinking = Some(format!(
191
- "Thinking ({} • Esc to interrupt)",
192
- format_elapsed(Duration::from_secs(elapsed_secs))
193
- ));
194
+ let label = if let (Some(a), Some(m)) = (agent_name, model_name) {
195
+ format!("{} ({}) is thinking ({} • Esc to interrupt)", a, m, format_elapsed(Duration::from_secs(elapsed_secs)))
196
+ } else {
197
+ format!("Thinking ({} • Esc to interrupt)", format_elapsed(Duration::from_secs(elapsed_secs)))
198
+ };
199
+ status.thinking = Some(label);
194
200
  render_live_status(&mut status);
195
201
  }
196
202
  }
@@ -325,6 +331,16 @@ pub async fn run_code_agent_with_options(
325
331
  render_live_status(&mut status);
326
332
  }
327
333
  }
334
+ // Parse web search sources from the result and store them for display
335
+ if action == "web_search"
336
+ && !result.starts_with("Web search error:")
337
+ && result != "No web search results found."
338
+ {
339
+ if let Ok(mut status) = progress_live_status.lock() {
340
+ let sources = parse_web_search_sources(&result);
341
+ status.web_sources.extend(sources);
342
+ }
343
+ }
328
344
  }
329
345
  };
330
346
 
@@ -337,9 +353,23 @@ pub async fn run_code_agent_with_options(
337
353
  clear_live_status(&mut status);
338
354
  }
339
355
  }
340
- let formatted_summary = format_markdown_bold(&summary);
341
- print!("\n{MINT}Mint:{RESET} ");
356
+ let formatted_summary = format_markdown_bold(&sanitize_latex(&summary));
357
+ print!("\n {MINT}Mint:{RESET} ");
342
358
  render_live_summary(&formatted_summary);
359
+
360
+ // Print web search sources if any were collected
361
+ if let Ok(mut status) = chunk_live_status.lock() {
362
+ if !status.web_sources.is_empty() {
363
+ println!();
364
+ println!(" {DIM}Sources:{RESET}");
365
+ for (i, (title, url)) in status.web_sources.iter().enumerate() {
366
+ println!(" {DIM}{}.{RESET} {BLUE}{}{RESET}", i + 1, title);
367
+ println!(" {DIM}{}{RESET}", url);
368
+ }
369
+ status.web_sources.clear();
370
+ }
371
+ }
372
+
343
373
  println!();
344
374
  };
345
375
 
@@ -349,6 +379,7 @@ pub async fn run_code_agent_with_options(
349
379
  root,
350
380
  image_data_uri,
351
381
  Some(CHAT_CLI_ID),
382
+ None,
352
383
  options.fast_mode,
353
384
  approve_cb,
354
385
  progress_cb,
@@ -377,16 +408,16 @@ pub async fn run_code_agent_with_options(
377
408
  let res = res.map_err(|e| anyhow!("{}", e))?;
378
409
 
379
410
  if should_show_verification(&res.verification) {
380
- println!("Verification: {}", res.verification);
411
+ println!(" Verification: {}", res.verification);
381
412
  }
382
413
  println!(
383
- "{DIM}─ Worked for {}{RESET}",
414
+ " {DIM}─ Worked for {}{RESET}",
384
415
  format_elapsed(started_at.elapsed())
385
416
  );
386
417
 
387
418
  let (tw, _) = crossterm::terminal::size().unwrap_or((80, 24));
388
419
  let width = tw as usize;
389
- println!("{DIM}{}{RESET}", "─".repeat(width));
420
+ println!(" {DIM}{}{RESET}", "─".repeat(width.saturating_sub(2)));
390
421
 
391
422
  Ok(res)
392
423
  }
@@ -396,6 +427,34 @@ fn clear_working_status() {
396
427
  let _ = io::stdout().flush();
397
428
  }
398
429
 
430
+ /// Parse (title, url) pairs from the formatted `web_search` ToolEnd result text.
431
+ /// The format produced by orchestration.rs is:
432
+ /// 1. Title\n URL: https://...\n Snippet\n
433
+ fn parse_web_search_sources(result: &str) -> Vec<(String, String)> {
434
+ let mut sources = Vec::new();
435
+ let mut current_title: Option<String> = None;
436
+ for line in result.lines() {
437
+ let trimmed = line.trim();
438
+ // Match numbered title lines: "1. Title text"
439
+ if let Some(rest) = trimmed.split_once(". ") {
440
+ if rest.0.parse::<usize>().is_ok() {
441
+ current_title = Some(rest.1.trim().to_owned());
442
+ continue;
443
+ }
444
+ }
445
+ // Match URL lines: "URL: https://..."
446
+ if let Some(url) = trimmed.strip_prefix("URL: ") {
447
+ if let Some(title) = current_title.take() {
448
+ let url = url.trim().to_owned();
449
+ if !url.is_empty() {
450
+ sources.push((title, url));
451
+ }
452
+ }
453
+ }
454
+ }
455
+ sources
456
+ }
457
+
399
458
  fn format_elapsed(duration: Duration) -> String {
400
459
  let total_seconds = duration.as_secs();
401
460
  let minutes = total_seconds / 60;
@@ -408,19 +467,26 @@ fn format_elapsed(duration: Duration) -> String {
408
467
  }
409
468
 
410
469
  fn render_live_summary(summary: &str) {
411
- let mut chunk = String::new();
412
- for character in summary.chars() {
413
- chunk.push(character);
414
- if chunk.chars().count() >= 96 {
415
- print!("{chunk}");
416
- let _ = io::stdout().flush();
417
- chunk.clear();
470
+ let (tw, _) = crossterm::terminal::size().unwrap_or((80, 24));
471
+ let width = tw as usize;
472
+
473
+ let mut is_first = true;
474
+ for line in summary.split('\n') {
475
+ let indent = if is_first { "" } else { " " };
476
+ let options = textwrap::Options::new(width)
477
+ .initial_indent(indent)
478
+ .subsequent_indent(" ")
479
+ .break_words(true);
480
+ let wrapped = textwrap::fill(line, &options);
481
+
482
+ if is_first {
483
+ print!("{wrapped}");
484
+ is_first = false;
485
+ } else {
486
+ print!("\n{wrapped}");
418
487
  }
419
488
  }
420
- if !chunk.is_empty() {
421
- print!("{chunk}");
422
- let _ = io::stdout().flush();
423
- }
489
+ let _ = io::stdout().flush();
424
490
  }
425
491
 
426
492
  fn print_colored_diff(diff: &str) {
@@ -480,6 +546,8 @@ struct LiveStatus {
480
546
  committed_tasks: usize,
481
547
  rendered_lines: usize,
482
548
  spinner_tick: usize,
549
+ /// Sources collected from web_search ToolEnd results (title, url)
550
+ web_sources: Vec<(String, String)>,
483
551
  }
484
552
 
485
553
  #[derive(Debug, Clone)]
@@ -497,13 +565,21 @@ fn explored_action_label(action: &str, input: &serde_json::Value) -> Option<Expl
497
565
  kind: "[list_files] List",
498
566
  target: display_tool_target(path),
499
567
  }),
500
- "read_file" => input
501
- .get("path")
502
- .and_then(|v| v.as_str())
503
- .map(|path| ExploredAction {
568
+ "read_file" => {
569
+ let path = input.get("path").and_then(|v| v.as_str())?;
570
+ let start = input.get("startLine").and_then(|v| v.as_u64());
571
+ let end = input.get("endLine").and_then(|v| v.as_u64());
572
+ let file_name = display_tool_target(path);
573
+ let target = match (start, end) {
574
+ (Some(s), Some(e)) => format!("{} #L{}-{}", file_name, s, e),
575
+ (Some(s), None) => format!("{} #L{}", file_name, s),
576
+ _ => file_name,
577
+ };
578
+ Some(ExploredAction {
504
579
  kind: "[read_file] Read",
505
- target: display_tool_target(path),
506
- }),
580
+ target,
581
+ })
582
+ }
507
583
  "search_code" => {
508
584
  let query = input.get("query").and_then(|v| v.as_str())?;
509
585
  let path = input.get("path").and_then(|v| v.as_str()).unwrap_or(".");
@@ -540,6 +616,30 @@ fn display_tool_target(path: &str) -> String {
540
616
  }
541
617
  }
542
618
 
619
+ fn strip_ansi_escapes(s: &str) -> String {
620
+ let mut result = String::new();
621
+ let mut in_escape = false;
622
+ for c in s.chars() {
623
+ if c == '\x1b' {
624
+ in_escape = true;
625
+ } else if in_escape {
626
+ if c.is_ascii_alphabetic() {
627
+ in_escape = false;
628
+ }
629
+ } else {
630
+ result.push(c);
631
+ }
632
+ }
633
+ result
634
+ }
635
+
636
+ fn is_thai_combining(c: char) -> bool {
637
+ matches!(c,
638
+ '\u{0e31}' | '\u{0e34}'..='\u{0e37}' | '\u{0e38}'..='\u{0e39}' |
639
+ '\u{0e47}'..='\u{0e4e}'
640
+ )
641
+ }
642
+
543
643
  fn render_live_status(status: &mut LiveStatus) {
544
644
  clear_live_status(status);
545
645
  let mut lines = Vec::new();
@@ -547,23 +647,48 @@ fn render_live_status(status: &mut LiveStatus) {
547
647
  let activities_start = status.committed_activities.min(status.activities.len());
548
648
  let tasks_start = status.committed_tasks.min(status.tasks.len());
549
649
 
550
- lines.extend(plan_lines(&status.plan_steps));
551
- lines.extend(tasks_lines(&status.tasks[tasks_start..]));
552
- lines.extend(activities_lines(&status.activities[activities_start..]));
553
- lines.extend(explored_lines(&status.explored[explored_start..]));
650
+ let is_thinking = status.thinking.is_some();
651
+ let tick = status.spinner_tick;
652
+
653
+ lines.extend(plan_lines(&status.plan_steps, is_thinking, tick));
654
+ lines.extend(tasks_lines(&status.tasks[tasks_start..], is_thinking, tick));
655
+ lines.extend(activities_lines(
656
+ &status.activities[activities_start..],
657
+ is_thinking,
658
+ tick,
659
+ ));
660
+ lines.extend(explored_lines(
661
+ &status.explored[explored_start..],
662
+ is_thinking,
663
+ tick,
664
+ ));
554
665
  if let Some(thinking) = &status.thinking {
555
- let frames = &["⣾", "⣽", "⣻", "⢿", "⡿", "⣟", "⣯", "⣷"];
666
+ let frames = &["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
556
667
  let frame = frames[status.spinner_tick % frames.len()];
557
668
  status.spinner_tick += 1;
558
- lines.push(format!("{MINT}{frame}{RESET} {BRIGHT}{thinking}{RESET}"));
669
+ lines.push(format!(" {MINT}{frame}{RESET} {BRIGHT}{thinking}{RESET}"));
559
670
  }
560
671
  if lines.is_empty() {
561
672
  return;
562
673
  }
674
+
675
+ let (tw, _) = crossterm::terminal::size().unwrap_or((80, 24));
676
+ let width = tw as usize;
677
+
678
+ let mut physical_lines_count = 0;
563
679
  for line in &lines {
680
+ let stripped = strip_ansi_escapes(line);
681
+ let line_len = stripped.chars().filter(|&c| !is_thai_combining(c)).count();
682
+ let physical_lines = if width > 0 {
683
+ (line_len + width - 1) / width
684
+ } else {
685
+ 1
686
+ }
687
+ .max(1);
688
+ physical_lines_count += physical_lines;
564
689
  println!("{line}");
565
690
  }
566
- status.rendered_lines = lines.len();
691
+ status.rendered_lines = physical_lines_count;
567
692
  let _ = io::stdout().flush();
568
693
  }
569
694
 
@@ -573,9 +698,13 @@ fn commit_activity_snapshot(status: &mut LiveStatus) {
573
698
  let activities_start = status.committed_activities.min(status.activities.len());
574
699
  let tasks_start = status.committed_tasks.min(status.tasks.len());
575
700
 
576
- let mut lines = explored_lines(&status.explored[explored_start..]);
577
- lines.extend(activities_lines(&status.activities[activities_start..]));
578
- lines.extend(tasks_lines(&status.tasks[tasks_start..]));
701
+ let mut lines = explored_lines(&status.explored[explored_start..], false, 0);
702
+ lines.extend(activities_lines(
703
+ &status.activities[activities_start..],
704
+ false,
705
+ 0,
706
+ ));
707
+ lines.extend(tasks_lines(&status.tasks[tasks_start..], false, 0));
579
708
  if lines.is_empty() {
580
709
  return;
581
710
  }
@@ -594,7 +723,14 @@ fn print_timeline_note(thought: &str) {
594
723
  if thought.is_empty() {
595
724
  return;
596
725
  }
597
- println!("{DIM}• {thought}{RESET}");
726
+ let (tw, _) = crossterm::terminal::size().unwrap_or((80, 24));
727
+ let width = tw as usize;
728
+ let options = textwrap::Options::new(width)
729
+ .initial_indent(" • ")
730
+ .subsequent_indent(" ")
731
+ .break_words(true);
732
+ let wrapped = textwrap::fill(thought, &options);
733
+ println!("{DIM}{wrapped}{RESET}");
598
734
  }
599
735
 
600
736
  fn print_timeline_separator() {
@@ -615,18 +751,34 @@ fn clear_live_status(status: &mut LiveStatus) {
615
751
  let _ = io::stdout().flush();
616
752
  }
617
753
 
618
- fn explored_lines(actions: &[ExploredAction]) -> Vec<String> {
754
+ fn get_bullet(name: &str, is_thinking: bool, tick: usize) -> String {
755
+ let char_str = if is_thinking {
756
+ if (tick / 4) % 2 == 0 { "●" } else { "○" }
757
+ } else {
758
+ "●"
759
+ };
760
+
761
+ match name {
762
+ "plan" => format!("{BLUE}{char_str}{RESET} plan"),
763
+ "tasks" => format!("{MINT}{char_str}{RESET} tasks"),
764
+ "activity" => format!("{BLUE}{char_str}{RESET} activity"),
765
+ "explored" => format!("{DIM}{char_str}{RESET} explored"),
766
+ _ => char_str.to_string(),
767
+ }
768
+ }
769
+
770
+ fn explored_lines(actions: &[ExploredAction], is_thinking: bool, tick: usize) -> Vec<String> {
619
771
  if actions.is_empty() {
620
772
  return Vec::new();
621
773
  }
622
774
  let grouped = grouped_explored_actions(actions);
623
- let mut lines = vec![format!("{BLUE}○ explored{RESET}")];
775
+ let mut lines = vec![format!(" {}", get_bullet("explored", is_thinking, tick))];
624
776
  lines.extend(grouped.iter().take(24).enumerate().map(|(index, action)| {
625
- let prefix = if index == 0 { " └" } else { " " };
777
+ let prefix = if index == 0 { " └" } else { " " };
626
778
  format!("{DIM}{prefix} {action}{RESET}")
627
779
  }));
628
780
  if grouped.len() > 24 {
629
- lines.push(format!("{DIM} ... {} more{RESET}", grouped.len() - 24));
781
+ lines.push(format!("{DIM} ... {} more{RESET}", grouped.len() - 24));
630
782
  }
631
783
  lines
632
784
  }
@@ -658,17 +810,20 @@ fn command_was_run(result: &str) -> bool {
658
810
  result.lines().any(|line| line.starts_with("exit: "))
659
811
  }
660
812
 
661
- fn activities_lines(activities: &[String]) -> Vec<String> {
813
+ fn activities_lines(activities: &[String], is_thinking: bool, tick: usize) -> Vec<String> {
662
814
  if activities.is_empty() {
663
815
  return Vec::new();
664
816
  }
665
- let mut lines = vec![format!("{BLUE}◇ activity{RESET}")];
817
+ let mut lines = vec![format!(" {}", get_bullet("activity", is_thinking, tick))];
666
818
  lines.extend(activities.iter().take(24).enumerate().map(|(index, act)| {
667
- let prefix = if index == 0 { " └" } else { " " };
819
+ let prefix = if index == 0 { " └" } else { " " };
668
820
  format!("{DIM}{prefix} {act}{RESET}")
669
821
  }));
670
822
  if activities.len() > 24 {
671
- lines.push(format!("{DIM} ... {} more{RESET}", activities.len() - 24));
823
+ lines.push(format!(
824
+ "{DIM} ... {} more{RESET}",
825
+ activities.len() - 24
826
+ ));
672
827
  }
673
828
  lines
674
829
  }
@@ -685,16 +840,16 @@ fn extract_plan_steps(input: &serde_json::Value) -> Option<Vec<String>> {
685
840
  Some(steps)
686
841
  }
687
842
 
688
- fn plan_lines(steps: &[String]) -> Vec<String> {
843
+ fn plan_lines(steps: &[String], is_thinking: bool, tick: usize) -> Vec<String> {
689
844
  if steps.is_empty() {
690
845
  return Vec::new();
691
846
  }
692
- let mut lines = vec![format!("{BLUE}● plan{RESET}")];
847
+ let mut lines = vec![format!(" {}", get_bullet("plan", is_thinking, tick))];
693
848
  for (index, step) in steps.iter().enumerate() {
694
849
  let prefix = if index == steps.len() - 1 {
695
- " └"
850
+ " └"
696
851
  } else {
697
- " ├"
852
+ " ├"
698
853
  };
699
854
  let (checked, text) = if step.to_lowercase().starts_with("done:") {
700
855
  (format!("{MINT}[x]{RESET}"), step["done:".len()..].trim())
@@ -722,17 +877,17 @@ fn plan_lines(steps: &[String]) -> Vec<String> {
722
877
  lines
723
878
  }
724
879
 
725
- fn tasks_lines(tasks: &[String]) -> Vec<String> {
880
+ fn tasks_lines(tasks: &[String], is_thinking: bool, tick: usize) -> Vec<String> {
726
881
  if tasks.is_empty() {
727
882
  return Vec::new();
728
883
  }
729
- let mut lines = vec![format!("{BLUE}● tasks{RESET}")];
884
+ let mut lines = vec![format!(" {}", get_bullet("tasks", is_thinking, tick))];
730
885
  lines.extend(tasks.iter().take(24).enumerate().map(|(index, task)| {
731
- let prefix = if index == 0 { " └" } else { " " };
886
+ let prefix = if index == 0 { " └" } else { " " };
732
887
  format!("{DIM}{prefix} {task}{RESET}")
733
888
  }));
734
889
  if tasks.len() > 24 {
735
- lines.push(format!("{DIM} ... {} more{RESET}", tasks.len() - 24));
890
+ lines.push(format!("{DIM} ... {} more{RESET}", tasks.len() - 24));
736
891
  }
737
892
  lines
738
893
  }
@@ -881,3 +1036,36 @@ fn process_inline_bold(text: &str) -> String {
881
1036
  }
882
1037
  result
883
1038
  }
1039
+
1040
+ /// Replace common LaTeX math symbols with Unicode equivalents.
1041
+ /// Fixes garbled output like "ightarrow$" from models that emit LaTeX notation.
1042
+ fn sanitize_latex(text: &str) -> String {
1043
+ let mut s = text.to_owned();
1044
+ for (pat, uni) in [
1045
+ // arrows
1046
+ ("$\\rightarrow$", "→"),
1047
+ ("$\\leftarrow$", "←"),
1048
+ ("$\\Rightarrow$", "⇒"),
1049
+ ("$\\Leftarrow$", "⇐"),
1050
+ ("$\\leftrightarrow$", "↔"),
1051
+ // comparison
1052
+ ("$\\leq$", "≤"),
1053
+ ("$\\geq$", "≥"),
1054
+ ("$\\neq$", "≠"),
1055
+ ("$\\approx$", "≈"),
1056
+ // math
1057
+ ("$\\times$", "×"),
1058
+ ("$\\div$", "÷"),
1059
+ ("$\\pm$", "±"),
1060
+ ("$\\infty$", "∞"),
1061
+ ("$\\cdot$", "·"),
1062
+ // sets
1063
+ ("$\\in$", "∈"),
1064
+ ("$\\subset$", "⊂"),
1065
+ ("$\\cup$", "∪"),
1066
+ ("$\\cap$", "∩"),
1067
+ ] {
1068
+ s = s.replace(pat, uni);
1069
+ }
1070
+ s
1071
+ }