@geminilight/mindos 0.6.18 → 0.6.20
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/app/app/api/ask/route.ts +171 -121
- package/app/app/api/settings/list-models/route.ts +96 -0
- package/app/app/api/settings/test-key/route.ts +37 -37
- package/app/components/OrganizeToast.tsx +237 -82
- package/app/components/Panel.tsx +6 -5
- package/app/components/settings/AiTab.tsx +123 -8
- package/app/hooks/useAiOrganize.ts +1 -0
- package/app/lib/agent/model.ts +2 -1
- package/app/lib/agent/prompt.ts +23 -0
- package/app/lib/agent/tools.ts +12 -0
- package/app/lib/i18n-en.ts +9 -1
- package/app/lib/i18n-zh.ts +9 -1
- package/app/next-env.d.ts +1 -1
- package/package.json +1 -1
package/app/app/api/ask/route.ts
CHANGED
|
@@ -16,8 +16,8 @@ import fs from 'fs';
|
|
|
16
16
|
import path from 'path';
|
|
17
17
|
import { getFileContent, getMindRoot } from '@/lib/fs';
|
|
18
18
|
import { getModelConfig } from '@/lib/agent/model';
|
|
19
|
-
import { getRequestScopedTools, WRITE_TOOLS, truncate } from '@/lib/agent/tools';
|
|
20
|
-
import { AGENT_SYSTEM_PROMPT } from '@/lib/agent/prompt';
|
|
19
|
+
import { getRequestScopedTools, getOrganizeTools, WRITE_TOOLS, truncate } from '@/lib/agent/tools';
|
|
20
|
+
import { AGENT_SYSTEM_PROMPT, ORGANIZE_SYSTEM_PROMPT } from '@/lib/agent/prompt';
|
|
21
21
|
import { toAgentMessages } from '@/lib/agent/to-agent-messages';
|
|
22
22
|
import { logAgentOp } from '@/lib/agent/log';
|
|
23
23
|
import { readSettings } from '@/lib/settings';
|
|
@@ -252,6 +252,8 @@ export async function POST(req: NextRequest) {
|
|
|
252
252
|
attachedFiles?: string[];
|
|
253
253
|
uploadedFiles?: Array<{ name: string; content: string }>;
|
|
254
254
|
maxSteps?: number;
|
|
255
|
+
/** 'organize' = lean prompt for file import organize; default = full prompt */
|
|
256
|
+
mode?: 'organize' | 'default';
|
|
255
257
|
};
|
|
256
258
|
try {
|
|
257
259
|
body = await req.json();
|
|
@@ -260,6 +262,7 @@ export async function POST(req: NextRequest) {
|
|
|
260
262
|
}
|
|
261
263
|
|
|
262
264
|
const { messages, currentFile, attachedFiles, uploadedFiles } = body;
|
|
265
|
+
const isOrganizeMode = body.mode === 'organize';
|
|
263
266
|
|
|
264
267
|
// Read agent config from settings
|
|
265
268
|
const serverSettings = readSettings();
|
|
@@ -271,99 +274,7 @@ export async function POST(req: NextRequest) {
|
|
|
271
274
|
const thinkingBudget = agentConfig.thinkingBudget ?? 5000;
|
|
272
275
|
const contextStrategy = agentConfig.contextStrategy ?? 'auto';
|
|
273
276
|
|
|
274
|
-
//
|
|
275
|
-
// 1. SKILL.md — complete skill with operating rules (always loaded)
|
|
276
|
-
// 2. user-skill-rules.md — user's personalized rules from KB root (if exists)
|
|
277
|
-
const isZh = serverSettings.disabledSkills?.includes('mindos') ?? false;
|
|
278
|
-
const skillDirName = isZh ? 'mindos-zh' : 'mindos';
|
|
279
|
-
const appDir = process.env.MINDOS_PROJECT_ROOT
|
|
280
|
-
? path.join(process.env.MINDOS_PROJECT_ROOT, 'app')
|
|
281
|
-
: process.cwd();
|
|
282
|
-
const skillPath = path.join(appDir, `data/skills/${skillDirName}/SKILL.md`);
|
|
283
|
-
const skill = readAbsoluteFile(skillPath);
|
|
284
|
-
|
|
285
|
-
const mindRoot = getMindRoot();
|
|
286
|
-
const userSkillRules = readKnowledgeFile('user-skill-rules.md');
|
|
287
|
-
|
|
288
|
-
const targetDir = dirnameOf(currentFile);
|
|
289
|
-
const bootstrap = {
|
|
290
|
-
instruction: readKnowledgeFile('INSTRUCTION.md'),
|
|
291
|
-
index: readKnowledgeFile('README.md'),
|
|
292
|
-
config_json: readKnowledgeFile('CONFIG.json'),
|
|
293
|
-
config_md: readKnowledgeFile('CONFIG.md'),
|
|
294
|
-
target_readme: targetDir ? readKnowledgeFile(`${targetDir}/README.md`) : null,
|
|
295
|
-
target_instruction: targetDir ? readKnowledgeFile(`${targetDir}/INSTRUCTION.md`) : null,
|
|
296
|
-
target_config_json: targetDir ? readKnowledgeFile(`${targetDir}/CONFIG.json`) : null,
|
|
297
|
-
target_config_md: targetDir ? readKnowledgeFile(`${targetDir}/CONFIG.md`) : null,
|
|
298
|
-
};
|
|
299
|
-
|
|
300
|
-
// Only report failures + truncation warnings
|
|
301
|
-
const initFailures: string[] = [];
|
|
302
|
-
const truncationWarnings: string[] = [];
|
|
303
|
-
if (!skill.ok) initFailures.push(`skill.mindos: failed (${skill.error})`);
|
|
304
|
-
if (skill.ok && skill.truncated) truncationWarnings.push('skill.mindos was truncated');
|
|
305
|
-
if (userSkillRules.ok && userSkillRules.truncated) truncationWarnings.push('user-skill-rules.md was truncated');
|
|
306
|
-
if (!bootstrap.instruction.ok) initFailures.push(`bootstrap.instruction: failed (${bootstrap.instruction.error})`);
|
|
307
|
-
if (bootstrap.instruction.ok && bootstrap.instruction.truncated) truncationWarnings.push('bootstrap.instruction was truncated');
|
|
308
|
-
if (!bootstrap.index.ok) initFailures.push(`bootstrap.index: failed (${bootstrap.index.error})`);
|
|
309
|
-
if (bootstrap.index.ok && bootstrap.index.truncated) truncationWarnings.push('bootstrap.index was truncated');
|
|
310
|
-
if (!bootstrap.config_json.ok) initFailures.push(`bootstrap.config_json: failed (${bootstrap.config_json.error})`);
|
|
311
|
-
if (bootstrap.config_json.ok && bootstrap.config_json.truncated) truncationWarnings.push('bootstrap.config_json was truncated');
|
|
312
|
-
if (!bootstrap.config_md.ok) initFailures.push(`bootstrap.config_md: failed (${bootstrap.config_md.error})`);
|
|
313
|
-
if (bootstrap.config_md.ok && bootstrap.config_md.truncated) truncationWarnings.push('bootstrap.config_md was truncated');
|
|
314
|
-
if (bootstrap.target_readme && !bootstrap.target_readme.ok) initFailures.push(`bootstrap.target_readme: failed (${bootstrap.target_readme.error})`);
|
|
315
|
-
if (bootstrap.target_readme?.ok && bootstrap.target_readme.truncated) truncationWarnings.push('bootstrap.target_readme was truncated');
|
|
316
|
-
if (bootstrap.target_instruction && !bootstrap.target_instruction.ok) initFailures.push(`bootstrap.target_instruction: failed (${bootstrap.target_instruction.error})`);
|
|
317
|
-
if (bootstrap.target_instruction?.ok && bootstrap.target_instruction.truncated) truncationWarnings.push('bootstrap.target_instruction was truncated');
|
|
318
|
-
if (bootstrap.target_config_json && !bootstrap.target_config_json.ok) initFailures.push(`bootstrap.target_config_json: failed (${bootstrap.target_config_json.error})`);
|
|
319
|
-
if (bootstrap.target_config_json?.ok && bootstrap.target_config_json.truncated) truncationWarnings.push('bootstrap.target_config_json was truncated');
|
|
320
|
-
if (bootstrap.target_config_md && !bootstrap.target_config_md.ok) initFailures.push(`bootstrap.target_config_md: failed (${bootstrap.target_config_md.error})`);
|
|
321
|
-
if (bootstrap.target_config_md?.ok && bootstrap.target_config_md.truncated) truncationWarnings.push('bootstrap.target_config_md was truncated');
|
|
322
|
-
|
|
323
|
-
const initStatus = initFailures.length === 0
|
|
324
|
-
? `All initialization contexts loaded successfully. mind_root=${getMindRoot()}${targetDir ? `, target_dir=${targetDir}` : ''}${truncationWarnings.length > 0 ? ` ⚠️ ${truncationWarnings.length} files truncated` : ''}`
|
|
325
|
-
: `Initialization issues:\n${initFailures.join('\n')}\nmind_root=${getMindRoot()}${targetDir ? `, target_dir=${targetDir}` : ''}${truncationWarnings.length > 0 ? `\n⚠️ Warnings:\n${truncationWarnings.join('\n')}` : ''}`;
|
|
326
|
-
|
|
327
|
-
const initContextBlocks: string[] = [];
|
|
328
|
-
if (skill.ok) initContextBlocks.push(`## mindos_skill_md\n\n${skill.content}`);
|
|
329
|
-
// User personalization rules (from knowledge base root)
|
|
330
|
-
if (userSkillRules.ok && !userSkillRules.truncated && userSkillRules.content.trim()) {
|
|
331
|
-
initContextBlocks.push(`## user_skill_rules\n\nUser personalization rules (user-skill-rules.md):\n\n${userSkillRules.content}`);
|
|
332
|
-
}
|
|
333
|
-
if (bootstrap.instruction.ok) initContextBlocks.push(`## bootstrap_instruction\n\n${bootstrap.instruction.content}`);
|
|
334
|
-
if (bootstrap.index.ok) initContextBlocks.push(`## bootstrap_index\n\n${bootstrap.index.content}`);
|
|
335
|
-
if (bootstrap.config_json.ok) initContextBlocks.push(`## bootstrap_config_json\n\n${bootstrap.config_json.content}`);
|
|
336
|
-
if (bootstrap.config_md.ok) initContextBlocks.push(`## bootstrap_config_md\n\n${bootstrap.config_md.content}`);
|
|
337
|
-
if (bootstrap.target_readme?.ok) initContextBlocks.push(`## bootstrap_target_readme\n\n${bootstrap.target_readme.content}`);
|
|
338
|
-
if (bootstrap.target_instruction?.ok) initContextBlocks.push(`## bootstrap_target_instruction\n\n${bootstrap.target_instruction.content}`);
|
|
339
|
-
if (bootstrap.target_config_json?.ok) initContextBlocks.push(`## bootstrap_target_config_json\n\n${bootstrap.target_config_json.content}`);
|
|
340
|
-
if (bootstrap.target_config_md?.ok) initContextBlocks.push(`## bootstrap_target_config_md\n\n${bootstrap.target_config_md.content}`);
|
|
341
|
-
|
|
342
|
-
// Build initial context from attached/current files
|
|
343
|
-
const contextParts: string[] = [];
|
|
344
|
-
const seen = new Set<string>();
|
|
345
|
-
const hasAttached = Array.isArray(attachedFiles) && attachedFiles.length > 0;
|
|
346
|
-
|
|
347
|
-
if (hasAttached) {
|
|
348
|
-
for (const filePath of attachedFiles!) {
|
|
349
|
-
if (seen.has(filePath)) continue;
|
|
350
|
-
seen.add(filePath);
|
|
351
|
-
try {
|
|
352
|
-
const content = truncate(getFileContent(filePath));
|
|
353
|
-
contextParts.push(`## Attached: ${filePath}\n\n${content}`);
|
|
354
|
-
} catch { /* ignore missing files */ }
|
|
355
|
-
}
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
if (currentFile && !seen.has(currentFile)) {
|
|
359
|
-
seen.add(currentFile);
|
|
360
|
-
try {
|
|
361
|
-
const content = truncate(getFileContent(currentFile));
|
|
362
|
-
contextParts.push(`## Current file: ${currentFile}\n\n${content}`);
|
|
363
|
-
} catch { /* ignore */ }
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
// Uploaded files
|
|
277
|
+
// Uploaded files — shared by both modes
|
|
367
278
|
const uploadedParts: string[] = [];
|
|
368
279
|
if (Array.isArray(uploadedFiles) && uploadedFiles.length > 0) {
|
|
369
280
|
for (const f of uploadedFiles.slice(0, 8)) {
|
|
@@ -372,40 +283,154 @@ export async function POST(req: NextRequest) {
|
|
|
372
283
|
}
|
|
373
284
|
}
|
|
374
285
|
|
|
375
|
-
//
|
|
376
|
-
|
|
377
|
-
|
|
286
|
+
// ---------------------------------------------------------------------------
|
|
287
|
+
// Build system prompt — lean path for organize mode, full path otherwise
|
|
288
|
+
// ---------------------------------------------------------------------------
|
|
289
|
+
let systemPrompt: string;
|
|
290
|
+
|
|
291
|
+
if (isOrganizeMode) {
|
|
292
|
+
// Organize mode: minimal prompt — only KB structure + uploaded files
|
|
293
|
+
const promptParts: string[] = [ORGANIZE_SYSTEM_PROMPT];
|
|
294
|
+
|
|
295
|
+
promptParts.push(`---\n\nmind_root=${getMindRoot()}`);
|
|
296
|
+
|
|
297
|
+
// Only load root README.md for KB structure awareness (skip SKILL.md, configs, target dir, time, etc.)
|
|
298
|
+
const bootstrapIndex = readKnowledgeFile('README.md');
|
|
299
|
+
if (bootstrapIndex.ok) {
|
|
300
|
+
promptParts.push(`---\n\n## Knowledge Base Structure\n\n${bootstrapIndex.content}`);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
if (uploadedParts.length > 0) {
|
|
304
|
+
promptParts.push(
|
|
305
|
+
`---\n\n## ⚠️ USER-UPLOADED FILES\n\n` +
|
|
306
|
+
`Their FULL CONTENT is below. Use this directly — do NOT call read tools on them.\n\n` +
|
|
307
|
+
uploadedParts.join('\n\n---\n\n'),
|
|
308
|
+
);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
systemPrompt = promptParts.join('\n\n');
|
|
312
|
+
} else {
|
|
313
|
+
// Full mode: original prompt assembly
|
|
314
|
+
// Auto-load skill + bootstrap context for each request.
|
|
315
|
+
const isZh = serverSettings.disabledSkills?.includes('mindos') ?? false;
|
|
316
|
+
const skillDirName = isZh ? 'mindos-zh' : 'mindos';
|
|
317
|
+
const appDir = process.env.MINDOS_PROJECT_ROOT
|
|
318
|
+
? path.join(process.env.MINDOS_PROJECT_ROOT, 'app')
|
|
319
|
+
: process.cwd();
|
|
320
|
+
const skillPath = path.join(appDir, `data/skills/${skillDirName}/SKILL.md`);
|
|
321
|
+
const skill = readAbsoluteFile(skillPath);
|
|
322
|
+
|
|
323
|
+
const userSkillRules = readKnowledgeFile('user-skill-rules.md');
|
|
324
|
+
|
|
325
|
+
const targetDir = dirnameOf(currentFile);
|
|
326
|
+
const bootstrap = {
|
|
327
|
+
instruction: readKnowledgeFile('INSTRUCTION.md'),
|
|
328
|
+
index: readKnowledgeFile('README.md'),
|
|
329
|
+
config_json: readKnowledgeFile('CONFIG.json'),
|
|
330
|
+
config_md: readKnowledgeFile('CONFIG.md'),
|
|
331
|
+
target_readme: targetDir ? readKnowledgeFile(`${targetDir}/README.md`) : null,
|
|
332
|
+
target_instruction: targetDir ? readKnowledgeFile(`${targetDir}/INSTRUCTION.md`) : null,
|
|
333
|
+
target_config_json: targetDir ? readKnowledgeFile(`${targetDir}/CONFIG.json`) : null,
|
|
334
|
+
target_config_md: targetDir ? readKnowledgeFile(`${targetDir}/CONFIG.md`) : null,
|
|
335
|
+
};
|
|
336
|
+
|
|
337
|
+
// Only report failures + truncation warnings
|
|
338
|
+
const initFailures: string[] = [];
|
|
339
|
+
const truncationWarnings: string[] = [];
|
|
340
|
+
if (!skill.ok) initFailures.push(`skill.mindos: failed (${skill.error})`);
|
|
341
|
+
if (skill.ok && skill.truncated) truncationWarnings.push('skill.mindos was truncated');
|
|
342
|
+
if (userSkillRules.ok && userSkillRules.truncated) truncationWarnings.push('user-skill-rules.md was truncated');
|
|
343
|
+
if (!bootstrap.instruction.ok) initFailures.push(`bootstrap.instruction: failed (${bootstrap.instruction.error})`);
|
|
344
|
+
if (bootstrap.instruction.ok && bootstrap.instruction.truncated) truncationWarnings.push('bootstrap.instruction was truncated');
|
|
345
|
+
if (!bootstrap.index.ok) initFailures.push(`bootstrap.index: failed (${bootstrap.index.error})`);
|
|
346
|
+
if (bootstrap.index.ok && bootstrap.index.truncated) truncationWarnings.push('bootstrap.index was truncated');
|
|
347
|
+
if (!bootstrap.config_json.ok) initFailures.push(`bootstrap.config_json: failed (${bootstrap.config_json.error})`);
|
|
348
|
+
if (bootstrap.config_json.ok && bootstrap.config_json.truncated) truncationWarnings.push('bootstrap.config_json was truncated');
|
|
349
|
+
if (!bootstrap.config_md.ok) initFailures.push(`bootstrap.config_md: failed (${bootstrap.config_md.error})`);
|
|
350
|
+
if (bootstrap.config_md.ok && bootstrap.config_md.truncated) truncationWarnings.push('bootstrap.config_md was truncated');
|
|
351
|
+
if (bootstrap.target_readme && !bootstrap.target_readme.ok) initFailures.push(`bootstrap.target_readme: failed (${bootstrap.target_readme.error})`);
|
|
352
|
+
if (bootstrap.target_readme?.ok && bootstrap.target_readme.truncated) truncationWarnings.push('bootstrap.target_readme was truncated');
|
|
353
|
+
if (bootstrap.target_instruction && !bootstrap.target_instruction.ok) initFailures.push(`bootstrap.target_instruction: failed (${bootstrap.target_instruction.error})`);
|
|
354
|
+
if (bootstrap.target_instruction?.ok && bootstrap.target_instruction.truncated) truncationWarnings.push('bootstrap.target_instruction was truncated');
|
|
355
|
+
if (bootstrap.target_config_json && !bootstrap.target_config_json.ok) initFailures.push(`bootstrap.target_config_json: failed (${bootstrap.target_config_json.error})`);
|
|
356
|
+
if (bootstrap.target_config_json?.ok && bootstrap.target_config_json.truncated) truncationWarnings.push('bootstrap.target_config_json was truncated');
|
|
357
|
+
if (bootstrap.target_config_md && !bootstrap.target_config_md.ok) initFailures.push(`bootstrap.target_config_md: failed (${bootstrap.target_config_md.error})`);
|
|
358
|
+
if (bootstrap.target_config_md?.ok && bootstrap.target_config_md.truncated) truncationWarnings.push('bootstrap.target_config_md was truncated');
|
|
359
|
+
|
|
360
|
+
const initStatus = initFailures.length === 0
|
|
361
|
+
? `All initialization contexts loaded successfully. mind_root=${getMindRoot()}${targetDir ? `, target_dir=${targetDir}` : ''}${truncationWarnings.length > 0 ? ` ⚠️ ${truncationWarnings.length} files truncated` : ''}`
|
|
362
|
+
: `Initialization issues:\n${initFailures.join('\n')}\nmind_root=${getMindRoot()}${targetDir ? `, target_dir=${targetDir}` : ''}${truncationWarnings.length > 0 ? `\n⚠️ Warnings:\n${truncationWarnings.join('\n')}` : ''}`;
|
|
363
|
+
|
|
364
|
+
const initContextBlocks: string[] = [];
|
|
365
|
+
if (skill.ok) initContextBlocks.push(`## mindos_skill_md\n\n${skill.content}`);
|
|
366
|
+
if (userSkillRules.ok && !userSkillRules.truncated && userSkillRules.content.trim()) {
|
|
367
|
+
initContextBlocks.push(`## user_skill_rules\n\nUser personalization rules (user-skill-rules.md):\n\n${userSkillRules.content}`);
|
|
368
|
+
}
|
|
369
|
+
if (bootstrap.instruction.ok) initContextBlocks.push(`## bootstrap_instruction\n\n${bootstrap.instruction.content}`);
|
|
370
|
+
if (bootstrap.index.ok) initContextBlocks.push(`## bootstrap_index\n\n${bootstrap.index.content}`);
|
|
371
|
+
if (bootstrap.config_json.ok) initContextBlocks.push(`## bootstrap_config_json\n\n${bootstrap.config_json.content}`);
|
|
372
|
+
if (bootstrap.config_md.ok) initContextBlocks.push(`## bootstrap_config_md\n\n${bootstrap.config_md.content}`);
|
|
373
|
+
if (bootstrap.target_readme?.ok) initContextBlocks.push(`## bootstrap_target_readme\n\n${bootstrap.target_readme.content}`);
|
|
374
|
+
if (bootstrap.target_instruction?.ok) initContextBlocks.push(`## bootstrap_target_instruction\n\n${bootstrap.target_instruction.content}`);
|
|
375
|
+
if (bootstrap.target_config_json?.ok) initContextBlocks.push(`## bootstrap_target_config_json\n\n${bootstrap.target_config_json.content}`);
|
|
376
|
+
if (bootstrap.target_config_md?.ok) initContextBlocks.push(`## bootstrap_target_config_md\n\n${bootstrap.target_config_md.content}`);
|
|
377
|
+
|
|
378
|
+
// Build initial context from attached/current files
|
|
379
|
+
const contextParts: string[] = [];
|
|
380
|
+
const seen = new Set<string>();
|
|
381
|
+
const hasAttached = Array.isArray(attachedFiles) && attachedFiles.length > 0;
|
|
382
|
+
|
|
383
|
+
if (hasAttached) {
|
|
384
|
+
for (const filePath of attachedFiles!) {
|
|
385
|
+
if (seen.has(filePath)) continue;
|
|
386
|
+
seen.add(filePath);
|
|
387
|
+
try {
|
|
388
|
+
const content = truncate(getFileContent(filePath));
|
|
389
|
+
contextParts.push(`## Attached: ${filePath}\n\n${content}`);
|
|
390
|
+
} catch { /* ignore missing files */ }
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
if (currentFile && !seen.has(currentFile)) {
|
|
395
|
+
seen.add(currentFile);
|
|
396
|
+
try {
|
|
397
|
+
const content = truncate(getFileContent(currentFile));
|
|
398
|
+
contextParts.push(`## Current file: ${currentFile}\n\n${content}`);
|
|
399
|
+
} catch { /* ignore */ }
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
const now = new Date();
|
|
403
|
+
const timeContext = `## Current Time Context
|
|
378
404
|
- Current UTC Time: ${now.toISOString()}
|
|
379
405
|
- System Local Time: ${new Intl.DateTimeFormat('en-US', { dateStyle: 'full', timeStyle: 'long' }).format(now)}
|
|
380
406
|
- Unix Timestamp: ${Math.floor(now.getTime() / 1000)}
|
|
381
407
|
|
|
382
408
|
*Note: The times listed above represent "NOW". The user may have sent messages hours or days ago in this same conversation thread. Each user message in the history contains its own specific timestamp which you should refer to when understanding historical context.*`;
|
|
383
409
|
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
promptParts.push(`---\n\nInitialization status (auto-loaded at request start):\n\n${initStatus}`);
|
|
410
|
+
const promptParts: string[] = [AGENT_SYSTEM_PROMPT];
|
|
411
|
+
promptParts.push(`---\n\n${timeContext}`);
|
|
412
|
+
promptParts.push(`---\n\nInitialization status (auto-loaded at request start):\n\n${initStatus}`);
|
|
389
413
|
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
414
|
+
if (initContextBlocks.length > 0) {
|
|
415
|
+
promptParts.push(`---\n\nInitialization context:\n\n${initContextBlocks.join('\n\n---\n\n')}`);
|
|
416
|
+
}
|
|
393
417
|
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
418
|
+
if (contextParts.length > 0) {
|
|
419
|
+
promptParts.push(`---\n\nThe user is currently viewing these files:\n\n${contextParts.join('\n\n---\n\n')}`);
|
|
420
|
+
}
|
|
397
421
|
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
422
|
+
if (uploadedParts.length > 0) {
|
|
423
|
+
promptParts.push(
|
|
424
|
+
`---\n\n## ⚠️ USER-UPLOADED FILES (ACTIVE ATTACHMENTS)\n\n` +
|
|
425
|
+
`The user has uploaded the following file(s) in this conversation. ` +
|
|
426
|
+
`Their FULL CONTENT is provided below. You MUST use this content directly when the user refers to these files. ` +
|
|
427
|
+
`Do NOT use read_file or search tools to find them — they exist only here, not in the knowledge base.\n\n` +
|
|
428
|
+
uploadedParts.join('\n\n---\n\n'),
|
|
429
|
+
);
|
|
430
|
+
}
|
|
407
431
|
|
|
408
|
-
|
|
432
|
+
systemPrompt = promptParts.join('\n\n');
|
|
433
|
+
}
|
|
409
434
|
|
|
410
435
|
try {
|
|
411
436
|
const { model, modelName, apiKey, provider } = getModelConfig();
|
|
@@ -424,7 +449,7 @@ export async function POST(req: NextRequest) {
|
|
|
424
449
|
// Capture API key for this request — safe since each POST creates a new Agent instance.
|
|
425
450
|
const requestApiKey = apiKey;
|
|
426
451
|
const projectRoot = process.env.MINDOS_PROJECT_ROOT || path.resolve(process.cwd(), '..');
|
|
427
|
-
const requestTools = await getRequestScopedTools();
|
|
452
|
+
const requestTools = isOrganizeMode ? getOrganizeTools() : await getRequestScopedTools();
|
|
428
453
|
const customTools = toPiCustomToolDefinitions(requestTools);
|
|
429
454
|
|
|
430
455
|
const authStorage = AuthStorage.create();
|
|
@@ -492,12 +517,18 @@ export async function POST(req: NextRequest) {
|
|
|
492
517
|
}
|
|
493
518
|
}
|
|
494
519
|
|
|
520
|
+
let hasContent = false;
|
|
521
|
+
let lastModelError = '';
|
|
522
|
+
|
|
495
523
|
session.subscribe((event: AgentEvent) => {
|
|
496
524
|
if (isTextDeltaEvent(event)) {
|
|
525
|
+
hasContent = true;
|
|
497
526
|
send({ type: 'text_delta', delta: getTextDelta(event) });
|
|
498
527
|
} else if (isThinkingDeltaEvent(event)) {
|
|
528
|
+
hasContent = true;
|
|
499
529
|
send({ type: 'thinking_delta', delta: getThinkingDelta(event) });
|
|
500
530
|
} else if (isToolExecutionStartEvent(event)) {
|
|
531
|
+
hasContent = true;
|
|
501
532
|
const { toolCallId, toolName, args } = getToolExecutionStart(event);
|
|
502
533
|
const safeArgs = sanitizeToolArgs(toolName, args);
|
|
503
534
|
send({
|
|
@@ -555,12 +586,30 @@ export async function POST(req: NextRequest) {
|
|
|
555
586
|
}
|
|
556
587
|
|
|
557
588
|
console.log(`[ask] Step ${stepCount}/${stepLimit}`);
|
|
589
|
+
} else if (event.type === 'agent_end') {
|
|
590
|
+
// Capture model errors from the last assistant message.
|
|
591
|
+
// pi-coding-agent resolves prompt() without throwing after retries;
|
|
592
|
+
// the error is only visible in agent_end event messages.
|
|
593
|
+
const msgs = (event as any).messages;
|
|
594
|
+
if (Array.isArray(msgs)) {
|
|
595
|
+
for (let i = msgs.length - 1; i >= 0; i--) {
|
|
596
|
+
const m = msgs[i];
|
|
597
|
+
if (m?.role === 'assistant' && m?.stopReason === 'error' && m?.errorMessage) {
|
|
598
|
+
lastModelError = m.errorMessage;
|
|
599
|
+
break;
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
}
|
|
558
603
|
}
|
|
559
604
|
});
|
|
560
605
|
|
|
561
606
|
session.prompt(lastUserContent).then(() => {
|
|
562
607
|
metrics.recordRequest(Date.now() - requestStartTime);
|
|
563
|
-
|
|
608
|
+
if (!hasContent && lastModelError) {
|
|
609
|
+
send({ type: 'error', message: lastModelError });
|
|
610
|
+
} else {
|
|
611
|
+
send({ type: 'done' });
|
|
612
|
+
}
|
|
564
613
|
controller.close();
|
|
565
614
|
}).catch((err) => {
|
|
566
615
|
metrics.recordRequest(Date.now() - requestStartTime);
|
|
@@ -587,3 +636,4 @@ export async function POST(req: NextRequest) {
|
|
|
587
636
|
return apiError(ErrorCodes.MODEL_INIT_FAILED, err instanceof Error ? err.message : 'Failed to initialize AI model', 500);
|
|
588
637
|
}
|
|
589
638
|
}
|
|
639
|
+
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
export const dynamic = 'force-dynamic';
|
|
2
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
3
|
+
import { effectiveAiConfig } from '@/lib/settings';
|
|
4
|
+
|
|
5
|
+
const TIMEOUT = 10_000;
|
|
6
|
+
|
|
7
|
+
export async function POST(req: NextRequest) {
|
|
8
|
+
try {
|
|
9
|
+
const body = await req.json();
|
|
10
|
+
const { provider, apiKey, baseUrl } = body as {
|
|
11
|
+
provider?: string;
|
|
12
|
+
apiKey?: string;
|
|
13
|
+
baseUrl?: string;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
if (provider !== 'anthropic' && provider !== 'openai') {
|
|
17
|
+
return NextResponse.json({ ok: false, error: 'Invalid provider' }, { status: 400 });
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const cfg = effectiveAiConfig();
|
|
21
|
+
let resolvedKey = apiKey || '';
|
|
22
|
+
if (!resolvedKey || resolvedKey === '***set***') {
|
|
23
|
+
resolvedKey = provider === 'anthropic' ? cfg.anthropicApiKey : cfg.openaiApiKey;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (!resolvedKey) {
|
|
27
|
+
return NextResponse.json({ ok: false, error: 'No API key configured' });
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const ctrl = new AbortController();
|
|
31
|
+
const timer = setTimeout(() => ctrl.abort(), TIMEOUT);
|
|
32
|
+
|
|
33
|
+
try {
|
|
34
|
+
let models: string[] = [];
|
|
35
|
+
|
|
36
|
+
if (provider === 'openai') {
|
|
37
|
+
const resolvedBaseUrl = (baseUrl || cfg.openaiBaseUrl || 'https://api.openai.com/v1').replace(/\/+$/, '');
|
|
38
|
+
const res = await fetch(`${resolvedBaseUrl}/models`, {
|
|
39
|
+
headers: { Authorization: `Bearer ${resolvedKey}` },
|
|
40
|
+
signal: ctrl.signal,
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
if (!res.ok) {
|
|
44
|
+
const errBody = await res.text().catch(() => '');
|
|
45
|
+
return NextResponse.json({
|
|
46
|
+
ok: false,
|
|
47
|
+
error: `Failed to list models: HTTP ${res.status} ${errBody.slice(0, 200)}`,
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const json = await res.json();
|
|
52
|
+
if (Array.isArray(json?.data)) {
|
|
53
|
+
models = json.data
|
|
54
|
+
.map((m: any) => m.id as string)
|
|
55
|
+
.filter(Boolean)
|
|
56
|
+
.sort((a: string, b: string) => a.localeCompare(b));
|
|
57
|
+
}
|
|
58
|
+
} else {
|
|
59
|
+
const res = await fetch('https://api.anthropic.com/v1/models', {
|
|
60
|
+
headers: {
|
|
61
|
+
'x-api-key': resolvedKey,
|
|
62
|
+
'anthropic-version': '2023-06-01',
|
|
63
|
+
},
|
|
64
|
+
signal: ctrl.signal,
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
if (!res.ok) {
|
|
68
|
+
const errBody = await res.text().catch(() => '');
|
|
69
|
+
return NextResponse.json({
|
|
70
|
+
ok: false,
|
|
71
|
+
error: `Failed to list models: HTTP ${res.status} ${errBody.slice(0, 200)}`,
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const json = await res.json();
|
|
76
|
+
if (Array.isArray(json?.data)) {
|
|
77
|
+
models = json.data
|
|
78
|
+
.map((m: any) => m.id as string)
|
|
79
|
+
.filter(Boolean)
|
|
80
|
+
.sort((a: string, b: string) => a.localeCompare(b));
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return NextResponse.json({ ok: true, models });
|
|
85
|
+
} catch (e: unknown) {
|
|
86
|
+
if (e instanceof Error && e.name === 'AbortError') {
|
|
87
|
+
return NextResponse.json({ ok: false, error: 'Request timed out' });
|
|
88
|
+
}
|
|
89
|
+
return NextResponse.json({ ok: false, error: e instanceof Error ? e.message : 'Network error' });
|
|
90
|
+
} finally {
|
|
91
|
+
clearTimeout(timer);
|
|
92
|
+
}
|
|
93
|
+
} catch (err) {
|
|
94
|
+
return NextResponse.json({ ok: false, error: String(err) }, { status: 500 });
|
|
95
|
+
}
|
|
96
|
+
}
|
|
@@ -58,45 +58,45 @@ async function testOpenAI(apiKey: string, model: string, baseUrl: string): Promi
|
|
|
58
58
|
'Content-Type': 'application/json',
|
|
59
59
|
'Authorization': `Bearer ${apiKey}`,
|
|
60
60
|
},
|
|
61
|
-
body: JSON.stringify({ model,
|
|
61
|
+
body: JSON.stringify({ model, messages: [{ role: 'user', content: 'hi' }] }),
|
|
62
62
|
signal: ctrl.signal,
|
|
63
63
|
});
|
|
64
64
|
const latency = Date.now() - start;
|
|
65
|
-
if (res.ok) {
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
method: 'POST',
|
|
70
|
-
headers: {
|
|
71
|
-
'Content-Type': 'application/json',
|
|
72
|
-
'Authorization': `Bearer ${apiKey}`,
|
|
73
|
-
},
|
|
74
|
-
body: JSON.stringify({
|
|
75
|
-
model,
|
|
76
|
-
max_tokens: 1,
|
|
77
|
-
messages: [
|
|
78
|
-
{ role: 'system', content: 'You are a helpful assistant.' },
|
|
79
|
-
{ role: 'user', content: 'hi' },
|
|
80
|
-
],
|
|
81
|
-
tools: [{
|
|
82
|
-
type: 'function',
|
|
83
|
-
function: {
|
|
84
|
-
name: 'noop',
|
|
85
|
-
description: 'No-op function used for compatibility checks.',
|
|
86
|
-
parameters: {
|
|
87
|
-
type: 'object',
|
|
88
|
-
properties: {},
|
|
89
|
-
additionalProperties: false,
|
|
90
|
-
},
|
|
91
|
-
},
|
|
92
|
-
}],
|
|
93
|
-
tool_choice: 'none',
|
|
94
|
-
}),
|
|
95
|
-
signal: ctrl.signal,
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
if (toolRes.ok) return { ok: true, latency };
|
|
65
|
+
if (!res.ok) {
|
|
66
|
+
const body = await res.text();
|
|
67
|
+
return { ok: false, ...classifyError(res.status, body) };
|
|
68
|
+
}
|
|
99
69
|
|
|
70
|
+
// Tool compatibility test — `/api/ask` always sends tool definitions.
|
|
71
|
+
const toolRes = await fetch(url, {
|
|
72
|
+
method: 'POST',
|
|
73
|
+
headers: {
|
|
74
|
+
'Content-Type': 'application/json',
|
|
75
|
+
'Authorization': `Bearer ${apiKey}`,
|
|
76
|
+
},
|
|
77
|
+
body: JSON.stringify({
|
|
78
|
+
model,
|
|
79
|
+
messages: [
|
|
80
|
+
{ role: 'system', content: 'You are a helpful assistant.' },
|
|
81
|
+
{ role: 'user', content: 'hi' },
|
|
82
|
+
],
|
|
83
|
+
tools: [{
|
|
84
|
+
type: 'function',
|
|
85
|
+
function: {
|
|
86
|
+
name: 'noop',
|
|
87
|
+
description: 'No-op function used for compatibility checks.',
|
|
88
|
+
parameters: {
|
|
89
|
+
type: 'object',
|
|
90
|
+
properties: {},
|
|
91
|
+
additionalProperties: false,
|
|
92
|
+
},
|
|
93
|
+
},
|
|
94
|
+
}],
|
|
95
|
+
tool_choice: 'none',
|
|
96
|
+
}),
|
|
97
|
+
signal: ctrl.signal,
|
|
98
|
+
});
|
|
99
|
+
if (!toolRes.ok) {
|
|
100
100
|
const toolBody = await toolRes.text();
|
|
101
101
|
const toolErr = classifyError(toolRes.status, toolBody);
|
|
102
102
|
return {
|
|
@@ -105,8 +105,8 @@ async function testOpenAI(apiKey: string, model: string, baseUrl: string): Promi
|
|
|
105
105
|
error: `Model endpoint passes basic test but is incompatible with agent tool calls: ${toolErr.error}`,
|
|
106
106
|
};
|
|
107
107
|
}
|
|
108
|
-
|
|
109
|
-
return { ok:
|
|
108
|
+
|
|
109
|
+
return { ok: true, latency };
|
|
110
110
|
} catch (e: unknown) {
|
|
111
111
|
if (e instanceof Error && e.name === 'AbortError') return { ok: false, code: 'network_error', error: 'Request timed out' };
|
|
112
112
|
return { ok: false, code: 'network_error', error: e instanceof Error ? e.message : 'Network error' };
|
|
@@ -5,6 +5,7 @@ import { useRouter } from 'next/navigation';
|
|
|
5
5
|
import {
|
|
6
6
|
Check, X, Loader2, Sparkles, AlertCircle, Undo2,
|
|
7
7
|
ChevronDown, FilePlus, FileEdit, ExternalLink,
|
|
8
|
+
Maximize2, Minimize2, FileIcon,
|
|
8
9
|
} from 'lucide-react';
|
|
9
10
|
import { useLocale } from '@/lib/LocaleContext';
|
|
10
11
|
import type { useAiOrganize } from '@/hooks/useAiOrganize';
|
|
@@ -84,6 +85,7 @@ export default function OrganizeToast({
|
|
|
84
85
|
const { elapsed, displayHint } = useOrganizeTimer(isOrganizing, aiOrganize.stageHint);
|
|
85
86
|
|
|
86
87
|
const [expanded, setExpanded] = useState(false);
|
|
88
|
+
const [maximized, setMaximized] = useState(false);
|
|
87
89
|
const [undoing, setUndoing] = useState(false);
|
|
88
90
|
const dismissTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
89
91
|
const historyIdRef = useRef<string | null>(null);
|
|
@@ -197,7 +199,211 @@ export default function OrganizeToast({
|
|
|
197
199
|
|
|
198
200
|
if (!isActive) return null;
|
|
199
201
|
|
|
200
|
-
//
|
|
202
|
+
// ── Shared file-change row renderer ──
|
|
203
|
+
function renderChangeRow(c: typeof aiOrganize.changes[number], idx: number) {
|
|
204
|
+
const wasUndone = c.undone;
|
|
205
|
+
const undoable = aiOrganize.canUndo(c.path);
|
|
206
|
+
const fileName = c.path.split('/').pop() ?? c.path;
|
|
207
|
+
return (
|
|
208
|
+
<div
|
|
209
|
+
key={`${c.path}-${idx}`}
|
|
210
|
+
className={`flex items-center gap-2 px-3 py-2 rounded-md text-sm transition-colors ${wasUndone ? 'bg-muted/30 opacity-50' : 'bg-muted/50'}`}
|
|
211
|
+
>
|
|
212
|
+
{wasUndone ? (
|
|
213
|
+
<Undo2 size={14} className="text-muted-foreground shrink-0" />
|
|
214
|
+
) : c.action === 'create' ? (
|
|
215
|
+
<FilePlus size={14} className="text-success shrink-0" />
|
|
216
|
+
) : (
|
|
217
|
+
<FileEdit size={14} className="text-[var(--amber)] shrink-0" />
|
|
218
|
+
)}
|
|
219
|
+
<span className={`truncate flex-1 ${wasUndone ? 'line-through text-muted-foreground' : 'text-foreground'}`}>
|
|
220
|
+
{fileName}
|
|
221
|
+
</span>
|
|
222
|
+
{wasUndone ? (
|
|
223
|
+
<span className="text-xs text-muted-foreground shrink-0">{fi.organizeUndone as string}</span>
|
|
224
|
+
) : (
|
|
225
|
+
<span className={`text-xs shrink-0 ${c.ok ? 'text-muted-foreground' : 'text-error'}`}>
|
|
226
|
+
{!c.ok ? fi.organizeFailed as string
|
|
227
|
+
: c.action === 'create' ? fi.organizeCreated as string
|
|
228
|
+
: fi.organizeUpdated as string}
|
|
229
|
+
</span>
|
|
230
|
+
)}
|
|
231
|
+
{undoable && (
|
|
232
|
+
<button
|
|
233
|
+
type="button"
|
|
234
|
+
onClick={() => handleUndoOne(c.path)}
|
|
235
|
+
disabled={undoing}
|
|
236
|
+
className="text-2xs text-muted-foreground/60 hover:text-foreground transition-colors shrink-0 px-1 disabled:opacity-40"
|
|
237
|
+
title={fi.organizeUndoOne as string}
|
|
238
|
+
>
|
|
239
|
+
<Undo2 size={12} />
|
|
240
|
+
</button>
|
|
241
|
+
)}
|
|
242
|
+
{c.ok && !c.undone && (
|
|
243
|
+
<button
|
|
244
|
+
type="button"
|
|
245
|
+
onClick={() => handleViewFile(c.path)}
|
|
246
|
+
className="text-2xs text-muted-foreground/60 hover:text-[var(--amber)] transition-colors shrink-0 px-1"
|
|
247
|
+
title={fi.organizeViewFile as string}
|
|
248
|
+
>
|
|
249
|
+
<ExternalLink size={12} />
|
|
250
|
+
</button>
|
|
251
|
+
)}
|
|
252
|
+
</div>
|
|
253
|
+
);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// ── Shared footer actions ──
|
|
257
|
+
function renderActions() {
|
|
258
|
+
return (
|
|
259
|
+
<div className="flex items-center justify-end gap-3 px-4 py-3 border-t border-border">
|
|
260
|
+
{isDone && aiOrganize.hasAnyUndoable && (
|
|
261
|
+
<button
|
|
262
|
+
onClick={handleUndoAll}
|
|
263
|
+
disabled={undoing}
|
|
264
|
+
className="flex items-center gap-1.5 text-xs text-muted-foreground hover:text-foreground transition-colors px-2 py-1.5 disabled:opacity-50"
|
|
265
|
+
>
|
|
266
|
+
{undoing ? <Loader2 size={12} className="animate-spin" /> : <Undo2 size={12} />}
|
|
267
|
+
{fi.organizeUndoAll as string}
|
|
268
|
+
</button>
|
|
269
|
+
)}
|
|
270
|
+
<button
|
|
271
|
+
onClick={handleDismiss}
|
|
272
|
+
className="flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-xs font-medium bg-[var(--amber)] text-[var(--amber-foreground)] hover:opacity-90 transition-all"
|
|
273
|
+
>
|
|
274
|
+
<Check size={12} />
|
|
275
|
+
{fi.organizeDone as string}
|
|
276
|
+
</button>
|
|
277
|
+
</div>
|
|
278
|
+
);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
282
|
+
// Maximized Modal — full detail view with source files, summary, changes
|
|
283
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
284
|
+
if (maximized) {
|
|
285
|
+
return (
|
|
286
|
+
<div
|
|
287
|
+
className="fixed inset-0 z-50 overlay-backdrop flex items-center justify-center p-4"
|
|
288
|
+
onClick={(e) => { if (e.target === e.currentTarget) setMaximized(false); }}
|
|
289
|
+
>
|
|
290
|
+
<div
|
|
291
|
+
className="w-full max-w-xl max-h-[80vh] flex flex-col bg-card rounded-xl shadow-xl border border-border animate-in fade-in-0 zoom-in-95 duration-200"
|
|
292
|
+
onClick={handleUserAction}
|
|
293
|
+
role="dialog"
|
|
294
|
+
aria-modal="true"
|
|
295
|
+
aria-label={fi.organizeDetailTitle as string}
|
|
296
|
+
>
|
|
297
|
+
{/* Header */}
|
|
298
|
+
<div className="flex items-center justify-between px-5 pt-5 pb-3 shrink-0">
|
|
299
|
+
<div className="flex items-center gap-2">
|
|
300
|
+
{isOrganizing ? (
|
|
301
|
+
<div className="relative shrink-0">
|
|
302
|
+
<Sparkles size={16} className="text-[var(--amber)]" />
|
|
303
|
+
<Loader2 size={10} className="absolute -bottom-0.5 -right-0.5 text-[var(--amber)] animate-spin" />
|
|
304
|
+
</div>
|
|
305
|
+
) : isDone ? (
|
|
306
|
+
<Check size={16} className="text-success" />
|
|
307
|
+
) : (
|
|
308
|
+
<AlertCircle size={16} className="text-error" />
|
|
309
|
+
)}
|
|
310
|
+
<h2 className="text-base font-semibold text-foreground">
|
|
311
|
+
{fi.organizeDetailTitle as string}
|
|
312
|
+
</h2>
|
|
313
|
+
{isOrganizing && (
|
|
314
|
+
<span className="text-xs text-muted-foreground/60 tabular-nums">
|
|
315
|
+
{(fi.organizeElapsed as (s: number) => string)(elapsed)}
|
|
316
|
+
</span>
|
|
317
|
+
)}
|
|
318
|
+
</div>
|
|
319
|
+
<div className="flex items-center gap-1">
|
|
320
|
+
<button
|
|
321
|
+
type="button"
|
|
322
|
+
onClick={() => setMaximized(false)}
|
|
323
|
+
className="p-1 rounded hover:bg-muted text-muted-foreground hover:text-foreground transition-colors"
|
|
324
|
+
title={fi.organizeMinimizeModal as string}
|
|
325
|
+
>
|
|
326
|
+
<Minimize2 size={14} />
|
|
327
|
+
</button>
|
|
328
|
+
<button
|
|
329
|
+
type="button"
|
|
330
|
+
onClick={handleDismiss}
|
|
331
|
+
className="p-1 rounded hover:bg-muted text-muted-foreground hover:text-foreground transition-colors"
|
|
332
|
+
>
|
|
333
|
+
<X size={14} />
|
|
334
|
+
</button>
|
|
335
|
+
</div>
|
|
336
|
+
</div>
|
|
337
|
+
|
|
338
|
+
{/* Scrollable body */}
|
|
339
|
+
<div className="flex-1 min-h-0 overflow-y-auto px-5 pb-2 space-y-4">
|
|
340
|
+
{/* Live progress during organizing */}
|
|
341
|
+
{isOrganizing && (
|
|
342
|
+
<div className="flex items-center gap-2 px-3 py-2.5 rounded-lg bg-[var(--amber-subtle)] border border-[var(--amber-dim)]">
|
|
343
|
+
<Loader2 size={14} className="text-[var(--amber)] animate-spin shrink-0" />
|
|
344
|
+
<span className="text-xs text-foreground">{stageText(t, displayHint)}</span>
|
|
345
|
+
</div>
|
|
346
|
+
)}
|
|
347
|
+
|
|
348
|
+
{/* Source files */}
|
|
349
|
+
{aiOrganize.sourceFileNames.length > 0 && (
|
|
350
|
+
<div>
|
|
351
|
+
<h3 className="text-xs font-medium text-muted-foreground mb-1.5">
|
|
352
|
+
{fi.organizeSourceFiles as string}
|
|
353
|
+
</h3>
|
|
354
|
+
<div className="flex flex-wrap gap-1.5">
|
|
355
|
+
{aiOrganize.sourceFileNames.map((name, i) => (
|
|
356
|
+
<span key={i} className="inline-flex items-center gap-1 px-2 py-1 rounded-md bg-muted/60 text-xs text-foreground">
|
|
357
|
+
<FileIcon size={11} className="text-muted-foreground shrink-0" />
|
|
358
|
+
{name}
|
|
359
|
+
</span>
|
|
360
|
+
))}
|
|
361
|
+
</div>
|
|
362
|
+
</div>
|
|
363
|
+
)}
|
|
364
|
+
|
|
365
|
+
{/* AI Summary */}
|
|
366
|
+
{(aiOrganize.summary || isOrganizing) && (
|
|
367
|
+
<div>
|
|
368
|
+
<h3 className="text-xs font-medium text-muted-foreground mb-1.5">
|
|
369
|
+
{fi.organizeSummaryLabel as string}
|
|
370
|
+
</h3>
|
|
371
|
+
<div className="px-3 py-2.5 rounded-lg bg-muted/30 border border-border text-sm text-foreground leading-relaxed whitespace-pre-wrap">
|
|
372
|
+
{aiOrganize.summary || (fi.organizeNoSummary as string)}
|
|
373
|
+
</div>
|
|
374
|
+
</div>
|
|
375
|
+
)}
|
|
376
|
+
|
|
377
|
+
{/* File changes */}
|
|
378
|
+
{aiOrganize.changes.length > 0 && (
|
|
379
|
+
<div>
|
|
380
|
+
<h3 className="text-xs font-medium text-muted-foreground mb-1.5">
|
|
381
|
+
{(fi.organizeChangesLabel as (n: number) => string)(aiOrganize.changes.length)}
|
|
382
|
+
</h3>
|
|
383
|
+
<div className="space-y-0.5">
|
|
384
|
+
{aiOrganize.changes.map((c, idx) => renderChangeRow(c, idx))}
|
|
385
|
+
</div>
|
|
386
|
+
</div>
|
|
387
|
+
)}
|
|
388
|
+
|
|
389
|
+
{/* Error detail */}
|
|
390
|
+
{isError && (
|
|
391
|
+
<div className="px-3 py-2.5 rounded-lg bg-error/5 border border-error/20 text-xs text-error">
|
|
392
|
+
{aiOrganize.error}
|
|
393
|
+
</div>
|
|
394
|
+
)}
|
|
395
|
+
</div>
|
|
396
|
+
|
|
397
|
+
{/* Footer actions */}
|
|
398
|
+
{(isDone || isError) && renderActions()}
|
|
399
|
+
</div>
|
|
400
|
+
</div>
|
|
401
|
+
);
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
405
|
+
// Expanded panel (file list with per-file undo) — bottom toast size
|
|
406
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
201
407
|
if (expanded && (isDone || isError)) {
|
|
202
408
|
return (
|
|
203
409
|
<div
|
|
@@ -212,71 +418,29 @@ export default function OrganizeToast({
|
|
|
212
418
|
{isDone ? fi.organizeReviewTitle as string : fi.organizeErrorTitle as string}
|
|
213
419
|
</span>
|
|
214
420
|
</div>
|
|
215
|
-
<
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
421
|
+
<div className="flex items-center gap-1">
|
|
422
|
+
<button
|
|
423
|
+
type="button"
|
|
424
|
+
onClick={() => { setExpanded(false); setMaximized(true); }}
|
|
425
|
+
className="p-1 rounded hover:bg-muted text-muted-foreground hover:text-foreground transition-colors"
|
|
426
|
+
title={fi.organizeDetailTitle as string}
|
|
427
|
+
>
|
|
428
|
+
<Maximize2 size={13} />
|
|
429
|
+
</button>
|
|
430
|
+
<button
|
|
431
|
+
type="button"
|
|
432
|
+
onClick={() => setExpanded(false)}
|
|
433
|
+
className="p-1 rounded hover:bg-muted text-muted-foreground hover:text-foreground transition-colors"
|
|
434
|
+
>
|
|
435
|
+
<ChevronDown size={14} />
|
|
436
|
+
</button>
|
|
437
|
+
</div>
|
|
222
438
|
</div>
|
|
223
439
|
|
|
224
440
|
{/* File list */}
|
|
225
441
|
{isDone && (
|
|
226
442
|
<div className="max-h-[240px] overflow-y-auto p-2 space-y-0.5">
|
|
227
|
-
{aiOrganize.changes.map((c, idx) =>
|
|
228
|
-
const wasUndone = c.undone;
|
|
229
|
-
const undoable = aiOrganize.canUndo(c.path);
|
|
230
|
-
const fileName = c.path.split('/').pop() ?? c.path;
|
|
231
|
-
|
|
232
|
-
return (
|
|
233
|
-
<div
|
|
234
|
-
key={`${c.path}-${idx}`}
|
|
235
|
-
className={`flex items-center gap-2 px-3 py-2 rounded-md text-sm transition-colors ${wasUndone ? 'bg-muted/30 opacity-50' : 'bg-muted/50'}`}
|
|
236
|
-
>
|
|
237
|
-
{wasUndone ? (
|
|
238
|
-
<Undo2 size={14} className="text-muted-foreground shrink-0" />
|
|
239
|
-
) : c.action === 'create' ? (
|
|
240
|
-
<FilePlus size={14} className="text-success shrink-0" />
|
|
241
|
-
) : (
|
|
242
|
-
<FileEdit size={14} className="text-[var(--amber)] shrink-0" />
|
|
243
|
-
)}
|
|
244
|
-
<span className={`truncate flex-1 ${wasUndone ? 'line-through text-muted-foreground' : 'text-foreground'}`}>
|
|
245
|
-
{fileName}
|
|
246
|
-
</span>
|
|
247
|
-
{wasUndone ? (
|
|
248
|
-
<span className="text-xs text-muted-foreground shrink-0">{fi.organizeUndone as string}</span>
|
|
249
|
-
) : (
|
|
250
|
-
<span className={`text-xs shrink-0 ${c.ok ? 'text-muted-foreground' : 'text-error'}`}>
|
|
251
|
-
{!c.ok ? fi.organizeFailed as string
|
|
252
|
-
: c.action === 'create' ? fi.organizeCreated as string
|
|
253
|
-
: fi.organizeUpdated as string}
|
|
254
|
-
</span>
|
|
255
|
-
)}
|
|
256
|
-
{undoable && (
|
|
257
|
-
<button
|
|
258
|
-
type="button"
|
|
259
|
-
onClick={() => handleUndoOne(c.path)}
|
|
260
|
-
disabled={undoing}
|
|
261
|
-
className="text-2xs text-muted-foreground/60 hover:text-foreground transition-colors shrink-0 px-1 disabled:opacity-40"
|
|
262
|
-
title={fi.organizeUndoOne as string}
|
|
263
|
-
>
|
|
264
|
-
<Undo2 size={12} />
|
|
265
|
-
</button>
|
|
266
|
-
)}
|
|
267
|
-
{c.ok && !c.undone && (
|
|
268
|
-
<button
|
|
269
|
-
type="button"
|
|
270
|
-
onClick={() => handleViewFile(c.path)}
|
|
271
|
-
className="text-2xs text-muted-foreground/60 hover:text-[var(--amber)] transition-colors shrink-0 px-1"
|
|
272
|
-
title={fi.organizeViewFile as string}
|
|
273
|
-
>
|
|
274
|
-
<ExternalLink size={12} />
|
|
275
|
-
</button>
|
|
276
|
-
)}
|
|
277
|
-
</div>
|
|
278
|
-
);
|
|
279
|
-
})}
|
|
443
|
+
{aiOrganize.changes.map((c, idx) => renderChangeRow(c, idx))}
|
|
280
444
|
</div>
|
|
281
445
|
)}
|
|
282
446
|
|
|
@@ -286,31 +450,14 @@ export default function OrganizeToast({
|
|
|
286
450
|
</div>
|
|
287
451
|
)}
|
|
288
452
|
|
|
289
|
-
{
|
|
290
|
-
<div className="flex items-center justify-end gap-3 px-4 py-3 border-t border-border">
|
|
291
|
-
{isDone && aiOrganize.hasAnyUndoable && (
|
|
292
|
-
<button
|
|
293
|
-
onClick={handleUndoAll}
|
|
294
|
-
disabled={undoing}
|
|
295
|
-
className="flex items-center gap-1.5 text-xs text-muted-foreground hover:text-foreground transition-colors px-2 py-1.5 disabled:opacity-50"
|
|
296
|
-
>
|
|
297
|
-
{undoing ? <Loader2 size={12} className="animate-spin" /> : <Undo2 size={12} />}
|
|
298
|
-
{fi.organizeUndoAll as string}
|
|
299
|
-
</button>
|
|
300
|
-
)}
|
|
301
|
-
<button
|
|
302
|
-
onClick={handleDismiss}
|
|
303
|
-
className="flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-xs font-medium bg-[var(--amber)] text-[var(--amber-foreground)] hover:opacity-90 transition-all"
|
|
304
|
-
>
|
|
305
|
-
<Check size={12} />
|
|
306
|
-
{fi.organizeDone as string}
|
|
307
|
-
</button>
|
|
308
|
-
</div>
|
|
453
|
+
{renderActions()}
|
|
309
454
|
</div>
|
|
310
455
|
);
|
|
311
456
|
}
|
|
312
457
|
|
|
313
|
-
//
|
|
458
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
459
|
+
// Compact toast bar
|
|
460
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
314
461
|
return (
|
|
315
462
|
<div
|
|
316
463
|
className="fixed bottom-4 left-1/2 -translate-x-1/2 z-50 flex items-center gap-3 bg-card border border-border rounded-xl shadow-lg px-4 py-3 max-w-md animate-in fade-in-0 slide-in-from-bottom-2 duration-200"
|
|
@@ -372,6 +519,14 @@ export default function OrganizeToast({
|
|
|
372
519
|
<span className="text-xs text-muted-foreground/60 tabular-nums shrink-0">
|
|
373
520
|
{(fi.organizeElapsed as (s: number) => string)(elapsed)}
|
|
374
521
|
</span>
|
|
522
|
+
<button
|
|
523
|
+
type="button"
|
|
524
|
+
onClick={() => { setMaximized(true); handleUserAction(); }}
|
|
525
|
+
className="text-muted-foreground/50 hover:text-muted-foreground transition-colors shrink-0"
|
|
526
|
+
title={fi.organizeDetailTitle as string}
|
|
527
|
+
>
|
|
528
|
+
<Maximize2 size={13} />
|
|
529
|
+
</button>
|
|
375
530
|
<button
|
|
376
531
|
type="button"
|
|
377
532
|
onClick={handleDismiss}
|
package/app/components/Panel.tsx
CHANGED
|
@@ -107,11 +107,12 @@ export default function Panel({
|
|
|
107
107
|
};
|
|
108
108
|
}, [newPopover]);
|
|
109
109
|
|
|
110
|
-
// Double-click hint: show only until user has used it once
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
110
|
+
// Double-click hint: show only until user has used it once.
|
|
111
|
+
// Initialize false to match SSR; hydrate from localStorage in useEffect.
|
|
112
|
+
const [dblHintSeen, setDblHintSeen] = useState(false);
|
|
113
|
+
useEffect(() => {
|
|
114
|
+
try { if (localStorage.getItem('mindos-tree-dblclick-hint') === '1') setDblHintSeen(true); } catch { /* ignore */ }
|
|
115
|
+
}, []);
|
|
115
116
|
const markDblHintSeen = useCallback(() => {
|
|
116
117
|
if (!dblHintSeen) {
|
|
117
118
|
setDblHintSeen(true);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import { useState, useRef, useCallback, useEffect } from 'react';
|
|
4
|
-
import { AlertCircle, Loader2 } from 'lucide-react';
|
|
4
|
+
import { AlertCircle, ChevronDown, Loader2 } from 'lucide-react';
|
|
5
5
|
import type { AiSettings, AgentSettings, ProviderConfig, SettingsData, AiTabProps } from './types';
|
|
6
6
|
import { Field, Select, Input, EnvBadge, ApiKeyInput, Toggle, SectionLabel } from './Primitives';
|
|
7
7
|
import { useLocale } from '@/lib/LocaleContext';
|
|
@@ -73,11 +73,10 @@ export function AiTab({ data, updateAi, updateAgent, t }: AiTabProps) {
|
|
|
73
73
|
|
|
74
74
|
if (json.ok) {
|
|
75
75
|
setTestResult(prev => ({ ...prev, [providerName]: { state: 'ok', latency: json.latency } }));
|
|
76
|
-
// Auto-clear after 5s
|
|
77
76
|
if (okTimerRef.current) clearTimeout(okTimerRef.current);
|
|
78
77
|
okTimerRef.current = setTimeout(() => {
|
|
79
78
|
setTestResult(prev => ({ ...prev, [providerName]: { state: 'idle' } }));
|
|
80
|
-
},
|
|
79
|
+
}, 8000);
|
|
81
80
|
} else {
|
|
82
81
|
setTestResult(prev => ({
|
|
83
82
|
...prev,
|
|
@@ -139,7 +138,9 @@ export function AiTab({ data, updateAi, updateAgent, t }: AiTabProps) {
|
|
|
139
138
|
)}
|
|
140
139
|
</button>
|
|
141
140
|
{result.state === 'ok' && result.latency != null && (
|
|
142
|
-
<span className="text-xs text-success">
|
|
141
|
+
<span className="text-xs text-success">
|
|
142
|
+
{t.settings.ai.testKeyOk(result.latency)}
|
|
143
|
+
</span>
|
|
143
144
|
)}
|
|
144
145
|
{result.state === 'error' && (
|
|
145
146
|
<span className="text-xs text-error">✗ {errorMessage(t, result.code)}</span>
|
|
@@ -163,10 +164,14 @@ export function AiTab({ data, updateAi, updateAgent, t }: AiTabProps) {
|
|
|
163
164
|
{provider === 'anthropic' ? (
|
|
164
165
|
<>
|
|
165
166
|
<Field label={<>{t.settings.ai.model} <EnvBadge overridden={env.ANTHROPIC_MODEL} /></>}>
|
|
166
|
-
<
|
|
167
|
+
<ModelInput
|
|
167
168
|
value={anthropic.model}
|
|
168
|
-
onChange={
|
|
169
|
+
onChange={v => patchProvider('anthropic', { model: v })}
|
|
169
170
|
placeholder={envVal.ANTHROPIC_MODEL || 'claude-sonnet-4-6'}
|
|
171
|
+
provider="anthropic"
|
|
172
|
+
apiKey={anthropic.apiKey}
|
|
173
|
+
envKey={env.ANTHROPIC_API_KEY}
|
|
174
|
+
t={t}
|
|
170
175
|
/>
|
|
171
176
|
</Field>
|
|
172
177
|
<Field
|
|
@@ -183,10 +188,15 @@ export function AiTab({ data, updateAi, updateAgent, t }: AiTabProps) {
|
|
|
183
188
|
) : (
|
|
184
189
|
<>
|
|
185
190
|
<Field label={<>{t.settings.ai.model} <EnvBadge overridden={env.OPENAI_MODEL} /></>}>
|
|
186
|
-
<
|
|
191
|
+
<ModelInput
|
|
187
192
|
value={openai.model}
|
|
188
|
-
onChange={
|
|
193
|
+
onChange={v => patchProvider('openai', { model: v })}
|
|
189
194
|
placeholder={envVal.OPENAI_MODEL || 'gpt-5.4'}
|
|
195
|
+
provider="openai"
|
|
196
|
+
apiKey={openai.apiKey}
|
|
197
|
+
envKey={env.OPENAI_API_KEY}
|
|
198
|
+
baseUrl={openai.baseUrl}
|
|
199
|
+
t={t}
|
|
190
200
|
/>
|
|
191
201
|
</Field>
|
|
192
202
|
<Field
|
|
@@ -309,6 +319,111 @@ export function AiTab({ data, updateAi, updateAgent, t }: AiTabProps) {
|
|
|
309
319
|
);
|
|
310
320
|
}
|
|
311
321
|
|
|
322
|
+
/* ── Model Input with "List models" picker ── */
|
|
323
|
+
|
|
324
|
+
function ModelInput({
|
|
325
|
+
value, onChange, placeholder, provider, apiKey, envKey, baseUrl, t,
|
|
326
|
+
}: {
|
|
327
|
+
value: string;
|
|
328
|
+
onChange: (v: string) => void;
|
|
329
|
+
placeholder: string;
|
|
330
|
+
provider: 'anthropic' | 'openai';
|
|
331
|
+
apiKey: string;
|
|
332
|
+
envKey?: boolean;
|
|
333
|
+
baseUrl?: string;
|
|
334
|
+
t: AiTabProps['t'];
|
|
335
|
+
}) {
|
|
336
|
+
const [models, setModels] = useState<string[] | null>(null);
|
|
337
|
+
const [loading, setLoading] = useState(false);
|
|
338
|
+
const [open, setOpen] = useState(false);
|
|
339
|
+
const [error, setError] = useState('');
|
|
340
|
+
const containerRef = useRef<HTMLDivElement>(null);
|
|
341
|
+
|
|
342
|
+
const hasKey = !!apiKey || !!envKey;
|
|
343
|
+
|
|
344
|
+
const fetchModels = useCallback(async () => {
|
|
345
|
+
if (loading) return;
|
|
346
|
+
setLoading(true);
|
|
347
|
+
setError('');
|
|
348
|
+
try {
|
|
349
|
+
const body: Record<string, string> = { provider };
|
|
350
|
+
if (apiKey) body.apiKey = apiKey;
|
|
351
|
+
if (baseUrl) body.baseUrl = baseUrl;
|
|
352
|
+
|
|
353
|
+
const res = await fetch('/api/settings/list-models', {
|
|
354
|
+
method: 'POST',
|
|
355
|
+
headers: { 'Content-Type': 'application/json' },
|
|
356
|
+
body: JSON.stringify(body),
|
|
357
|
+
});
|
|
358
|
+
const json = await res.json();
|
|
359
|
+
if (json.ok && Array.isArray(json.models)) {
|
|
360
|
+
setModels(json.models);
|
|
361
|
+
setOpen(true);
|
|
362
|
+
} else {
|
|
363
|
+
setError(json.error || 'Failed to fetch models');
|
|
364
|
+
}
|
|
365
|
+
} catch {
|
|
366
|
+
setError('Network error');
|
|
367
|
+
} finally {
|
|
368
|
+
setLoading(false);
|
|
369
|
+
}
|
|
370
|
+
}, [provider, apiKey, baseUrl, loading]);
|
|
371
|
+
|
|
372
|
+
useEffect(() => {
|
|
373
|
+
if (!open) return;
|
|
374
|
+
function handleClickOutside(e: MouseEvent) {
|
|
375
|
+
if (containerRef.current && !containerRef.current.contains(e.target as Node)) {
|
|
376
|
+
setOpen(false);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
document.addEventListener('mousedown', handleClickOutside);
|
|
380
|
+
return () => document.removeEventListener('mousedown', handleClickOutside);
|
|
381
|
+
}, [open]);
|
|
382
|
+
|
|
383
|
+
return (
|
|
384
|
+
<div ref={containerRef} className="relative">
|
|
385
|
+
<div className="flex gap-1.5">
|
|
386
|
+
<Input
|
|
387
|
+
value={value}
|
|
388
|
+
onChange={e => onChange(e.target.value)}
|
|
389
|
+
placeholder={placeholder}
|
|
390
|
+
className="flex-1"
|
|
391
|
+
/>
|
|
392
|
+
<button
|
|
393
|
+
type="button"
|
|
394
|
+
disabled={!hasKey || loading}
|
|
395
|
+
onClick={fetchModels}
|
|
396
|
+
title={t.settings.ai.listModels}
|
|
397
|
+
className="inline-flex items-center gap-1 px-2 py-1 text-xs rounded-lg border border-border text-muted-foreground hover:text-foreground hover:border-foreground/20 transition-colors disabled:opacity-40 disabled:cursor-not-allowed shrink-0"
|
|
398
|
+
>
|
|
399
|
+
{loading ? <Loader2 size={12} className="animate-spin" /> : <ChevronDown size={12} />}
|
|
400
|
+
{t.settings.ai.listModels}
|
|
401
|
+
</button>
|
|
402
|
+
</div>
|
|
403
|
+
{error && <p className="text-xs text-error mt-1">{error}</p>}
|
|
404
|
+
{open && models && models.length > 0 && (
|
|
405
|
+
<div className="absolute z-50 mt-1 w-full max-h-48 overflow-y-auto rounded-lg border border-border bg-popover shadow-lg">
|
|
406
|
+
{models.map(m => (
|
|
407
|
+
<button
|
|
408
|
+
key={m}
|
|
409
|
+
type="button"
|
|
410
|
+
className={`w-full text-left px-3 py-1.5 text-xs hover:bg-accent transition-colors ${m === value ? 'bg-accent/60 font-medium' : ''}`}
|
|
411
|
+
onClick={() => { onChange(m); setOpen(false); }}
|
|
412
|
+
>
|
|
413
|
+
{m}
|
|
414
|
+
</button>
|
|
415
|
+
))}
|
|
416
|
+
</div>
|
|
417
|
+
)}
|
|
418
|
+
{open && models && models.length === 0 && (
|
|
419
|
+
<div className="absolute z-50 mt-1 w-full rounded-lg border border-border bg-popover shadow-lg px-3 py-2 text-xs text-muted-foreground">
|
|
420
|
+
{t.settings.ai.noModelsFound}
|
|
421
|
+
</div>
|
|
422
|
+
)}
|
|
423
|
+
</div>
|
|
424
|
+
);
|
|
425
|
+
}
|
|
426
|
+
|
|
312
427
|
/* ── Ask AI Display Mode (localStorage-based, no server roundtrip) ── */
|
|
313
428
|
|
|
314
429
|
function AskDisplayMode() {
|
package/app/lib/agent/model.ts
CHANGED
|
@@ -57,6 +57,8 @@ export function getModelConfig(): {
|
|
|
57
57
|
// For custom proxy endpoints, set conservative compat flags.
|
|
58
58
|
// Most proxies (Azure, Bedrock relays, corporate gateways) only support
|
|
59
59
|
// a subset of OpenAI's features. These defaults prevent silent failures.
|
|
60
|
+
// NOTE: maxTokensField is NOT overridden — pi-ai auto-detects the correct
|
|
61
|
+
// field based on URL (defaults to max_completion_tokens for modern APIs).
|
|
60
62
|
if (hasCustomBase) {
|
|
61
63
|
model = {
|
|
62
64
|
...model,
|
|
@@ -68,7 +70,6 @@ export function getModelConfig(): {
|
|
|
68
70
|
supportsReasoningEffort: false,
|
|
69
71
|
supportsUsageInStreaming: false,
|
|
70
72
|
supportsStrictMode: false,
|
|
71
|
-
maxTokensField: 'max_tokens' as const,
|
|
72
73
|
},
|
|
73
74
|
};
|
|
74
75
|
if (customApiVariant) {
|
package/app/lib/agent/prompt.ts
CHANGED
|
@@ -35,3 +35,26 @@ Persona: Methodical, strictly objective, execution-oriented. Zero fluff. Never u
|
|
|
35
35
|
- Reply in the user's language.
|
|
36
36
|
- Use clean Markdown (tables, lists, bold).
|
|
37
37
|
- End with concrete next actions if the task is incomplete.`;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Lean system prompt for "organize uploaded files" mode.
|
|
41
|
+
*
|
|
42
|
+
* Design goal: ~200 tokens (vs ~600 for general). Strips everything the
|
|
43
|
+
* organize task doesn't need: anti-hallucination (no KB Q&A), cite sources,
|
|
44
|
+
* smart recovery, skills/MCP discovery, output formatting.
|
|
45
|
+
*
|
|
46
|
+
* The full SKILL.md is NOT loaded in organize mode — only the bootstrap
|
|
47
|
+
* README.md (for KB structure awareness) is injected by route.ts.
|
|
48
|
+
*/
|
|
49
|
+
export const ORGANIZE_SYSTEM_PROMPT = `You are MindOS Agent — an expert at organizing information into a local Markdown knowledge base.
|
|
50
|
+
|
|
51
|
+
Your ONLY job: read the user's uploaded files, extract key information, and save well-structured Markdown notes into the knowledge base using file tools.
|
|
52
|
+
|
|
53
|
+
Rules:
|
|
54
|
+
1. Read uploaded file content from the "USER-UPLOADED FILES" section below — do NOT call read tools on them.
|
|
55
|
+
2. Use \`list_files\` to understand the existing KB structure before deciding where to place notes.
|
|
56
|
+
3. Create new files or update existing ones. Prefer \`create_file\` for new content, \`update_section\` / \`append_to_file\` for additions to existing files.
|
|
57
|
+
4. Match the language of the source files when writing notes.
|
|
58
|
+
5. Batch parallel tool calls in a single turn for efficiency.
|
|
59
|
+
6. Do NOT write to the KB root directory — place files under the most fitting subdirectory.
|
|
60
|
+
7. After writing, provide a brief summary of what you created/updated.`;
|
package/app/lib/agent/tools.ts
CHANGED
|
@@ -173,6 +173,18 @@ export const WRITE_TOOLS = new Set([
|
|
|
173
173
|
'update_section', 'edit_lines', 'delete_file', 'rename_file', 'move_file', 'append_csv',
|
|
174
174
|
]);
|
|
175
175
|
|
|
176
|
+
/** Tool names sufficient for the "organize uploaded files" task. */
|
|
177
|
+
const ORGANIZE_TOOL_NAMES = new Set([
|
|
178
|
+
'list_files', 'read_file', 'search',
|
|
179
|
+
'create_file', 'batch_create_files', 'write_file',
|
|
180
|
+
'append_to_file', 'insert_after_heading', 'update_section',
|
|
181
|
+
]);
|
|
182
|
+
|
|
183
|
+
/** Lean tool set for organize mode — skips MCP discovery, history, backlinks, etc. */
|
|
184
|
+
export function getOrganizeTools(): AgentTool<any>[] {
|
|
185
|
+
return knowledgeBaseTools.filter(t => ORGANIZE_TOOL_NAMES.has(t.name));
|
|
186
|
+
}
|
|
187
|
+
|
|
176
188
|
export async function getRequestScopedTools(): Promise<AgentTool<any>[]> {
|
|
177
189
|
try {
|
|
178
190
|
const result = await listMcporterServers();
|
package/app/lib/i18n-en.ts
CHANGED
|
@@ -144,7 +144,7 @@ export const en = {
|
|
|
144
144
|
searching: 'Searching knowledge base...',
|
|
145
145
|
generating: 'Generating response...',
|
|
146
146
|
stopped: 'Generation stopped.',
|
|
147
|
-
errorNoResponse: '
|
|
147
|
+
errorNoResponse: 'AI returned no content. Possible causes: model does not support streaming, proxy compatibility issue, or request exceeds context limit.',
|
|
148
148
|
reconnecting: (attempt: number, max: number) => `Connection lost. Reconnecting (${attempt}/${max})...`,
|
|
149
149
|
reconnectFailed: 'Connection failed after multiple attempts.',
|
|
150
150
|
retry: 'Retry',
|
|
@@ -789,6 +789,12 @@ export const en = {
|
|
|
789
789
|
organizeUndone: 'Undone',
|
|
790
790
|
organizeViewFile: 'View file',
|
|
791
791
|
organizeUndoSuccess: (n: number) => `Reverted ${n} file${n !== 1 ? 's' : ''}`,
|
|
792
|
+
organizeDetailTitle: 'AI Organize Details',
|
|
793
|
+
organizeSourceFiles: 'Source Files',
|
|
794
|
+
organizeSummaryLabel: 'AI Summary',
|
|
795
|
+
organizeChangesLabel: (n: number) => `Changes (${n})`,
|
|
796
|
+
organizeNoSummary: 'AI is working...',
|
|
797
|
+
organizeMinimizeModal: 'Minimize',
|
|
792
798
|
},
|
|
793
799
|
importHistory: {
|
|
794
800
|
title: 'Import History',
|
|
@@ -836,6 +842,8 @@ export const en = {
|
|
|
836
842
|
testKeyNetworkError: 'Network error',
|
|
837
843
|
testKeyNoKey: 'No API key configured',
|
|
838
844
|
testKeyUnknown: 'Test failed',
|
|
845
|
+
listModels: 'Browse',
|
|
846
|
+
noModelsFound: 'No models found',
|
|
839
847
|
},
|
|
840
848
|
agent: {
|
|
841
849
|
title: 'Agent Behavior',
|
package/app/lib/i18n-zh.ts
CHANGED
|
@@ -169,7 +169,7 @@ export const zh = {
|
|
|
169
169
|
searching: '正在搜索知识库...',
|
|
170
170
|
generating: '正在生成回复...',
|
|
171
171
|
stopped: '已停止生成。',
|
|
172
|
-
errorNoResponse: 'AI
|
|
172
|
+
errorNoResponse: 'AI 未返回有效内容。可能原因:模型不支持流式输出、中转站兼容性问题、或请求超出上下文限制。',
|
|
173
173
|
reconnecting: (attempt: number, max: number) => `连接中断,正在重连 (${attempt}/${max})...`,
|
|
174
174
|
reconnectFailed: '多次重连失败,请检查网络后重试。',
|
|
175
175
|
retry: '重试',
|
|
@@ -813,6 +813,12 @@ export const zh = {
|
|
|
813
813
|
organizeUndone: '已撤销',
|
|
814
814
|
organizeViewFile: '查看文件',
|
|
815
815
|
organizeUndoSuccess: (n: number) => `已撤销 ${n} 个文件`,
|
|
816
|
+
organizeDetailTitle: 'AI 整理详情',
|
|
817
|
+
organizeSourceFiles: '源文件',
|
|
818
|
+
organizeSummaryLabel: 'AI 总结',
|
|
819
|
+
organizeChangesLabel: (n: number) => `变更列表 (${n})`,
|
|
820
|
+
organizeNoSummary: 'AI 正在处理中...',
|
|
821
|
+
organizeMinimizeModal: '最小化',
|
|
816
822
|
},
|
|
817
823
|
importHistory: {
|
|
818
824
|
title: '导入历史',
|
|
@@ -860,6 +866,8 @@ export const zh = {
|
|
|
860
866
|
testKeyNetworkError: '网络错误',
|
|
861
867
|
testKeyNoKey: '未配置 API Key',
|
|
862
868
|
testKeyUnknown: '测试失败',
|
|
869
|
+
listModels: '选择模型',
|
|
870
|
+
noModelsFound: '未找到可用模型',
|
|
863
871
|
},
|
|
864
872
|
agent: {
|
|
865
873
|
title: 'Agent 行为',
|
package/app/next-env.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/// <reference types="next" />
|
|
2
2
|
/// <reference types="next/image-types/global" />
|
|
3
|
-
import "./.next/types/routes.d.ts";
|
|
3
|
+
import "./.next/dev/types/routes.d.ts";
|
|
4
4
|
|
|
5
5
|
// NOTE: This file should not be edited
|
|
6
6
|
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
package/package.json
CHANGED