@pheem49/mint 1.9.0 → 1.10.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 (90) hide show
  1. package/Cargo.lock +55 -3
  2. package/Cargo.toml +2 -1
  3. package/Release_Note.md +285 -140
  4. package/bin/mint +0 -0
  5. package/crates/mint-cli/Cargo.toml +1 -0
  6. package/crates/mint-cli/src/actions.rs +233 -0
  7. package/crates/mint-cli/src/agent.rs +287 -43
  8. package/crates/mint-cli/src/gmail.rs +22 -2
  9. package/crates/mint-cli/src/interactive.rs +2888 -0
  10. package/crates/mint-cli/src/main.rs +336 -2854
  11. package/crates/mint-cli/src/markdown.rs +294 -0
  12. package/crates/mint-cli/src/onboard.rs +44 -1
  13. package/crates/mint-cli/src/updater.rs +48 -6
  14. package/crates/mint-core/Cargo.toml +1 -0
  15. package/crates/mint-core/src/agent_loop.rs +1 -0
  16. package/crates/mint-core/src/api_server.rs +376 -13
  17. package/crates/mint-core/src/browser.rs +629 -101
  18. package/crates/mint-core/src/channels.rs +105 -6
  19. package/crates/mint-core/src/chat.rs +52 -4
  20. package/crates/mint-core/src/code_tools.rs +29 -11
  21. package/crates/mint-core/src/config.rs +118 -0
  22. package/crates/mint-core/src/image_gen.rs +294 -33
  23. package/crates/mint-core/src/lib.rs +5 -2
  24. package/crates/mint-core/src/mcp.rs +119 -6
  25. package/crates/mint-core/src/orchestration.rs +133 -11
  26. package/crates/mint-core/src/pictures.rs +64 -1
  27. package/crates/mint-core/src/skills.rs +82 -6
  28. package/crates/mint-core/src/video_gen.rs +235 -0
  29. package/package.json +8 -2
  30. package/shared-modules/luajit/luajit.json +2 -2
  31. package/skills-lock.json +41 -0
  32. package/src/renderer/index-web.html +1 -1
  33. package/src/renderer/index.html +1 -1
  34. package/src/renderer/shared/constants/slashCommands.ts +27 -0
  35. package/src/renderer/shared/platform.ts +3 -0
  36. package/src/renderer/shared/types.ts +4 -0
  37. package/src/renderer/shared/utils/approval.tsx +86 -7
  38. package/src/renderer/shared/utils/markdown.tsx +217 -88
  39. package/src/renderer/shared/utils/slashCommandProcessor.ts +239 -0
  40. package/src/renderer/src/components/ChatPanel.tsx +461 -64
  41. package/src/renderer/src/components/DashboardSidebar.tsx +11 -2
  42. package/src/renderer/src/components/ImageStudioPanel.tsx +87 -4
  43. package/src/renderer/src/components/MintDashboard.tsx +150 -5
  44. package/src/renderer/src/components/PicturesLibrary.tsx +156 -15
  45. package/src/renderer/src/components/Settings/AutomationTab.tsx +316 -18
  46. package/src/renderer/src/components/Settings/GeneralTab.tsx +762 -634
  47. package/src/renderer/src/components/Settings/PluginsTab.tsx +304 -70
  48. package/src/renderer/src/components/Settings/ThemeTab.tsx +6 -1
  49. package/src/renderer/src/components/SettingsWindow.tsx +38 -2
  50. package/src/renderer/src/components/VeoStudioPanel.tsx +549 -0
  51. package/src/renderer/src/css/image-studio.css +106 -0
  52. package/src/renderer/src/css/settings.css +158 -0
  53. package/src/renderer/src/css/styles.css +352 -1
  54. package/src/renderer/src/css/veo-studio.css +626 -0
  55. package/src/renderer/src/index.css +1 -0
  56. package/src/renderer/src/tauri.ts +144 -17
  57. package/src/renderer/src-web/components/ChatPanel.tsx +360 -88
  58. package/src/renderer/src-web/components/DashboardSidebar.tsx +16 -1
  59. package/src/renderer/src-web/components/ImageStudioPanel.tsx +87 -4
  60. package/src/renderer/src-web/components/MintDashboard.tsx +137 -4
  61. package/src/renderer/src-web/components/PicturesLibrary.tsx +155 -15
  62. package/src/renderer/src-web/components/Settings/AutomationTab.tsx +316 -18
  63. package/src/renderer/src-web/components/Settings/GeneralTab.tsx +762 -629
  64. package/src/renderer/src-web/components/Settings/PluginsTab.tsx +304 -70
  65. package/src/renderer/src-web/components/Settings/ThemeTab.tsx +6 -1
  66. package/src/renderer/src-web/components/SettingsWindow.tsx +35 -2
  67. package/src/renderer/src-web/components/VeoStudioPanel.tsx +549 -0
  68. package/src/renderer/src-web/css/image-studio.css +106 -0
  69. package/src/renderer/src-web/css/settings.css +158 -0
  70. package/src/renderer/src-web/css/styles.css +353 -1
  71. package/src/renderer/src-web/css/veo-studio.css +650 -0
  72. package/src/renderer/src-web/index.css +1 -0
  73. package/src/renderer/src-web/tauri.ts +135 -16
  74. package/src-tauri/src/lib.rs +114 -8
  75. package/src-tauri/src/webhooks.rs +45 -31
  76. package/src-tauri/src/workflows.rs +1 -0
  77. package/src-tauri/tauri.conf.json +1 -1
  78. package/temp_gridgeist/AGENTS.md +8 -0
  79. package/temp_gridgeist/CHANGELOG.md +130 -0
  80. package/temp_gridgeist/LICENSE +21 -0
  81. package/temp_gridgeist/README.md +323 -0
  82. package/temp_gridgeist/README.th.md +325 -0
  83. package/temp_gridgeist/skills/gridgeist/SKILL.md +88 -0
  84. package/temp_gridgeist/skills/gridgeist/agents/openai.yaml +9 -0
  85. package/temp_gridgeist/skills/gridgeist/assets/gridgeist-small.svg +10 -0
  86. package/temp_gridgeist/skills/gridgeist/assets/gridgeist.png +0 -0
  87. package/temp_gridgeist/skills/gridgeist/assets/gridgeist.svg +18 -0
  88. package/temp_gridgeist/skills/gridgeist/references/design-language.md +88 -0
  89. package/temp_gridgeist/skills/gridgeist/references/review-checklist.md +128 -0
  90. package/temp_gridgeist/skills/gridgeist/references/system-contract.md +109 -0
