@pheem49/mint 1.8.2 → 1.9.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.toml +1 -1
- package/Release_Note.md +48 -3
- package/crates/mint-cli/src/agent.rs +95 -84
- package/crates/mint-cli/src/image.rs +8 -8
- package/crates/mint-cli/src/main.rs +806 -403
- package/crates/mint-cli/src/onboard.rs +108 -108
- package/crates/mint-cli/src/setup.rs +143 -143
- package/crates/mint-core/src/api_server.rs +57 -54
- package/crates/mint-core/src/browser.rs +1 -1
- package/crates/mint-core/src/chat.rs +123 -8
- package/crates/mint-core/src/code_tools.rs +18 -20
- package/crates/mint-core/src/config.rs +184 -17
- package/crates/mint-core/src/image_gen.rs +5 -5
- package/crates/mint-core/src/lib.rs +5 -5
- package/crates/mint-core/src/memory.rs +6 -1
- package/crates/mint-core/src/orchestration.rs +104 -85
- package/crates/mint-core/src/pictures.rs +3 -4
- 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 +46 -19
- package/package.json +1 -1
- 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/src/renderer/shared/components/AgentActivityTable.tsx +25 -6
- package/src/renderer/shared/utils/fileIcons.ts +135 -0
- package/src/renderer/src/components/ChatPanel.tsx +25 -5
- package/src/renderer/src/components/MintDashboard.tsx +45 -2
- package/src/renderer/src/components/Settings/AgentsTab.tsx +21 -2
- package/src/renderer/src/components/Settings/GeneralTab.tsx +256 -2
- package/src/renderer/src/components/SettingsWindow.tsx +68 -1
- package/src/renderer/src/components/WorkspacePanel.tsx +7 -125
- package/src/renderer/src/css/styles.css +16 -0
- package/src/renderer/src-web/components/ChatPanel.tsx +25 -5
- package/src/renderer/src-web/components/MintDashboard.tsx +19 -1
- package/src/renderer/src-web/components/Settings/AgentsTab.tsx +21 -2
- package/src/renderer/src-web/components/Settings/GeneralTab.tsx +254 -3
- package/src/renderer/src-web/components/SettingsWindow.tsx +75 -1
- package/src/renderer/src-web/css/styles.css +16 -0
- package/src-tauri/src/desktop.rs +18 -16
- package/src-tauri/src/discord_rpc.rs +1 -1
- package/src-tauri/src/lib.rs +41 -17
- package/src-tauri/tauri.conf.json +1 -1
|
@@ -213,16 +213,15 @@ pub async fn start_api_server(port: u16) -> Result<(), std::io::Error> {
|
|
|
213
213
|
new_title: String,
|
|
214
214
|
}
|
|
215
215
|
|
|
216
|
-
if let Ok(req) = serde_json::from_str::<RenameRequest>(body)
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
}
|
|
216
|
+
if let Ok(req) = serde_json::from_str::<RenameRequest>(body)
|
|
217
|
+
&& let Ok(memory) = MemoryStore::open_default()
|
|
218
|
+
{
|
|
219
|
+
let updated = memory
|
|
220
|
+
.rename_chat_session(&req.chat_id, &req.new_title)
|
|
221
|
+
.unwrap_or(0);
|
|
222
|
+
let response = serde_json::json!({ "status": "ok", "updated": updated });
|
|
223
|
+
send_json_response(socket, "200 OK", &response.to_string()).await;
|
|
224
|
+
return;
|
|
226
225
|
}
|
|
227
226
|
send_json_response(
|
|
228
227
|
socket,
|
|
@@ -252,12 +251,12 @@ pub async fn start_api_server(port: u16) -> Result<(), std::io::Error> {
|
|
|
252
251
|
key: String,
|
|
253
252
|
value: String,
|
|
254
253
|
}
|
|
255
|
-
if let Ok(req) = serde_json::from_str::<ProfileRequest>(body)
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
254
|
+
if let Ok(req) = serde_json::from_str::<ProfileRequest>(body)
|
|
255
|
+
&& let Ok(memory) = MemoryStore::open_default()
|
|
256
|
+
{
|
|
257
|
+
let _ = memory.set_profile(&req.key, &req.value);
|
|
258
|
+
send_json_response(socket, "200 OK", "{\"status\":\"ok\"}").await;
|
|
259
|
+
return;
|
|
261
260
|
}
|
|
262
261
|
send_json_response(
|
|
263
262
|
socket,
|
|
@@ -341,25 +340,18 @@ pub async fn start_api_server(port: u16) -> Result<(), std::io::Error> {
|
|
|
341
340
|
activity: Vec<AgentProgress>,
|
|
342
341
|
}
|
|
343
342
|
|
|
344
|
-
if let Ok(payload) = serde_json::from_str::<SaveAgentActivityRequest>(body)
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
.await;
|
|
357
|
-
return;
|
|
358
|
-
}
|
|
359
|
-
}
|
|
360
|
-
Err(_) => {}
|
|
361
|
-
}
|
|
362
|
-
}
|
|
343
|
+
if let Ok(payload) = serde_json::from_str::<SaveAgentActivityRequest>(body)
|
|
344
|
+
&& let Ok(memory) = MemoryStore::open_default()
|
|
345
|
+
&& let Ok(activity_json) = serde_json::to_string(&payload.activity)
|
|
346
|
+
&& memory
|
|
347
|
+
.set_interaction_agent_activity_json(
|
|
348
|
+
payload.interaction_id,
|
|
349
|
+
&activity_json,
|
|
350
|
+
)
|
|
351
|
+
.is_ok()
|
|
352
|
+
{
|
|
353
|
+
send_json_response(socket, "200 OK", "{\"status\":\"ok\"}").await;
|
|
354
|
+
return;
|
|
363
355
|
}
|
|
364
356
|
send_json_response(
|
|
365
357
|
socket,
|
|
@@ -406,11 +398,11 @@ pub async fn start_api_server(port: u16) -> Result<(), std::io::Error> {
|
|
|
406
398
|
}
|
|
407
399
|
}
|
|
408
400
|
("POST", "/api/config") => {
|
|
409
|
-
if let Ok(new_config) = serde_json::from_str::<MintConfig>(body)
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
401
|
+
if let Ok(new_config) = serde_json::from_str::<MintConfig>(body)
|
|
402
|
+
&& save_config(&new_config).is_ok()
|
|
403
|
+
{
|
|
404
|
+
send_json_response(socket, "200 OK", "{\"status\":\"ok\"}").await;
|
|
405
|
+
return;
|
|
414
406
|
}
|
|
415
407
|
send_json_response(
|
|
416
408
|
socket,
|
|
@@ -693,22 +685,27 @@ pub async fn start_api_server(port: u16) -> Result<(), std::io::Error> {
|
|
|
693
685
|
"text": format!("Error orchestrating chat: {e}")
|
|
694
686
|
}
|
|
695
687
|
});
|
|
696
|
-
let _ = tx_done
|
|
697
|
-
.send(format!("{}\n", err_json.to_string()));
|
|
688
|
+
let _ = tx_done.send(format!("{}\n", err_json));
|
|
698
689
|
}
|
|
699
690
|
}
|
|
700
691
|
});
|
|
701
692
|
|
|
702
693
|
let abort_handle = join_handle.abort_handle();
|
|
703
694
|
if !chat_id_str.is_empty() {
|
|
704
|
-
crate::ACTIVE_AGENTS
|
|
695
|
+
crate::ACTIVE_AGENTS
|
|
696
|
+
.lock()
|
|
697
|
+
.unwrap()
|
|
698
|
+
.insert(chat_id_str.clone(), abort_handle);
|
|
705
699
|
}
|
|
706
700
|
|
|
707
701
|
let chat_id_str_cleanup = chat_id_str.clone();
|
|
708
702
|
tokio::spawn(async move {
|
|
709
703
|
let _ = join_handle.await;
|
|
710
704
|
if !chat_id_str_cleanup.is_empty() {
|
|
711
|
-
crate::ACTIVE_AGENTS
|
|
705
|
+
crate::ACTIVE_AGENTS
|
|
706
|
+
.lock()
|
|
707
|
+
.unwrap()
|
|
708
|
+
.remove(&chat_id_str_cleanup);
|
|
712
709
|
}
|
|
713
710
|
});
|
|
714
711
|
} else {
|
|
@@ -768,22 +765,27 @@ pub async fn start_api_server(port: u16) -> Result<(), std::io::Error> {
|
|
|
768
765
|
"text": format!("Error orchestrating agent: {e}")
|
|
769
766
|
}
|
|
770
767
|
});
|
|
771
|
-
let _ = tx_done
|
|
772
|
-
.send(format!("{}\n", err_json.to_string()));
|
|
768
|
+
let _ = tx_done.send(format!("{}\n", err_json));
|
|
773
769
|
}
|
|
774
770
|
}
|
|
775
771
|
});
|
|
776
772
|
|
|
777
773
|
let abort_handle = join_handle.abort_handle();
|
|
778
774
|
if !chat_id_str.is_empty() {
|
|
779
|
-
crate::ACTIVE_AGENTS
|
|
775
|
+
crate::ACTIVE_AGENTS
|
|
776
|
+
.lock()
|
|
777
|
+
.unwrap()
|
|
778
|
+
.insert(chat_id_str.clone(), abort_handle);
|
|
780
779
|
}
|
|
781
780
|
|
|
782
781
|
let chat_id_str_cleanup = chat_id_str.clone();
|
|
783
782
|
tokio::spawn(async move {
|
|
784
783
|
let _ = join_handle.await;
|
|
785
784
|
if !chat_id_str_cleanup.is_empty() {
|
|
786
|
-
crate::ACTIVE_AGENTS
|
|
785
|
+
crate::ACTIVE_AGENTS
|
|
786
|
+
.lock()
|
|
787
|
+
.unwrap()
|
|
788
|
+
.remove(&chat_id_str_cleanup);
|
|
787
789
|
}
|
|
788
790
|
});
|
|
789
791
|
}
|
|
@@ -1135,12 +1137,13 @@ fn percent_decode(raw: &str) -> String {
|
|
|
1135
1137
|
let mut output = Vec::with_capacity(bytes.len());
|
|
1136
1138
|
let mut index = 0;
|
|
1137
1139
|
while index < bytes.len() {
|
|
1138
|
-
if bytes[index] == b'%'
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1140
|
+
if bytes[index] == b'%'
|
|
1141
|
+
&& index + 2 < bytes.len()
|
|
1142
|
+
&& let Ok(hex) = u8::from_str_radix(&raw[index + 1..index + 3], 16)
|
|
1143
|
+
{
|
|
1144
|
+
output.push(hex);
|
|
1145
|
+
index += 3;
|
|
1146
|
+
continue;
|
|
1144
1147
|
}
|
|
1145
1148
|
output.push(if bytes[index] == b'+' {
|
|
1146
1149
|
b' '
|
|
@@ -243,7 +243,7 @@ pub async fn spawn_automation_browser(config: &MintConfig) -> Result<(), String>
|
|
|
243
243
|
.unwrap_or("chromium");
|
|
244
244
|
|
|
245
245
|
let profile_dir = dirs::config_dir()
|
|
246
|
-
.unwrap_or_else(
|
|
246
|
+
.unwrap_or_else(std::env::temp_dir)
|
|
247
247
|
.join("mint")
|
|
248
248
|
.join("automation-profile");
|
|
249
249
|
let profile_arg = format!("--user-data-dir={}", profile_dir.to_string_lossy());
|
|
@@ -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)
|
|
@@ -230,6 +230,7 @@ pub async fn send_chat(
|
|
|
230
230
|
"ollama" => call_ollama(&client, config, request).await?,
|
|
231
231
|
"anthropic" => call_anthropic(&client, config, request).await?,
|
|
232
232
|
"huggingface" => call_huggingface(&client, config, request).await?,
|
|
233
|
+
p if p.starts_with("custom:") => call_custom_provider(&client, config, request).await?,
|
|
233
234
|
other => return Err(ChatError::UnsupportedProvider(other.into())),
|
|
234
235
|
};
|
|
235
236
|
Ok(ChatResponse {
|
|
@@ -259,6 +260,9 @@ where
|
|
|
259
260
|
"ollama" => stream_ollama(&client, config, request, &mut on_chunk).await?,
|
|
260
261
|
"anthropic" => stream_anthropic(&client, config, request, &mut on_chunk).await?,
|
|
261
262
|
"huggingface" => stream_huggingface(&client, config, request, &mut on_chunk).await?,
|
|
263
|
+
p if p.starts_with("custom:") => {
|
|
264
|
+
stream_custom_provider(&client, config, request, &mut on_chunk).await?
|
|
265
|
+
}
|
|
262
266
|
other => return Err(ChatError::UnsupportedProvider(other.into())),
|
|
263
267
|
};
|
|
264
268
|
Ok(ChatResponse {
|
|
@@ -708,6 +712,109 @@ fn required_key(provider: &str, key: &str) -> Result<(), ChatError> {
|
|
|
708
712
|
}
|
|
709
713
|
}
|
|
710
714
|
|
|
715
|
+
/// Resolve which model to use for the current custom provider.
|
|
716
|
+
///
|
|
717
|
+
/// Checks `config.extra["customModelSelections"]` first (set by the UI when the
|
|
718
|
+
/// user picks a specific model from the multi-model dropdown), then falls back to
|
|
719
|
+
/// the first model defined in the `CustomProvider`.
|
|
720
|
+
fn resolve_custom_model(config: &crate::MintConfig) -> String {
|
|
721
|
+
let provider_id = config
|
|
722
|
+
.ai_provider
|
|
723
|
+
.strip_prefix("custom:")
|
|
724
|
+
.unwrap_or(&config.ai_provider);
|
|
725
|
+
|
|
726
|
+
// Check if a per-provider model selection was persisted in extra config.
|
|
727
|
+
if let Some(selections) = config
|
|
728
|
+
.extra
|
|
729
|
+
.get("customModelSelections")
|
|
730
|
+
.and_then(|v| v.as_object())
|
|
731
|
+
&& let Some(model) = selections.get(provider_id).and_then(|v| v.as_str())
|
|
732
|
+
&& !model.is_empty()
|
|
733
|
+
{
|
|
734
|
+
return model.to_owned();
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
// Fall back to the first model in the provider definition.
|
|
738
|
+
config
|
|
739
|
+
.resolve_custom_provider(&config.ai_provider)
|
|
740
|
+
.and_then(|cp| cp.models.first())
|
|
741
|
+
.map(|m| m.model_id.clone())
|
|
742
|
+
.unwrap_or_default()
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
async fn call_custom_provider(
|
|
746
|
+
client: &Client,
|
|
747
|
+
config: &crate::MintConfig,
|
|
748
|
+
request: &ChatRequest,
|
|
749
|
+
) -> Result<(String, String), ChatError> {
|
|
750
|
+
let provider = config
|
|
751
|
+
.resolve_custom_provider(&config.ai_provider)
|
|
752
|
+
.ok_or_else(|| ChatError::UnsupportedProvider(config.ai_provider.clone()))?;
|
|
753
|
+
|
|
754
|
+
let base_url = provider.base_url.trim_end_matches('/');
|
|
755
|
+
let model = resolve_custom_model(config);
|
|
756
|
+
let payload = openai_chat_payload(&model, request, false);
|
|
757
|
+
|
|
758
|
+
let mut builder = client
|
|
759
|
+
.post(format!("{base_url}/chat/completions"))
|
|
760
|
+
.json(&payload);
|
|
761
|
+
|
|
762
|
+
if !provider.api_key.is_empty() {
|
|
763
|
+
builder = builder.bearer_auth(&provider.api_key);
|
|
764
|
+
}
|
|
765
|
+
for h in &provider.headers {
|
|
766
|
+
if !h.name.is_empty() {
|
|
767
|
+
builder = builder.header(h.name.as_str(), h.value.as_str());
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
let response: serde_json::Value = builder.send().await?.error_for_status()?.json().await?;
|
|
772
|
+
|
|
773
|
+
Ok((
|
|
774
|
+
model,
|
|
775
|
+
response["choices"][0]["message"]["content"]
|
|
776
|
+
.as_str()
|
|
777
|
+
.ok_or(ChatError::MissingResponseText)?
|
|
778
|
+
.into(),
|
|
779
|
+
))
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
async fn stream_custom_provider<F>(
|
|
783
|
+
client: &Client,
|
|
784
|
+
config: &crate::MintConfig,
|
|
785
|
+
request: &ChatRequest,
|
|
786
|
+
on_chunk: &mut F,
|
|
787
|
+
) -> Result<(String, String), ChatError>
|
|
788
|
+
where
|
|
789
|
+
F: FnMut(String),
|
|
790
|
+
{
|
|
791
|
+
let provider = config
|
|
792
|
+
.resolve_custom_provider(&config.ai_provider)
|
|
793
|
+
.ok_or_else(|| ChatError::UnsupportedProvider(config.ai_provider.clone()))?;
|
|
794
|
+
|
|
795
|
+
let base_url = provider.base_url.trim_end_matches('/');
|
|
796
|
+
let model = resolve_custom_model(config);
|
|
797
|
+
let payload = openai_chat_payload(&model, request, true);
|
|
798
|
+
|
|
799
|
+
let mut builder = client
|
|
800
|
+
.post(format!("{base_url}/chat/completions"))
|
|
801
|
+
.json(&payload);
|
|
802
|
+
|
|
803
|
+
if !provider.api_key.is_empty() {
|
|
804
|
+
builder = builder.bearer_auth(&provider.api_key);
|
|
805
|
+
}
|
|
806
|
+
for h in &provider.headers {
|
|
807
|
+
if !h.name.is_empty() {
|
|
808
|
+
builder = builder.header(h.name.as_str(), h.value.as_str());
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
let response = builder.send().await?.error_for_status()?;
|
|
813
|
+
collect_stream(response, StreamFormat::OpenAi, on_chunk)
|
|
814
|
+
.await
|
|
815
|
+
.map(|text| (model, text))
|
|
816
|
+
}
|
|
817
|
+
|
|
711
818
|
fn provider_key(configured: &str, environment_variable: &str) -> String {
|
|
712
819
|
if configured.trim().is_empty() {
|
|
713
820
|
std::env::var(environment_variable).unwrap_or_default()
|
|
@@ -720,16 +827,24 @@ fn require_supported_attachments(provider: &str, request: &ChatRequest) -> Resul
|
|
|
720
827
|
let has_image = request
|
|
721
828
|
.image_data_uri
|
|
722
829
|
.as_ref()
|
|
723
|
-
.
|
|
830
|
+
.is_some_and(|s| !s.trim().is_empty());
|
|
724
831
|
let has_audio = request
|
|
725
832
|
.audio_data_uri
|
|
726
833
|
.as_ref()
|
|
727
|
-
.
|
|
834
|
+
.is_some_and(|s| !s.trim().is_empty());
|
|
728
835
|
|
|
729
836
|
match provider {
|
|
730
837
|
"gemini" => Ok(()),
|
|
731
838
|
"ollama" if has_audio => Err(ChatError::UnsupportedAttachments(provider.into())),
|
|
732
839
|
"ollama" => Ok(()),
|
|
840
|
+
// Custom providers are text-only for now; reject any multimodal content.
|
|
841
|
+
p if p.starts_with("custom:") => {
|
|
842
|
+
if has_image || has_audio {
|
|
843
|
+
Err(ChatError::UnsupportedAttachments(provider.into()))
|
|
844
|
+
} else {
|
|
845
|
+
Ok(())
|
|
846
|
+
}
|
|
847
|
+
}
|
|
733
848
|
_ if has_image || has_audio => Err(ChatError::UnsupportedAttachments(provider.into())),
|
|
734
849
|
_ => Ok(()),
|
|
735
850
|
}
|
|
@@ -536,18 +536,17 @@ pub async fn fetch_github_repo_summary(owner: &str, repo: &str) -> Result<String
|
|
|
536
536
|
.await
|
|
537
537
|
.map_err(|e| e.to_string())?;
|
|
538
538
|
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
|
-
}
|
|
539
|
+
if contents_resp.status().is_success()
|
|
540
|
+
&& let Ok(contents_info) = contents_resp.json::<serde_json::Value>().await
|
|
541
|
+
&& let Some(arr) = contents_info.as_array()
|
|
542
|
+
{
|
|
543
|
+
let mut files = Vec::new();
|
|
544
|
+
for item in arr {
|
|
545
|
+
let name = item["name"].as_str().unwrap_or("");
|
|
546
|
+
let r#type = item["type"].as_str().unwrap_or("");
|
|
547
|
+
files.push(format!("- {} ({})", name, r#type));
|
|
550
548
|
}
|
|
549
|
+
file_tree = files.join("\n");
|
|
551
550
|
}
|
|
552
551
|
|
|
553
552
|
// 3. Fetch README.md
|
|
@@ -558,15 +557,14 @@ pub async fn fetch_github_repo_summary(owner: &str, repo: &str) -> Result<String
|
|
|
558
557
|
.await
|
|
559
558
|
.map_err(|e| e.to_string())?;
|
|
560
559
|
let mut readme_text = String::from("No README available.");
|
|
561
|
-
if readme_resp.status().is_success()
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
}
|
|
560
|
+
if readme_resp.status().is_success()
|
|
561
|
+
&& let Ok(readme_info) = readme_resp.json::<serde_json::Value>().await
|
|
562
|
+
&& let Some(content_b64) = readme_info["content"].as_str()
|
|
563
|
+
{
|
|
564
|
+
let cleaned_b64 = content_b64.replace(['\n', '\r'], "");
|
|
565
|
+
use base64::{Engine as _, engine::general_purpose::STANDARD};
|
|
566
|
+
if let Ok(decoded_bytes) = STANDARD.decode(cleaned_b64) {
|
|
567
|
+
readme_text = String::from_utf8_lossy(&decoded_bytes).to_string();
|
|
570
568
|
}
|
|
571
569
|
}
|
|
572
570
|
|