@pheem49/mint 1.6.4 → 1.7.0

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 (34) hide show
  1. package/Cargo.toml +1 -1
  2. package/bin/mint +0 -0
  3. package/crates/mint-cli/src/agent.rs +42 -14
  4. package/crates/mint-cli/src/main.rs +389 -2
  5. package/crates/mint-cli/src/setup.rs +173 -32
  6. package/crates/mint-core/src/api_server.rs +39 -0
  7. package/crates/mint-core/src/config.rs +2 -2
  8. package/crates/mint-core/src/memory.rs +27 -1
  9. package/crates/mint-core/src/orchestration.rs +1 -1
  10. package/crates/mint-core/src/plugins.rs +32 -0
  11. package/crates/mint-core/tests/memory_persistence.rs +28 -0
  12. package/package.json +1 -1
  13. package/src/renderer/src/agentProgress.ts +39 -0
  14. package/src/renderer/src/components/ChatPanel.tsx +308 -34
  15. package/src/renderer/src/components/DashboardSidebar.tsx +3 -3
  16. package/src/renderer/src/components/MintDashboard.tsx +71 -27
  17. package/src/renderer/src/components/ModelPanel.tsx +2 -2
  18. package/src/renderer/src/components/PicturesLibrary.tsx +5 -1
  19. package/src/renderer/src/components/SpotlightWindow.tsx +8 -8
  20. package/src/renderer/src/components/WorkspacePanel.tsx +1 -1
  21. package/src/renderer/src/css/styles.css +195 -17
  22. package/src/renderer/src/index.css +46 -0
  23. package/src/renderer/src/tauri.ts +25 -0
  24. package/src/renderer/src-web/App.tsx +16 -1
  25. package/src/renderer/src-web/agentProgress.ts +39 -0
  26. package/src/renderer/src-web/components/ChatPanel.tsx +309 -35
  27. package/src/renderer/src-web/components/DashboardSidebar.tsx +1 -1
  28. package/src/renderer/src-web/components/MintDashboard.tsx +53 -16
  29. package/src/renderer/src-web/components/PicturesLibrary.tsx +5 -1
  30. package/src/renderer/src-web/css/styles.css +242 -18
  31. package/src/renderer/src-web/index.css +46 -0
  32. package/src/renderer/src-web/tauri.ts +25 -0
  33. package/src-tauri/src/lib.rs +17 -0
  34. package/src-tauri/tauri.conf.json +1 -1
@@ -1,7 +1,7 @@
1
1
  use anyhow::Result;
2
2
  use crossterm::event::{self, Event, KeyCode};
3
3
  use crossterm::terminal::{disable_raw_mode, enable_raw_mode};
4
- use mint_core::{load_config, save_config};
4
+ use mint_core::{load_config, save_config, native_plugins};
5
5
  use std::io::{self, Write};
6
6
 