@@ -0,0 +1,235 @@
1
+ use base64::{Engine as _, engine::general_purpose::STANDARD};
2
+ use serde::{Deserialize, Serialize};
3
+ use serde_json::{Value, json};
4
+ use thiserror::Error;
5
+
6
+ use crate::{MintConfig, PictureEntry};
7
+
8
+ #[derive(Debug, Clone, Deserialize)]
9
+ #[serde(rename_all = "camelCase")]
10
+ pub struct VideoGenRequest {
11
+ pub prompt: String,
12
+ pub negative_prompt: Option<String>,
13
+ pub aspect_ratio: String,
14
+ pub duration: u32,
15
+ pub model: Option<String>,
16
+ pub provider: String,
17
+ }
18
+
19
+ #[derive(Debug, Clone, Serialize)]
20
+ #[serde(rename_all = "camelCase")]
21
+ pub struct VideoGenResponse {
22
+ pub videos: Vec<PictureEntry>,
23
+ pub model: String,
24
+ pub provider: String,
25
+ pub prompt: String,
26
+ }
27
+
28
+ #[derive(Debug, Error)]
29
+ pub enum VideoGenError {
30
+ #[error("missing Gemini API key — set GEMINI_API_KEY or configure api_key in config")]
31
+ MissingApiKey,
32
+ #[error("API request failed: {0}")]
33
+ Request(#[from] reqwest::Error),
34
+ #[error("unexpected response structure: {0}")]
35
+ UnexpectedResponse(String),
36
+ #[error("video generation timed out")]
37
+ Timeout,
38
+ #[error("failed to save generated video: {0}")]
39
+ SaveError(String),
40
+ #[error(
41
+ "Google Veo model endpoint returned 404 Not Found. Google Veo video generation requires an API Key or Google Cloud project with Veo Model access enabled: {0}"
42
+ )]
43
+ ModelNotFound(String),
44
+ }
45
+
46
+ pub async fn generate_video(
47
+ config: &MintConfig,
48
+ request: &VideoGenRequest,
49
+ ) -> Result<VideoGenResponse, VideoGenError> {
50
+ let api_key = if config.api_key.trim().is_empty() {
51
+ std::env::var("GEMINI_API_KEY").unwrap_or_default()
52
+ } else {
53
+ config.api_key.clone()
54
+ };
55
+ if api_key.trim().is_empty() {
56
+ return Err(VideoGenError::MissingApiKey);
57
+ }
58
+
59
+ let mut model_owned = request
60
+ .model
61
+ .as_deref()
62
+ .map(str::trim)
63
+ .filter(|m| !m.is_empty())
64
+ .map(str::to_owned)
65
+ .unwrap_or_else(|| {
66
+ config
67
+ .extra
68
+ .get("veoModel")
69
+ .and_then(|v| v.as_str())
70
+ .unwrap_or("veo-2.0-generate-001")
71
+ .to_string()
72
+ });
73
+
74
+ if model_owned == "veo-2.0-flash-exp" {
75
+ model_owned = "veo-2.0-generate-001".to_string();
76
+ }
77
+
78
+ let client = crate::HTTP_CLIENT.clone();
79
+ let mut url = format!(
80
+ "https://generativelanguage.googleapis.com/v1beta/models/{model_owned}:predictLongRunning"
81
+ );
82
+
83
+ let payload = json!({
84
+ "instances": [
85
+ {
86
+ "prompt": request.prompt
87
+ }
88
+ ],
89
+ "parameters": {
90
+ "aspectRatio": request.aspect_ratio,
91
+ "durationSeconds": request.duration,
92
+ "sampleCount": 1
93
+ }
94
+ });
95
+
96
+ let mut response_res = client
97
+ .post(&url)
98
+ .header("x-goog-api-key", &api_key)
99
+ .json(&payload)
100
+ .send()
101
+ .await?;
102
+
103
+ if response_res.status() == reqwest::StatusCode::NOT_FOUND && model_owned != "veo-2.0-flash-001"
104
+ {
105
+ model_owned = "veo-2.0-flash-001".to_string();
106
+ url = format!(
107
+ "https://generativelanguage.googleapis.com/v1beta/models/{model_owned}:predictLongRunning"
108
+ );
109
+ response_res = client
110
+ .post(&url)
111
+ .header("x-goog-api-key", &api_key)
112
+ .json(&payload)
113
+ .send()
114
+ .await?;
115
+ }
116
+
117
+ if !response_res.status().is_success() {
118
+ let status = response_res.status();
119
+ let err_text = response_res.text().await.unwrap_or_default();
120
+ if status == reqwest::StatusCode::NOT_FOUND {
121
+ return Err(VideoGenError::ModelNotFound(format!(
122
+ "404 for model {model_owned} on generativelanguage.googleapis.com API. ({err_text})"
123
+ )));
124
+ }
125
+ return Err(VideoGenError::UnexpectedResponse(format!(
126
+ "HTTP {status}: {err_text}"
127
+ )));
128
+ }
129
+
130
+ let res: Value = response_res.json().await?;
131
+
132
+ let operation_name = res
133
+ .get("name")
134
+ .and_then(|v| v.as_str())
135
+ .ok_or_else(|| VideoGenError::UnexpectedResponse("Missing operation name".to_string()))?;
136
+
137
+ let poll_url = format!("https://generativelanguage.googleapis.com/v1beta/{operation_name}");
138
+ let mut done = false;
139
+ let mut final_response = None;
140
+
141
+ // Poll every 5 seconds, up to 60 times (5 minutes max)
142
+ for _ in 0..60 {
143
+ tokio::time::sleep(tokio::time::Duration::from_secs(5)).await;
144
+ let poll_res: Value = client
145
+ .get(&poll_url)
146
+ .header("x-goog-api-key", &api_key)
147
+ .send()
148
+ .await?
149
+ .error_for_status()?
150
+ .json()
151
+ .await?;
152
+
153
+ if let Some(d) = poll_res.get("done").and_then(|v| v.as_bool()) {
154
+ if d {
155
+ done = true;
156
+ final_response = Some(poll_res);
157
+ break;
158
+ }
159
+ }
160
+ }
161
+
162
+ if !done {
163
+ return Err(VideoGenError::Timeout);
164
+ }
165
+
166
+ let op_res = final_response.ok_or(VideoGenError::Timeout)?;
167
+
168
+ // Check if error occurred in operation
169
+ if let Some(err) = op_res.get("error") {
170
+ let msg = err["message"].as_str().unwrap_or("unknown operation error");
171
+ return Err(VideoGenError::UnexpectedResponse(msg.to_string()));
172
+ }
173
+
174
+ let response_obj = op_res
175
+ .get("response")
176
+ .ok_or_else(|| VideoGenError::UnexpectedResponse("Missing response object".to_string()))?;
177
+
178
+ // Try to find generated samples or outputs
179
+ let generated_samples = response_obj
180
+ .get("generateVideoResponse")
181
+ .and_then(|gvr| gvr.get("generatedSamples"))
182
+ .or_else(|| {
183
+ response_obj
184
+ .get("outputs")
185
+ .and_then(|o| o.as_array()?.first()?.get("generatedVideos"))
186
+ })
187
+ .ok_or_else(|| {
188
+ VideoGenError::UnexpectedResponse(
189
+ "No generated samples or videos in response".to_string(),
190
+ )
191
+ })?;
192
+
193
+ let sample = generated_samples
194
+ .as_array()
195
+ .and_then(|arr| arr.first())
196
+ .ok_or_else(|| {
197
+ VideoGenError::UnexpectedResponse("Generated samples array is empty".to_string())
198
+ })?;
199
+
200
+ let video_uri = sample
201
+ .get("video")
202
+ .and_then(|v| v.get("uri"))
203
+ .and_then(|u| u.as_str())
204
+ .ok_or_else(|| VideoGenError::UnexpectedResponse("Missing video URI".to_string()))?;
205
+
206
+ // Download the video file bytes
207
+ let video_bytes = client
208
+ .get(video_uri)
209
+ .header("x-goog-api-key", &api_key)
210
+ .send()
211
+ .await?
212
+ .error_for_status()?
213
+ .bytes()
214
+ .await?
215
+ .to_vec();
216
+
217
+ // Convert to data URI for saving
218
+ let data_b64 = STANDARD.encode(&video_bytes);
219
+ let data_uri = format!("data:video/mp4;base64,{data_b64}");
220
+
221
+ // Save to the pictures directory and record index entry
222
+ let saved = crate::pictures::save_chat_images(
223
+ vec![data_uri],
224
+ Some("veo".to_string()),
225
+ Some(request.prompt.clone()),
226
+ )
227
+ .map_err(|e| VideoGenError::SaveError(e.to_string()))?;
228
+
229
+ Ok(VideoGenResponse {
230
+ videos: saved,
231
+ model: model_owned,
232
+ provider: "veo".to_string(),
233
+ prompt: request.prompt.clone(),
234
+ })
235
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pheem49/mint",
3
- "version": "1.9.0",
3
+ "version": "1.10.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",
@@ -56,6 +56,7 @@
56
56
  "mint": "src/bin/index.js"
57
57
  },
