@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.
- package/Cargo.lock +55 -3
- package/Cargo.toml +2 -1
- package/Release_Note.md +285 -140
- 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 +287 -43
- package/crates/mint-cli/src/gmail.rs +22 -2
- package/crates/mint-cli/src/interactive.rs +2888 -0
- package/crates/mint-cli/src/main.rs +336 -2854
- package/crates/mint-cli/src/markdown.rs +294 -0
- package/crates/mint-cli/src/onboard.rs +44 -1
- 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 +376 -13
- package/crates/mint-core/src/browser.rs +629 -101
- package/crates/mint-core/src/channels.rs +105 -6
- package/crates/mint-core/src/chat.rs +52 -4
- package/crates/mint-core/src/code_tools.rs +29 -11
- package/crates/mint-core/src/config.rs +118 -0
- package/crates/mint-core/src/image_gen.rs +294 -33
- package/crates/mint-core/src/lib.rs +5 -2
- package/crates/mint-core/src/mcp.rs +119 -6
- package/crates/mint-core/src/orchestration.rs +133 -11
- package/crates/mint-core/src/pictures.rs +64 -1
- package/crates/mint-core/src/skills.rs +82 -6
- package/crates/mint-core/src/video_gen.rs +235 -0
- package/package.json +8 -2
- 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/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/markdown.tsx +217 -88
- package/src/renderer/shared/utils/slashCommandProcessor.ts +239 -0
- package/src/renderer/src/components/ChatPanel.tsx +461 -64
- 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 +150 -5
- package/src/renderer/src/components/PicturesLibrary.tsx +156 -15
- package/src/renderer/src/components/Settings/AutomationTab.tsx +316 -18
- package/src/renderer/src/components/Settings/GeneralTab.tsx +762 -634
- 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 +38 -2
- package/src/renderer/src/components/VeoStudioPanel.tsx +549 -0
- 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 +352 -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 +360 -88
- 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 +137 -4
- package/src/renderer/src-web/components/PicturesLibrary.tsx +155 -15
- package/src/renderer/src-web/components/Settings/AutomationTab.tsx +316 -18
- package/src/renderer/src-web/components/Settings/GeneralTab.tsx +762 -629
- 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 +35 -2
- 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 +353 -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/lib.rs +114 -8
- 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
|
@@ -57,6 +57,40 @@ export async function getRuntimeStatus(): Promise<RuntimeStatus> {
|
|
|
57
57
|
return invoke<RuntimeStatus>('get_runtime_status')
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
+
export async function setActiveModel(provider: string, model?: string): Promise<string> {
|
|
61
|
+
if (typeof window === 'undefined' || !isTauriRuntime()) {
|
|
62
|
+
const API_BASE = getApiBase()
|
|
63
|
+
try {
|
|
64
|
+
const res = await fetch(`${API_BASE}/active-model`, {
|
|
65
|
+
method: 'POST',
|
|
66
|
+
headers: { 'Content-Type': 'application/json' },
|
|
67
|
+
body: JSON.stringify({ provider, model }),
|
|
68
|
+
})
|
|
69
|
+
const data = await res.json()
|
|
70
|
+
return data.displayName || `Changed model to ${provider}`
|
|
71
|
+
} catch (e) {
|
|
72
|
+
console.error('Failed to set active model via API:', e)
|
|
73
|
+
return `Changed model to ${provider}`
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
const { invoke } = await import('@tauri-apps/api/core')
|
|
77
|
+
return invoke<string>('set_active_model', { provider, model })
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export async function uploadFile(file: File): Promise<string> {
|
|
81
|
+
const API_BASE = getApiBase();
|
|
82
|
+
const res = await fetch(`${API_BASE}/uploads?filename=${encodeURIComponent(file.name)}`, {
|
|
83
|
+
method: 'POST',
|
|
84
|
+
body: file,
|
|
85
|
+
});
|
|
86
|
+
if (!res.ok) {
|
|
87
|
+
const data = await res.json().catch(() => null);
|
|
88
|
+
throw new Error(data?.error || `HTTP ${res.status}`);
|
|
89
|
+
}
|
|
90
|
+
const data = await res.json();
|
|
91
|
+
return data.url;
|
|
92
|
+
}
|
|
93
|
+
|
|
60
94
|
export interface DetectedTools {
|
|
61
95
|
docker: boolean
|
|
62
96
|
git: boolean
|
|
@@ -83,19 +117,20 @@ export async function sendChatMessage(
|
|
|
83
117
|
message: string,
|
|
84
118
|
imageDataUri?: string | null,
|
|
85
119
|
audioDataUri?: string | null,
|
|
120
|
+
videoDataUri?: string | null,
|
|
86
121
|
documentAttachment?: DocumentAttachment | null,
|
|
87
122
|
workspacePath?: string | null,
|
|
88
123
|
chatId?: string | null,
|
|
89
124
|
agentId?: string | null,
|
|
90
125
|
): Promise<ChatResponse> {
|
|
91
|
-
const outgoingMessage = withImagePlaceholder(message, imageDataUri)
|
|
126
|
+
const outgoingMessage = withImagePlaceholder(message, imageDataUri, videoDataUri)
|
|
92
127
|
if (typeof window === 'undefined' || !isTauriRuntime()) {
|
|
93
128
|
const API_BASE = getApiBase();
|
|
94
129
|
try {
|
|
95
130
|
const res = await fetch(`${API_BASE}/chat`, {
|
|
96
131
|
method: 'POST',
|
|
97
132
|
headers: { 'Content-Type': 'application/json' },
|
|
98
|
-
body: JSON.stringify({ message: outgoingMessage, systemInstruction: '', chatId, imageDataUri, audioDataUri, documentAttachment, agentId })
|
|
133
|
+
body: JSON.stringify({ message: outgoingMessage, systemInstruction: '', chatId, imageDataUri, audioDataUri, videoDataUri, documentAttachment, agentId })
|
|
99
134
|
});
|
|
100
135
|
const data = await res.json().catch(() => null);
|
|
101
136
|
if (!res.ok) {
|
|
@@ -120,7 +155,7 @@ export async function sendChatMessage(
|
|
|
120
155
|
}
|
|
121
156
|
const { invoke } = await import('@tauri-apps/api/core')
|
|
122
157
|
const response = await invoke<ChatResponse>('send_chat_message', {
|
|
123
|
-
request: { message: outgoingMessage, systemInstruction: '', chatId, imageDataUri, audioDataUri, documentAttachment, workspacePath, agentId },
|
|
158
|
+
request: { message: outgoingMessage, systemInstruction: '', chatId, imageDataUri, audioDataUri, videoDataUri, documentAttachment, workspacePath, agentId },
|
|
124
159
|
})
|
|
125
160
|
if (imageDataUri) {
|
|
126
161
|
await invoke('save_pictures', {
|
|
@@ -129,6 +164,13 @@ export async function sendChatMessage(
|
|
|
129
164
|
message: outgoingMessage,
|
|
130
165
|
})
|
|
131
166
|
}
|
|
167
|
+
if (videoDataUri) {
|
|
168
|
+
await invoke('save_pictures', {
|
|
169
|
+
images: videoDataUri.split(' '),
|
|
170
|
+
source: 'chat',
|
|
171
|
+
message: outgoingMessage,
|
|
172
|
+
})
|
|
173
|
+
}
|
|
132
174
|
return response
|
|
133
175
|
}
|
|
134
176
|
|
|
@@ -137,6 +179,7 @@ export async function streamChatMessage(
|
|
|
137
179
|
onChunk: (chunk: string) => void,
|
|
138
180
|
imageDataUri?: string | null,
|
|
139
181
|
audioDataUri?: string | null,
|
|
182
|
+
videoDataUri?: string | null,
|
|
140
183
|
systemInstruction = '',
|
|
141
184
|
onProgress?: (progress: AgentProgress) => void,
|
|
142
185
|
documentAttachment?: DocumentAttachment | null,
|
|
@@ -146,11 +189,11 @@ export async function streamChatMessage(
|
|
|
146
189
|
): Promise<ChatResponse> {
|
|
147
190
|
if (typeof window === 'undefined' || !isTauriRuntime()) {
|
|
148
191
|
const API_BASE = getApiBase();
|
|
149
|
-
const outgoingMessage = withImagePlaceholder(message, imageDataUri);
|
|
192
|
+
const outgoingMessage = withImagePlaceholder(message, imageDataUri, videoDataUri);
|
|
150
193
|
const res = await fetch(`${API_BASE}/chat-stream`, {
|
|
151
194
|
method: 'POST',
|
|
152
195
|
headers: { 'Content-Type': 'application/json' },
|
|
153
|
-
body: JSON.stringify({ message: outgoingMessage, systemInstruction, chatId, imageDataUri, audioDataUri, documentAttachment, agentId })
|
|
196
|
+
body: JSON.stringify({ message: outgoingMessage, systemInstruction, chatId, imageDataUri, audioDataUri, videoDataUri, documentAttachment, agentId })
|
|
154
197
|
});
|
|
155
198
|
if (!res.ok) {
|
|
156
199
|
const data = await res.json().catch(() => null);
|
|
@@ -189,14 +232,14 @@ export async function streamChatMessage(
|
|
|
189
232
|
throw new Error("Stream closed without a final response");
|
|
190
233
|
}
|
|
191
234
|
const { invoke, Channel } = await import('@tauri-apps/api/core')
|
|
192
|
-
const outgoingMessage = withImagePlaceholder(message, imageDataUri)
|
|
235
|
+
const outgoingMessage = withImagePlaceholder(message, imageDataUri, videoDataUri)
|
|
193
236
|
const onEvent = new Channel<DesktopStreamEvent>()
|
|
194
237
|
onEvent.onmessage = (event) => {
|
|
195
238
|
if (event.type === 'chunk') onChunk(event.chunk)
|
|
196
239
|
else onProgress?.(event.progress)
|
|
197
240
|
}
|
|
198
241
|
const response = await invoke<ChatResponse>('stream_chat_message', {
|
|
199
|
-
request: { message: outgoingMessage, systemInstruction, chatId, imageDataUri, audioDataUri, documentAttachment, workspacePath, agentId },
|
|
242
|
+
request: { message: outgoingMessage, systemInstruction, chatId, imageDataUri, audioDataUri, videoDataUri, documentAttachment, workspacePath, agentId },
|
|
200
243
|
onEvent,
|
|
201
244
|
})
|
|
202
245
|
if (imageDataUri) {
|
|
@@ -206,14 +249,29 @@ export async function streamChatMessage(
|
|
|
206
249
|
message: outgoingMessage,
|
|
207
250
|
})
|
|
208
251
|
}
|
|
252
|
+
if (videoDataUri) {
|
|
253
|
+
await invoke('save_pictures', {
|
|
254
|
+
images: videoDataUri.split(' '),
|
|
255
|
+
source: 'chat',
|
|
256
|
+
message: outgoingMessage,
|
|
257
|
+
})
|
|
258
|
+
}
|
|
209
259
|
return response
|
|
210
260
|
}
|
|
211
261
|
|
|
212
|
-
function withImagePlaceholder(message: string, imageDataUri?: string | null) {
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
262
|
+
function withImagePlaceholder(message: string, imageDataUri?: string | null, videoDataUri?: string | null) {
|
|
263
|
+
let finalMessage = message
|
|
264
|
+
if (imageDataUri && !finalMessage.includes('[Image #1]')) {
|
|
265
|
+
const imageCount = imageDataUri.split(/\s+/).filter(Boolean).length
|
|
266
|
+
const markers = Array.from({ length: imageCount }, (_, index) => `[Image #${index + 1}]`).join(' ')
|
|
267
|
+
if (markers) finalMessage = `${finalMessage} ${markers}`
|
|
268
|
+
}
|
|
269
|
+
if (videoDataUri && !finalMessage.includes('[Video #1]')) {
|
|
270
|
+
const videoCount = videoDataUri.split(/\s+/).filter(Boolean).length
|
|
271
|
+
const markers = Array.from({ length: videoCount }, (_, index) => `[Video #${index + 1}]`).join(' ')
|
|
272
|
+
if (markers) finalMessage = `${finalMessage} ${markers}`
|
|
273
|
+
}
|
|
274
|
+
return finalMessage
|
|
217
275
|
}
|
|
218
276
|
|
|
219
277
|
export async function getTtsUrls(text: string): Promise<TtsUrl[]> {
|
|
@@ -257,6 +315,7 @@ export async function getRecentInteractions(limit = 50, chatId?: string | null):
|
|
|
257
315
|
export async function saveSystemInteraction(
|
|
258
316
|
chatId: string,
|
|
259
317
|
userText: string,
|
|
318
|
+
aiText: string,
|
|
260
319
|
provider: string,
|
|
261
320
|
model: string,
|
|
262
321
|
): Promise<any> {
|
|
@@ -266,7 +325,7 @@ export async function saveSystemInteraction(
|
|
|
266
325
|
const res = await fetch(`${API_BASE}/interactions`, {
|
|
267
326
|
method: 'POST',
|
|
268
327
|
headers: { 'Content-Type': 'application/json' },
|
|
269
|
-
body: JSON.stringify({ chatId, userText, provider, model }),
|
|
328
|
+
body: JSON.stringify({ chatId, userText, aiText, provider, model }),
|
|
270
329
|
});
|
|
271
330
|
return await res.json();
|
|
272
331
|
} catch (e) {
|
|
@@ -275,7 +334,7 @@ export async function saveSystemInteraction(
|
|
|
275
334
|
}
|
|
276
335
|
}
|
|
277
336
|
const { invoke } = await import('@tauri-apps/api/core')
|
|
278
|
-
return invoke('save_system_interaction', { chatId, userText, provider, model })
|
|
337
|
+
return invoke('save_system_interaction', { chatId, userText, aiText, provider, model })
|
|
279
338
|
}
|
|
280
339
|
|
|
281
340
|
export async function saveInteractionAgentActivity(
|
|
@@ -972,7 +1031,7 @@ export function installTauriAdapters() {
|
|
|
972
1031
|
}
|
|
973
1032
|
|
|
974
1033
|
window.api = {
|
|
975
|
-
sendMessage: (message, imageDataUri, audioDataUri, documentAttachment) => sendChatMessage(message, imageDataUri, audioDataUri, documentAttachment),
|
|
1034
|
+
sendMessage: (message, imageDataUri, audioDataUri, documentAttachment) => sendChatMessage(message, imageDataUri, audioDataUri, undefined, documentAttachment),
|
|
976
1035
|
closeWindow: async () => {
|
|
977
1036
|
const { invoke } = await import('@tauri-apps/api/core')
|
|
978
1037
|
const { getCurrentWindow } = await import('@tauri-apps/api/window')
|
|
@@ -1119,13 +1178,18 @@ async function captureSharedScreen(): Promise<string> {
|
|
|
1119
1178
|
export async function listLearnedSkills(workspacePath?: string): Promise<LearnedSkill[]> {
|
|
1120
1179
|
if (typeof window === 'undefined' || !isTauriRuntime()) {
|
|
1121
1180
|
try {
|
|
1181
|
+
const API_BASE = getLocalApiBase()
|
|
1182
|
+
const res = await fetch(`${API_BASE}/learned-skills`)
|
|
1183
|
+
if (res.ok) {
|
|
1184
|
+
return await res.json()
|
|
1185
|
+
}
|
|
1122
1186
|
const val = await getProfileValue('learned-skills-web-mock')
|
|
1123
1187
|
if (val) {
|
|
1124
1188
|
return JSON.parse(val)
|
|
1125
1189
|
}
|
|
1126
1190
|
return []
|
|
1127
1191
|
} catch (e) {
|
|
1128
|
-
console.error("Failed to load web
|
|
1192
|
+
console.error("Failed to load web skills:", e)
|
|
1129
1193
|
return []
|
|
1130
1194
|
}
|
|
1131
1195
|
}
|
|
@@ -1230,6 +1294,61 @@ export async function readClipboardImage(): Promise<string | null> {
|
|
|
1230
1294
|
}
|
|
1231
1295
|
}
|
|
1232
1296
|
|
|
1297
|
+
// ─── Veo Studio — Video Generation (stub; real Tauri command in follow-up) ───
|
|
1298
|
+
|
|
1299
|
+
export interface VideoGenRequest {
|
|
1300
|
+
prompt: string
|
|
1301
|
+
negativePrompt?: string
|
|
1302
|
+
aspectRatio: '16:9' | '9:16' | '1:1'
|
|
1303
|
+
duration: 5 | 8
|
|
1304
|
+
model?: string
|
|
1305
|
+
provider: string
|
|
1306
|
+
}
|
|
1307
|
+
|
|
1308
|
+
export interface VideoGenEntry {
|
|
1309
|
+
id: string
|
|
1310
|
+
url: string
|
|
1311
|
+
path: string
|
|
1312
|
+
message?: string
|
|
1313
|
+
createdAt?: string
|
|
1314
|
+
}
|
|
1315
|
+
|
|
1316
|
+
export interface VideoGenResponse {
|
|
1317
|
+
videos: VideoGenEntry[]
|
|
1318
|
+
provider: string
|
|
1319
|
+
model: string
|
|
1320
|
+
description?: string
|
|
1321
|
+
}
|
|
1322
|
+
|
|
1323
|
+
export interface VideoGenProviders {
|
|
1324
|
+
active: string
|
|
1325
|
+
available: string[]
|
|
1326
|
+
}
|
|
1327
|
+
|
|
1328
|
+
/**
|
|
1329
|
+
* Generate a video using the configured provider.
|
|
1330
|
+
* NOTE: This is a frontend stub — the real Tauri backend command will be
|
|
1331
|
+
* added in a follow-up PR once the Veo REST API integration is ready.
|
|
1332
|
+
*/
|
|
1333
|
+
export async function generateVideo(request: VideoGenRequest): Promise<VideoGenResponse> {
|
|
1334
|
+
const apiBase = getLocalApiBase()
|
|
1335
|
+
const response = await fetch(`${apiBase}/video-generate`, {
|
|
1336
|
+
method: 'POST',
|
|
1337
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1338
|
+
body: JSON.stringify(request)
|
|
1339
|
+
})
|
|
1340
|
+
if (!response.ok) {
|
|
1341
|
+
const errorData = await response.json().catch(() => ({}))
|
|
1342
|
+
throw new Error(errorData.error || `HTTP ${response.status} from API`)
|
|
1343
|
+
}
|
|
1344
|
+
return response.json()
|
|
1345
|
+
}
|
|
1346
|
+
|
|
1347
|
+
export async function getVideoGenProviders(): Promise<VideoGenProviders> {
|
|
1348
|
+
return { active: 'veo', available: ['veo'] }
|
|
1349
|
+
}
|
|
1350
|
+
|
|
1351
|
+
|
|
1233
1352
|
// Enforce compile-time check against the shared platform interface
|
|
1234
1353
|
const _apiCheck: MintPlatformApi = {
|
|
1235
1354
|
getRuntimeStatus,
|
package/src-tauri/src/lib.rs
CHANGED
|
@@ -31,11 +31,11 @@ use integrations::{channel_inventory, list_plugins};
|
|
|
31
31
|
use mint_core::{
|
|
32
32
|
AgentApproval, AgentProgress, AppliedCodeEdit, ApprovalOutcome, ChatRequest, ChatResponse,
|
|
33
33
|
ChatSession, CodeEdit, CodeEditProposal, ImageGenRequest, InteractionMemory, MemoryStore,
|
|
34
|
-
MintConfig, PictureEntry, TtsUrl,
|
|
35
|
-
|
|
36
|
-
orchestrate_agent_loop, orchestrate_chat_stream_with_fallback,
|
|
37
|
-
propose_code_edits, save_chat_images, save_config,
|
|
38
|
-
workflows_path,
|
|
34
|
+
MintConfig, PictureEntry, TtsUrl, VideoGenRequest, VideoGenResponse, WeatherReport,
|
|
35
|
+
apply_code_edits, classify_shell_command, config_path, google_tts_urls, list_saved_pictures,
|
|
36
|
+
load_config, load_workflows, orchestrate_agent_loop, orchestrate_chat_stream_with_fallback,
|
|
37
|
+
orchestrate_chat_with_fallback, propose_code_edits, save_chat_images, save_config,
|
|
38
|
+
save_workflows, start_channels, weather, workflows_path,
|
|
39
39
|
};
|
|
40
40
|
use plugins::execute_plugin;
|
|
41
41
|
|
|
@@ -73,6 +73,7 @@ struct RuntimeStatus {
|
|
|
73
73
|
backend: &'static str,
|
|
74
74
|
config_path: String,
|
|
75
75
|
active_provider: String,
|
|
76
|
+
active_model: String,
|
|
76
77
|
available_providers: Vec<String>,
|
|
77
78
|
integrations: Value,
|
|
78
79
|
}
|
|
@@ -119,6 +120,7 @@ fn get_runtime_status() -> Result<RuntimeStatus, String> {
|
|
|
119
120
|
.display()
|
|
120
121
|
.to_string(),
|
|
121
122
|
active_provider: config.ai_provider.clone(),
|
|
123
|
+
active_model: config.active_model().to_string(),
|
|
122
124
|
available_providers: config.available_providers(),
|
|
123
125
|
integrations: integration_status(&config),
|
|
124
126
|
})
|
|
@@ -305,6 +307,20 @@ fn update_config(app: AppHandle, config: MintConfig) -> Result<(), String> {
|
|
|
305
307
|
Ok(())
|
|
306
308
|
}
|
|
307
309
|
|
|
310
|
+
#[tauri::command]
|
|
311
|
+
fn set_active_model(
|
|
312
|
+
app: AppHandle,
|
|
313
|
+
provider: String,
|
|
314
|
+
model: Option<String>,
|
|
315
|
+
) -> Result<String, String> {
|
|
316
|
+
let mut config = load_config().map_err(|error| error.to_string())?;
|
|
317
|
+
let display_name = config
|
|
318
|
+
.set_active_model(&provider, model.as_deref())
|
|
319
|
+
.map_err(|error| error.to_string())?;
|
|
320
|
+
let _ = app.emit("settings-changed", &config);
|
|
321
|
+
Ok(display_name)
|
|
322
|
+
}
|
|
323
|
+
|
|
308
324
|
#[tauri::command]
|
|
309
325
|
fn inspect_shell_command(command: String) -> mint_core::ShellClassification {
|
|
310
326
|
classify_shell_command(&command)
|
|
@@ -312,7 +328,21 @@ fn inspect_shell_command(command: String) -> mint_core::ShellClassification {
|
|
|
312
328
|
|
|
313
329
|
#[tauri::command]
|
|
314
330
|
async fn send_chat_message(app: AppHandle, request: ChatRequest) -> Result<ChatResponse, String> {
|
|
315
|
-
let config = load_config().map_err(|error| error.to_string())?;
|
|
331
|
+
let mut config = load_config().map_err(|error| error.to_string())?;
|
|
332
|
+
if let Some(ref path) = request.workspace_path {
|
|
333
|
+
if !path.trim().is_empty()
|
|
334
|
+
&& config
|
|
335
|
+
.extra
|
|
336
|
+
.get("activeWorkspacePath")
|
|
337
|
+
.and_then(Value::as_str)
|
|
338
|
+
!= Some(path.as_str())
|
|
339
|
+
{
|
|
340
|
+
config
|
|
341
|
+
.extra
|
|
342
|
+
.insert("activeWorkspacePath".into(), Value::String(path.clone()));
|
|
343
|
+
let _ = save_config(&config);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
316
346
|
let request = request.with_document_context(&config)?;
|
|
317
347
|
|
|
318
348
|
if request.message.starts_with("/chat ") {
|
|
@@ -392,6 +422,7 @@ async fn send_chat_message(app: AppHandle, request: ChatRequest) -> Result<ChatR
|
|
|
392
422
|
let message_clone = request.message.clone();
|
|
393
423
|
let root_clone = root.clone();
|
|
394
424
|
let image_data_uri_clone = request.image_data_uri.clone();
|
|
425
|
+
let video_data_uri_clone = request.video_data_uri.clone();
|
|
395
426
|
let chat_id_clone = request.chat_id.clone();
|
|
396
427
|
let agent_id_clone = request.agent_id.clone();
|
|
397
428
|
|
|
@@ -401,6 +432,7 @@ async fn send_chat_message(app: AppHandle, request: ChatRequest) -> Result<ChatR
|
|
|
401
432
|
&message_clone,
|
|
402
433
|
&root_clone,
|
|
403
434
|
image_data_uri_clone,
|
|
435
|
+
video_data_uri_clone,
|
|
404
436
|
chat_id_clone.as_deref(),
|
|
405
437
|
agent_id_clone.as_deref(),
|
|
406
438
|
fast_mode,
|
|
@@ -449,7 +481,21 @@ async fn stream_chat_message(
|
|
|
449
481
|
request: ChatRequest,
|
|
450
482
|
on_event: Channel<DesktopStreamEvent>,
|
|
451
483
|
) -> Result<ChatResponse, String> {
|
|
452
|
-
let config = load_config().map_err(|error| error.to_string())?;
|
|
484
|
+
let mut config = load_config().map_err(|error| error.to_string())?;
|
|
485
|
+
if let Some(ref path) = request.workspace_path {
|
|
486
|
+
if !path.trim().is_empty()
|
|
487
|
+
&& config
|
|
488
|
+
.extra
|
|
489
|
+
.get("activeWorkspacePath")
|
|
490
|
+
.and_then(Value::as_str)
|
|
491
|
+
!= Some(path.as_str())
|
|
492
|
+
{
|
|
493
|
+
config
|
|
494
|
+
.extra
|
|
495
|
+
.insert("activeWorkspacePath".into(), Value::String(path.clone()));
|
|
496
|
+
let _ = save_config(&config);
|
|
497
|
+
}
|
|
498
|
+
}
|
|
453
499
|
let request = request.with_document_context(&config)?;
|
|
454
500
|
|
|
455
501
|
if request.message.starts_with("/chat ") {
|
|
@@ -548,6 +594,7 @@ async fn stream_chat_message(
|
|
|
548
594
|
let message_clone = request.message.clone();
|
|
549
595
|
let root_clone = root.clone();
|
|
550
596
|
let image_data_uri_clone = request.image_data_uri.clone();
|
|
597
|
+
let video_data_uri_clone = request.video_data_uri.clone();
|
|
551
598
|
let chat_id_clone = request.chat_id.clone();
|
|
552
599
|
let agent_id_clone = request.agent_id.clone();
|
|
553
600
|
|
|
@@ -557,6 +604,7 @@ async fn stream_chat_message(
|
|
|
557
604
|
&message_clone,
|
|
558
605
|
&root_clone,
|
|
559
606
|
image_data_uri_clone,
|
|
607
|
+
video_data_uri_clone,
|
|
560
608
|
chat_id_clone.as_deref(),
|
|
561
609
|
agent_id_clone.as_deref(),
|
|
562
610
|
fast_mode,
|
|
@@ -624,12 +672,19 @@ fn save_interaction_agent_activity(
|
|
|
624
672
|
fn save_system_interaction(
|
|
625
673
|
chat_id: String,
|
|
626
674
|
user_text: String,
|
|
675
|
+
ai_text: Option<String>,
|
|
627
676
|
provider: String,
|
|
628
677
|
model: String,
|
|
629
678
|
) -> Result<i64, String> {
|
|
630
679
|
MemoryStore::open_default()
|
|
631
680
|
.and_then(|memory| {
|
|
632
|
-
memory.add_interaction_for_chat(
|
|
681
|
+
memory.add_interaction_for_chat(
|
|
682
|
+
&chat_id,
|
|
683
|
+
&user_text,
|
|
684
|
+
ai_text.as_deref().unwrap_or(""),
|
|
685
|
+
&provider,
|
|
686
|
+
&model,
|
|
687
|
+
)
|
|
633
688
|
})
|
|
634
689
|
.map_err(|error| error.to_string())
|
|
635
690
|
}
|
|
@@ -704,6 +759,9 @@ fn list_learned_skills(workspace_path: Option<String>) -> Result<Vec<LearnedSkil
|
|
|
704
759
|
|
|
705
760
|
let mut global_skills = Vec::new();
|
|
706
761
|
if let Some(home) = dirs::home_dir() {
|
|
762
|
+
let global_agents_path = home.join(".gemini").join("config").join("AGENTS.md");
|
|
763
|
+
mint_core::skills::load_agent_rules_file(&global_agents_path, &mut global_skills);
|
|
764
|
+
|
|
707
765
|
let global_skills_path = home.join(".config").join("mint").join("mint-skills");
|
|
708
766
|
if !global_skills_path.exists() {
|
|
709
767
|
let _ = std::fs::create_dir_all(&global_skills_path);
|
|
@@ -713,6 +771,12 @@ fn list_learned_skills(workspace_path: Option<String>) -> Result<Vec<LearnedSkil
|
|
|
713
771
|
|
|
714
772
|
let mut workspace_skills = Vec::new();
|
|
715
773
|
if let Ok(root) = workspace_root(workspace_path.as_deref()) {
|
|
774
|
+
let workspace_agents_path1 = root.join(".agents").join("AGENTS.md");
|
|
775
|
+
mint_core::skills::load_agent_rules_file(&workspace_agents_path1, &mut workspace_skills);
|
|
776
|
+
|
|
777
|
+
let workspace_agents_path2 = root.join("AGENTS.md");
|
|
778
|
+
mint_core::skills::load_agent_rules_file(&workspace_agents_path2, &mut workspace_skills);
|
|
779
|
+
|
|
716
780
|
let workspace_skills_path1 = root.join(".agents").join("skills");
|
|
717
781
|
mint_core::skills::load_skills_from_dir(&workspace_skills_path1, &mut workspace_skills);
|
|
718
782
|
|
|
@@ -867,6 +931,14 @@ async fn generate_images(request: ImageGenRequest) -> Result<DesktopImageGenResp
|
|
|
867
931
|
})
|
|
868
932
|
}
|
|
869
933
|
|
|
934
|
+
#[tauri::command]
|
|
935
|
+
async fn generate_video(request: VideoGenRequest) -> Result<VideoGenResponse, String> {
|
|
936
|
+
let config = load_config().map_err(|error| error.to_string())?;
|
|
937
|
+
mint_core::generate_video(&config, &request)
|
|
938
|
+
.await
|
|
939
|
+
.map_err(|error| error.to_string())
|
|
940
|
+
}
|
|
941
|
+
|
|
870
942
|
#[tauri::command]
|
|
871
943
|
fn list_pictures() -> Result<Vec<PictureEntry>, String> {
|
|
872
944
|
list_saved_pictures().map_err(|error| error.to_string())
|
|
@@ -881,6 +953,37 @@ fn save_pictures(
|
|
|
881
953
|
save_chat_images(images, source, message).map_err(|error| error.to_string())
|
|
882
954
|
}
|
|
883
955
|
|
|
956
|
+
#[tauri::command]
|
|
957
|
+
fn upload_file(filename: String, data_b64: String) -> Result<String, String> {
|
|
958
|
+
// Determine mime from filename
|
|
959
|
+
let mime = filename
|
|
960
|
+
.rsplit_once('.')
|
|
961
|
+
.map(|(_, ext)| match ext.to_ascii_lowercase().as_str() {
|
|
962
|
+
"mp4" => "video/mp4",
|
|
963
|
+
"webm" => "video/webm",
|
|
964
|
+
"mov" => "video/quicktime",
|
|
965
|
+
"mkv" => "video/x-matroska",
|
|
966
|
+
"avi" => "video/x-msvideo",
|
|
967
|
+
_ => "application/octet-stream",
|
|
968
|
+
})
|
|
969
|
+
.unwrap_or("application/octet-stream");
|
|
970
|
+
|
|
971
|
+
let data_uri = format!("data:{};base64,{}", mime, data_b64);
|
|
972
|
+
match save_chat_images(
|
|
973
|
+
vec![data_uri],
|
|
974
|
+
Some("upload".into()),
|
|
975
|
+
Some("uploaded".into()),
|
|
976
|
+
) {
|
|
977
|
+
Ok(mut saved) => {
|
|
978
|
+
if let Some(entry) = saved.pop() {
|
|
979
|
+
return Ok(format!("/api/pictures/{}", entry.filename));
|
|
980
|
+
}
|
|
981
|
+
Err("no entry saved".into())
|
|
982
|
+
}
|
|
983
|
+
Err(e) => Err(e.to_string()),
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
|
|
884
987
|
#[tauri::command]
|
|
885
988
|
fn open_folder(path: String) -> Result<ActionResult, String> {
|
|
886
989
|
let target_path = PathBuf::from(path.trim());
|
|
@@ -1241,12 +1344,14 @@ pub fn run() {
|
|
|
1241
1344
|
create_workspace_folder,
|
|
1242
1345
|
delete_workspace_item,
|
|
1243
1346
|
generate_images,
|
|
1347
|
+
generate_video,
|
|
1244
1348
|
select_workspace_directory,
|
|
1245
1349
|
get_config,
|
|
1246
1350
|
get_updater_status,
|
|
1247
1351
|
check_for_updates,
|
|
1248
1352
|
install_available_update,
|
|
1249
1353
|
update_config,
|
|
1354
|
+
set_active_model,
|
|
1250
1355
|
inspect_shell_command,
|
|
1251
1356
|
send_chat_message,
|
|
1252
1357
|
stream_chat_message,
|
|
@@ -1265,6 +1370,7 @@ pub fn run() {
|
|
|
1265
1370
|
delete_learned_skill,
|
|
1266
1371
|
list_pictures,
|
|
1267
1372
|
save_pictures,
|
|
1373
|
+
upload_file,
|
|
1268
1374
|
open_folder,
|
|
1269
1375
|
get_tts_urls,
|
|
1270
1376
|
get_weather,
|
|
@@ -2,7 +2,7 @@ use std::collections::BTreeMap;
|
|
|
2
2
|
|
|
3
3
|
use base64::{Engine as _, engine::general_purpose::STANDARD};
|
|
4
4
|
use hmac::{Hmac, Mac};
|
|
5
|
-
use mint_core::
|
|
5
|
+
use mint_core::load_config;
|
|
6
6
|
use serde_json::{Value, json};
|
|
7
7
|
use sha2::Sha256;
|
|
8
8
|
use tokio::{
|
|
@@ -14,10 +14,16 @@ type HmacSha256 = Hmac<Sha256>;
|
|
|
14
14
|
|
|
15
15
|
pub fn start_webhooks() {
|
|
16
16
|
tauri::async_runtime::spawn(async {
|
|
17
|
-
let
|
|
17
|
+
let (host, port) = load_config()
|
|
18
|
+
.map(|c| (c.line_webhook_host(), c.line_webhook_port()))
|
|
19
|
+
.unwrap_or_else(|_| ("127.0.0.1".into(), 3000));
|
|
20
|
+
let _ = serve(&host, port, Service::Line).await;
|
|
18
21
|
});
|
|
19
22
|
tauri::async_runtime::spawn(async {
|
|
20
|
-
let
|
|
23
|
+
let (host, port) = load_config()
|
|
24
|
+
.map(|c| (c.whatsapp_webhook_host(), c.whatsapp_webhook_port()))
|
|
25
|
+
.unwrap_or_else(|_| ("127.0.0.1".into(), 3001));
|
|
26
|
+
let _ = serve(&host, port, Service::Whatsapp).await;
|
|
21
27
|
});
|
|
22
28
|
}
|
|
23
29
|
|
|
@@ -27,8 +33,9 @@ enum Service {
|
|
|
27
33
|
Whatsapp,
|
|
28
34
|
}
|
|
29
35
|
|
|
30
|
-
async fn serve(port: u16, service: Service) -> Result<(), String> {
|
|
31
|
-
let
|
|
36
|
+
async fn serve(host: &str, port: u16, service: Service) -> Result<(), String> {
|
|
37
|
+
let addr = format!("{host}:{port}");
|
|
38
|
+
let listener = TcpListener::bind(&addr)
|
|
32
39
|
.await
|
|
33
40
|
.map_err(|error| error.to_string())?;
|
|
34
41
|
loop {
|
|
@@ -78,9 +85,28 @@ async fn handle_line(request: HttpRequest) -> Result<(&'static str, String), Str
|
|
|
78
85
|
) else {
|
|
79
86
|
continue;
|
|
80
87
|
};
|
|
81
|
-
let
|
|
82
|
-
let
|
|
83
|
-
|
|
88
|
+
let user_id = event["source"]["userId"].as_str().unwrap_or("unknown");
|
|
89
|
+
let formatted_chat_id = format!("line:{user_id}");
|
|
90
|
+
let answer = mint_core::channels::answer_channel(
|
|
91
|
+
text,
|
|
92
|
+
"Reply concisely for a LINE chat.",
|
|
93
|
+
Some(formatted_chat_id),
|
|
94
|
+
)
|
|
95
|
+
.await;
|
|
96
|
+
|
|
97
|
+
let mut messages = Vec::new();
|
|
98
|
+
if config.bridge_ack_enabled() {
|
|
99
|
+
messages.push(json!({ "type": "text", "text": config.bridge_ack_message() }));
|
|
100
|
+
}
|
|
101
|
+
messages.push(json!({ "type": "text", "text": answer }));
|
|
102
|
+
|
|
103
|
+
let _ = mint_core::HTTP_CLIENT
|
|
104
|
+
.clone()
|
|
105
|
+
.post("https://api.line.me/v2/bot/message/reply")
|
|
106
|
+
.bearer_auth(&token)
|
|
107
|
+
.json(&json!({ "replyToken": reply_token, "messages": messages }))
|
|
108
|
+
.send()
|
|
109
|
+
.await;
|
|
84
110
|
}
|
|
85
111
|
Ok(("200 OK", "ok".into()))
|
|
86
112
|
}
|
|
@@ -133,35 +159,23 @@ async fn handle_whatsapp(request: HttpRequest) -> Result<(&'static str, String),
|
|
|
133
159
|
else {
|
|
134
160
|
continue;
|
|
135
161
|
};
|
|
136
|
-
|
|
162
|
+
if config.bridge_ack_enabled() {
|
|
163
|
+
let _ = mint_core::HTTP_CLIENT.clone().post(format!("https://graph.facebook.com/v23.0/{phone_id}/messages")).bearer_auth(&access_token)
|
|
164
|
+
.json(&json!({ "messaging_product": "whatsapp", "to": to, "type": "text", "text": { "body": config.bridge_ack_message() } })).send().await;
|
|
165
|
+
}
|
|
166
|
+
let formatted_chat_id = format!("whatsapp:{to}");
|
|
167
|
+
let answer = mint_core::channels::answer_channel(
|
|
168
|
+
text,
|
|
169
|
+
"Reply concisely for a WhatsApp chat.",
|
|
170
|
+
Some(formatted_chat_id),
|
|
171
|
+
)
|
|
172
|
+
.await;
|
|
137
173
|
let _ = mint_core::HTTP_CLIENT.clone().post(format!("https://graph.facebook.com/v23.0/{phone_id}/messages")).bearer_auth(&access_token)
|
|
138
174
|
.json(&json!({ "messaging_product": "whatsapp", "to": to, "type": "text", "text": { "body": answer } })).send().await;
|
|
139
175
|
}
|
|
140
176
|
Ok(("200 OK", "ok".into()))
|
|
141
177
|
}
|
|
142
178
|
|
|
143
|
-
async fn answer(text: &str, system: &str) -> String {
|
|
144
|
-
let Ok(config) = load_config() else {
|
|
145
|
-
return "Mint config error".into();
|
|
146
|
-
};
|
|
147
|
-
orchestrate_chat(
|
|
148
|
-
&config,
|
|
149
|
-
&ChatRequest {
|
|
150
|
-
message: text.into(),
|
|
151
|
-
system_instruction: system.into(),
|
|
152
|
-
chat_id: None,
|
|
153
|
-
image_data_uri: None,
|
|
154
|
-
audio_data_uri: None,
|
|
155
|
-
document_attachment: None,
|
|
156
|
-
workspace_path: None,
|
|
157
|
-
agent_id: None,
|
|
158
|
-
},
|
|
159
|
-
)
|
|
160
|
-
.await
|
|
161
|
-
.map(|response| response.text)
|
|
162
|
-
.unwrap_or_else(|error| format!("Mint error: {error}"))
|
|
163
|
-
}
|
|
164
|
-
|
|
165
179
|
struct HttpRequest {
|
|
166
180
|
method: String,
|
|
167
181
|
path: String,
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
# Project instructions
|
|
2
|
+
|
|
3
|
+
- Do not use any `superpowers:*` skills in this repository.
|
|
4
|
+
- Do not create or restore files under `docs/superpowers/`.
|
|
5
|
+
- Use the `solodeveling` skill at the start of every task in this repository. Resume `.solodeveling/state.md` when present and let the skill route the work to the smallest relevant lifecycle workflow.
|
|
6
|
+
- Keep one primary agent. Do not use subagents unless the user explicitly requests delegation.
|
|
7
|
+
- If `solodeveling` is unavailable, state that briefly and continue with the same evidence-driven lifecycle rather than blocking routine work.
|
|
8
|
+
- Work directly from the user's request and use only the minimum additional relevant skills.
|