@pheem49/mint 1.5.2 → 1.5.3

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.
@@ -39,6 +39,9 @@ const DEFAULT_CONFIG = {
39
39
  customBgStart: '#0f172a',
40
40
  customBgEnd: '#1e1b4b',
41
41
  customPanelBg: '#1e293b',
42
+ glassBlur: 'blur(16px)',
43
+ fontFamily: "'Outfit', sans-serif",
44
+ fontSize: '15px',
42
45
  apiKey: '',
43
46
  geminiModel: 'gemini-2.5-flash',
44
47
  language: 'th-TH',
@@ -187,4 +190,4 @@ function isPlaceholder(val) {
187
190
  return !val || val.startsWith('your_') || val.includes('key_here') || val.trim() === '';
188
191
  }
189
192
 
190
- module.exports = { readConfig, writeConfig, getAvailableProviders, isPlaceholder, CONFIG_PATH };
193
+ module.exports = { readConfig, writeConfig, getAvailableProviders, isPlaceholder, CONFIG_PATH, CONFIG_DIR };
@@ -17,6 +17,8 @@ function registerIpcHandlers({
17
17
  getWeather,
18
18
  readConfig,
19
19
  writeConfig,
20
+ saveChatImages,
21
+ listSavedPictures,
20
22
  parseCommand,
21
23
  executeAction,
22
24
  getGoogleTtsUrls,
@@ -25,6 +27,10 @@ function registerIpcHandlers({
25
27
 
26
28
  ipcMain.handle('chat-message', async (event, message, base64Image = null, base64Audio = null) => {
27
29
  try {
30
+ if (base64Image && saveChatImages) {
31
+ saveChatImages(base64Image, { source: 'chat', message });
32
+ }
33
+
28
34
  const rawResponse = await handleChat(message, base64Image, base64Audio);
29
35
  const aiResponse = parseCommand(rawResponse);
30
36
 
@@ -79,6 +85,10 @@ function registerIpcHandlers({
79
85
 
80
86
  ipcMain.handle('get-chat-history', () => getChatTranscript());
81
87
 
88
+ ipcMain.handle('list-saved-pictures', () => {
89
+ return listSavedPictures ? listSavedPictures() : [];
90
+ });
91
+
82
92
  ipcMain.handle('open-settings', () => {
83
93
  windowManager.createSettingsWindow();
84
94
  });
@@ -0,0 +1,109 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const os = require('os');
4
+ const crypto = require('crypto');
5
+ const { pathToFileURL } = require('url');
6
+
7
+ const PICTURES_DIR = path.join(os.homedir(), '.config', 'mint', 'Pictures');
8
+ const INDEX_PATH = path.join(PICTURES_DIR, 'pictures.json');
9
+
10
+ const EXTENSIONS = {
11
+ 'image/png': 'png',
12
+ 'image/jpeg': 'jpg',
13
+ 'image/jpg': 'jpg',
14
+ 'image/webp': 'webp',
15
+ 'image/gif': 'gif'
16
+ };
17
+
18
+ function ensurePicturesDir() {
19
+ fs.mkdirSync(PICTURES_DIR, { recursive: true });
20
+ }
21
+
22
+ function readIndex() {
23
+ try {
24
+ if (!fs.existsSync(INDEX_PATH)) return [];
25
+ const parsed = JSON.parse(fs.readFileSync(INDEX_PATH, 'utf8'));
26
+ return Array.isArray(parsed) ? parsed : [];
27
+ } catch (error) {
28
+ console.error('[Pictures] Failed to read index:', error.message);
29
+ return [];
30
+ }
31
+ }
32
+
33
+ function writeIndex(entries) {
34
+ ensurePicturesDir();
35
+ fs.writeFileSync(INDEX_PATH, JSON.stringify(entries, null, 2), 'utf8');
36
+ }
37
+
38
+ function parseImageDataUri(dataUri) {
39
+ if (!dataUri || typeof dataUri !== 'string') return null;
40
+ const match = dataUri.match(/^data:(image\/[\w.+-]+);base64,([\s\S]+)$/);
41
+ if (!match) return null;
42
+
43
+ const mimeType = match[1].toLowerCase();
44
+ const extension = EXTENSIONS[mimeType] || 'png';
45
+ return {
46
+ mimeType,
47
+ extension,
48
+ buffer: Buffer.from(match[2], 'base64')
49
+ };
50
+ }
51
+
52
+ function createFilename(extension) {
53
+ const stamp = new Date().toISOString()
54
+ .replace(/[-:]/g, '')
55
+ .replace(/\..+$/, '')
56
+ .replace('T', '-');
57
+ const id = crypto.randomBytes(4).toString('hex');
58
+ return `mint-${stamp}-${id}.${extension}`;
59
+ }
60
+
61
+ function saveChatImages(base64Image, metadata = {}) {
62
+ const images = Array.isArray(base64Image) ? base64Image : (base64Image ? [base64Image] : []);
63
+ const saved = [];
64
+ if (images.length === 0) return saved;
65
+
66
+ ensurePicturesDir();
67
+ const index = readIndex();
68
+
69
+ for (const item of images) {
70
+ const parsed = parseImageDataUri(item);
71
+ if (!parsed || parsed.buffer.length === 0) continue;
72
+
73
+ const filename = createFilename(parsed.extension);
74
+ const filePath = path.join(PICTURES_DIR, filename);
75
+ fs.writeFileSync(filePath, parsed.buffer);
76
+
77
+ const entry = {
78
+ id: path.basename(filename, path.extname(filename)),
79
+ filename,
80
+ path: filePath,
81
+ mimeType: parsed.mimeType,
82
+ createdAt: new Date().toISOString(),
83
+ source: metadata.source || 'chat',
84
+ message: String(metadata.message || '').slice(0, 240)
85
+ };
86
+
87
+ index.unshift(entry);
88
+ saved.push(entry);
89
+ }
90
+
91
+ writeIndex(index);
92
+ return saved;
93
+ }
94
+
95
+ function listSavedPictures() {
96
+ ensurePicturesDir();
97
+ return readIndex()
98
+ .filter(entry => entry && entry.path && fs.existsSync(entry.path))
99
+ .map(entry => ({
100
+ ...entry,
101
+ url: pathToFileURL(entry.path).href
102
+ }));
103
+ }
104
+
105
+ module.exports = {
106
+ PICTURES_DIR,
107
+ saveChatImages,
108
+ listSavedPictures
109
+ };
@@ -57,6 +57,12 @@ function addTask(description) {
57
57
  createdAt: new Date().toISOString(),
58
58
  updatedAt: new Date().toISOString(),
59
59
  steps: [],
60
+ subtasks: [],
61
+ checkpoints: [],
62
+ artifacts: [],
63
+ retryCount: 0,
64
+ maxRetries: 1,
65
+ lastCheckpointAt: null,
60
66
  result: null
61
67
  };
62
68
  tasks.push(newTask);
@@ -80,6 +86,120 @@ function updateTask(id, updates) {
80
86
  return null;
81
87
  }
82
88
 
89
+ function getTask(id) {
90
+ return readTasks().find(t => t.id === id) || null;
91
+ }
92
+
93
+ function normalizeTask(task) {
94
+ return {
95
+ ...task,
96
+ steps: Array.isArray(task.steps) ? task.steps : [],
97
+ subtasks: Array.isArray(task.subtasks) ? task.subtasks : [],
98
+ checkpoints: Array.isArray(task.checkpoints) ? task.checkpoints : [],
99
+ artifacts: Array.isArray(task.artifacts) ? task.artifacts : [],
100
+ retryCount: Number.isFinite(task.retryCount) ? task.retryCount : 0,
101
+ maxRetries: Number.isFinite(task.maxRetries) ? task.maxRetries : 1
102
+ };
103
+ }
104
+
105
+ function mutateTask(id, mutator) {
106
+ const tasks = readTasks();
107
+ const idx = tasks.findIndex(t => t.id === id);
108
+ if (idx === -1) return null;
109
+ const next = normalizeTask(tasks[idx]);
110
+ mutator(next);
111
+ next.updatedAt = new Date().toISOString();
112
+ tasks[idx] = next;
113
+ writeTasks(tasks);
114
+ return next;
115
+ }
116
+
117
+ function addSubtask(taskId, title, extra = {}) {
118
+ return mutateTask(taskId, task => {
119
+ task.subtasks.push({
120
+ id: `${taskId}-${task.subtasks.length + 1}`,
121
+ title,
122
+ status: extra.status || 'pending',
123
+ createdAt: new Date().toISOString(),
124
+ updatedAt: new Date().toISOString(),
125
+ ...extra
126
+ });
127
+ });
128
+ }
129
+
130
+ function updateSubtask(taskId, subtaskId, updates = {}) {
131
+ return mutateTask(taskId, task => {
132
+ const subtask = task.subtasks.find(item => item.id === subtaskId);
133
+ if (!subtask) return;
134
+ Object.assign(subtask, updates, { updatedAt: new Date().toISOString() });
135
+ });
136
+ }
137
+
138
+ function addCheckpoint(taskId, checkpoint = {}) {
139
+ return mutateTask(taskId, task => {
140
+ const entry = {
141
+ id: `${taskId}-checkpoint-${task.checkpoints.length + 1}`,
142
+ time: new Date().toISOString(),
143
+ ...checkpoint
144
+ };
145
+ task.checkpoints.push(entry);
146
+ task.lastCheckpointAt = entry.time;
147
+ task.steps.push(entry);
148
+ });
149
+ }
150
+
151
+ function addArtifact(taskId, artifact = {}) {
152
+ return mutateTask(taskId, task => {
153
+ task.artifacts.push({
154
+ id: `${taskId}-artifact-${task.artifacts.length + 1}`,
155
+ time: new Date().toISOString(),
156
+ ...artifact
157
+ });
158
+ });
159
+ }
160
+
161
+ function failTaskWithRetry(id, errorMessage) {
162
+ return mutateTask(id, task => {
163
+ const retryCount = Number(task.retryCount) || 0;
164
+ const maxRetries = Number.isFinite(task.maxRetries) ? task.maxRetries : 1;
165
+ task.result = errorMessage;
166
+ task.retryCount = retryCount + 1;
167
+ task.status = task.retryCount <= maxRetries ? 'pending' : 'failed';
168
+ const checkpoint = {
169
+ id: `${id}-checkpoint-${task.checkpoints.length + 1}`,
170
+ time: new Date().toISOString(),
171
+ phase: task.status === 'pending' ? 'retry_scheduled' : 'failed',
172
+ message: errorMessage,
173
+ retryCount: task.retryCount,
174
+ maxRetries
175
+ };
176
+ task.checkpoints.push(checkpoint);
177
+ task.steps.push(checkpoint);
178
+ });
179
+ }
180
+
181
+ function resumeRunningTasks() {
182
+ const resumed = [];
183
+ const tasks = readTasks().map(task => {
184
+ if (task.status !== 'running') return task;
185
+ const normalized = normalizeTask(task);
186
+ normalized.status = 'pending';
187
+ const checkpoint = {
188
+ id: `${normalized.id}-checkpoint-${normalized.checkpoints.length + 1}`,
189
+ time: new Date().toISOString(),
190
+ phase: 'resume_after_restart',
191
+ message: 'Task was running during shutdown and has been re-queued.'
192
+ };
193
+ normalized.checkpoints.push(checkpoint);
194
+ normalized.steps.push(checkpoint);
195
+ normalized.updatedAt = new Date().toISOString();
196
+ resumed.push(normalized);
197
+ return normalized;
198
+ });
199
+ writeTasks(tasks);
200
+ return resumed;
201
+ }
202
+
83
203
  function clearCompletedTasks() {
84
204
  const tasks = readTasks();
85
205
  const activeTasks = tasks.filter(t => t.status === 'pending' || t.status === 'running');
@@ -88,8 +208,15 @@ function clearCompletedTasks() {
88
208
 
89
209
  module.exports = {
90
210
  addTask,
211
+ addArtifact,
212
+ addCheckpoint,
213
+ addSubtask,
214
+ failTaskWithRetry,
215
+ getTask,
91
216
  getPendingTask,
217
+ resumeRunningTasks,
92
218
  updateTask,
219
+ updateSubtask,
93
220
  readTasks,
94
221
  clearCompletedTasks
95
222
  };
@@ -41,6 +41,19 @@ const TOOL_REGISTRY = Object.freeze({
41
41
  important: true,
42
42
  description: 'Run a non-destructive shell command after user approval.'
43
43
  },
44
+ verify: {
45
+ permission: 'approval',
46
+ required: [],
47
+ codeAgentOnly: true,
48
+ important: true,
49
+ description: 'Run test/build/lint verification commands after user approval.'
50
+ },
51
+ plan: {
52
+ permission: 'approval',
53
+ required: ['plan'],
54
+ codeAgentOnly: true,
55
+ description: 'Present a multi-file edit plan before changing files.'
56
+ },
44
57
  apply_patch: {
45
58
  permission: 'approval',
46
59
  required: ['patch'],
@@ -9,12 +9,19 @@ function createWindowManager(projectRoot) {
9
9
  let tray = null;
10
10
 
11
11
  function createMainWindow() {
12
+ const iconPath = path.join(projectRoot, 'assets', 'icon.png');
13
+ const { width: screenWidth, height: screenHeight } = screen.getPrimaryDisplay().workAreaSize;
14
+ const windowWidth = Math.min(1360, Math.max(1180, screenWidth - 40));
15
+ const windowHeight = Math.min(920, Math.max(860, screenHeight - 40));
16
+
12
17
  mainWindow = new BrowserWindow({
13
- width: 1180,
14
- height: 860,
18
+ width: windowWidth,
19
+ height: windowHeight,
15
20
  minWidth: 900,
16
21
  minHeight: 680,
17
- icon: path.join(projectRoot, 'assets', 'icon.png'),
22
+ x: Math.floor((screenWidth - windowWidth) / 2),
23
+ y: Math.floor((screenHeight - windowHeight) / 2),
24
+ icon: nativeImage.createFromPath(iconPath),
18
25
  webPreferences: {
19
26
  preload: path.join(projectRoot, 'preload.js'),
20
27
  nodeIntegration: false,
@@ -75,12 +82,13 @@ function createWindowManager(projectRoot) {
75
82
  return settingsWindow;
76
83
  }
77
84
 
85
+ const iconPath = path.join(projectRoot, 'assets', 'icon.png');
78
86
  settingsWindow = new BrowserWindow({
79
- width: 720,
80
- height: 620,
81
- minWidth: 640,
82
- minHeight: 560,
83
- icon: path.join(projectRoot, 'assets', 'icon.png'),
87
+ width: 1020,
88
+ height: 720,
89
+ minWidth: 860,
90
+ minHeight: 620,
91
+ icon: nativeImage.createFromPath(iconPath),
84
92
  webPreferences: {
85
93
  preload: path.join(projectRoot, 'preload-settings.js'),
86
94
  nodeIntegration: false,
@@ -18,6 +18,9 @@ window.Live2DManager = {
18
18
  cat: { paramId: 'Param54', label: 'Cat Filter' }
19
19
  },
20
20
  pointerTrackingEnabled: true,
21
+ zoomMultiplier: 1,
22
+ interactionZoneOrigin: { x: 0.5, y: 0.58 },
23
+ fitModelToMount: null,
21
24
  pointerTrackingFrame: null,
22
25
  pointerTracking: {
23
26
  targetX: 0,
@@ -135,7 +138,7 @@ window.Live2DManager = {
135
138
  const heightScale = mountHeight / Math.max(modelHeight, 1);
136
139
 
137
140
  // Reduced zoom to 2.0 as requested
138
- const scale = Math.min(widthScale, heightScale) * 1.85;
141
+ const scale = Math.min(widthScale, heightScale) * 1.85 * this.zoomMultiplier;
139
142
 
140
143
  this.model.scale.set(scale);
141
144
  // Adjusted Y offset to 1.0 as requested
@@ -145,6 +148,7 @@ window.Live2DManager = {
145
148
  };
146
149
  this.applyModelFollowOffset();
147
150
  };
151
+ this.fitModelToMount = fitModel;
148
152
 
149
153
  requestAnimationFrame(() => {
150
154
  fitModel();
@@ -214,9 +218,9 @@ window.Live2DManager = {
214
218
  try {
215
219
  const point = this.getPointerViewportPoint(event);
216
220
  if (!point) return null;
217
- const { x, y } = point;
221
+ const { x, y } = this.toInteractionZonePoint(point);
218
222
 
219
- if (this.isPointInZone(x, y, 0.38, 0.40, 0.24, 0.115)) {
223
+ if (this.isPointInZone(x, y, 0.36, 0.375, 0.28, 0.12)) {
220
224
  return {
221
225
  id: 'face',
222
226
  label: 'Cat Ears',
@@ -225,7 +229,7 @@ window.Live2DManager = {
225
229
  };
226
230
  }
227
231
 
228
- if (this.isPointInZone(x, y, 0.34, 0.255, 0.32, 0.15)) {
232
+ if (this.isPointInZone(x, y, 0.34, 0.205, 0.32, 0.155)) {
229
233
  return {
230
234
  id: 'head',
231
235
  label: 'Head Pat',
@@ -234,8 +238,8 @@ window.Live2DManager = {
234
238
  };
235
239
  }
236
240
 
237
- const isLeftHand = this.isPointInZone(x, y, 0.22, 0.68, 0.20, 0.16);
238
- const isRightHand = this.isPointInZone(x, y, 0.61, 0.68, 0.19, 0.16);
241
+ const isLeftHand = this.isPointInZone(x, y, 0.14, 0.70, 0.22, 0.16);
242
+ const isRightHand = this.isPointInZone(x, y, 0.65, 0.69, 0.23, 0.17);
239
243
  if (isLeftHand || isRightHand) {
240
244
  return {
241
245
  id: isLeftHand ? 'left-hand' : 'right-hand',
@@ -245,7 +249,7 @@ window.Live2DManager = {
245
249
  };
246
250
  }
247
251
 
248
- if (this.isPointInZone(x, y, 0.38, 0.77, 0.30, 0.23)) {
252
+ if (this.isPointInZone(x, y, 0.34, 0.74, 0.30, 0.24)) {
249
253
  return {
250
254
  id: 'lower-body',
251
255
  label: 'Careful',
@@ -254,7 +258,7 @@ window.Live2DManager = {
254
258
  };
255
259
  }
256
260
 
257
- if (this.isPointInZone(x, y, 0.37, 0.555, 0.29, 0.14)) {
261
+ if (this.isPointInZone(x, y, 0.36, 0.53, 0.29, 0.145)) {
258
262
  return {
259
263
  id: 'body',
260
264
  label: 'Shoulder Tap',
@@ -293,6 +297,17 @@ window.Live2DManager = {
293
297
  return x >= left && x <= left + width && y >= top && y <= top + height;
294
298
  },
295
299
 
300
+ toInteractionZonePoint(point) {
301
+ const scale = this.zoomMultiplier || 1;
302
+ if (Math.abs(scale - 1) < 0.001) return point;
303
+
304
+ const origin = this.interactionZoneOrigin;
305
+ return {
306
+ x: origin.x + (point.x - origin.x) / scale,
307
+ y: origin.y + (point.y - origin.y) / scale
308
+ };
309
+ },
310
+
296
311
  cycleExpression() {
297
312
  if (!this.model) return;
298
313
  this.expIndex = (this.expIndex + 1) % this.expressionNames.length;
@@ -316,6 +331,25 @@ window.Live2DManager = {
316
331
  this.model.buttonMode = this.interactionEnabled;
317
332
  },
318
333
 
334
+ setPointerTrackingEnabled(isEnabled) {
335
+ this.pointerTrackingEnabled = Boolean(isEnabled);
336
+ if (this.pointerTrackingEnabled) return;
337
+
338
+ this.resetPointerTrackingTarget();
339
+ this.pointerTracking.currentX = 0;
340
+ this.pointerTracking.currentY = 0;
341
+ this.applyModelFollowOffset();
342
+ },
343
+
344
+ setZoomMultiplier(multiplier) {
345
+ const value = Number(multiplier);
346
+ this.zoomMultiplier = this.clamp(Number.isFinite(value) ? value : 1, 0.78, 1.28);
347
+ document.documentElement.style.setProperty('--model-zone-scale', String(this.zoomMultiplier));
348
+ if (typeof this.fitModelToMount === 'function') {
349
+ this.fitModelToMount();
350
+ }
351
+ },
352
+
319
353
  getSavedInteractionEnabled() {
320
354
  try {
321
355
  return localStorage.getItem(this.interactionStorageKey) !== 'false';