@pheem49/mint 1.8.0 → 1.8.1

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 (41) 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 +31 -0
  5. package/Release_Note.md +95 -0
  6. package/assets/Mint_CLI.png +0 -0
  7. package/crates/mint-cli/Cargo.toml +1 -0
  8. package/crates/mint-cli/src/agent.rs +142 -48
  9. package/crates/mint-cli/src/main.rs +231 -22
  10. package/crates/mint-core/src/api_server.rs +9 -0
  11. package/crates/mint-core/src/browser.rs +401 -0
  12. package/crates/mint-core/src/config.rs +69 -1
  13. package/crates/mint-core/src/lib.rs +5 -0
  14. package/crates/mint-core/src/orchestration.rs +137 -1
  15. package/crates/mint-core/src/skills.rs +113 -4
  16. package/crates/mint-core/src/workflows.rs +1 -22
  17. package/package.json +1 -1
  18. package/src/renderer/src/components/ChatPanel.tsx +170 -33
  19. package/src/renderer/src/components/ImageStudioPanel.tsx +11 -1
  20. package/src/renderer/src/components/MintDashboard.tsx +40 -2
  21. package/src/renderer/src/components/Settings/PluginsTab.tsx +221 -41
  22. package/src/renderer/src/components/SettingsWindow.tsx +1 -9
  23. package/src/renderer/src/components/WorkspacePanel.tsx +66 -9
  24. package/src/renderer/src/css/image-studio.css +6 -0
  25. package/src/renderer/src/css/styles.css +64 -11
  26. package/src/renderer/src/index.css +12 -1
  27. package/src/renderer/src/tauri.ts +45 -4
  28. package/src/renderer/src-web/components/ChatPanel.tsx +163 -32
  29. package/src/renderer/src-web/components/ImageStudioPanel.tsx +11 -1
  30. package/src/renderer/src-web/components/MintDashboard.tsx +8 -3
  31. package/src/renderer/src-web/components/Settings/PluginsTab.tsx +221 -41
  32. package/src/renderer/src-web/components/SettingsWindow.tsx +1 -9
  33. package/src/renderer/src-web/css/image-studio.css +6 -0
  34. package/src/renderer/src-web/css/styles.css +100 -12
  35. package/src/renderer/src-web/index.css +12 -1
  36. package/src/renderer/src-web/tauri.ts +24 -2
  37. package/src-tauri/src/browser.rs +1 -141
  38. package/src-tauri/src/headless.rs +3 -2
  39. package/src-tauri/src/lib.rs +117 -26
  40. package/src-tauri/src/workflows.rs +56 -4
  41. package/src-tauri/tauri.conf.json +1 -1
@@ -76,4 +76,4 @@ jobs:
76
76
  uses: softprops/action-gh-release@v2
77
77
  with:
