@pheem49/mint 1.8.2 → 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.
- package/Cargo.lock +55 -3
- package/Cargo.toml +2 -1
- package/Release_Note.md +297 -107
- package/bin/mint +0 -0
- package/crates/mint-cli/Cargo.toml +1 -0
- package/crates/mint-cli/src/actions.rs +233 -0
- package/crates/mint-cli/src/agent.rs +382 -127
- package/crates/mint-cli/src/gmail.rs +22 -2
- package/crates/mint-cli/src/image.rs +8 -8
- package/crates/mint-cli/src/interactive.rs +2888 -0
- package/crates/mint-cli/src/main.rs +391 -2506
- package/crates/mint-cli/src/markdown.rs +294 -0
- package/crates/mint-cli/src/onboard.rs +152 -109
- package/crates/mint-cli/src/setup.rs +143 -143
- package/crates/mint-cli/src/updater.rs +48 -6
- package/crates/mint-core/Cargo.toml +1 -0
- package/crates/mint-core/src/agent_loop.rs +1 -0
- package/crates/mint-core/src/api_server.rs +431 -65
- package/crates/mint-core/src/browser.rs +630 -102
- package/crates/mint-core/src/channels.rs +105 -6
- package/crates/mint-core/src/chat.rs +174 -11
- package/crates/mint-core/src/code_tools.rs +47 -31
- package/crates/mint-core/src/config.rs +302 -17
- package/crates/mint-core/src/image_gen.rs +299 -38
- package/crates/mint-core/src/lib.rs +10 -7
- package/crates/mint-core/src/mcp.rs +119 -6
- package/crates/mint-core/src/memory.rs +6 -1
- package/crates/mint-core/src/orchestration.rs +237 -96
- package/crates/mint-core/src/pictures.rs +67 -5
- package/crates/mint-core/src/semantic.rs +1 -1
- package/crates/mint-core/src/shell.rs +1 -1
- package/crates/mint-core/src/skills.rs +128 -25
- package/crates/mint-core/src/video_gen.rs +235 -0
- package/package.json +8 -2
- package/shared-modules/linux-audio/fluidsynth2-static.json +9 -3
- package/shared-modules/linux-audio/fluidsynth2.json +8 -2
- package/shared-modules/linux-audio/lilv.json +26 -19
- package/shared-modules/linux-audio/lv2.json +0 -1
- package/shared-modules/luajit/luajit.json +2 -2
- package/skills-lock.json +41 -0
- package/src/renderer/index-web.html +1 -1
- package/src/renderer/index.html +1 -1
- package/src/renderer/shared/components/AgentActivityTable.tsx +25 -6
- package/src/renderer/shared/constants/slashCommands.ts +27 -0
- package/src/renderer/shared/platform.ts +3 -0
- package/src/renderer/shared/types.ts +4 -0
- package/src/renderer/shared/utils/approval.tsx +86 -7
- package/src/renderer/shared/utils/fileIcons.ts +135 -0
- package/src/renderer/shared/utils/markdown.tsx +217 -88
- package/src/renderer/shared/utils/slashCommandProcessor.ts +239 -0
- package/src/renderer/src/components/ChatPanel.tsx +486 -69
- package/src/renderer/src/components/DashboardSidebar.tsx +11 -2
- package/src/renderer/src/components/ImageStudioPanel.tsx +87 -4
- package/src/renderer/src/components/MintDashboard.tsx +195 -7
- package/src/renderer/src/components/PicturesLibrary.tsx +156 -15
- package/src/renderer/src/components/Settings/AgentsTab.tsx +21 -2
- package/src/renderer/src/components/Settings/AutomationTab.tsx +316 -18
- package/src/renderer/src/components/Settings/GeneralTab.tsx +795 -413
- package/src/renderer/src/components/Settings/PluginsTab.tsx +304 -70
- package/src/renderer/src/components/Settings/ThemeTab.tsx +6 -1
- package/src/renderer/src/components/SettingsWindow.tsx +106 -3
- package/src/renderer/src/components/VeoStudioPanel.tsx +549 -0
- package/src/renderer/src/components/WorkspacePanel.tsx +7 -125
- package/src/renderer/src/css/image-studio.css +106 -0
- package/src/renderer/src/css/settings.css +158 -0
- package/src/renderer/src/css/styles.css +368 -1
- package/src/renderer/src/css/veo-studio.css +626 -0
- package/src/renderer/src/index.css +1 -0
- package/src/renderer/src/tauri.ts +144 -17
- package/src/renderer/src-web/components/ChatPanel.tsx +385 -93
- package/src/renderer/src-web/components/DashboardSidebar.tsx +16 -1
- package/src/renderer/src-web/components/ImageStudioPanel.tsx +87 -4
- package/src/renderer/src-web/components/MintDashboard.tsx +156 -5
- package/src/renderer/src-web/components/PicturesLibrary.tsx +155 -15
- package/src/renderer/src-web/components/Settings/AgentsTab.tsx +21 -2
- package/src/renderer/src-web/components/Settings/AutomationTab.tsx +316 -18
- package/src/renderer/src-web/components/Settings/GeneralTab.tsx +795 -411
- package/src/renderer/src-web/components/Settings/PluginsTab.tsx +304 -70
- package/src/renderer/src-web/components/Settings/ThemeTab.tsx +6 -1
- package/src/renderer/src-web/components/SettingsWindow.tsx +110 -3
- package/src/renderer/src-web/components/VeoStudioPanel.tsx +549 -0
- package/src/renderer/src-web/css/image-studio.css +106 -0
- package/src/renderer/src-web/css/settings.css +158 -0
- package/src/renderer/src-web/css/styles.css +369 -1
- package/src/renderer/src-web/css/veo-studio.css +650 -0
- package/src/renderer/src-web/index.css +1 -0
- package/src/renderer/src-web/tauri.ts +135 -16
- package/src-tauri/src/desktop.rs +18 -16
- package/src-tauri/src/discord_rpc.rs +1 -1
- package/src-tauri/src/lib.rs +155 -25
- package/src-tauri/src/webhooks.rs +45 -31
- package/src-tauri/src/workflows.rs +1 -0
- package/src-tauri/tauri.conf.json +1 -1
- package/temp_gridgeist/AGENTS.md +8 -0
- package/temp_gridgeist/CHANGELOG.md +130 -0
- package/temp_gridgeist/LICENSE +21 -0
- package/temp_gridgeist/README.md +323 -0
- package/temp_gridgeist/README.th.md +325 -0
- package/temp_gridgeist/skills/gridgeist/SKILL.md +88 -0
- package/temp_gridgeist/skills/gridgeist/agents/openai.yaml +9 -0
- package/temp_gridgeist/skills/gridgeist/assets/gridgeist-small.svg +10 -0
- package/temp_gridgeist/skills/gridgeist/assets/gridgeist.png +0 -0
- package/temp_gridgeist/skills/gridgeist/assets/gridgeist.svg +18 -0
- package/temp_gridgeist/skills/gridgeist/references/design-language.md +88 -0
- package/temp_gridgeist/skills/gridgeist/references/review-checklist.md +128 -0
- package/temp_gridgeist/skills/gridgeist/references/system-contract.md +109 -0
|
@@ -61,7 +61,24 @@ async fn telegram_loop() -> Result<(), String> {
|
|
|
61
61
|
) else {
|
|
62
62
|
continue;
|
|
63
63
|
};
|
|
64
|
-
let
|
|
64
|
+
let formatted_chat_id = format!("telegram:{chat_id}");
|
|
65
|
+
if let Ok(config) = load_config() {
|
|
66
|
+
if config.bridge_ack_enabled() {
|
|
67
|
+
let _ = client
|
|
68
|
+
.post(format!(
|
|
69
|
+
"https://api.telegram.org/bot{token}/sendChatAction"
|
|
70
|
+
))
|
|
71
|
+
.json(&json!({ "chat_id": chat_id, "action": "typing" }))
|
|
72
|
+
.send()
|
|
73
|
+
.await;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
let answer = answer_channel(
|
|
77
|
+
text,
|
|
78
|
+
"Reply concisely for a Telegram chat.",
|
|
79
|
+
Some(formatted_chat_id),
|
|
80
|
+
)
|
|
81
|
+
.await;
|
|
65
82
|
let _ = client
|
|
66
83
|
.post(format!("https://api.telegram.org/bot{token}/sendMessage"))
|
|
67
84
|
.json(&json!({ "chat_id": chat_id, "text": answer }))
|
|
@@ -116,7 +133,14 @@ async fn discord_loop() -> Result<(), String> {
|
|
|
116
133
|
mentions.iter().any(|mention| mention["id"].as_str() == Some(&bot_id))
|
|
117
134
|
});
|
|
118
135
|
if !direct_message && !mentioned { continue }
|
|
119
|
-
let
|
|
136
|
+
if let Ok(config) = load_config() {
|
|
137
|
+
if config.bridge_ack_enabled() {
|
|
138
|
+
let _ = crate::HTTP_CLIENT.clone().post(format!("https://discord.com/api/v10/channels/{channel}/typing"))
|
|
139
|
+
.header("Authorization", format!("Bot {token}")).send().await;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
let formatted_chat_id = format!("discord:{channel}");
|
|
143
|
+
let reply = answer_channel(text, "Reply concisely for a Discord chat.", Some(formatted_chat_id)).await;
|
|
120
144
|
let _ = crate::HTTP_CLIENT.clone().post(format!("https://discord.com/api/v10/channels/{channel}/messages"))
|
|
121
145
|
.header("Authorization", format!("Bot {token}")).json(&json!({ "content": reply })).send().await;
|
|
122
146
|
}
|
|
@@ -168,7 +192,13 @@ async fn slack_loop() -> Result<(), String> {
|
|
|
168
192
|
else {
|
|
169
193
|
continue;
|
|
170
194
|
};
|
|
171
|
-
let
|
|
195
|
+
let formatted_chat_id = format!("slack:{channel}");
|
|
196
|
+
let reply = answer_channel(
|
|
197
|
+
text,
|
|
198
|
+
"Reply concisely for a Slack chat.",
|
|
199
|
+
Some(formatted_chat_id),
|
|
200
|
+
)
|
|
201
|
+
.await;
|
|
172
202
|
let _ = crate::HTTP_CLIENT
|
|
173
203
|
.clone()
|
|
174
204
|
.post("https://slack.com/api/chat.postMessage")
|
|
@@ -180,20 +210,89 @@ async fn slack_loop() -> Result<(), String> {
|
|
|
180
210
|
Ok(())
|
|
181
211
|
}
|
|
182
212
|
|
|
183
|
-
|
|
213
|
+
pub fn is_action_intent(text: &str) -> bool {
|
|
214
|
+
let lower = text.to_lowercase();
|
|
215
|
+
let action_keywords = [
|
|
216
|
+
"แก้",
|
|
217
|
+
"สร้าง",
|
|
218
|
+
"ลบ",
|
|
219
|
+
"รัน",
|
|
220
|
+
"ตรวจ",
|
|
221
|
+
"ค้นหา",
|
|
222
|
+
"เช็ค",
|
|
223
|
+
"ดูไฟล์",
|
|
224
|
+
"อ่านไฟล์",
|
|
225
|
+
"วิเคราะห์ไฟล์",
|
|
226
|
+
"fix",
|
|
227
|
+
"create",
|
|
228
|
+
"build",
|
|
229
|
+
"run",
|
|
230
|
+
"delete",
|
|
231
|
+
"edit",
|
|
232
|
+
"update",
|
|
233
|
+
"check",
|
|
234
|
+
"find",
|
|
235
|
+
"search",
|
|
236
|
+
"read file",
|
|
237
|
+
"git",
|
|
238
|
+
"patch",
|
|
239
|
+
"test",
|
|
240
|
+
"analyze file",
|
|
241
|
+
];
|
|
242
|
+
action_keywords
|
|
243
|
+
.iter()
|
|
244
|
+
.any(|&keyword| lower.contains(keyword))
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
pub async fn answer_channel(
|
|
248
|
+
text: &str,
|
|
249
|
+
system_instruction: &str,
|
|
250
|
+
chat_id: Option<String>,
|
|
251
|
+
) -> String {
|
|
184
252
|
let Ok(config) = load_config() else {
|
|
185
253
|
return "Mint config error".into();
|
|
186
254
|
};
|
|
255
|
+
let workspace = config.active_workspace_path();
|
|
256
|
+
let workspace_str = workspace.as_ref().map(|p| p.to_string_lossy().to_string());
|
|
257
|
+
|
|
258
|
+
if is_action_intent(text)
|
|
259
|
+
&& let Some(ref root_path) = workspace
|
|
260
|
+
{
|
|
261
|
+
if root_path.exists() {
|
|
262
|
+
let agent_result = crate::orchestrate_agent_loop(
|
|
263
|
+
&config,
|
|
264
|
+
text,
|
|
265
|
+
root_path,
|
|
266
|
+
None,
|
|
267
|
+
None,
|
|
268
|
+
chat_id.as_deref(),
|
|
269
|
+
None,
|
|
270
|
+
true,
|
|
271
|
+
|_approval| Ok(crate::ApprovalOutcome::Approved),
|
|
272
|
+
|_progress| {},
|
|
273
|
+
|_chunk| {},
|
|
274
|
+
)
|
|
275
|
+
.await;
|
|
276
|
+
|
|
277
|
+
if let Ok(res) = agent_result {
|
|
278
|
+
if !res.summary.trim().is_empty() {
|
|
279
|
+
return res.summary;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
187
285
|
orchestrate_chat(
|
|
188
286
|
&config,
|
|
189
287
|
&ChatRequest {
|
|
190
288
|
message: text.into(),
|
|
191
289
|
system_instruction: system_instruction.into(),
|
|
192
|
-
chat_id
|
|
290
|
+
chat_id,
|
|
193
291
|
image_data_uri: None,
|
|
194
292
|
audio_data_uri: None,
|
|
293
|
+
video_data_uri: None,
|
|
195
294
|
document_attachment: None,
|
|
196
|
-
workspace_path:
|
|
295
|
+
workspace_path: workspace_str,
|
|
197
296
|
agent_id: None,
|
|
198
297
|
},
|
|
199
298
|
)
|
|
@@ -19,13 +19,13 @@ pub async fn send_chat_with_fallback(
|
|
|
19
19
|
Err(_) => {}
|
|
20
20
|
}
|
|
21
21
|
for provider in config.available_providers() {
|
|
22
|
-
if provider == config.ai_provider.as_str() {
|
|
22
|
+
if provider.as_str() == config.ai_provider.as_str() {
|
|
23
23
|
continue;
|
|
24
24
|
}
|
|
25
|
-
let alt = config_for_provider(config, provider);
|
|
25
|
+
let alt = config_for_provider(config, &provider);
|
|
26
26
|
if let Ok(mut r) = send_chat(&alt, request).await {
|
|
27
27
|
r.fallback_provider = Some(config.ai_provider.clone());
|
|
28
|
-
return Ok((r, Some(provider
|
|
28
|
+
return Ok((r, Some(provider)));
|
|
29
29
|
}
|
|
30
30
|
}
|
|
31
31
|
// all fallbacks failed — retry primary to surface original error
|
|
@@ -50,13 +50,13 @@ where
|
|
|
50
50
|
}
|
|
51
51
|
}
|
|
52
52
|
for provider in config.available_providers() {
|
|
53
|
-
if provider == config.ai_provider.as_str() {
|
|
53
|
+
if provider.as_str() == config.ai_provider.as_str() {
|
|
54
54
|
continue;
|
|
55
55
|
}
|
|
56
|
-
let alt = config_for_provider(config, provider);
|
|
56
|
+
let alt = config_for_provider(config, &provider);
|
|
57
57
|
if let Ok(mut r) = stream_chat(&alt, request, &mut on_chunk).await {
|
|
58
58
|
r.fallback_provider = Some(config.ai_provider.clone());
|
|
59
|
-
return Ok((r, Some(provider
|
|
59
|
+
return Ok((r, Some(provider)));
|
|
60
60
|
}
|
|
61
61
|
}
|
|
62
62
|
stream_chat(config, request, &mut on_chunk)
|
|
@@ -96,6 +96,8 @@ pub struct ChatRequest {
|
|
|
96
96
|
#[serde(default)]
|
|
97
97
|
pub audio_data_uri: Option<String>,
|
|
98
98
|
#[serde(default)]
|
|
99
|
+
pub video_data_uri: Option<String>,
|
|
100
|
+
#[serde(default)]
|
|
99
101
|
pub document_attachment: Option<DocumentAttachment>,
|
|
100
102
|
#[serde(default)]
|
|
101
103
|
pub workspace_path: Option<String>,
|
|
@@ -230,6 +232,7 @@ pub async fn send_chat(
|
|
|
230
232
|
"ollama" => call_ollama(&client, config, request).await?,
|
|
231
233
|
"anthropic" => call_anthropic(&client, config, request).await?,
|
|
232
234
|
"huggingface" => call_huggingface(&client, config, request).await?,
|
|
235
|
+
p if p.starts_with("custom:") => call_custom_provider(&client, config, request).await?,
|
|
233
236
|
other => return Err(ChatError::UnsupportedProvider(other.into())),
|
|
234
237
|
};
|
|
235
238
|
Ok(ChatResponse {
|
|
@@ -259,6 +262,9 @@ where
|
|
|
259
262
|
"ollama" => stream_ollama(&client, config, request, &mut on_chunk).await?,
|
|
260
263
|
"anthropic" => stream_anthropic(&client, config, request, &mut on_chunk).await?,
|
|
261
264
|
"huggingface" => stream_huggingface(&client, config, request, &mut on_chunk).await?,
|
|
265
|
+
p if p.starts_with("custom:") => {
|
|
266
|
+
stream_custom_provider(&client, config, request, &mut on_chunk).await?
|
|
267
|
+
}
|
|
262
268
|
other => return Err(ChatError::UnsupportedProvider(other.into())),
|
|
263
269
|
};
|
|
264
270
|
Ok(ChatResponse {
|
|
@@ -280,10 +286,12 @@ async fn call_gemini(
|
|
|
280
286
|
let url =
|
|
281
287
|
format!("https://generativelanguage.googleapis.com/v1beta/models/{model}:generateContent");
|
|
282
288
|
|
|
289
|
+
let payload = gemini_chat_payload(config, request)?;
|
|
290
|
+
|
|
283
291
|
let response: Value = client
|
|
284
292
|
.post(url)
|
|
285
293
|
.header("x-goog-api-key", api_key)
|
|
286
|
-
.json(&
|
|
294
|
+
.json(&payload)
|
|
287
295
|
.send()
|
|
288
296
|
.await?
|
|
289
297
|
.error_for_status()?
|
|
@@ -708,6 +716,109 @@ fn required_key(provider: &str, key: &str) -> Result<(), ChatError> {
|
|
|
708
716
|
}
|
|
709
717
|
}
|
|
710
718
|
|
|
719
|
+
/// Resolve which model to use for the current custom provider.
|
|
720
|
+
///
|
|
721
|
+
/// Checks `config.extra["customModelSelections"]` first (set by the UI when the
|
|
722
|
+
/// user picks a specific model from the multi-model dropdown), then falls back to
|
|
723
|
+
/// the first model defined in the `CustomProvider`.
|
|
724
|
+
fn resolve_custom_model(config: &crate::MintConfig) -> String {
|
|
725
|
+
let provider_id = config
|
|
726
|
+
.ai_provider
|
|
727
|
+
.strip_prefix("custom:")
|
|
728
|
+
.unwrap_or(&config.ai_provider);
|
|
729
|
+
|
|
730
|
+
// Check if a per-provider model selection was persisted in extra config.
|
|
731
|
+
if let Some(selections) = config
|
|
732
|
+
.extra
|
|
733
|
+
.get("customModelSelections")
|
|
734
|
+
.and_then(|v| v.as_object())
|
|
735
|
+
&& let Some(model) = selections.get(provider_id).and_then(|v| v.as_str())
|
|
736
|
+
&& !model.is_empty()
|
|
737
|
+
{
|
|
738
|
+
return model.to_owned();
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
// Fall back to the first model in the provider definition.
|
|
742
|
+
config
|
|
743
|
+
.resolve_custom_provider(&config.ai_provider)
|
|
744
|
+
.and_then(|cp| cp.models.first())
|
|
745
|
+
.map(|m| m.model_id.clone())
|
|
746
|
+
.unwrap_or_default()
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
async fn call_custom_provider(
|
|
750
|
+
client: &Client,
|
|
751
|
+
config: &crate::MintConfig,
|
|
752
|
+
request: &ChatRequest,
|
|
753
|
+
) -> Result<(String, String), ChatError> {
|
|
754
|
+
let provider = config
|
|
755
|
+
.resolve_custom_provider(&config.ai_provider)
|
|
756
|
+
.ok_or_else(|| ChatError::UnsupportedProvider(config.ai_provider.clone()))?;
|
|
757
|
+
|
|
758
|
+
let base_url = provider.base_url.trim_end_matches('/');
|
|
759
|
+
let model = resolve_custom_model(config);
|
|
760
|
+
let payload = openai_chat_payload(&model, request, false);
|
|
761
|
+
|
|
762
|
+
let mut builder = client
|
|
763
|
+
.post(format!("{base_url}/chat/completions"))
|
|
764
|
+
.json(&payload);
|
|
765
|
+
|
|
766
|
+
if !provider.api_key.is_empty() {
|
|
767
|
+
builder = builder.bearer_auth(&provider.api_key);
|
|
768
|
+
}
|
|
769
|
+
for h in &provider.headers {
|
|
770
|
+
if !h.name.is_empty() {
|
|
771
|
+
builder = builder.header(h.name.as_str(), h.value.as_str());
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
let response: serde_json::Value = builder.send().await?.error_for_status()?.json().await?;
|
|
776
|
+
|
|
777
|
+
Ok((
|
|
778
|
+
model,
|
|
779
|
+
response["choices"][0]["message"]["content"]
|
|
780
|
+
.as_str()
|
|
781
|
+
.ok_or(ChatError::MissingResponseText)?
|
|
782
|
+
.into(),
|
|
783
|
+
))
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
async fn stream_custom_provider<F>(
|
|
787
|
+
client: &Client,
|
|
788
|
+
config: &crate::MintConfig,
|
|
789
|
+
request: &ChatRequest,
|
|
790
|
+
on_chunk: &mut F,
|
|
791
|
+
) -> Result<(String, String), ChatError>
|
|
792
|
+
where
|
|
793
|
+
F: FnMut(String),
|
|
794
|
+
{
|
|
795
|
+
let provider = config
|
|
796
|
+
.resolve_custom_provider(&config.ai_provider)
|
|
797
|
+
.ok_or_else(|| ChatError::UnsupportedProvider(config.ai_provider.clone()))?;
|
|
798
|
+
|
|
799
|
+
let base_url = provider.base_url.trim_end_matches('/');
|
|
800
|
+
let model = resolve_custom_model(config);
|
|
801
|
+
let payload = openai_chat_payload(&model, request, true);
|
|
802
|
+
|
|
803
|
+
let mut builder = client
|
|
804
|
+
.post(format!("{base_url}/chat/completions"))
|
|
805
|
+
.json(&payload);
|
|
806
|
+
|
|
807
|
+
if !provider.api_key.is_empty() {
|
|
808
|
+
builder = builder.bearer_auth(&provider.api_key);
|
|
809
|
+
}
|
|
810
|
+
for h in &provider.headers {
|
|
811
|
+
if !h.name.is_empty() {
|
|
812
|
+
builder = builder.header(h.name.as_str(), h.value.as_str());
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
let response = builder.send().await?.error_for_status()?;
|
|
817
|
+
collect_stream(response, StreamFormat::OpenAi, on_chunk)
|
|
818
|
+
.await
|
|
819
|
+
.map(|text| (model, text))
|
|
820
|
+
}
|
|
821
|
+
|
|
711
822
|
fn provider_key(configured: &str, environment_variable: &str) -> String {
|
|
712
823
|
if configured.trim().is_empty() {
|
|
713
824
|
std::env::var(environment_variable).unwrap_or_default()
|
|
@@ -720,17 +831,33 @@ fn require_supported_attachments(provider: &str, request: &ChatRequest) -> Resul
|
|
|
720
831
|
let has_image = request
|
|
721
832
|
.image_data_uri
|
|
722
833
|
.as_ref()
|
|
723
|
-
.
|
|
834
|
+
.is_some_and(|s| !s.trim().is_empty());
|
|
724
835
|
let has_audio = request
|
|
725
836
|
.audio_data_uri
|
|
726
837
|
.as_ref()
|
|
727
|
-
.
|
|
838
|
+
.is_some_and(|s| !s.trim().is_empty());
|
|
839
|
+
let has_video = request
|
|
840
|
+
.video_data_uri
|
|
841
|
+
.as_ref()
|
|
842
|
+
.is_some_and(|s| !s.trim().is_empty());
|
|
728
843
|
|
|
729
844
|
match provider {
|
|
730
845
|
"gemini" => Ok(()),
|
|
731
|
-
"ollama" if has_audio =>
|
|
846
|
+
"ollama" if has_audio || has_video => {
|
|
847
|
+
Err(ChatError::UnsupportedAttachments(provider.into()))
|
|
848
|
+
}
|
|
732
849
|
"ollama" => Ok(()),
|
|
733
|
-
|
|
850
|
+
// Custom providers are text-only for now; reject any multimodal content.
|
|
851
|
+
p if p.starts_with("custom:") => {
|
|
852
|
+
if has_image || has_audio || has_video {
|
|
853
|
+
Err(ChatError::UnsupportedAttachments(provider.into()))
|
|
854
|
+
} else {
|
|
855
|
+
Ok(())
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
_ if has_image || has_audio || has_video => {
|
|
859
|
+
Err(ChatError::UnsupportedAttachments(provider.into()))
|
|
860
|
+
}
|
|
734
861
|
_ => Ok(()),
|
|
735
862
|
}
|
|
736
863
|
}
|
|
@@ -920,6 +1047,21 @@ fn gemini_parts(request: &ChatRequest) -> Result<Vec<Value>, ChatError> {
|
|
|
920
1047
|
}));
|
|
921
1048
|
}
|
|
922
1049
|
}
|
|
1050
|
+
if let Some(ref video_data) = request.video_data_uri {
|
|
1051
|
+
for vid in video_data.split_whitespace() {
|
|
1052
|
+
let payload = vid
|
|
1053
|
+
.strip_prefix("data:")
|
|
1054
|
+
.and_then(|payload| payload.split_once(";base64,"))
|
|
1055
|
+
.filter(|(mime_type, data)| mime_type.starts_with("video/") && !data.is_empty())
|
|
1056
|
+
.ok_or(ChatError::InvalidAttachment)?;
|
|
1057
|
+
parts.push(json!({
|
|
1058
|
+
"inlineData": {
|
|
1059
|
+
"mimeType": payload.0,
|
|
1060
|
+
"data": payload.1
|
|
1061
|
+
}
|
|
1062
|
+
}));
|
|
1063
|
+
}
|
|
1064
|
+
}
|
|
923
1065
|
Ok(parts)
|
|
924
1066
|
}
|
|
925
1067
|
|
|
@@ -933,6 +1075,26 @@ mod tests {
|
|
|
933
1075
|
assert!(matches!(error, ChatError::MissingApiKey(provider) if provider == "gemini"));
|
|
934
1076
|
}
|
|
935
1077
|
|
|
1078
|
+
#[test]
|
|
1079
|
+
fn rejects_video_attachments_for_non_gemini_providers() {
|
|
1080
|
+
let request = ChatRequest {
|
|
1081
|
+
message: "describe this video".into(),
|
|
1082
|
+
system_instruction: String::new(),
|
|
1083
|
+
chat_id: None,
|
|
1084
|
+
image_data_uri: None,
|
|
1085
|
+
audio_data_uri: None,
|
|
1086
|
+
video_data_uri: Some("data:video/mp4;base64,AAAA".into()),
|
|
1087
|
+
document_attachment: None,
|
|
1088
|
+
workspace_path: None,
|
|
1089
|
+
agent_id: None,
|
|
1090
|
+
};
|
|
1091
|
+
|
|
1092
|
+
let error = require_supported_attachments("openai", &request).unwrap_err();
|
|
1093
|
+
assert!(
|
|
1094
|
+
matches!(error, ChatError::UnsupportedAttachments(provider) if provider == "openai")
|
|
1095
|
+
);
|
|
1096
|
+
}
|
|
1097
|
+
|
|
936
1098
|
#[test]
|
|
937
1099
|
fn parses_stream_provider_formats() {
|
|
938
1100
|
assert_eq!(
|
|
@@ -965,6 +1127,7 @@ mod tests {
|
|
|
965
1127
|
chat_id: None,
|
|
966
1128
|
image_data_uri: Some("data:image/png;base64,aGk= data:image/jpeg;base64,Ynl5".into()),
|
|
967
1129
|
audio_data_uri: None,
|
|
1130
|
+
video_data_uri: None,
|
|
968
1131
|
document_attachment: None,
|
|
969
1132
|
workspace_path: None,
|
|
970
1133
|
agent_id: None,
|
|
@@ -6,6 +6,7 @@ use std::{
|
|
|
6
6
|
|
|
7
7
|
use serde::{Deserialize, Serialize};
|
|
8
8
|
use sha2::{Digest, Sha256};
|
|
9
|
+
use similar::{ChangeTag, TextDiff};
|
|
9
10
|
use thiserror::Error;
|
|
10
11
|
|
|
11
12
|
use crate::{Capability, MintConfig, SafetyError, assert_path_capability};
|
|
@@ -401,17 +402,34 @@ fn sha256(content: &str) -> String {
|
|
|
401
402
|
|
|
402
403
|
fn full_file_diff(path: &Path, previous: &str, next: &str) -> String {
|
|
403
404
|
let label = path.display();
|
|
404
|
-
let
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
405
|
+
let diff = TextDiff::from_lines(previous, next);
|
|
406
|
+
let mut lines = vec![format!("--- a/{label}"), format!("+++ b/{label}")];
|
|
407
|
+
|
|
408
|
+
for group in diff.grouped_ops(3) {
|
|
409
|
+
let mut first = true;
|
|
410
|
+
for op in group {
|
|
411
|
+
if first {
|
|
412
|
+
let old_start = op.old_range().start + 1;
|
|
413
|
+
let old_len = op.old_range().len();
|
|
414
|
+
let new_start = op.new_range().start + 1;
|
|
415
|
+
let new_len = op.new_range().len();
|
|
416
|
+
lines.push(format!(
|
|
417
|
+
"@@ -{old_start},{old_len} +{new_start},{new_len} @@"
|
|
418
|
+
));
|
|
419
|
+
first = false;
|
|
420
|
+
}
|
|
421
|
+
for change in diff.iter_changes(&op) {
|
|
422
|
+
let sign = match change.tag() {
|
|
423
|
+
ChangeTag::Delete => "-",
|
|
424
|
+
ChangeTag::Insert => "+",
|
|
425
|
+
ChangeTag::Equal => " ",
|
|
426
|
+
};
|
|
427
|
+
let content = change.value().trim_end_matches(&['\r', '\n'][..]);
|
|
428
|
+
lines.push(format!("{sign}{content}"));
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
|
|
415
433
|
lines.join("\n")
|
|
416
434
|
}
|
|
417
435
|
|
|
@@ -536,18 +554,17 @@ pub async fn fetch_github_repo_summary(owner: &str, repo: &str) -> Result<String
|
|
|
536
554
|
.await
|
|
537
555
|
.map_err(|e| e.to_string())?;
|
|
538
556
|
let mut file_tree = String::from("Unavailable");
|
|
539
|
-
if contents_resp.status().is_success()
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
file_tree = files.join("\n");
|
|
549
|
-
}
|
|
557
|
+
if contents_resp.status().is_success()
|
|
558
|
+
&& let Ok(contents_info) = contents_resp.json::<serde_json::Value>().await
|
|
559
|
+
&& let Some(arr) = contents_info.as_array()
|
|
560
|
+
{
|
|
561
|
+
let mut files = Vec::new();
|
|
562
|
+
for item in arr {
|
|
563
|
+
let name = item["name"].as_str().unwrap_or("");
|
|
564
|
+
let r#type = item["type"].as_str().unwrap_or("");
|
|
565
|
+
files.push(format!("- {} ({})", name, r#type));
|
|
550
566
|
}
|
|
567
|
+
file_tree = files.join("\n");
|
|
551
568
|
}
|
|
552
569
|
|
|
553
570
|
// 3. Fetch README.md
|
|
@@ -558,15 +575,14 @@ pub async fn fetch_github_repo_summary(owner: &str, repo: &str) -> Result<String
|
|
|
558
575
|
.await
|
|
559
576
|
.map_err(|e| e.to_string())?;
|
|
560
577
|
let mut readme_text = String::from("No README available.");
|
|
561
|
-
if readme_resp.status().is_success()
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
}
|
|
578
|
+
if readme_resp.status().is_success()
|
|
579
|
+
&& let Ok(readme_info) = readme_resp.json::<serde_json::Value>().await
|
|
580
|
+
&& let Some(content_b64) = readme_info["content"].as_str()
|
|
581
|
+
{
|
|
582
|
+
let cleaned_b64 = content_b64.replace(['\n', '\r'], "");
|
|
583
|
+
use base64::{Engine as _, engine::general_purpose::STANDARD};
|
|
584
|
+
if let Ok(decoded_bytes) = STANDARD.decode(cleaned_b64) {
|
|
585
|
+
readme_text = String::from_utf8_lossy(&decoded_bytes).to_string();
|
|
570
586
|
}
|
|
571
587
|
}
|
|
572
588
|
|