58
58
  "dependencies": {
59
+ "@pouyanafisi/gmail-mcp": "^2.0.0",
59
60
  "@tauri-apps/api": "^2.0.0",
60
61
  "@vitejs/plugin-react": "^6.0.1",
61
62
  "@xyflow/react": "^12.11.1",
@@ -71,6 +72,11 @@
71
72
  "@tauri-apps/cli": "^2.0.0"
72
73
  },
73
74
  "overrides": {
74
- "gh-pages": "^5.0.0"
75
+ "gh-pages": "^5.0.0",
76
+ "@modelcontextprotocol/sdk": "^1.24.0",
77
+ "nodemailer": "^9.0.0",
78
+ "uuid": "^11.1.1",
79
+ "brace-expansion": "^5.0.8",
80
+ "@hono/node-server": "^2.0.5"
75
81
  }
76
82
  }
@@ -12,8 +12,8 @@
12
12
  {
13
13
  "type": "git",
14
14
  "url": "https://github.com/openresty/luajit2.git",
15
- "tag": "v2.1-20260620",
16
- "commit": "b411bec3ce550ef9968fc83bca094455cf812c1f",
15
+ "tag": "v2.1-20260717",
16
+ "commit": "1e5c0db1a86c9d0645bdd05e88e3e7a295831074",
17
17
  "x-checker-data": {
18
18
  "type": "git",
19
19
  "tag-pattern": "^v([\\d.-]+)$",
@@ -0,0 +1,41 @@
1
+ {
2
+ "version": 1,
3
+ "skills": {
4
+ "debug-mantra": {
5
+ "source": "thananon/9arm-skills",
6
+ "sourceType": "github",
7
+ "skillPath": "skills/engineering/debug-mantra/SKILL.md",
8
+ "computedHash": "7c519ffda61c6a44db8ef12f6a9ed06de920b9b4faa2e7b47b0032b9c78056e3"
9
+ },
10
+ "management-talk": {
11
+ "source": "thananon/9arm-skills",
12
+ "sourceType": "github",
13
+ "skillPath": "skills/productivity/management-talk/SKILL.md",
14
+ "computedHash": "65bb8510492db4e31f465e0ba4cc73570772d1dde75839c49993cd85a8437a54"
15
+ },
16
+ "post-mortem": {
17
+ "source": "thananon/9arm-skills",
18
+ "sourceType": "github",
19
+ "skillPath": "skills/engineering/post-mortem/SKILL.md",
20
+ "computedHash": "124d195540d43cc922ee93d398996ac8c4294528e92e4d3d6ad83deaf5cfe59a"
21
+ },
22
+ "qwen-agent": {
23
+ "source": "thananon/9arm-skills",
24
+ "sourceType": "github",
25
+ "skillPath": "skills/engineering/qwen-agent/SKILL.md",
26
+ "computedHash": "237e1a708655b68ab8066455d16b1e14ccba55ab682764e21843f2e47848f0a3"
27
+ },
28
+ "qwenchance": {
29
+ "source": "thananon/9arm-skills",
30
+ "sourceType": "github",
31
+ "skillPath": "skills/productivity/qwenchance/SKILL.md",
32
+ "computedHash": "116b0e8cc306b2ee891dd9a70160842528420153273e0109933224b49c22adf8"
33
+ },
34
+ "scrutinize": {
35
+ "source": "thananon/9arm-skills",
36
+ "sourceType": "github",
37
+ "skillPath": "skills/engineering/scrutinize/SKILL.md",
38
+ "computedHash": "d82a0b76617a00069f972cd0efd57c591e18b56f7aa0652aec63f6cb37cfd735"
39
+ }
40
+ }
41
+ }
@@ -6,7 +6,7 @@
6
6
  <title>Mint Agent (Web)</title>
7
7
  <link rel="icon" type="image/png" href="./assets/icon.png">
8
8
  <!-- Load font families used in settings -->
9
- <link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600&family=Mali:wght@400;500;600&family=Prompt:wght@400;500&family=Sarabun:wght@400;500&display=swap" rel="stylesheet">
9
+ <link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600&family=Mali:wght@400;500;600&family=Prompt:wght@400;500;600&family=Sarabun:wght@400;500;600&family=Kanit:wght@400;500;600&family=Mitr:wght@400;500&family=Noto+Sans+Thai:wght@400;500;600&family=Inter:wght@400;500;600&family=Fira+Code:wght@400;500&display=swap" rel="stylesheet">
10
10
  <!-- Cubism must be available before the Live2D module is imported. -->
11
11
  <script src="./Live2DCubismCore.js"></script>
12
12
  </head>
@@ -6,7 +6,7 @@
6
6
  <title>Mint Agent</title>
7
7
  <link rel="icon" type="image/png" href="./assets/icon.png">
8
8
  <!-- Load font families used in settings -->
9
- <link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600&family=Mali:wght@400;500;600&family=Prompt:wght@400;500&family=Sarabun:wght@400;500&display=swap" rel="stylesheet">
9
+ <link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600&family=Mali:wght@400;500;600&family=Prompt:wght@400;500;600&family=Sarabun:wght@400;500;600&family=Kanit:wght@400;500;600&family=Mitr:wght@400;500&family=Noto+Sans+Thai:wght@400;500;600&family=Inter:wght@400;500;600&family=Fira+Code:wght@400;500&display=swap" rel="stylesheet">
10
10
  <!-- Cubism must be available before the Live2D module is imported. -->
11
11
  <script src="./Live2DCubismCore.js"></script>
12
12
  </head>
@@ -0,0 +1,27 @@
1
+ export interface SlashCommand {
2
+ command: string
3
+ description: string
4
+ usage?: string
5
+ category?: 'system' | 'workspace' | 'models' | 'tools'
6
+ }
7
+
8
+ export const SLASH_COMMANDS: SlashCommand[] = [
9
+ { command: '/cd', description: 'Change active workspace directory', usage: '/cd <path>', category: 'workspace' },
10
+ { command: '/code', description: 'Run in code-agent mode', usage: '/code <task>', category: 'tools' },
11
+ { command: '/fast', description: 'Toggle fast mode (hide thinking traces)', usage: '/fast [on|off]', category: 'system' },
12
+ { command: '/help', description: 'Show interactive help menu', usage: '/help', category: 'system' },
13
+ { command: '/image', description: 'Attach image from disk', usage: '/image [path] [prompt]', category: 'tools' },
14
+ { command: '/image-provider', description: 'List or switch default image gen provider', usage: '/image-provider [name]', category: 'models' },
15
+ { command: '/learn', description: 'Import persistent skill/instruction file', usage: '/learn [path]', category: 'tools' },
16
+ { command: '/mcp', description: 'List configured MCP servers & status', usage: '/mcp [list|allow]', category: 'tools' },
17
+ { command: '/memory', description: 'Manage long-term memory store', usage: '/memory [list|clear|get|set]', category: 'tools' },
18
+ { command: '/models', description: 'List AI providers or switch active provider/model', usage: '/models [provider/model]', category: 'models' },
19
+ { command: '/multi-agent', description: 'Toggle Multi-Agent Collaboration system', usage: '/multi-agent [on|off]', category: 'system' },
20
+ { command: '/paste', description: 'Attach image from clipboard', usage: '/paste [prompt]', category: 'tools' },
21
+ { command: '/plugins', description: 'List or generate plugins/skills', usage: '/plugins [name]', category: 'tools' },
22
+ { command: '/skill add', description: 'Add or install global skill file or folder', usage: '/skill add <path>', category: 'tools' },
23
+ { command: '/stats', description: 'Show session statistics', usage: '/stats', category: 'system' },
24
+ { command: '/veo', description: 'Generate video using Google Veo', usage: '/veo <prompt>', category: 'tools' },
25
+ { command: '/video-provider', description: 'List or switch default video gen provider', usage: '/video-provider [name]', category: 'models' },
26
+ ]
27
+
@@ -28,6 +28,7 @@ export interface MintPlatformApi {
28
28
  message: string,
29
29
  imageDataUri?: string | null,
30
30
  audioDataUri?: string | null,
31
+ videoDataUri?: string | null,
31
32
  documentAttachment?: any | null,
32
33
  workspacePath?: string | null,
33
34
  chatId?: string | null,
@@ -38,6 +39,7 @@ export interface MintPlatformApi {
38
39
  onChunk: (chunk: string) => void,
39
40
  imageDataUri?: string | null,
40
41
  audioDataUri?: string | null,
42
+ videoDataUri?: string | null,
41
43
  systemInstruction?: string,
42
44
  onProgress?: (progress: AgentProgress) => void,
43
45
  documentAttachment?: any | null,
@@ -51,6 +53,7 @@ export interface MintPlatformApi {
51
53
  saveSystemInteraction(
52
54
  chatId: string,
53
55
  userText: string,
56
+ aiText: string,
54
57
  provider: string,
55
58
  model: string,
56
59
  ): Promise<any>
@@ -8,6 +8,7 @@ export interface RuntimeStatus {
8
8
  backend: string
9
9
  configPath: string
10
10
  activeProvider: string
11
+ activeModel?: string
11
12
  availableProviders: string[]
12
13
  integrations: Record<string, unknown>
13
14
  localIp?: string
@@ -76,6 +77,9 @@ export interface ImageGenRequest {
76
77
  numImages?: number
77
78
  model?: string
78
79
  provider?: string
80
+ imageDataUri?: string
81
+ maskDataUri?: string
82
+ mode?: 'generate' | 'edit' | 'inpaint'
79
83
  }
80
84
 
81
85
  export interface ImageGenProviders {
@@ -13,11 +13,30 @@ export interface ApprovalDetails {
13
13
  isDangerous: boolean
14
14
  }
15
15
 
16
+ function parseDiffStats(diffText?: string): string {
17
+ if (!diffText) return ''
18
+ let additions = 0
19
+ let deletions = 0
20
+ const lines = diffText.split('\n')
21
+ for (const line of lines) {
22
+ if (line.startsWith('+++ ') || line.startsWith('--- ')) continue
23
+ if (line.startsWith('+')) additions++
24
+ else if (line.startsWith('-')) deletions++
25
+ }
26
+ return `(+${additions} -${deletions})`
27
+ }
28
+
16
29
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
17
30
  export function renderApprovalDetails(approval: any): ApprovalDetails {
18
31
  if (!approval) return { title: 'Action Pending Approval', body: 'No action details available.', isDangerous: false }
19
- if (approval.WriteFile) return { title: 'Write File', body: `Path: ${approval.WriteFile.path}`, reason: approval.WriteFile.diff ? `Diff:\n${approval.WriteFile.diff}` : 'Writing new file content.', isDangerous: false }
20
- if (approval.ApplyPatch) return { title: 'Apply Patch', body: `Path: ${approval.ApplyPatch.path}`, reason: approval.ApplyPatch.diff ? `Diff:\n${approval.ApplyPatch.diff}` : 'Applying code patch.', isDangerous: false }
32
+ if (approval.WriteFile) {
33
+ const stats = parseDiffStats(approval.WriteFile.diff)
34
+ return { title: 'Write File', body: `Path: ${approval.WriteFile.path}${stats ? ` ${stats}` : ''}`, reason: approval.WriteFile.diff ? `Diff:\n${approval.WriteFile.diff}` : 'Writing new file content.', isDangerous: false }
35
+ }
36
+ if (approval.ApplyPatch) {
37
+ const stats = parseDiffStats(approval.ApplyPatch.diff)
38
+ return { title: 'Apply Patch', body: `Path: ${approval.ApplyPatch.path}${stats ? ` ${stats}` : ''}`, reason: approval.ApplyPatch.diff ? `Diff:\n${approval.ApplyPatch.diff}` : 'Applying code patch.', isDangerous: false }
39
+ }
21
40
  if (approval.RunShell) return { title: 'Run Shell Command', body: approval.RunShell.command, reason: 'Executing shell commands can modify your system.', isDangerous: true }
22
41
  if (approval.NoteWrite) return { title: 'Write Note', body: `Path: ${approval.NoteWrite.path}`, reason: 'Creating or updating workspace notes.', isDangerous: false }
23
42
  if (approval.RunPlugin) return { title: `Run Plugin: ${approval.RunPlugin.name}`, body: approval.RunPlugin.instruction, reason: 'Executing a native plugin action.', isDangerous: false }
@@ -30,9 +49,35 @@ export function renderApprovalDetails(approval: any): ApprovalDetails {
30
49
  return { title: 'Unknown Action', body: JSON.stringify(approval, null, 2), reason: 'Requires approval to proceed.', isDangerous: false }
31
50
  }
32
51
 
52
+ function parseHunkHeader(line: string): { oldStart: number; newStart: number } | null {
53
+ const trimmed = line.trim()
54
+ if (!trimmed.startsWith('@@')) return null
55
+ const parts = trimmed.split('@@')
56
+ if (parts.length < 3) return null
57
+ const headerBody = parts[1].trim()
58
+ let oldStart = 1
59
+ let newStart = 1
60
+
61
+ for (const token of headerBody.split(/\s+/)) {
62
+ if (token.startsWith('-')) {
63
+ const numStr = token.slice(1).split(',')[0]
64
+ const parsed = parseInt(numStr, 10)
65
+ if (!isNaN(parsed)) oldStart = parsed
66
+ } else if (token.startsWith('+')) {
67
+ const numStr = token.slice(1).split(',')[0]
68
+ const parsed = parseInt(numStr, 10)
69
+ if (!isNaN(parsed)) newStart = parsed
70
+ }
71
+ }
72
+ return { oldStart, newStart }
73
+ }
74
+
33
75
  export function renderDiff(diffText: string): ReactNode {
34
76
  if (!diffText) return null
35
77
  const lines = diffText.split('\n')
78
+ let currentOldLine = 1
79
+ let currentNewLine = 1
80
+
36
81
  return (
37
82
  <div style={{
38
83
  background: '#0b0f19',
@@ -46,18 +91,52 @@ export function renderDiff(diffText: string): ReactNode {
46
91
  lineHeight: '1.4',
47
92
  }}>
48
93
  {lines.map((line, idx) => {
94
+ if (line.startsWith('@@')) {
95
+ const hunk = parseHunkHeader(line)
96
+ if (hunk) {
97
+ currentOldLine = hunk.oldStart
98
+ currentNewLine = hunk.newStart
99
+ }
100
+ return (
101
+ <div key={idx} style={{ color: '#64748b', fontWeight: 'bold', padding: '2px 6px' }}>
102
+ {' ' + line}
103
+ </div>
104
+ )
105
+ }
106
+ if (line.startsWith('---') || line.startsWith('+++')) {
107
+ return (
108
+ <div key={idx} style={{ color: '#64748b', fontWeight: 'bold', padding: '2px 6px' }}>
109
+ {' ' + line}
110
+ </div>
111
+ )
112
+ }
113
+
114
+ let lineNum = ''
49
115
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
50
- let style: any = { whiteSpace: 'pre-wrap', padding: '2px 6px' }
51
- if (line.startsWith('@@') || line.startsWith('---') || line.startsWith('+++')) {
52
- style = { ...style, color: '#64748b', fontWeight: 'bold' }
53
- } else if (line.startsWith('+')) {
116
+ let style: any = { whiteSpace: 'pre-wrap', padding: '2px 6px', display: 'flex', alignItems: 'center' }
117
+ if (line.startsWith('+')) {
118
+ lineNum = String(currentNewLine)
119
+ currentNewLine++
54
120
  style = { ...style, background: 'rgba(16, 185, 129, 0.12)', borderLeft: '3px solid #10b981', color: '#a7f3d0' }
55
121
  } else if (line.startsWith('-')) {
122
+ lineNum = String(currentOldLine)
123
+ currentOldLine++
56
124
  style = { ...style, background: 'rgba(239, 68, 68, 0.12)', borderLeft: '3px solid #ef4444', color: '#fca5a5' }
57
125
  } else {
126
+ lineNum = String(currentNewLine)
127
+ currentOldLine++
128
+ currentNewLine++
58
129
  style = { ...style, color: '#e2e8f0' }
59
130
  }
60
- return <div key={idx} style={style}>{line}</div>
131
+
132
+ return (
133
+ <div key={idx} style={style}>
134
+ <span style={{ color: '#64748b', marginRight: '12px', userSelect: 'none', display: 'inline-block', width: '36px', textAlign: 'right', flexShrink: 0 }}>
135
+ {lineNum}
136
+ </span>
137
+ <span>{line}</span>
138
+ </div>
139
+ )
61
140
  })}
62
141
  </div>
63
142
  )