@geminilight/mindos 0.6.31 → 0.6.33
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 +69 -29
- package/app/app/api/graph/route.ts +5 -76
- package/app/app/trash/page.tsx +1 -0
- package/app/app/view/[...path]/ViewPageClient.tsx +22 -8
- package/app/components/ExportModal.tsx +2 -2
- package/app/components/FileTree.tsx +26 -5
- package/app/components/HomeContent.tsx +4 -0
- package/app/components/SystemPulse.tsx +318 -0
- package/app/components/TrashPageClient.tsx +9 -9
- package/app/components/agents/AgentsSkillsSection.tsx +173 -102
- package/app/components/renderers/workflow-yaml/WorkflowEditor.tsx +11 -21
- package/app/components/renderers/workflow-yaml/WorkflowRunner.tsx +15 -2
- package/app/components/renderers/workflow-yaml/selectors.tsx +3 -3
- package/app/components/ui/Toaster.tsx +11 -2
- package/app/lib/actions.ts +20 -9
- package/app/lib/agent/context.ts +22 -11
- package/app/lib/agent/loop-detection.ts +52 -0
- package/app/lib/agent/retry.ts +19 -0
- package/app/lib/core/backlinks.ts +33 -9
- package/app/lib/core/index.ts +4 -1
- package/app/lib/core/link-index.ts +224 -0
- package/app/lib/core/search-index.ts +310 -14
- package/app/lib/core/search.ts +180 -29
- package/app/lib/fs.ts +67 -10
- package/app/lib/hooks/usePinnedFiles.ts +7 -2
- package/app/lib/i18n/modules/knowledge.ts +62 -0
- package/app/lib/toast.ts +7 -1
- package/app/next-env.d.ts +1 -1
- package/app/package.json +2 -0
- package/package.json +1 -1
- package/scripts/parse-syncinclude.sh +92 -0
- package/scripts/write-build-stamp.js +40 -0
package/app/app/api/ask/route.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
export const dynamic = 'force-dynamic';
|
|
2
2
|
import type { AgentTool, AgentToolResult } from '@mariozechner/pi-agent-core';
|
|
3
|
+
import { isTransientError } from '@/lib/agent/retry';
|
|
4
|
+
import { detectLoop } from '@/lib/agent/loop-detection';
|
|
3
5
|
import {
|
|
4
6
|
type AgentSessionEvent as AgentEvent,
|
|
5
7
|
AuthStorage,
|
|
@@ -45,7 +47,8 @@ type MindOSSSEvent =
|
|
|
45
47
|
| { type: 'tool_start'; toolCallId: string; toolName: string; args: unknown }
|
|
46
48
|
| { type: 'tool_end'; toolCallId: string; output: string; isError: boolean }
|
|
47
49
|
| { type: 'done'; usage?: { input: number; output: number } }
|
|
48
|
-
| { type: 'error'; message: string }
|
|
50
|
+
| { type: 'error'; message: string }
|
|
51
|
+
| { type: 'status'; message: string };
|
|
49
52
|
|
|
50
53
|
// ---------------------------------------------------------------------------
|
|
51
54
|
// Type Guards for AgentEvent variants (safe event handling)
|
|
@@ -335,16 +338,40 @@ export async function POST(req: NextRequest) {
|
|
|
335
338
|
const targetDir = dirnameOf(currentFile);
|
|
336
339
|
const bootstrap = {
|
|
337
340
|
instruction: readKnowledgeFile('INSTRUCTION.md'),
|
|
338
|
-
index: readKnowledgeFile('README.md'),
|
|
339
341
|
config_json: readKnowledgeFile('CONFIG.json'),
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
342
|
+
// Lazy-loaded: only read if the file exists and has content.
|
|
343
|
+
// README.md and CONFIG.md are often empty/boilerplate and waste tokens.
|
|
344
|
+
index: null as ReturnType<typeof readKnowledgeFile> | null,
|
|
345
|
+
config_md: null as ReturnType<typeof readKnowledgeFile> | null,
|
|
346
|
+
target_readme: null as ReturnType<typeof readKnowledgeFile> | null,
|
|
347
|
+
target_instruction: null as ReturnType<typeof readKnowledgeFile> | null,
|
|
348
|
+
target_config_json: null as ReturnType<typeof readKnowledgeFile> | null,
|
|
349
|
+
target_config_md: null as ReturnType<typeof readKnowledgeFile> | null,
|
|
345
350
|
};
|
|
346
351
|
|
|
347
|
-
// Only
|
|
352
|
+
// Only load secondary bootstrap files if they have meaningful content.
|
|
353
|
+
// Files with ≤10 chars are typically empty or just a heading — not worth
|
|
354
|
+
// injecting into the prompt (saves ~200-500 tokens per empty file).
|
|
355
|
+
const MIN_USEFUL_CONTENT_LENGTH = 10;
|
|
356
|
+
|
|
357
|
+
const indexResult = readKnowledgeFile('README.md');
|
|
358
|
+
if (indexResult.ok && indexResult.content.trim().length > MIN_USEFUL_CONTENT_LENGTH) bootstrap.index = indexResult;
|
|
359
|
+
|
|
360
|
+
const configMdResult = readKnowledgeFile('CONFIG.md');
|
|
361
|
+
if (configMdResult.ok && configMdResult.content.trim().length > MIN_USEFUL_CONTENT_LENGTH) bootstrap.config_md = configMdResult;
|
|
362
|
+
|
|
363
|
+
if (targetDir) {
|
|
364
|
+
const tr = readKnowledgeFile(`${targetDir}/README.md`);
|
|
365
|
+
if (tr.ok && tr.content.trim().length > MIN_USEFUL_CONTENT_LENGTH) bootstrap.target_readme = tr;
|
|
366
|
+
const ti = readKnowledgeFile(`${targetDir}/INSTRUCTION.md`);
|
|
367
|
+
if (ti.ok && ti.content.trim().length > MIN_USEFUL_CONTENT_LENGTH) bootstrap.target_instruction = ti;
|
|
368
|
+
const tc = readKnowledgeFile(`${targetDir}/CONFIG.json`);
|
|
369
|
+
if (tc.ok && tc.content.trim().length > MIN_USEFUL_CONTENT_LENGTH) bootstrap.target_config_json = tc;
|
|
370
|
+
const tm = readKnowledgeFile(`${targetDir}/CONFIG.md`);
|
|
371
|
+
if (tm.ok && tm.content.trim().length > MIN_USEFUL_CONTENT_LENGTH) bootstrap.target_config_md = tm;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// Only report failures + truncation warnings for loaded files
|
|
348
375
|
const initFailures: string[] = [];
|
|
349
376
|
const truncationWarnings: string[] = [];
|
|
350
377
|
if (!skill.ok) initFailures.push(`skill.mindos: failed (${skill.error})`);
|
|
@@ -352,19 +379,13 @@ export async function POST(req: NextRequest) {
|
|
|
352
379
|
if (userSkillRules.ok && userSkillRules.truncated) truncationWarnings.push('user-skill-rules.md was truncated');
|
|
353
380
|
if (!bootstrap.instruction.ok) initFailures.push(`bootstrap.instruction: failed (${bootstrap.instruction.error})`);
|
|
354
381
|
if (bootstrap.instruction.ok && bootstrap.instruction.truncated) truncationWarnings.push('bootstrap.instruction was truncated');
|
|
355
|
-
if (
|
|
356
|
-
if (bootstrap.index.ok && bootstrap.index.truncated) truncationWarnings.push('bootstrap.index was truncated');
|
|
382
|
+
if (bootstrap.index?.ok && bootstrap.index.truncated) truncationWarnings.push('bootstrap.index was truncated');
|
|
357
383
|
if (!bootstrap.config_json.ok) initFailures.push(`bootstrap.config_json: failed (${bootstrap.config_json.error})`);
|
|
358
384
|
if (bootstrap.config_json.ok && bootstrap.config_json.truncated) truncationWarnings.push('bootstrap.config_json was truncated');
|
|
359
|
-
if (
|
|
360
|
-
if (bootstrap.config_md.ok && bootstrap.config_md.truncated) truncationWarnings.push('bootstrap.config_md was truncated');
|
|
361
|
-
if (bootstrap.target_readme && !bootstrap.target_readme.ok) initFailures.push(`bootstrap.target_readme: failed (${bootstrap.target_readme.error})`);
|
|
385
|
+
if (bootstrap.config_md?.ok && bootstrap.config_md.truncated) truncationWarnings.push('bootstrap.config_md was truncated');
|
|
362
386
|
if (bootstrap.target_readme?.ok && bootstrap.target_readme.truncated) truncationWarnings.push('bootstrap.target_readme was truncated');
|
|
363
|
-
if (bootstrap.target_instruction && !bootstrap.target_instruction.ok) initFailures.push(`bootstrap.target_instruction: failed (${bootstrap.target_instruction.error})`);
|
|
364
387
|
if (bootstrap.target_instruction?.ok && bootstrap.target_instruction.truncated) truncationWarnings.push('bootstrap.target_instruction was truncated');
|
|
365
|
-
if (bootstrap.target_config_json && !bootstrap.target_config_json.ok) initFailures.push(`bootstrap.target_config_json: failed (${bootstrap.target_config_json.error})`);
|
|
366
388
|
if (bootstrap.target_config_json?.ok && bootstrap.target_config_json.truncated) truncationWarnings.push('bootstrap.target_config_json was truncated');
|
|
367
|
-
if (bootstrap.target_config_md && !bootstrap.target_config_md.ok) initFailures.push(`bootstrap.target_config_md: failed (${bootstrap.target_config_md.error})`);
|
|
368
389
|
if (bootstrap.target_config_md?.ok && bootstrap.target_config_md.truncated) truncationWarnings.push('bootstrap.target_config_md was truncated');
|
|
369
390
|
|
|
370
391
|
const initStatus = initFailures.length === 0
|
|
@@ -377,9 +398,9 @@ export async function POST(req: NextRequest) {
|
|
|
377
398
|
initContextBlocks.push(`## user_skill_rules\n\nUser personalization rules (user-skill-rules.md):\n\n${userSkillRules.content}`);
|
|
378
399
|
}
|
|
379
400
|
if (bootstrap.instruction.ok) initContextBlocks.push(`## bootstrap_instruction\n\n${bootstrap.instruction.content}`);
|
|
380
|
-
if (bootstrap.index
|
|
401
|
+
if (bootstrap.index?.ok) initContextBlocks.push(`## bootstrap_index\n\n${bootstrap.index.content}`);
|
|
381
402
|
if (bootstrap.config_json.ok) initContextBlocks.push(`## bootstrap_config_json\n\n${bootstrap.config_json.content}`);
|
|
382
|
-
if (bootstrap.config_md
|
|
403
|
+
if (bootstrap.config_md?.ok) initContextBlocks.push(`## bootstrap_config_md\n\n${bootstrap.config_md.content}`);
|
|
383
404
|
if (bootstrap.target_readme?.ok) initContextBlocks.push(`## bootstrap_target_readme\n\n${bootstrap.target_readme.content}`);
|
|
384
405
|
if (bootstrap.target_instruction?.ok) initContextBlocks.push(`## bootstrap_target_instruction\n\n${bootstrap.target_instruction.content}`);
|
|
385
406
|
if (bootstrap.target_config_json?.ok) initContextBlocks.push(`## bootstrap_target_config_json\n\n${bootstrap.target_config_json.content}`);
|
|
@@ -584,18 +605,12 @@ export async function POST(req: NextRequest) {
|
|
|
584
605
|
stepHistory.push(...newEntries);
|
|
585
606
|
}
|
|
586
607
|
|
|
587
|
-
// Loop detection: same tool
|
|
588
|
-
// Only trigger if we have 3+ history entries (prevent false positives on first turn).
|
|
589
|
-
const LOOP_DETECTION_THRESHOLD = 3;
|
|
608
|
+
// Loop detection: (1) same tool+args 3x in a row, (2) repeating pattern cycle
|
|
590
609
|
if (loopCooldown > 0) {
|
|
591
610
|
loopCooldown--;
|
|
592
|
-
} else if (stepHistory
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
loopCooldown = 3;
|
|
596
|
-
// TODO (metrics): Track loop detection rate — metrics.increment('agent.loop_detected', { model: modelName })
|
|
597
|
-
void session.steer('[SYSTEM WARNING] You have called the same tool with identical arguments 3 times in a row. This appears to be a loop. Try a completely different approach or ask the user for clarification.');
|
|
598
|
-
}
|
|
611
|
+
} else if (detectLoop(stepHistory)) {
|
|
612
|
+
loopCooldown = 3;
|
|
613
|
+
void session.steer('[SYSTEM WARNING] You appear to be in a loop — repeating the same tool calls in a cycle. Try a completely different approach or ask the user for clarification.');
|
|
599
614
|
}
|
|
600
615
|
|
|
601
616
|
// Step limit enforcement
|
|
@@ -708,7 +723,32 @@ export async function POST(req: NextRequest) {
|
|
|
708
723
|
safeClose();
|
|
709
724
|
} else {
|
|
710
725
|
// Route to MindOS agent (existing logic)
|
|
711
|
-
|
|
726
|
+
// Retry with exponential backoff for transient failures (timeout, rate limit, 5xx).
|
|
727
|
+
// Only retry if no content has been streamed yet — once the user sees partial
|
|
728
|
+
// output, retrying would produce duplicate/garbled content.
|
|
729
|
+
const MAX_RETRIES = 3;
|
|
730
|
+
let lastPromptError: Error | null = null;
|
|
731
|
+
|
|
732
|
+
for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
|
|
733
|
+
try {
|
|
734
|
+
await session.prompt(lastUserContent, lastUserImages ? { images: lastUserImages } : undefined);
|
|
735
|
+
lastPromptError = null;
|
|
736
|
+
break; // success
|
|
737
|
+
} catch (err) {
|
|
738
|
+
lastPromptError = err instanceof Error ? err : new Error(String(err));
|
|
739
|
+
|
|
740
|
+
// Only retry if: (1) no content streamed yet, (2) retries remaining, (3) transient error
|
|
741
|
+
const canRetry = !hasContent && attempt < MAX_RETRIES && isTransientError(lastPromptError);
|
|
742
|
+
if (!canRetry) break;
|
|
743
|
+
|
|
744
|
+
const delayMs = 1000 * Math.pow(2, attempt - 1); // 1s, 2s
|
|
745
|
+
send({ type: 'status', message: `Request failed, retrying (${attempt}/${MAX_RETRIES})...` });
|
|
746
|
+
await new Promise(resolve => setTimeout(resolve, delayMs));
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
if (lastPromptError) throw lastPromptError;
|
|
751
|
+
|
|
712
752
|
metrics.recordRequest(Date.now() - requestStartTime);
|
|
713
753
|
if (!hasContent && lastModelError) {
|
|
714
754
|
send({ type: 'error', message: lastModelError });
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export const dynamic = 'force-dynamic';
|
|
2
2
|
import { NextResponse } from 'next/server';
|
|
3
3
|
import path from 'path';
|
|
4
|
-
import { collectAllFiles,
|
|
4
|
+
import { collectAllFiles, getLinkIndex } from '@/lib/fs';
|
|
5
5
|
|
|
6
6
|
export interface GraphNode {
|
|
7
7
|
id: string; // relative file path
|
|
@@ -19,63 +19,9 @@ export interface GraphData {
|
|
|
19
19
|
edges: GraphEdge[];
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
-
function extractLinks(content: string, sourcePath: string, fileSet: Set<string>, basenameMap: Map<string, string[]>): string[] {
|
|
23
|
-
const targets: string[] = [];
|
|
24
|
-
const sourceDir = path.dirname(sourcePath);
|
|
25
|
-
|
|
26
|
-
// WikiLinks: [[target]] or [[target|alias]] or [[target#section]]
|
|
27
|
-
const wikiRe = /\[\[([^\]|#]+)(?:[|#][^\]]*)?/g;
|
|
28
|
-
let m: RegExpExecArray | null;
|
|
29
|
-
while ((m = wikiRe.exec(content)) !== null) {
|
|
30
|
-
const raw = m[1].trim();
|
|
31
|
-
if (!raw) continue;
|
|
32
|
-
|
|
33
|
-
// Try exact match
|
|
34
|
-
if (fileSet.has(raw)) {
|
|
35
|
-
targets.push(raw);
|
|
36
|
-
continue;
|
|
37
|
-
}
|
|
38
|
-
// Try with .md
|
|
39
|
-
const withMd = raw.endsWith('.md') ? raw : raw + '.md';
|
|
40
|
-
if (fileSet.has(withMd)) {
|
|
41
|
-
targets.push(withMd);
|
|
42
|
-
continue;
|
|
43
|
-
}
|
|
44
|
-
// Try basename lookup (case-insensitive)
|
|
45
|
-
const lower = path.basename(withMd).toLowerCase();
|
|
46
|
-
const candidates = basenameMap.get(lower);
|
|
47
|
-
if (candidates && candidates.length === 1) {
|
|
48
|
-
targets.push(candidates[0]);
|
|
49
|
-
}
|
|
50
|
-
// skip if ambiguous (multiple candidates)
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
// Markdown links: [text](relative/path.md) or [text](relative/path.md#section)
|
|
54
|
-
const mdLinkRe = /\[[^\]]+\]\(([^)]+\.md)(?:#[^)]*)?\)/g;
|
|
55
|
-
while ((m = mdLinkRe.exec(content)) !== null) {
|
|
56
|
-
const raw = m[1].trim();
|
|
57
|
-
if (!raw || raw.startsWith('http')) continue;
|
|
58
|
-
const resolved = path.normalize(path.join(sourceDir, raw));
|
|
59
|
-
if (fileSet.has(resolved)) {
|
|
60
|
-
targets.push(resolved);
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
return targets;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
22
|
export async function GET() {
|
|
68
23
|
try {
|
|
69
24
|
const allFiles = collectAllFiles().filter(f => f.endsWith('.md'));
|
|
70
|
-
const fileSet = new Set(allFiles);
|
|
71
|
-
|
|
72
|
-
// Build basename → relPath[] lookup
|
|
73
|
-
const basenameMap = new Map<string, string[]>();
|
|
74
|
-
for (const f of allFiles) {
|
|
75
|
-
const key = path.basename(f).toLowerCase();
|
|
76
|
-
if (!basenameMap.has(key)) basenameMap.set(key, []);
|
|
77
|
-
basenameMap.get(key)!.push(f);
|
|
78
|
-
}
|
|
79
25
|
|
|
80
26
|
const nodes: GraphNode[] = allFiles.map(f => ({
|
|
81
27
|
id: f,
|
|
@@ -83,27 +29,10 @@ export async function GET() {
|
|
|
83
29
|
folder: path.dirname(f),
|
|
84
30
|
}));
|
|
85
31
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
let content: string;
|
|
91
|
-
try {
|
|
92
|
-
content = getFileContent(filePath);
|
|
93
|
-
} catch {
|
|
94
|
-
continue;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
const targets = extractLinks(content, filePath, fileSet, basenameMap);
|
|
98
|
-
for (const target of targets) {
|
|
99
|
-
if (target === filePath) continue; // skip self-edges
|
|
100
|
-
const key = `${filePath}||${target}`;
|
|
101
|
-
if (!edgeSet.has(key)) {
|
|
102
|
-
edgeSet.add(key);
|
|
103
|
-
edges.push({ source: filePath, target });
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
}
|
|
32
|
+
// Use pre-built link index (O(1) per file) instead of extracting links
|
|
33
|
+
// from every file on each request (O(n * m))
|
|
34
|
+
const linkIndex = getLinkIndex();
|
|
35
|
+
const edges = linkIndex.getAllEdges();
|
|
107
36
|
|
|
108
37
|
return NextResponse.json({ nodes, edges } satisfies GraphData);
|
|
109
38
|
} catch (err) {
|
package/app/app/trash/page.tsx
CHANGED
|
@@ -2,6 +2,7 @@ import { listTrashAction } from '@/lib/actions';
|
|
|
2
2
|
import TrashPageClient from '@/components/TrashPageClient';
|
|
3
3
|
|
|
4
4
|
export default async function TrashPage() {
|
|
5
|
+
// listTrashAction auto-purges expired items (>30 days) on each call
|
|
5
6
|
const items = await listTrashAction();
|
|
6
7
|
return <TrashPageClient initialItems={items} />;
|
|
7
8
|
}
|
|
@@ -17,7 +17,8 @@ import { resolveRenderer, isRendererEnabled } from '@/lib/renderers/registry';
|
|
|
17
17
|
import { encodePath } from '@/lib/utils';
|
|
18
18
|
import { useLocale } from '@/lib/LocaleContext';
|
|
19
19
|
import DirPicker from '@/components/DirPicker';
|
|
20
|
-
import { renameFileAction, deleteFileAction } from '@/lib/actions';
|
|
20
|
+
import { renameFileAction, deleteFileAction, undoDeleteAction } from '@/lib/actions';
|
|
21
|
+
import { toast } from '@/lib/toast';
|
|
21
22
|
import { ConfirmDialog } from '@/components/agents/AgentsPrimitives';
|
|
22
23
|
import { buildLineDiff } from '@/components/changes/line-diff';
|
|
23
24
|
import { usePinnedFiles } from '@/lib/hooks/usePinnedFiles';
|
|
@@ -132,15 +133,28 @@ export default function ViewPageClient({
|
|
|
132
133
|
|
|
133
134
|
const handleConfirmDelete = useCallback(() => {
|
|
134
135
|
setShowDeleteConfirm(false);
|
|
136
|
+
const fileName = filePath.split('/').pop() ?? filePath;
|
|
135
137
|
startTransition(async () => {
|
|
136
138
|
const result = await deleteFileAction(filePath);
|
|
137
139
|
if (result.success) {
|
|
140
|
+
if (result.trashId) {
|
|
141
|
+
const trashId = result.trashId;
|
|
142
|
+
toast.undo(`${t.trash?.movedToTrash ?? 'Deleted'} ${fileName}`, async () => {
|
|
143
|
+
const undo = await undoDeleteAction(trashId);
|
|
144
|
+
if (undo.success) {
|
|
145
|
+
router.refresh();
|
|
146
|
+
window.dispatchEvent(new Event('mindos:files-changed'));
|
|
147
|
+
} else {
|
|
148
|
+
toast.error(undo.error ?? 'Undo failed');
|
|
149
|
+
}
|
|
150
|
+
}, { label: t.trash?.undo ?? 'Undo' });
|
|
151
|
+
}
|
|
138
152
|
router.push('/');
|
|
139
153
|
router.refresh();
|
|
140
154
|
window.dispatchEvent(new Event('mindos:files-changed'));
|
|
141
155
|
}
|
|
142
156
|
});
|
|
143
|
-
}, [filePath, router]);
|
|
157
|
+
}, [filePath, router, t]);
|
|
144
158
|
|
|
145
159
|
// Keep first paint deterministic between server and client to avoid hydration mismatch.
|
|
146
160
|
const effectiveUseRaw = hydrated ? useRaw : false;
|
|
@@ -588,8 +602,8 @@ export default function ViewPageClient({
|
|
|
588
602
|
className="w-full bg-muted border border-border rounded-md px-3 py-1.5 text-sm text-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
|
|
589
603
|
/>
|
|
590
604
|
<div className="flex justify-end gap-2 mt-3">
|
|
591
|
-
<button onClick={() => setRenaming(false)} className="px-3 py-1.5 rounded-md text-xs bg-muted text-muted-foreground hover:bg-accent transition-colors">Cancel</button>
|
|
592
|
-
<button onClick={handleCommitRename} className="px-3 py-1.5 rounded-md text-xs bg-[var(--amber)] text-[var(--amber-foreground)] transition-colors">Rename</button>
|
|
605
|
+
<button onClick={() => setRenaming(false)} className="px-3 py-1.5 rounded-md text-xs bg-muted text-muted-foreground hover:bg-accent transition-colors">{t.view?.cancel ?? 'Cancel'}</button>
|
|
606
|
+
<button onClick={handleCommitRename} className="px-3 py-1.5 rounded-md text-xs bg-[var(--amber)] text-[var(--amber-foreground)] transition-colors">{t.view?.rename ?? 'Rename'}</button>
|
|
593
607
|
</div>
|
|
594
608
|
</div>
|
|
595
609
|
</div>
|
|
@@ -598,10 +612,10 @@ export default function ViewPageClient({
|
|
|
598
612
|
{/* Delete confirm */}
|
|
599
613
|
<ConfirmDialog
|
|
600
614
|
open={showDeleteConfirm}
|
|
601
|
-
title=
|
|
602
|
-
message={`Delete "${filePath.split('/').pop()}"
|
|
603
|
-
confirmLabel=
|
|
604
|
-
cancelLabel=
|
|
615
|
+
title={t.view?.delete ?? 'Delete'}
|
|
616
|
+
message={t.view?.deleteConfirm?.(filePath.split('/').pop() ?? '') ?? `Delete "${filePath.split('/').pop()}"?`}
|
|
617
|
+
confirmLabel={t.view?.delete ?? 'Delete'}
|
|
618
|
+
cancelLabel={t.view?.cancel ?? 'Cancel'}
|
|
605
619
|
variant="destructive"
|
|
606
620
|
onCancel={() => setShowDeleteConfirm(false)}
|
|
607
621
|
onConfirm={handleConfirmDelete}
|
|
@@ -114,7 +114,7 @@ export default function ExportModal({ open, onClose, filePath, isDirectory, file
|
|
|
114
114
|
{isDirectory ? (t.export?.exportSpace ?? 'Export Space') : (t.export?.exportFile ?? 'Export File')}
|
|
115
115
|
</h3>
|
|
116
116
|
</div>
|
|
117
|
-
<button onClick={handleClose} className="p-1 rounded-md text-muted-foreground hover:text-foreground hover:bg-muted transition-colors">
|
|
117
|
+
<button onClick={handleClose} className="p-1 rounded-md text-muted-foreground hover:text-foreground hover:bg-muted transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring" aria-label={t.export?.cancel ?? 'Close'}>
|
|
118
118
|
<X size={14} />
|
|
119
119
|
</button>
|
|
120
120
|
</div>
|
|
@@ -179,7 +179,7 @@ export default function ExportModal({ open, onClose, filePath, isDirectory, file
|
|
|
179
179
|
{t.export?.downloadAgain ?? 'Download Again'}
|
|
180
180
|
</button>
|
|
181
181
|
<button onClick={handleClose} className="px-3 py-1.5 rounded-md text-xs font-medium bg-[var(--amber-dim)] text-[var(--amber-text)] hover:opacity-80 transition-colors">
|
|
182
|
-
{t.export?.
|
|
182
|
+
{t.export?.close ?? 'Done'}
|
|
183
183
|
</button>
|
|
184
184
|
</>
|
|
185
185
|
) : state === 'error' ? (
|
|
@@ -8,7 +8,8 @@ import {
|
|
|
8
8
|
ChevronDown, FileText, Table, Folder, FolderOpen, Plus, Loader2,
|
|
9
9
|
Trash2, Pencil, Layers, ScrollText, FolderInput, Copy, MoreHorizontal, Star,
|
|
10
10
|
} from 'lucide-react';
|
|
11
|
-
import { createFileAction, deleteFileAction, renameFileAction, renameSpaceAction, deleteSpaceAction, convertToSpaceAction, deleteFolderAction } from '@/lib/actions';
|
|
11
|
+
import { createFileAction, deleteFileAction, renameFileAction, renameSpaceAction, deleteSpaceAction, convertToSpaceAction, deleteFolderAction, undoDeleteAction } from '@/lib/actions';
|
|
12
|
+
import { toast } from '@/lib/toast';
|
|
12
13
|
import { useLocale } from '@/lib/LocaleContext';
|
|
13
14
|
import { ConfirmDialog } from '@/components/agents/AgentsPrimitives';
|
|
14
15
|
import { usePinnedFiles } from '@/lib/hooks/usePinnedFiles';
|
|
@@ -536,7 +537,7 @@ function DirectoryNode({ node, depth, currentPath, onNavigate, maxOpenDepth, onI
|
|
|
536
537
|
title={deleteConfirm === 'space' ? t.fileTree.deleteSpace : t.fileTree.deleteFolder}
|
|
537
538
|
message={deleteConfirm === 'space' ? t.fileTree.confirmDeleteSpace(node.name) : t.fileTree.confirmDeleteFolder(node.name)}
|
|
538
539
|
confirmLabel={deleteConfirm === 'space' ? t.fileTree.deleteSpace : t.fileTree.deleteFolder}
|
|
539
|
-
cancelLabel=
|
|
540
|
+
cancelLabel={t.view?.cancel ?? 'Cancel'}
|
|
540
541
|
variant="destructive"
|
|
541
542
|
onCancel={() => setDeleteConfirm(null)}
|
|
542
543
|
onConfirm={() => {
|
|
@@ -546,7 +547,16 @@ function DirectoryNode({ node, depth, currentPath, onNavigate, maxOpenDepth, onI
|
|
|
546
547
|
const result = kind === 'space'
|
|
547
548
|
? await deleteSpaceAction(node.path)
|
|
548
549
|
: await deleteFolderAction(node.path);
|
|
549
|
-
if (result.success
|
|
550
|
+
if (result.success && result.trashId) {
|
|
551
|
+
const trashId = result.trashId;
|
|
552
|
+
const name = node.path.split('/').pop() ?? node.path;
|
|
553
|
+
toast.undo(`${t.trash?.movedToTrash ?? 'Deleted'} ${name}`, async () => {
|
|
554
|
+
const undo = await undoDeleteAction(trashId);
|
|
555
|
+
if (undo.success) { router.refresh(); notifyFilesChanged(); }
|
|
556
|
+
else toast.error(undo.error ?? 'Undo failed');
|
|
557
|
+
}, { label: t.trash?.undo ?? 'Undo' });
|
|
558
|
+
router.push('/'); router.refresh(); notifyFilesChanged();
|
|
559
|
+
}
|
|
550
560
|
});
|
|
551
561
|
}}
|
|
552
562
|
/>
|
|
@@ -718,14 +728,25 @@ function FileNodeItem({ node, depth, currentPath, onNavigate }: {
|
|
|
718
728
|
title={t.fileTree.delete}
|
|
719
729
|
message={t.fileTree.confirmDelete(node.name)}
|
|
720
730
|
confirmLabel={t.fileTree.delete}
|
|
721
|
-
cancelLabel=
|
|
731
|
+
cancelLabel={t.view?.cancel ?? 'Cancel'}
|
|
722
732
|
variant="destructive"
|
|
723
733
|
onCancel={() => setShowDeleteConfirm(false)}
|
|
724
734
|
onConfirm={() => {
|
|
725
735
|
setShowDeleteConfirm(false);
|
|
726
736
|
startDeleteTransition(async () => {
|
|
727
737
|
const result = await deleteFileAction(node.path);
|
|
728
|
-
if (result.success) {
|
|
738
|
+
if (result.success) {
|
|
739
|
+
if (result.trashId) {
|
|
740
|
+
const trashId = result.trashId;
|
|
741
|
+
const name = node.path.split('/').pop() ?? node.path;
|
|
742
|
+
toast.undo(`${t.trash?.movedToTrash ?? 'Deleted'} ${name}`, async () => {
|
|
743
|
+
const undo = await undoDeleteAction(trashId);
|
|
744
|
+
if (undo.success) { router.refresh(); notifyFilesChanged(); }
|
|
745
|
+
else toast.error(undo.error ?? 'Undo failed');
|
|
746
|
+
}, { label: t.trash?.undo ?? 'Undo' });
|
|
747
|
+
}
|
|
748
|
+
router.refresh(); notifyFilesChanged();
|
|
749
|
+
}
|
|
729
750
|
});
|
|
730
751
|
}}
|
|
731
752
|
/>
|
|
@@ -10,6 +10,7 @@ import { usePinnedFiles } from '@/lib/hooks/usePinnedFiles';
|
|
|
10
10
|
import { getAllRenderers, getPluginRenderers } from '@/lib/renderers/registry';
|
|
11
11
|
import OnboardingView from './OnboardingView';
|
|
12
12
|
import GuideCard from './GuideCard';
|
|
13
|
+
import SystemPulse from './SystemPulse';
|
|
13
14
|
import { scanExampleFilesAction, cleanupExamplesAction } from '@/lib/actions';
|
|
14
15
|
import type { SpaceInfo } from '@/app/page';
|
|
15
16
|
|
|
@@ -332,6 +333,9 @@ export default function HomeContent({ recent, existingFiles, spaces }: { recent:
|
|
|
332
333
|
</div>
|
|
333
334
|
</div>
|
|
334
335
|
|
|
336
|
+
{/* ── System Pulse: Agent status + stats ── */}
|
|
337
|
+
<SystemPulse />
|
|
338
|
+
|
|
335
339
|
{/* ── Section: Pinned Files ── */}
|
|
336
340
|
<PinnedFilesSection formatTime={formatTime} />
|
|
337
341
|
|