78
78
  files: release-artifacts/*
79
- generate_release_notes: true
79
+ body_path: Release_Note.md
package/Cargo.lock CHANGED
@@ -2095,7 +2095,7 @@ dependencies = [
2095
2095
 
2096
2096
  [[package]]
2097
2097
  name = "mint-cli"
2098
- version = "1.8.0"
2098
+ version = "1.8.1"
2099
2099
  dependencies = [
2100
2100
  "anyhow",
2101
2101
  "base64 0.22.1",
@@ -2108,12 +2108,13 @@ dependencies = [
2108
2108
  "serde",
2109
2109
  "serde_json",
2110
2110
  "sha2",
2111
+ "textwrap",
2111
2112
  "tokio",
2112
2113
  ]
2113
2114
 
2114
2115
  [[package]]
2115
2116
  name = "mint-core"
2116
- version = "1.8.0"
2117
+ version = "1.8.1"
2117
2118
  dependencies = [
2118
2119
  "base64 0.22.1",
2119
2120
  "chrono",
@@ -2134,7 +2135,7 @@ dependencies = [
2134
2135
 
2135
2136
  [[package]]
2136
2137
  name = "mint-desktop"
2137
- version = "1.8.0"
2138
+ version = "1.8.1"
2138
2139
  dependencies = [
2139
2140
  "base64 0.22.1",
2140
2141
  "dirs",
@@ -3596,6 +3597,12 @@ version = "1.15.1"
3596
3597
  source = "registry+https://github.com/rust-lang/crates.io-index"
3597
3598
  checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
3598
3599
 
3600
+ [[package]]
3601
+ name = "smawk"
3602
+ version = "0.3.3"
3603
+ source = "registry+https://github.com/rust-lang/crates.io-index"
3604
+ checksum = "e8e2fb0f499abb4d162f2bedad68f5ef91a1682b5a03596ddb67efd37768d100"
3605
+
3599
3606
  [[package]]
3600
3607
  name = "socket2"
3601
3608
  version = "0.6.4"
@@ -4130,6 +4137,17 @@ dependencies = [
4130
4137
  "utf-8",
4131
4138
  ]
4132
4139
 
4140
+ [[package]]
4141
+ name = "textwrap"
4142
+ version = "0.16.2"
4143
+ source = "registry+https://github.com/rust-lang/crates.io-index"
4144
+ checksum = "c13547615a44dc9c452a8a534638acdf07120d4b6847c8178705da06306a3057"
4145
+ dependencies = [
4146
+ "smawk",
4147
+ "unicode-linebreak",
4148
+ "unicode-width",
4149
+ ]
4150
+
4133
4151
  [[package]]
4134
4152
  name = "thiserror"
4135
4153
  version = "1.0.69"
@@ -4236,6 +4254,7 @@ dependencies = [
4236
4254
  "libc",
4237
4255
  "mio",
4238
4256
  "pin-project-lite",
4257
+ "signal-hook-registry",
4239
4258
  "socket2",
4240
4259
  "tokio-macros",
4241
4260
  "windows-sys 0.61.2",
@@ -4581,12 +4600,24 @@ version = "1.0.24"
4581
4600
  source = "registry+https://github.com/rust-lang/crates.io-index"
4582
4601
  checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
4583
4602
 
4603
+ [[package]]
4604
+ name = "unicode-linebreak"
4605
+ version = "0.1.5"
4606
+ source = "registry+https://github.com/rust-lang/crates.io-index"
4607
+ checksum = "3b09c83c3c29d37506a3e260c08c03743a6bb66a9cd432c6934ab501a190571f"
4608
+
4584
4609
  [[package]]
4585
4610
  name = "unicode-segmentation"
4586
4611
  version = "1.13.2"
4587
4612
  source = "registry+https://github.com/rust-lang/crates.io-index"
4588
4613
  checksum = "9629274872b2bfaf8d66f5f15725007f635594914870f65218920345aa11aa8c"
4589
4614
 
4615
+ [[package]]
4616
+ name = "unicode-width"
4617
+ version = "0.2.2"
4618
+ source = "registry+https://github.com/rust-lang/crates.io-index"
4619
+ checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254"
4620
+
4590
4621
  [[package]]
4591
4622
  name = "unicode-xid"
4592
4623
  version = "0.2.6"
package/Cargo.toml CHANGED
@@ -7,7 +7,7 @@ members = [
7
7
  resolver = "2"
8
8
 
9
9
  [workspace.package]
10
- version = "1.8.0"
10
+ version = "1.8.1"
11
11
  edition = "2024"
12
12
  license = "AGPL-3.0-only"
13
13
 
@@ -28,5 +28,5 @@ serde = { version = "1", features = ["derive"] }
28
28
  serde_json = "1"
29
29
  sha2 = "0.10"
30
30
  thiserror = "2"
31
- tokio = { version = "1", features = ["io-util", "macros", "net", "rt-multi-thread", "time"] }
31
+ tokio = { version = "1", features = ["io-util", "macros", "net", "rt-multi-thread", "time", "signal"] }
32
32
  tokio-tungstenite = { version = "0.28", features = ["rustls-tls-webpki-roots"] }
package/README.md CHANGED
@@ -78,6 +78,14 @@ Mint is a local-first AI assistant running on your machine, capable of handling
78
78
 
79
79
  ---
80
80
 
81
+ ### 9. <img src="assets/tools.svg" width="18" height="18" valign="middle" /> Browser Automation (`mint auto`)
82
+ - Control and automate web pages directly from either the terminal or the GUI desktop chat.
83
+ - Runs a dedicated, isolated Chromium instance on port `9222` with state separation using the command `mint auto`.
84
+ - Supports opening URLs (`browser_open`), clicking buttons/elements (`browser_click`), typing text (`browser_type`), and extracting content (`browser_read`).
85
+ - **Dynamic Tool Injection:** The agent automatically registers these browser capability tools only when it detects that the automation browser is active on port `9222`.
86
+
87
+ ---
88
+
81
89
  ## Highlights
82
90
 
83
91
  - Multi-provider chat with Gemini, OpenAI, Anthropic, Ollama, Hugging Face, and
@@ -97,6 +105,24 @@ Mint is a local-first AI assistant running on your machine, capable of handling
97
105
  - Signed Tauri update checks with an explicit approval step before installation.
98
106
  - Dynamic local Ollama model fetching in the Settings Window to query and display the actual models installed on your machine.
99
107
  - Pill-styled clean horizontal system event dividers for provider and model change notifications in the chat panel.
108
+ - Global unrestricted text selection and copying enabled across all application components.
109
+ - Spacious 1100px widescreen layout for the Chat Panel when the interactive model is hidden.
110
+ - Advanced Workspace File Tree featuring:
111
+ - Automatic directory refreshing upon window focus and 15-second polling.
112
+ - Quick action buttons to create new files and folders.
113
+ - Right-click context menu to delete files/folders with confirmation modals.
114
+ - Drag-and-drop file mentions in the chat input with automatic spacing and dynamic accent-colored history bubble highlighting.
115
+
116
+ ## What's New in v1.8.1
117
+
118
+ - **File-System Based AI Skills & UI Badges**: Scan and load AI skills from the Global folder (`~/.config/mint/mint-skills/`) and Workspace folders (`.agents/skills/` & `skills/`). Shows source badges (`Workspace` 🟢, `Global` 🔵, `Taught` 🟣) in settings UI and CLI listings (`mint learn --list` and `/learn`) with a clean hidden-content list view.
119
+ - **Unified MCP Servers UI & Toggle Controls**: Merged system-discovered servers into a unified list with toggles to enable/disable or install, and red trash can buttons to remove custom servers.
120
+ - **Advanced Workspace File Tree & Operations**: Manage files directly from the sidebar UI with file/folder creation buttons, right-click deletion modals, and automatic polling + focus-based syncing.
121
+ - **Drag-and-Drop File Mentions**: Drag files from the sidebar to chat input to insert `@filename` with auto-spacing, rendered as outline pills styled with your selected accent color.
122
+ - **CLI Agent Interface Polish & Terminal Line Wrapping Fix**: Indented status blocks and confirmation prompts by 2 spaces to align text. Implemented the *Dynamic Breathing Circle System* (`●` ⇄ `○` pulsing during thinking) and fixed terminal duplicate line bugs by counting physical terminal wrapping width and filtering Thai tone marks.
123
+ - **AskUser Text Input Modal (Desktop & Web)**: When the agent asks questions, a text input field is rendered inside the approval card so users can write and submit typed answers back to the agent backend.
124
+ - **Dynamic Language AI Suggestions**: Auto-translates process suggestions dynamically based on your configured `config.language`.
125
+ - **Mobile & Navigation Optimizations**: Mobile-fit Image Studio layout, hamburger menu access, and headers glass-morphism overlay fixes.
100
126
 
101
127
  ## What's New in v1.8.0
102
128
 
@@ -339,6 +365,7 @@ mint setup
339
365
  mint status
340
366
  mint web
341
367
  mint api
368
+ mint auto
342
369
  mint chat "<message>"
343
370
  ```
344
371
 
@@ -351,6 +378,7 @@ mint chat "<message>"
351
378
  | `mint setup` | Interactively manage enabled agent tools |
352
379
  | `mint web` | Launch the web UI and local API server |
353
380
  | `mint api` | Start only the local API server |
381
+ | `mint auto` | Launch the GUI browser automation isolated port |
354
382
  | `mint status` | Show runtime status |
355
383
  | `mint config init` | Create the local configuration file |
356
384
  | `mint config path` | Print the configuration file path |
@@ -428,15 +456,18 @@ mint mcp call filesystem list_directory \
428
456
  | `/help` | Show interactive help |
429
457
  | `/fast [on\|off]` | Toggle fast response mode |
430
458
  | `/models [name]` | List or select a model |
459
+ | `/image-provider [name]` | List or select default image generation provider |
431
460
  | `/clear` or `/reset` | Clear the active conversation |
432
461
  | `/cd <path>` | Change workspace directory |
433
462
  | `/image <path> [prompt]` | Send an image with an optional prompt |
434
463
  | `/paste [prompt]` | Use an image from the clipboard |
435
464
  | `/learn <path>` | Import a local skill |
465
+ | `/plugins [name]` | List or interact with available plugins/skills |
436
466
  | `/memory list` | List stored memories |
437
467
  | `/memory clear` | Clear stored memories |
438
468
  | `/memory get <key>` | Read one memory value |
439
469
  | `/memory set <key> <value>` | Store one memory value |
470
+ | `/mcp [subcmd]` | Manage configured MCP servers (list, allow, disallow) |
440
471
  | `/stats` | Show session statistics |
441
472
  | `/code <task>` | Start a code-agent task |
442
473
  | `/exit` or `/quit` | Leave interactive mode |
@@ -0,0 +1,95 @@
1
+ # Release Notes - Mint Agent v1.8.1
2
+
3
+ We are excited to release **Mint Agent v1.8.1**! This release introduces powerful local workspace file management features, real-time AI-generated workflow suggestions in the user's active language, mobile layout optimization, and general UI/UX polish.
4
+
5
+ ---
6
+
7
+ ## 🚀 Key Features & Enhancements
8
+
9
+ ### 📂 1. Native Workspace File Explorer Operations
10
+ You can now manage your workspace directory directly from the sidebar without switching to an external editor or using the terminal:
11
+ - **New File & New Folder Buttons:** Replaced the legacy "Use Agent" action button with dedicated creation triggers. Inputting a name will immediately create the empty file/folder on disk.
12
+ - **Right-Click Deletion:** Right-clicking on any file or directory in the tree view prompts a safety confirmation modal. Upon approval, the item is securely removed from your filesystem.
13
+ - **Workspace Auto-Sync:** The file list stays up-to-date automatically using two smart mechanisms:
14
+ - **Window Focus Reloading:** Whenever you switch back to the Mint Desktop window (e.g., after editing code in VS Code), it refreshes the file list immediately.
15
+ - **Periodic Polling:** Auto-polls the directory structure every 15 seconds.
16
+
17
+ ### 💬 2. Drag-and-Drop Mentions with Accent Color Highlight
18
+ Referencing files in your conversations is now smoother and visually richer:
19
+ - **Trailing Spacing:** Dragging files from the sidebar into the chat input automatically appends spacing (e.g. `@index.html `), allowing you to keep typing immediately.
20
+ - **Accent Theme Highlight:** In your chat history, any `@filename` mention is parsed and rendered inside an outline pill container styled dynamically with your selected **Accent Color** theme.
21
+
22
+ ### 🌐 3. Dynamic Language AI Suggestions
23
+ - Process monitor triggers (such as opening terminal processes) now format real-time queries to your active AI provider.
24
+ - Suggestions are dynamically translated and rewritten to match your configured user language (`config.language`), replacing the Thai-only hardcoded templates.
25
+ - Features a safe fallback to static trigger defaults if the AI request times out or encounters network errors.
26
+
27
+ ### 📱 4. Mobile Layout & Navigation Optimizations (Web View)
28
+ - **Image Studio Mobile Fit:** Fixed position clashing media queries resetting Image Studio `.img-studio` offsets to fit smaller screen sizes correctly.
29
+ - **Hamburger Menu Visibility:** Rendered the sidebar drawer button (☰) inside the Image Studio header on mobile viewports so users can navigate back.
30
+ - **Header Overlaps Resolved:** Added a solid opaque background overlay, glass blur, and borders to the mobile chat header, preventing scrolled messages from clashing with header typography.
31
+ - **Mobile Sidebar Drawer Fix:** Prevented the sidebar from collapsing into desktop icon-only configurations on screen widths under 760px, resolving layout drawer compression bugs.
32
+
33
+ ### 💅 5. General UX Polish
34
+ - **Widescreen Chat Layout:** Expanded the chat conversation box max-width to `1100px` when the assistant avatar model is hidden, optimizing readability on widescreen displays.
35
+ - **Global Text Selection:** Unlocked full text selection and copying across the entire application interface (sidebars, panels, buttons, headers) by overriding restricted user-select rules.
36
+
37
+ ### 🖥️ 6. CLI Agent Interface Polish & Terminal Line Wrapping Fix
38
+ Improved CLI status rendering aesthetics, interactive pacing, and terminal output reliability:
39
+ - **Consistent Terminal Padding:** Indented status blocks (Plan, Tasks, Activity, Explored) and confirmation prompt headers by 2 spaces to align with the standard prompt margin and prevent text from sticking to the left edge of the terminal window.
40
+ - **Dynamic Breathing Circle System:** Consolidated status symbols into a unified, colored circle layout (`● plan`, `● tasks`, `● activity`, `● explored`) which pulses dynamically between solid and hollow (`●` ⇄ `○`) while the AI is thinking to signify active processing.
41
+ - **Wrapping & Thai Combining Characters Fix:** Solved the duplicate/orphaned terminal lines issue caused by text wrapping on narrow screens. It now strips ANSI sequences, filters out Thai combining characters/tone marks that stack vertically, and counts true physical display lines to correctly clear stdout between updates.
42
+
43
+ ### 💬 7. AskUser Text Input Modal in Desktop & Web App
44
+ Brought parity to the interactive questioning feature between the CLI and GUI versions:
45
+ - **Dynamic Input Field:** When the agent runs the `ask_user` tool (asking questions like requesting answers or inputs), a sleek text input textarea is displayed inside the Desktop and Web application's approval card.
46
+ - **Answer Submission Backend:** Updated the Tauri backend to handle `ApprovalOutcome::Intercepted(answer)` channels, sending user-typed answers back to the orchestration layer.
47
+ - **Activity Log Integration:** Once submitted, the user's typed answer is appended directly to the tool's execution target within the expandable "Working through task" drawer (e.g. `คุณภีมชอบสีอะไรคะ? (Answered: "สีเขียวมิ้นต์")`), maintaining a clear visual history of interactions.
48
+
49
+ ### 🐙 8. GitKraken MCP Auto-Detection
50
+ Enabled a seamless developer onboarding experience by auto-discovering GitKraken on the host machine:
51
+ - **Automatic Discovery:** Scans the user's local PATH during configuration load to check if `gk` (GitKraken CLI) is available on the system.
52
+ - **Auto-Config Injection:** If detected, automatically configures and adds the GitKraken MCP server (`gk mcp`) to the user's `mint-config.json` file.
53
+ - **Github Release Integration:** Updated the CI/CD `.github/workflows/release.yml` file to parse and use `Release_Note.md` directly for the body description of GitHub Releases.
54
+
55
+ ### 🏷️ 9. File-System Based AI Skills & Source Badges
56
+ Upgraded the AI skills loading pipeline and settings interface:
57
+ - **Dual Workspace Scanning:** Automatically discovers and loads `.md` / `.txt` skills from both `.agents/skills/` and `skills/` folders in the active workspace.
58
+ - **Location Color Badges:** Displays tags indicating whether skills come from Workspace (🟢 green), Global (🔵 blue), or Taught database (🟣 purple) in settings UI and CLI list output (`mint learn --list` and `/learn`).
59
+ - **Clean List UI:** Hides the lengthy file content preview block under each skill, rendering a much cleaner and concise layout.
60
+
61
+ ### 🛠️ 10. Unified MCP Servers & Toggle Management
62
+ - **Consolidated List:** Merged all system-discovered tools (Docker, Git, GitHub, Node) into the unified MCP Servers list.
63
+ - **Individual Toggle Switches:** Enables/disables or installs MCP servers dynamically with instant toggle state styling.
64
+ - **Red Trash Can Buttons:** Added a red trash icon button to easily remove custom/manually configured MCP servers.
65
+
66
+ ---
67
+
68
+ ## 🛠️ Codebase Changes
69
+
70
+ ### Tauri Backend (`src-tauri` & `crates/mint-core`)
71
+ - Register Tauri commands: `create_workspace_file`, `create_workspace_folder`, and `delete_workspace_item` in `src-tauri/src/lib.rs`.
72
+ - Add `location` field to `LearnedSkillDto` and populate it based on workspace, global config, or database sources in `src-tauri/src/lib.rs`.
73
+ - Update `workspace_root` path resolver in `src-tauri/src/lib.rs` to strip `src-tauri` from the active directory during dev mode.
74
+ - Standardize process suggestion monitor logic to run AI prompt translations in the background thread inside `src-tauri/src/workflows.rs`.
75
+ - Change `submit_tool_approval` signature and `ApprovalsState` pending channel type to support `ApprovalOutcome` in `src-tauri/src/lib.rs`.
76
+ - Add executable search helper `which` and GitKraken auto-detection hooks to `load_config_from` in `crates/mint-core/src/config.rs`.
77
+ - Support multiple source directory scanning and deduplication of learned skills in `crates/mint-core/src/skills.rs`.
78
+
79
+ ### Desktop/Web Frontend (`src/renderer`)
80
+ - Integrate creation/deletion actions and focus/polling lifecycle listeners inside `WorkspacePanel.tsx`.
81
+ - Redesign actions bar grid layouts inside `styles.css`.
82
+ - Update inline markdown parser inside `ChatPanel.tsx` to wrap `@` mentions, and style `.chat-mention` in `index.css`.
83
+ - Update `submitToolApproval` API and component state handlers (`MintDashboard.tsx`, `ChatPanel.tsx`) in both desktop and web directories to render and submit custom answers.
84
+ - Update `activitiesFrom` and `AgentActivity` structure to parse and append the user's answer into the active `ask_user` tool target block upon `ToolEnd`.
85
+ - Update `LearnedSkill` TypeScript interface in `src/renderer/src/tauri.ts` to include optional `location`.
86
+ - Render colored location badges, strip lengthy content text boxes, and unify MCP toggles/delete controls in `src/renderer/src/components/Settings/PluginsTab.tsx` and `src/renderer/src-web/components/Settings/PluginsTab.tsx`.
87
+
88
+ ### CLI Agent (`crates/mint-cli`)
89
+ - Redefine live status print lines (`plan_lines`, `tasks_lines`, `activities_lines`, `explored_lines`) to accept progress tick state and apply the `get_bullet` helper.
90
+ - Update `render_live_status` in `crates/mint-cli/src/agent.rs` to compute true physical lines of terminal wrapped text, using a new `is_thai_combining` filter.
91
+ - Modify `confirm` in `crates/mint-cli/src/main.rs` to indent confirmation prompts.
92
+ - Format `mint learn --list` and `/learn` CLI output into a bullet-point summary showing active skill locations and source paths.
93
+
94
+ ### Github CI/CD Workflows
95
+ - Modify `Publish GitHub release` step in `.github/workflows/release.yml` to use `body_path: Release_Note.md`.
Binary file
@@ -21,3 +21,4 @@ sha2.workspace = true
21
21
  tokio.workspace = true
22
22
  crossterm = "0.28"
23
23
  chrono.workspace = true
24
+ textwrap = "0.16"
@@ -338,7 +338,7 @@ pub async fn run_code_agent_with_options(
338
338
  }
339
339
  }
340
340
  let formatted_summary = format_markdown_bold(&summary);
341
- print!("\n{MINT}Mint:{RESET} ");
341
+ print!("\n {MINT}Mint:{RESET} ");
342
342
  render_live_summary(&formatted_summary);
343
343
  println!();
344
344
  };
@@ -377,16 +377,16 @@ pub async fn run_code_agent_with_options(
377
377
  let res = res.map_err(|e| anyhow!("{}", e))?;
378
378
 
379
379
  if should_show_verification(&res.verification) {
380
- println!("Verification: {}", res.verification);
380
+ println!(" Verification: {}", res.verification);
381
381
  }
382
382
  println!(
383
- "{DIM}─ Worked for {}{RESET}",
383
+ " {DIM}─ Worked for {}{RESET}",
384
384
  format_elapsed(started_at.elapsed())
385
385
  );
386
386
 
387
387
  let (tw, _) = crossterm::terminal::size().unwrap_or((80, 24));
388
388
  let width = tw as usize;
389
- println!("{DIM}{}{RESET}", "─".repeat(width));
389
+ println!(" {DIM}{}{RESET}", "─".repeat(width.saturating_sub(2)));
390
390
 
391
391
  Ok(res)
392
392
  }
@@ -408,19 +408,26 @@ fn format_elapsed(duration: Duration) -> String {
408
408
  }
409
409
 
410
410
  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();
411
+ let (tw, _) = crossterm::terminal::size().unwrap_or((80, 24));
412
+ let width = tw as usize;
413
+
414
+ let mut is_first = true;
415
+ for line in summary.split('\n') {
416
+ let indent = if is_first { "" } else { " " };
417
+ let options = textwrap::Options::new(width)
418
+ .initial_indent(indent)
419
+ .subsequent_indent(" ")
420
+ .break_words(true);
421
+ let wrapped = textwrap::fill(line, &options);
422
+
423
+ if is_first {
424
+ print!("{wrapped}");
425
+ is_first = false;
426
+ } else {
427
+ print!("\n{wrapped}");
418
428
  }
419
429
  }
420
- if !chunk.is_empty() {
421
- print!("{chunk}");
422
- let _ = io::stdout().flush();
423
- }
430
+ let _ = io::stdout().flush();
424
431
  }
425
432
 
426
433
  fn print_colored_diff(diff: &str) {
@@ -497,13 +504,21 @@ fn explored_action_label(action: &str, input: &serde_json::Value) -> Option<Expl
497
504
  kind: "[list_files] List",
498
505
  target: display_tool_target(path),
499
506
  }),
500
- "read_file" => input
501
- .get("path")
502
- .and_then(|v| v.as_str())
503
- .map(|path| ExploredAction {
507
+ "read_file" => {
508
+ let path = input.get("path").and_then(|v| v.as_str())?;
509
+ let start = input.get("startLine").and_then(|v| v.as_u64());
510
+ let end = input.get("endLine").and_then(|v| v.as_u64());
511
+ let file_name = display_tool_target(path);
512
+ let target = match (start, end) {
513
+ (Some(s), Some(e)) => format!("{} #L{}-{}", file_name, s, e),
514
+ (Some(s), None) => format!("{} #L{}", file_name, s),
515
+ _ => file_name,
516
+ };
517
+ Some(ExploredAction {
504
518
  kind: "[read_file] Read",
505
- target: display_tool_target(path),
506
- }),
519
+ target,
520
+ })
521
+ }
507
522
  "search_code" => {
508
523
  let query = input.get("query").and_then(|v| v.as_str())?;
509
524
  let path = input.get("path").and_then(|v| v.as_str()).unwrap_or(".");
@@ -540,6 +555,30 @@ fn display_tool_target(path: &str) -> String {
540
555
  }
541
556
  }
542
557
 
558
+ fn strip_ansi_escapes(s: &str) -> String {
559
+ let mut result = String::new();
560
+ let mut in_escape = false;
561
+ for c in s.chars() {
562
+ if c == '\x1b' {
563
+ in_escape = true;
564
+ } else if in_escape {
565
+ if c.is_ascii_alphabetic() {
566
+ in_escape = false;
567
+ }
568
+ } else {
569
+ result.push(c);
570
+ }
571
+ }
572
+ result
573
+ }
574
+
575
+ fn is_thai_combining(c: char) -> bool {
576
+ matches!(c,
577
+ '\u{0e31}' | '\u{0e34}'..='\u{0e37}' | '\u{0e38}'..='\u{0e39}' |
578
+ '\u{0e47}'..='\u{0e4e}'
579
+ )
580
+ }
581
+
543
582
  fn render_live_status(status: &mut LiveStatus) {
544
583
  clear_live_status(status);
545
584
  let mut lines = Vec::new();
@@ -547,23 +586,48 @@ fn render_live_status(status: &mut LiveStatus) {
547
586
  let activities_start = status.committed_activities.min(status.activities.len());
548
587
  let tasks_start = status.committed_tasks.min(status.tasks.len());
549
588
 
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..]));
589
+ let is_thinking = status.thinking.is_some();
590
+ let tick = status.spinner_tick;
591
+
592
+ lines.extend(plan_lines(&status.plan_steps, is_thinking, tick));
593
+ lines.extend(tasks_lines(&status.tasks[tasks_start..], is_thinking, tick));
594
+ lines.extend(activities_lines(
595
+ &status.activities[activities_start..],
596
+ is_thinking,
597
+ tick,
598
+ ));
599
+ lines.extend(explored_lines(
600
+ &status.explored[explored_start..],
601
+ is_thinking,
602
+ tick,
603
+ ));
554
604
  if let Some(thinking) = &status.thinking {
555
- let frames = &["⣾", "⣽", "⣻", "⢿", "⡿", "⣟", "⣯", "⣷"];
605
+ let frames = &["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
556
606
  let frame = frames[status.spinner_tick % frames.len()];
557
607
  status.spinner_tick += 1;
558
- lines.push(format!("{MINT}{frame}{RESET} {BRIGHT}{thinking}{RESET}"));
608
+ lines.push(format!(" {MINT}{frame}{RESET} {BRIGHT}{thinking}{RESET}"));
559
609
  }
560
610
  if lines.is_empty() {
561
611
  return;
562
612
  }
613
+
614
+ let (tw, _) = crossterm::terminal::size().unwrap_or((80, 24));
615
+ let width = tw as usize;
616
+
617
+ let mut physical_lines_count = 0;
563
618
  for line in &lines {
619
+ let stripped = strip_ansi_escapes(line);
620
+ let line_len = stripped.chars().filter(|&c| !is_thai_combining(c)).count();
621
+ let physical_lines = if width > 0 {
622
+ (line_len + width - 1) / width
623
+ } else {
624
+ 1
625
+ }
626
+ .max(1);
627
+ physical_lines_count += physical_lines;
564
628
  println!("{line}");
565
629
  }
566
- status.rendered_lines = lines.len();
630
+ status.rendered_lines = physical_lines_count;
567
631
  let _ = io::stdout().flush();
568
632
  }
569
633
 
@@ -573,9 +637,13 @@ fn commit_activity_snapshot(status: &mut LiveStatus) {
573
637
  let activities_start = status.committed_activities.min(status.activities.len());
574
638
  let tasks_start = status.committed_tasks.min(status.tasks.len());
575
639
 
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..]));
640
+ let mut lines = explored_lines(&status.explored[explored_start..], false, 0);
641
+ lines.extend(activities_lines(
642
+ &status.activities[activities_start..],
643
+ false,
644
+ 0,
645
+ ));
646
+ lines.extend(tasks_lines(&status.tasks[tasks_start..], false, 0));
579
647
  if lines.is_empty() {
580
648
  return;
581
649
  }
@@ -594,7 +662,14 @@ fn print_timeline_note(thought: &str) {
594
662
  if thought.is_empty() {
595
663
  return;
596
664
  }
597
- println!("{DIM}• {thought}{RESET}");
665
+ let (tw, _) = crossterm::terminal::size().unwrap_or((80, 24));
666
+ let width = tw as usize;
667
+ let options = textwrap::Options::new(width)
668
+ .initial_indent(" • ")
669
+ .subsequent_indent(" ")
670
+ .break_words(true);
671
+ let wrapped = textwrap::fill(thought, &options);
672
+ println!("{DIM}{wrapped}{RESET}");
598
673
  }
599
674
 
600
675
  fn print_timeline_separator() {
@@ -615,18 +690,34 @@ fn clear_live_status(status: &mut LiveStatus) {
615
690
  let _ = io::stdout().flush();
616
691
  }
617
692
 
618
- fn explored_lines(actions: &[ExploredAction]) -> Vec<String> {
693
+ fn get_bullet(name: &str, is_thinking: bool, tick: usize) -> String {
694
+ let char_str = if is_thinking {
695
+ if (tick / 4) % 2 == 0 { "●" } else { "○" }
696
+ } else {
697
+ "●"
698
+ };
699
+
700
+ match name {
701
+ "plan" => format!("{BLUE}{char_str}{RESET} plan"),
702
+ "tasks" => format!("{MINT}{char_str}{RESET} tasks"),
703
+ "activity" => format!("{BLUE}{char_str}{RESET} activity"),
704
+ "explored" => format!("{DIM}{char_str}{RESET} explored"),
705
+ _ => char_str.to_string(),
706
+ }
707
+ }
708
+
709
+ fn explored_lines(actions: &[ExploredAction], is_thinking: bool, tick: usize) -> Vec<String> {
619
710
  if actions.is_empty() {
620
711
  return Vec::new();
621
712
  }
622
713
  let grouped = grouped_explored_actions(actions);
623
- let mut lines = vec![format!("{BLUE}○ explored{RESET}")];
714
+ let mut lines = vec![format!(" {}", get_bullet("explored", is_thinking, tick))];
624
715
  lines.extend(grouped.iter().take(24).enumerate().map(|(index, action)| {
625
- let prefix = if index == 0 { " └" } else { " " };
716
+ let prefix = if index == 0 { " └" } else { " " };
626
717
  format!("{DIM}{prefix} {action}{RESET}")
627
718
  }));
628
719
  if grouped.len() > 24 {
629
- lines.push(format!("{DIM} ... {} more{RESET}", grouped.len() - 24));
720
+ lines.push(format!("{DIM} ... {} more{RESET}", grouped.len() - 24));
630
721
  }
631
722
  lines
632
723
  }
@@ -658,17 +749,20 @@ fn command_was_run(result: &str) -> bool {
658
749
  result.lines().any(|line| line.starts_with("exit: "))
659
750
  }
660
751
 
661
- fn activities_lines(activities: &[String]) -> Vec<String> {
752
+ fn activities_lines(activities: &[String], is_thinking: bool, tick: usize) -> Vec<String> {
662
753
  if activities.is_empty() {
663
754
  return Vec::new();
664
755
  }
665
- let mut lines = vec![format!("{BLUE}◇ activity{RESET}")];
756
+ let mut lines = vec![format!(" {}", get_bullet("activity", is_thinking, tick))];
666
757
  lines.extend(activities.iter().take(24).enumerate().map(|(index, act)| {
667
- let prefix = if index == 0 { " └" } else { " " };
758
+ let prefix = if index == 0 { " └" } else { " " };
668
759
  format!("{DIM}{prefix} {act}{RESET}")
669
760
  }));
670
761
  if activities.len() > 24 {
671
- lines.push(format!("{DIM} ... {} more{RESET}", activities.len() - 24));
762
+ lines.push(format!(
763
+ "{DIM} ... {} more{RESET}",
764
+ activities.len() - 24
765
+ ));
672
766
  }
673
767
  lines
674
768
  }
@@ -685,16 +779,16 @@ fn extract_plan_steps(input: &serde_json::Value) -> Option<Vec<String>> {
685
779
  Some(steps)
686
780
  }
687
781
 
688
- fn plan_lines(steps: &[String]) -> Vec<String> {
782
+ fn plan_lines(steps: &[String], is_thinking: bool, tick: usize) -> Vec<String> {
689
783
  if steps.is_empty() {
690
784
  return Vec::new();
691
785
  }
692
- let mut lines = vec![format!("{BLUE}● plan{RESET}")];
786
+ let mut lines = vec![format!(" {}", get_bullet("plan", is_thinking, tick))];
693
787
  for (index, step) in steps.iter().enumerate() {
694
788
  let prefix = if index == steps.len() - 1 {
695
- " └"
789
+ " └"
696
790
  } else {
697
- " ├"
791
+ " ├"
698
792
  };
699
793
  let (checked, text) = if step.to_lowercase().starts_with("done:") {
700
794
  (format!("{MINT}[x]{RESET}"), step["done:".len()..].trim())
@@ -722,17 +816,17 @@ fn plan_lines(steps: &[String]) -> Vec<String> {
722
816
  lines
723
817
  }
724
818
 
725
- fn tasks_lines(tasks: &[String]) -> Vec<String> {
819
+ fn tasks_lines(tasks: &[String], is_thinking: bool, tick: usize) -> Vec<String> {
726
820
  if tasks.is_empty() {
727
821
  return Vec::new();
728
822
  }
729
- let mut lines = vec![format!("{BLUE}● tasks{RESET}")];
823
+ let mut lines = vec![format!(" {}", get_bullet("tasks", is_thinking, tick))];
730
824
  lines.extend(tasks.iter().take(24).enumerate().map(|(index, task)| {
731
- let prefix = if index == 0 { " └" } else { " " };
825
+ let prefix = if index == 0 { " └" } else { " " };
732
826
  format!("{DIM}{prefix} {task}{RESET}")
733
827
  }));
734
828
  if tasks.len() > 24 {
735
- lines.push(format!("{DIM} ... {} more{RESET}", tasks.len() - 24));
829
+ lines.push(format!("{DIM} ... {} more{RESET}", tasks.len() - 24));
736
830
  }
737
831
  lines
738
832
  }