7
7
  struct ToolOption {
@@ -13,15 +13,6 @@ struct ToolOption {
13
13
  pub async fn run() -> Result<Option<String>> {
14
14
  let mut config = load_config()?;
15
15
 
16
- println!("\x1b[36m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\x1b[0m");
17
- println!("\x1b[32m Mint CLI Tool Manager Wizard\x1b[0m");
18
- println!("\x1b[36m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\x1b[0m");
19
- println!("Configure which agent tools are enabled or disabled:");
20
- println!(
21
- " \x1b[90m[Keyboard Controls: ↑/↓: Navigate | Space: Toggle | a: All | i: Invert | Enter: Confirm]\x1b[0m"
22
- );
23
- println!();
24
-
25
16
  let mut options = vec![
26
17
  ToolOption {
27
18
  name: "list_files (List Workspace Files)",
@@ -178,7 +169,7 @@ pub async fn run() -> Result<Option<String>> {
178
169
  ];
179
170
 
180
171
  let mut cursor = 0;
181
- print_options(&options, cursor);
172
+ redraw_phase_1(&options, cursor);
182
173
  enable_raw_mode()?;
183
174
 
184
175
  loop {
@@ -204,8 +195,7 @@ pub async fn run() -> Result<Option<String>> {
204
195
  cursor = options.len() - 1;
205
196
  }
206
197
  disable_raw_mode()?;
207
- print!("\x1b[{}A\x1b[J", options.len());
208
- print_options(&options, cursor);
198
+ redraw_phase_1(&options, cursor);
209
199
  enable_raw_mode()?;
210
200
  }
211
201
  KeyCode::Down => {
@@ -215,15 +205,13 @@ pub async fn run() -> Result<Option<String>> {
215
205
  cursor = 0;
216
206
  }
217
207
  disable_raw_mode()?;
218
- print!("\x1b[{}A\x1b[J", options.len());
219
- print_options(&options, cursor);
208
+ redraw_phase_1(&options, cursor);
220
209
  enable_raw_mode()?;
221
210
  }
222
211
  KeyCode::Char(' ') => {
223
212
  options[cursor].enabled = !options[cursor].enabled;
224
213
  disable_raw_mode()?;
225
- print!("\x1b[{}A\x1b[J", options.len());
226
- print_options(&options, cursor);
214
+ redraw_phase_1(&options, cursor);
227
215
  enable_raw_mode()?;
228
216
  }
229
217
  KeyCode::Char('a') => {
@@ -231,8 +219,7 @@ pub async fn run() -> Result<Option<String>> {
231
219
  opt.enabled = true;
232
220
  }
233
221
  disable_raw_mode()?;
234
- print!("\x1b[{}A\x1b[J", options.len());
235
- print_options(&options, cursor);
222
+ redraw_phase_1(&options, cursor);
236
223
  enable_raw_mode()?;
237
224
  }
238
225
  KeyCode::Char('i') => {
@@ -240,8 +227,7 @@ pub async fn run() -> Result<Option<String>> {
240
227
  opt.enabled = !opt.enabled;
241
228
  }
242
229
  disable_raw_mode()?;
243
- print!("\x1b[{}A\x1b[J", options.len());
244
- print_options(&options, cursor);
230
+ redraw_phase_1(&options, cursor);
245
231
  enable_raw_mode()?;
246
232
  }
247
233
  KeyCode::Enter => {
@@ -270,15 +256,135 @@ pub async fn run() -> Result<Option<String>> {
270
256
  config.disabled_tools = disabled;
271
257
  save_config(&config)?;
272
258
 
273
- println!("\x1b[32mSuccessfully updated tool configurations!\x1b[0m\n");
259
+ println!("\x1b[32mSuccessfully updated agent tool configurations!\x1b[0m\n");
274
260
 
275
- println!("\x1b[36m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\x1b[0m");
276
- println!("\x1b[32m Choose where to run Mint AI Agent\x1b[0m");
277
- println!("\x1b[36m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\x1b[0m");
278
- println!("Select the environment you want to launch:");
279
- println!(" \x1b[90m[Keyboard Controls: ↑/↓: Navigate | Enter: Confirm]\x1b[0m");
261
+ let all_native = native_plugins();
262
+ let allowed_plugins: std::collections::HashSet<String> = config
263
+ .extra
264
+ .get("allowedNativePlugins")
265
+ .and_then(|v| v.as_array())
266
+ .map(|arr| {
267
+ arr.iter()
268
+ .filter_map(|v| v.as_str().map(|s| s.to_string()))
269
+ .collect()
270
+ })
271
+ .unwrap_or_else(|| {
272
+ vec![
273
+ "dev_tools".to_string(),
274
+ "system_metrics".to_string(),
275
+ "github".to_string(),
276
+ ]
277
+ .into_iter()
278
+ .collect()
279
+ });
280
+
281
+ let mut native_options: Vec<ToolOption> = all_native
282
+ .iter()
283
+ .map(|p| {
284
+ let has_all_permission = allowed_plugins.contains("*");
285
+ let is_enabled = has_all_permission || allowed_plugins.contains(p.name);
286
+ ToolOption {
287
+ name: p.name,
288
+ key: p.name,
289
+ enabled: is_enabled,
290
+ }
291
+ })
292
+ .collect();
293
+
294
+ let mut native_cursor = 0;
295
+ redraw_phase_2(&native_options, native_cursor);
296
+ enable_raw_mode()?;
297
+
298
+ loop {
299
+ match event::poll(std::time::Duration::from_millis(100)) {
300
+ Ok(true) => {
301
+ if let Event::Key(key_event) = event::read()? {
302
+ if key_event.kind == event::KeyEventKind::Press {
303
+ let is_ctrl_c = matches!(key_event.code, KeyCode::Char('c'))
304
+ && key_event
305
+ .modifiers
306
+ .contains(crossterm::event::KeyModifiers::CONTROL);
307
+ if is_ctrl_c {
308
+ disable_raw_mode()?;
309
+ println!("\n\x1b[31mSetup cancelled.\x1b[0m");
310
+ return Ok(None);
311
+ }
312
+
313
+ match key_event.code {
314
+ KeyCode::Up => {
315
+ if native_cursor > 0 {
316
+ native_cursor -= 1;
317
+ } else {
318
+ native_cursor = native_options.len() - 1;
319
+ }
320
+ disable_raw_mode()?;
321
+ redraw_phase_2(&native_options, native_cursor);
322
+ enable_raw_mode()?;
323
+ }
324
+ KeyCode::Down => {
325
+ if native_cursor < native_options.len() - 1 {
326
+ native_cursor += 1;
327
+ } else {
328
+ native_cursor = 0;
329
+ }
330
+ disable_raw_mode()?;
331
+ redraw_phase_2(&native_options, native_cursor);
332
+ enable_raw_mode()?;
333
+ }
334
+ KeyCode::Char(' ') => {
335
+ native_options[native_cursor].enabled =
336
+ !native_options[native_cursor].enabled;
337
+ disable_raw_mode()?;
338
+ redraw_phase_2(&native_options, native_cursor);
339
+ enable_raw_mode()?;
340
+ }
341
+ KeyCode::Char('a') => {
342
+ for opt in &mut native_options {
343
+ opt.enabled = true;
344
+ }
345
+ disable_raw_mode()?;
346
+ redraw_phase_2(&native_options, native_cursor);
347
+ enable_raw_mode()?;
348
+ }
349
+ KeyCode::Char('i') => {
350
+ for opt in &mut native_options {
351
+ opt.enabled = !opt.enabled;
352
+ }
353
+ disable_raw_mode()?;
354
+ redraw_phase_2(&native_options, native_cursor);
355
+ enable_raw_mode()?;
356
+ }
357
+ KeyCode::Enter => {
358
+ break;
359
+ }
360
+ _ => {}
361
+ }
362
+ }
363
+ }
364
+ }
365
+ Ok(false) => {}
366
+ Err(_) => {
367
+ break;
368
+ }
369
+ }
370
+ }
371
+ disable_raw_mode()?;
280
372
  println!();
281
373
 
374
+ let enabled_natives: Vec<serde_json::Value> = native_options
375
+ .iter()
376
+ .filter(|o| o.enabled)
377
+ .map(|o| serde_json::Value::String(o.key.to_string()))
378
+ .collect();
379
+
380
+ config.extra.insert(
381
+ "allowedNativePlugins".to_string(),
382
+ serde_json::Value::Array(enabled_natives),
383
+ );
384
+ save_config(&config)?;
385
+
386
+ println!("\x1b[32mSuccessfully updated native plugins configurations!\x1b[0m\n");
387
+
282
388
  let run_options = vec![
283
389
  ToolOption {
284
390
  name: "1. CLI (Interactive Terminal Assistant)",
@@ -298,7 +404,7 @@ pub async fn run() -> Result<Option<String>> {
298
404
  ];
299
405
 
300
406
  let mut run_cursor = 0;
301
- print_run_options(&run_options, run_cursor);
407
+ redraw_phase_3(&run_options, run_cursor);
302
408
  enable_raw_mode()?;
303
409
 
304
410
  loop {
@@ -324,8 +430,7 @@ pub async fn run() -> Result<Option<String>> {
324
430
  run_cursor = run_options.len() - 1;
325
431
  }
326
432
  disable_raw_mode()?;
327
- print!("\x1b[{}A\x1b[J", run_options.len());
328
- print_run_options(&run_options, run_cursor);
433
+ redraw_phase_3(&run_options, run_cursor);
329
434
  enable_raw_mode()?;
330
435
  }
331
436
  KeyCode::Down => {
@@ -335,8 +440,7 @@ pub async fn run() -> Result<Option<String>> {
335
440
  run_cursor = 0;
336
441
  }
337
442
  disable_raw_mode()?;
338
- print!("\x1b[{}A\x1b[J", run_options.len());
339
- print_run_options(&run_options, run_cursor);
443
+ redraw_phase_3(&run_options, run_cursor);
340
444
  enable_raw_mode()?;
341
445
  }
342
446
  KeyCode::Enter => {
@@ -388,3 +492,40 @@ fn print_run_options(options: &[ToolOption], cursor: usize) {
388
492
  }
389
493
  let _ = io::stdout().flush();
390
494
  }
495
+
496
+ fn redraw_phase_1(options: &[ToolOption], cursor: usize) {
497
+ print!("\x1b[2J\x1b[1;1H");
498
+ println!("\x1b[36m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\x1b[0m");
499
+ println!("\x1b[32m Mint CLI Tool Manager Wizard\x1b[0m");
500
+ println!("\x1b[36m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\x1b[0m");
501
+ println!("Configure which agent tools are enabled or disabled:");
502
+ println!(
503
+ " \x1b[90m[Keyboard Controls: ↑/↓: Navigate | Space: Toggle | a: All | i: Invert | Enter: Confirm]\x1b[0m"
504
+ );
505
+ println!();
506
+ print_options(options, cursor);
507
+ }
508
+
509
+ fn redraw_phase_2(options: &[ToolOption], cursor: usize) {
510
+ print!("\x1b[2J\x1b[1;1H");
511
+ println!("\x1b[36m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\x1b[0m");
512
+ println!("\x1b[32m Configure Native Plugins Access\x1b[0m");
513
+ println!("\x1b[36m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\x1b[0m");
514
+ println!("Select which native plugins are allowed to run:");
515
+ println!(
516
+ " \x1b[90m[Keyboard Controls: ↑/↓: Navigate | Space: Toggle | a: All | i: Invert | Enter: Confirm]\x1b[0m"
517
+ );
518
+ println!();
519
+ print_options(options, cursor);
520
+ }
521
+
522
+ fn redraw_phase_3(options: &[ToolOption], cursor: usize) {
523
+ print!("\x1b[2J\x1b[1;1H");
524
+ println!("\x1b[36m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\x1b[0m");
525
+ println!("\x1b[32m Choose where to run Mint AI Agent\x1b[0m");
526
+ println!("\x1b[36m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\x1b[0m");
527
+ println!("Select the environment you want to launch:");
528
+ println!(" \x1b[90m[Keyboard Controls: ↑/↓: Navigate | Enter: Confirm]\x1b[0m");
529
+ println!();
530
+ print_run_options(options, cursor);
531
+ }
@@ -272,6 +272,45 @@ pub async fn start_api_server(port: u16) -> Result<(), std::io::Error> {
272
272
  .await;
273
273
  }
274
274
  }
275
+ ("POST", "/api/interactions/agent-activity") => {
276
+ #[derive(Deserialize)]
277
+ #[serde(rename_all = "camelCase")]
278
+ struct SaveAgentActivityRequest {
279
+ interaction_id: i64,
280
+ activity: Vec<AgentProgress>,
281
+ }
282
+
283
+ if let Ok(payload) = serde_json::from_str::<SaveAgentActivityRequest>(body) {
284
+ if let Ok(memory) = MemoryStore::open_default() {
285
+ match serde_json::to_string(&payload.activity) {
286
+ Ok(activity_json) => {
287
+ if memory
288
+ .set_interaction_agent_activity_json(
289
+ payload.interaction_id,
290
+ &activity_json,
291
+ )
292
+ .is_ok()
293
+ {
294
+ send_json_response(
295
+ socket,
296
+ "200 OK",
297
+ "{\"status\":\"ok\"}",
298
+ )
299
+ .await;
300
+ return;
301
+ }
302
+ }
303
+ Err(_) => {}
304
+ }
305
+ }
306
+ }
307
+ send_json_response(
308
+ socket,
309
+ "400 Bad Request",
310
+ "{\"status\":\"invalid agent activity payload\"}",
311
+ )
312
+ .await;
313
+ }
275
314
  ("GET", "/api/pictures") => match list_saved_pictures() {
276
315
  Ok(mut pictures) => {
277
316
  for picture in &mut pictures {
@@ -310,7 +310,7 @@ fn runtime_extra_defaults() -> BTreeMap<String, Value> {
310
310
  "notionPageId": "",
311
311
  "notionTitleProperty": "Name",
312
312
  "allowedShellModes": ["readOnly", "test", "mutating", "network"],
313
- "allowedNativePlugins": ["dev_tools", "system_metrics"],
313
+ "allowedNativePlugins": ["dev_tools", "system_metrics", "github"],
314
314
  "allowedMcpTools": {},
315
315
  "mcpServers": {}
316
316
  }))
@@ -380,7 +380,7 @@ mod tests {
380
380
  );
381
381
  assert_eq!(
382
382
  config.extra["allowedNativePlugins"],
383
- serde_json::json!(["dev_tools", "system_metrics"])
383
+ serde_json::json!(["dev_tools", "system_metrics", "github"])
384
384
  );
385
385
  assert_eq!(config.extra["allowedMcpTools"], serde_json::json!({}));
386
386
  }
@@ -36,6 +36,8 @@ pub struct InteractionMemory {
36
36
  pub model: String,
37
37
  pub fallback_provider: Option<String>,
38
38
  pub created_at: String,
39
+ #[serde(default, skip_serializing_if = "Option::is_none")]
40
+ pub agent_activity: Option<serde_json::Value>,
39
41
  }
40
42
 
41
43
  #[derive(Debug, Clone, Serialize, PartialEq, Eq)]
@@ -172,7 +174,7 @@ impl MemoryStore {
172
174
  ensure_builtin_chat_sessions(&connection)?;
173
175
  ensure_chat_session_row(&connection, &chat_id)?;
174
176
  let mut statement = connection.prepare(
175
- "SELECT id, chat_id, user_text, ai_text, provider, model, fallback_provider, created_at
177
+ "SELECT id, chat_id, user_text, ai_text, provider, model, fallback_provider, created_at, agent_activity_json
176
178
  FROM interaction_memories
177
179
  WHERE chat_id = ?1
178
180
  ORDER BY id DESC
@@ -202,6 +204,20 @@ impl MemoryStore {
202
204
  rows.collect::<Result<Vec<_>, _>>().map_err(Into::into)
203
205
  }
204
206
 
207
+ pub fn set_interaction_agent_activity_json(
208
+ &self,
209
+ interaction_id: i64,
210
+ activity_json: &str,
211
+ ) -> Result<usize, MemoryError> {
212
+ let connection = self.connection()?;
213
+ Ok(connection.execute(
214
+ "UPDATE interaction_memories
215
+ SET agent_activity_json = ?2
216
+ WHERE id = ?1",
217
+ params![interaction_id, activity_json],
218
+ )?)
219
+ }
220
+
205
221
  pub fn clear_interactions(&self) -> Result<usize, MemoryError> {
206
222
  self.clear_interactions_for_chat(DEFAULT_CONVERSATION_ID)
207
223
  }
@@ -551,6 +567,12 @@ fn initialize(
551
567
  "fallback_provider",
552
568
  "TEXT DEFAULT NULL",
553
569
  )?;
570
+ ensure_column(
571
+ connection,
572
+ "interaction_memories",
573
+ "agent_activity_json",
574
+ "TEXT DEFAULT NULL",
575
+ )?;
554
576
  ensure_column(
555
577
  connection,
556
578
  "chat_sessions",
@@ -612,6 +634,9 @@ fn learned_skill_row(row: &rusqlite::Row<'_>) -> Result<LearnedSkill, rusqlite::
612
634
  }
613
635
 
614
636
  fn interaction_row(row: &rusqlite::Row<'_>) -> Result<InteractionMemory, rusqlite::Error> {
637
+ let agent_activity = row
638
+ .get::<_, Option<String>>(8)?
639
+ .and_then(|raw| serde_json::from_str(&raw).ok());
615
640
  Ok(InteractionMemory {
616
641
  id: row.get(0)?,
617
642
  chat_id: row.get(1)?,
@@ -621,6 +646,7 @@ fn interaction_row(row: &rusqlite::Row<'_>) -> Result<InteractionMemory, rusqlit
621
646
  model: row.get(5)?,
622
647
  fallback_provider: row.get(6)?,
623
648
  created_at: row.get(7)?,
649
+ agent_activity,
624
650
  })
625
651
  }
626
652
 
@@ -385,7 +385,7 @@ pub fn build_system_prompt(config: &MintConfig) -> String {
385
385
  "- write_file: {\"path\":\"new/relative/path\",\"fileContent\":\"full file content\"}",
386
386
  );
387
387
  }
388
- input_formats.push("- finish: {\"summary\":\"concise final answer\",\"verification\":\"checks run or not run\"}");
388
+ input_formats.push("- finish: {\"summary\":\"concise final answer in Thai when the user writes Thai\",\"verification\":\"checks run or not run\"}");
389
389
 
390
390
  let input_formats_str = input_formats.join("\n");
391
391
 
@@ -84,6 +84,10 @@ pub fn native_plugins() -> Vec<NativePlugin> {
84
84
  name: "notion",
85
85
  description: "Create notes, read databases, and append blocks through Notion API.",
86
86
  },
87
+ NativePlugin {
88
+ name: "github",
89
+ description: "List issues, view issues, list pull requests, or check PR status via gh CLI.",
90
+ },
87
91
  ]
88
92
  }
89
93
 
@@ -104,6 +108,7 @@ pub async fn execute_native_plugin(
104
108
  "gmail" => gmail(config, instruction).await,
105
109
  "google_calendar" => calendar(config, instruction).await,
106
110
  "notion" => notion(config, instruction).await,
111
+ "github" => github(instruction),
107
112
  _ => Err(PluginError::UnknownPlugin(name.into())),
108
113
  }
109
114
  }
@@ -229,6 +234,33 @@ fn spotify(instruction: &str) -> Result<String, PluginError> {
229
234
  run("playerctl", &args)
230
235
  }
231
236
 
237
+ fn github(instruction: &str) -> Result<String, PluginError> {
238
+ let parts = instruction.split_whitespace().collect::<Vec<_>>();
239
+ match parts.as_slice() {
240
+ ["list_issues"] | ["issue", "list"] => run("gh", &["issue", "list", "--limit", "10"]),
241
+ ["view_issue", id] | ["issue", "view", id] => {
242
+ if id.chars().all(|c| c.is_ascii_digit()) {
243
+ run("gh", &["issue", "view", id])
244
+ } else {
245
+ invalid("github", "view_issue/issue view requires a numeric issue ID")
246
+ }
247
+ }
248
+ ["list_prs"] | ["pr", "list"] => run("gh", &["pr", "list", "--limit", "10"]),
249
+ ["pr_status"] | ["pr", "status"] => run("gh", &["pr", "status"]),
250
+ ["view_pr", id] | ["pr", "view", id] => {
251
+ if id.chars().all(|c| c.is_ascii_digit()) {
252
+ run("gh", &["pr", "view", id])
253
+ } else {
254
+ invalid("github", "view_pr/pr view requires a numeric PR ID")
255
+ }
256
+ }
257
+ _ => invalid(
258
+ "github",
259
+ "expected list_issues, view_issue <id>, list_prs, pr_status, or view_pr <id>",
260
+ ),
261
+ }
262
+ }
263
+
232
264
  fn system_metrics(instruction: &str) -> Result<String, PluginError> {
233
265
  let uptime = fs::read_to_string("/proc/uptime")
234
266
  .ok()
@@ -57,6 +57,34 @@ fn stores_recent_interactions_with_provider_metadata() {
57
57
  assert_eq!(interactions[1].fallback_provider, None);
58
58
  }
59
59
 
60
+ #[test]
61
+ fn stores_and_reads_interaction_agent_activity() {
62
+ let store = store("agent-activity");
63
+ let interaction_id = store
64
+ .add_interaction_with_metadata("hello", "hi", "gemini", "gemini-test")
65
+ .unwrap();
66
+ store
67
+ .set_interaction_agent_activity_json(
68
+ interaction_id,
69
+ r#"[{"type":"Thought","data":{"thought":"step one"}}]"#,
70
+ )
71
+ .unwrap();
72
+
73
+ let interactions = store.recent_interactions(1).unwrap();
74
+ assert_eq!(interactions.len(), 1);
75
+ let activity = interactions[0]
76
+ .agent_activity
77
+ .as_ref()
78
+ .and_then(|value| value.as_array())
79
+ .cloned()
80
+ .unwrap_or_default();
81
+ assert_eq!(activity.len(), 1);
82
+ assert_eq!(
83
+ activity[0].get("type").and_then(|value| value.as_str()),
84
+ Some("Thought")
85
+ );
86
+ }
87
+
60
88
  #[test]
61
89
  fn preserves_chat_history_when_store_is_reopened() {
62
90
  let path = test_path("reopen-history", "sqlite");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pheem49/mint",
3
- "version": "1.6.4",
3
+ "version": "1.7.0",
4
4
  "description": "A native Tauri desktop AI assistant with a Rust backend and React UI.",
5
5
  "scripts": {
6
6
  "dev:web": "vite --config vite.config.web.ts",
@@ -0,0 +1,39 @@
1
+ import type { AgentProgress, InteractionMemory } from './tauri'
2
+
3
+ export function thoughtsFrom(progress: AgentProgress[]): string[] {
4
+ return progress
5
+ .filter((event) => event.type === 'Thought')
6
+ .map((event) => (event as Extract<AgentProgress, { type: 'Thought' }>).data.thought)
7
+ .filter(Boolean)
8
+ }
9
+
10
+ export function trimAgentProgress(progress: AgentProgress[], maxNonThought = 20): AgentProgress[] {
11
+ if (progress.length === 0) return progress
12
+ const keptNonThought = new Set(
13
+ progress.filter((event) => event.type !== 'Thought').slice(-maxNonThought),
14
+ )
15
+ return progress.filter((event) => event.type === 'Thought' || keptNonThought.has(event))
16
+ }
17
+
18
+ export function hasAgentToolActivity(progress: AgentProgress[]): boolean {
19
+ return progress.some((event) => event.type === 'ToolStart' || event.type === 'ToolEnd')
20
+ }
21
+
22
+ export function mergeActivitySnapshots(
23
+ current: Record<string, AgentProgress[]>,
24
+ interactions: InteractionMemory[],
25
+ ): Record<string, AgentProgress[]> {
26
+ const merged = { ...current }
27
+ for (const interaction of interactions) {
28
+ const key = String(interaction.id)
29
+ if (interaction.agentActivity?.length && !merged[key]) {
30
+ merged[key] = interaction.agentActivity
31
+ }
32
+ }
33
+ return merged
34
+ }
35
+
36
+ export function parseAgentActivity(value: unknown): AgentProgress[] | undefined {
37
+ if (!Array.isArray(value)) return undefined
38
+ return value as AgentProgress[]
39
+ }