@geminilight/mindos 0.5.68 → 0.5.70
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 +12 -4
- package/app/app/api/file/import/route.ts +197 -0
- package/app/app/api/mcp/install/route.ts +99 -12
- package/app/app/api/mcp/status/route.ts +1 -1
- package/app/components/ActivityBar.tsx +3 -4
- package/app/components/FileTree.tsx +35 -9
- package/app/components/ImportModal.tsx +415 -0
- package/app/components/OnboardingView.tsx +9 -0
- package/app/components/Panel.tsx +4 -2
- package/app/components/SidebarLayout.tsx +83 -8
- package/app/components/TableOfContents.tsx +1 -0
- package/app/components/agents/AgentDetailContent.tsx +37 -28
- package/app/components/agents/AgentsMcpSection.tsx +16 -12
- package/app/components/agents/AgentsOverviewSection.tsx +48 -34
- package/app/components/agents/AgentsPrimitives.tsx +41 -20
- package/app/components/agents/AgentsSkillsSection.tsx +16 -7
- package/app/components/agents/SkillDetailPopover.tsx +13 -11
- package/app/components/ask/AskContent.tsx +11 -0
- package/app/components/panels/AgentsPanelAgentGroups.tsx +8 -6
- package/app/components/panels/AgentsPanelHubNav.tsx +3 -3
- package/app/components/panels/DiscoverPanel.tsx +88 -2
- package/app/hooks/useFileImport.ts +191 -0
- package/app/hooks/useFileUpload.ts +11 -0
- package/app/lib/agent/context.ts +7 -2
- package/app/lib/agent/tools.ts +245 -6
- package/app/lib/core/backlinks.ts +12 -4
- package/app/lib/core/file-convert.ts +97 -0
- package/app/lib/core/organize.ts +105 -0
- package/app/lib/core/search.ts +17 -3
- package/app/lib/fs.ts +5 -3
- package/app/lib/i18n-en.ts +51 -0
- package/app/lib/i18n-zh.ts +51 -0
- package/package.json +1 -1
|
@@ -177,6 +177,17 @@ export default function AskContent({ visible, currentFile, initialMessage, onFir
|
|
|
177
177
|
const upload = useFileUpload();
|
|
178
178
|
const mention = useMention();
|
|
179
179
|
|
|
180
|
+
useEffect(() => {
|
|
181
|
+
const handler = (e: Event) => {
|
|
182
|
+
const files = (e as CustomEvent).detail?.files;
|
|
183
|
+
if (Array.isArray(files) && files.length > 0) {
|
|
184
|
+
upload.injectFiles(files);
|
|
185
|
+
}
|
|
186
|
+
};
|
|
187
|
+
window.addEventListener('mindos:inject-ask-files', handler);
|
|
188
|
+
return () => window.removeEventListener('mindos:inject-ask-files', handler);
|
|
189
|
+
}, [upload]);
|
|
190
|
+
|
|
180
191
|
// Focus and init session when becoming visible (edge-triggered for panel, level-triggered for modal)
|
|
181
192
|
const prevVisibleRef = useRef(false);
|
|
182
193
|
useEffect(() => {
|
|
@@ -34,13 +34,14 @@ export function AgentsPanelAgentGroups({
|
|
|
34
34
|
}) {
|
|
35
35
|
return (
|
|
36
36
|
<div>
|
|
37
|
-
<div className="px-0 py-1 mb-
|
|
38
|
-
<span className="text-2xs font-semibold text-muted-foreground uppercase tracking-wider">{p.rosterLabel}</span>
|
|
37
|
+
<div className="px-0 py-1.5 mb-1">
|
|
38
|
+
<span className="text-2xs font-semibold text-muted-foreground/70 uppercase tracking-wider">{p.rosterLabel}</span>
|
|
39
39
|
</div>
|
|
40
40
|
{connected.length > 0 && (
|
|
41
41
|
<section className="mb-3">
|
|
42
|
-
<h3 className="text-[11px] font-medium text-muted-foreground/
|
|
43
|
-
|
|
42
|
+
<h3 className="flex items-center gap-1.5 text-[11px] font-medium text-muted-foreground/80 uppercase tracking-wider mb-2 pl-0.5">
|
|
43
|
+
<span className="w-1 h-3 rounded-full bg-[var(--success)]/50" aria-hidden="true" />
|
|
44
|
+
{p.sectionConnected} <span className="text-muted-foreground/50 tabular-nums">({connected.length})</span>
|
|
44
45
|
</h3>
|
|
45
46
|
<div className="space-y-1.5">
|
|
46
47
|
{connected.map(agent => (
|
|
@@ -60,8 +61,9 @@ export function AgentsPanelAgentGroups({
|
|
|
60
61
|
|
|
61
62
|
{detected.length > 0 && (
|
|
62
63
|
<section className="mb-3">
|
|
63
|
-
<h3 className="text-[11px] font-medium text-muted-foreground/
|
|
64
|
-
|
|
64
|
+
<h3 className="flex items-center gap-1.5 text-[11px] font-medium text-muted-foreground/80 uppercase tracking-wider mb-2 pl-0.5">
|
|
65
|
+
<span className="w-1 h-3 rounded-full bg-[var(--amber)]/50" aria-hidden="true" />
|
|
66
|
+
{p.sectionDetected} <span className="text-muted-foreground/50 tabular-nums">({detected.length})</span>
|
|
65
67
|
</h3>
|
|
66
68
|
<div className="space-y-1.5">
|
|
67
69
|
{detected.map(agent => (
|
|
@@ -27,18 +27,18 @@ export function AgentsPanelHubNav({
|
|
|
27
27
|
<PanelNavRow
|
|
28
28
|
icon={<LayoutDashboard size={14} className="text-[var(--amber)]" />}
|
|
29
29
|
title={copy.navOverview}
|
|
30
|
-
badge={<span className="text-2xs tabular-nums text-muted-foreground">{connectedCount}</span>}
|
|
30
|
+
badge={<span className="text-2xs tabular-nums text-muted-foreground/60 px-1.5 py-0.5 rounded bg-muted/40 font-medium">{connectedCount}</span>}
|
|
31
31
|
href="/agents"
|
|
32
32
|
active={inAgentsRoute && (tab === null || tab === 'overview')}
|
|
33
33
|
/>
|
|
34
34
|
<PanelNavRow
|
|
35
|
-
icon={<Server size={14} className=
|
|
35
|
+
icon={<Server size={14} className={inAgentsRoute && tab === 'mcp' ? 'text-[var(--amber)]' : 'text-muted-foreground'} />}
|
|
36
36
|
title={copy.navMcp}
|
|
37
37
|
href="/agents?tab=mcp"
|
|
38
38
|
active={inAgentsRoute && tab === 'mcp'}
|
|
39
39
|
/>
|
|
40
40
|
<PanelNavRow
|
|
41
|
-
icon={<Zap size={14} className=
|
|
41
|
+
icon={<Zap size={14} className={inAgentsRoute && tab === 'skills' ? 'text-[var(--amber)]' : 'text-muted-foreground'} />}
|
|
42
42
|
title={copy.navSkills}
|
|
43
43
|
href="/agents?tab=skills"
|
|
44
44
|
active={inAgentsRoute && tab === 'skills'}
|
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { useState, useEffect, useCallback, useRef } from 'react';
|
|
4
|
+
import { useRouter } from 'next/navigation';
|
|
5
|
+
import { Lightbulb, Blocks, Zap, LayoutTemplate, User, Download, RefreshCw, Repeat, Rocket, Search, Handshake, ShieldCheck, ChevronDown } from 'lucide-react';
|
|
4
6
|
import PanelHeader from './PanelHeader';
|
|
5
7
|
import { PanelNavRow, ComingSoonBadge } from './PanelNavRow';
|
|
6
8
|
import { useLocale } from '@/lib/LocaleContext';
|
|
7
9
|
import { useCases } from '@/components/explore/use-cases';
|
|
8
10
|
import { openAskModal } from '@/hooks/useAskModal';
|
|
11
|
+
import { getPluginRenderers, isRendererEnabled, setRendererEnabled, loadDisabledState } from '@/lib/renderers/registry';
|
|
12
|
+
import { Toggle } from '../settings/Primitives';
|
|
9
13
|
|
|
10
14
|
interface DiscoverPanelProps {
|
|
11
15
|
active: boolean;
|
|
@@ -56,8 +60,44 @@ export default function DiscoverPanel({ active, maximized, onMaximize }: Discove
|
|
|
56
60
|
const { t } = useLocale();
|
|
57
61
|
const d = t.panels.discover;
|
|
58
62
|
const e = t.explore;
|
|
63
|
+
const p = t.panels.plugins;
|
|
64
|
+
const router = useRouter();
|
|
65
|
+
|
|
66
|
+
const [pluginsMounted, setPluginsMounted] = useState(false);
|
|
67
|
+
const [showPlugins, setShowPlugins] = useState(false);
|
|
68
|
+
const [, forceUpdate] = useState(0);
|
|
69
|
+
const [existingFiles, setExistingFiles] = useState<Set<string>>(new Set());
|
|
70
|
+
const fetchedRef = useRef(false);
|
|
71
|
+
|
|
72
|
+
useEffect(() => {
|
|
73
|
+
loadDisabledState();
|
|
74
|
+
setPluginsMounted(true);
|
|
75
|
+
}, []);
|
|
76
|
+
|
|
77
|
+
useEffect(() => {
|
|
78
|
+
if (!pluginsMounted || fetchedRef.current) return;
|
|
79
|
+
fetchedRef.current = true;
|
|
80
|
+
const entryPaths = getPluginRenderers().map(r => r.entryPath).filter((ep): ep is string => !!ep);
|
|
81
|
+
if (entryPaths.length === 0) return;
|
|
82
|
+
fetch('/api/files')
|
|
83
|
+
.then(r => r.ok ? r.json() : [])
|
|
84
|
+
.then((allPaths: string[]) => {
|
|
85
|
+
const pathSet = new Set(allPaths);
|
|
86
|
+
setExistingFiles(new Set(entryPaths.filter(ep => pathSet.has(ep))));
|
|
87
|
+
})
|
|
88
|
+
.catch(() => {});
|
|
89
|
+
}, [pluginsMounted]);
|
|
90
|
+
|
|
91
|
+
const handleToggle = useCallback((id: string, enabled: boolean) => {
|
|
92
|
+
setRendererEnabled(id, enabled);
|
|
93
|
+
forceUpdate(n => n + 1);
|
|
94
|
+
window.dispatchEvent(new Event('renderer-state-changed'));
|
|
95
|
+
}, []);
|
|
96
|
+
|
|
97
|
+
const handleOpenPlugin = useCallback((entryPath: string) => {
|
|
98
|
+
router.push(`/view/${entryPath.split('/').map(encodeURIComponent).join('/')}`);
|
|
99
|
+
}, [router]);
|
|
59
100
|
|
|
60
|
-
/** Type-safe lookup for use case i18n */
|
|
61
101
|
const getUseCaseText = (id: string): { title: string; prompt: string } | undefined => {
|
|
62
102
|
const map: Record<string, { title: string; desc: string; prompt: string }> = {
|
|
63
103
|
c1: e.c1, c2: e.c2, c3: e.c3, c4: e.c4, c5: e.c5,
|
|
@@ -66,6 +106,9 @@ export default function DiscoverPanel({ active, maximized, onMaximize }: Discove
|
|
|
66
106
|
return map[id];
|
|
67
107
|
};
|
|
68
108
|
|
|
109
|
+
const renderers = pluginsMounted ? getPluginRenderers() : [];
|
|
110
|
+
const enabledCount = pluginsMounted ? renderers.filter(r => isRendererEnabled(r.id)).length : 0;
|
|
111
|
+
|
|
69
112
|
return (
|
|
70
113
|
<div className={`flex flex-col h-full ${active ? '' : 'hidden'}`}>
|
|
71
114
|
<PanelHeader title={d.title} maximized={maximized} onMaximize={onMaximize} />
|
|
@@ -97,6 +140,49 @@ export default function DiscoverPanel({ active, maximized, onMaximize }: Discove
|
|
|
97
140
|
|
|
98
141
|
<div className="mx-4 border-t border-border" />
|
|
99
142
|
|
|
143
|
+
{/* Installed extensions (merged from Plugins panel) */}
|
|
144
|
+
<div className="py-2">
|
|
145
|
+
<button
|
|
146
|
+
type="button"
|
|
147
|
+
onClick={() => setShowPlugins(v => !v)}
|
|
148
|
+
className="w-full flex items-center gap-1.5 px-4 py-1.5 text-left"
|
|
149
|
+
>
|
|
150
|
+
<ChevronDown size={11} className={`text-muted-foreground transition-transform duration-150 ${showPlugins ? '' : '-rotate-90'}`} />
|
|
151
|
+
<Blocks size={13} className="text-muted-foreground shrink-0" />
|
|
152
|
+
<span className="text-2xs font-medium text-muted-foreground uppercase tracking-wider flex-1">
|
|
153
|
+
{p.title}
|
|
154
|
+
</span>
|
|
155
|
+
<span className="text-2xs text-muted-foreground tabular-nums">{enabledCount}/{renderers.length}</span>
|
|
156
|
+
</button>
|
|
157
|
+
{showPlugins && renderers.map(r => {
|
|
158
|
+
const enabled = isRendererEnabled(r.id);
|
|
159
|
+
const fileExists = r.entryPath ? existingFiles.has(r.entryPath) : false;
|
|
160
|
+
const canOpen = enabled && r.entryPath && fileExists;
|
|
161
|
+
return (
|
|
162
|
+
<div
|
|
163
|
+
key={r.id}
|
|
164
|
+
className={`flex items-center gap-2 px-4 py-1.5 mx-1 rounded-sm transition-colors ${canOpen ? 'cursor-pointer hover:bg-muted/50' : ''} ${!enabled ? 'opacity-50' : ''}`}
|
|
165
|
+
onClick={canOpen ? () => handleOpenPlugin(r.entryPath!) : undefined}
|
|
166
|
+
role={canOpen ? 'link' : undefined}
|
|
167
|
+
tabIndex={canOpen ? 0 : undefined}
|
|
168
|
+
onKeyDown={canOpen ? (e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); handleOpenPlugin(r.entryPath!); } } : undefined}
|
|
169
|
+
>
|
|
170
|
+
<span className="text-sm shrink-0" suppressHydrationWarning>{r.icon}</span>
|
|
171
|
+
<span className="text-xs text-foreground truncate flex-1">{r.name}</span>
|
|
172
|
+
{r.core ? (
|
|
173
|
+
<span className="text-2xs px-1.5 py-0.5 rounded bg-muted text-muted-foreground shrink-0">{p.core}</span>
|
|
174
|
+
) : (
|
|
175
|
+
<div onClick={e => e.stopPropagation()}>
|
|
176
|
+
<Toggle checked={enabled} onChange={v => handleToggle(r.id, v)} size="sm" />
|
|
177
|
+
</div>
|
|
178
|
+
)}
|
|
179
|
+
</div>
|
|
180
|
+
);
|
|
181
|
+
})}
|
|
182
|
+
</div>
|
|
183
|
+
|
|
184
|
+
<div className="mx-4 border-t border-border" />
|
|
185
|
+
|
|
100
186
|
{/* Quick try — use case list */}
|
|
101
187
|
<div className="py-2">
|
|
102
188
|
<div className="px-4 py-1.5">
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState, useCallback, useRef } from 'react';
|
|
4
|
+
import { ALLOWED_IMPORT_EXTENSIONS } from '@/lib/core/file-convert';
|
|
5
|
+
|
|
6
|
+
export type ImportIntent = 'archive' | 'digest';
|
|
7
|
+
export type ImportStep = 'select' | 'archive_config' | 'importing' | 'done';
|
|
8
|
+
export type ConflictMode = 'skip' | 'rename' | 'overwrite';
|
|
9
|
+
|
|
10
|
+
export interface ImportFile {
|
|
11
|
+
file: File;
|
|
12
|
+
name: string;
|
|
13
|
+
size: number;
|
|
14
|
+
content: string | null;
|
|
15
|
+
loading: boolean;
|
|
16
|
+
error: string | null;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const MAX_FILE_SIZE = 5 * 1024 * 1024;
|
|
20
|
+
const MAX_PDF_SIZE = 12 * 1024 * 1024;
|
|
21
|
+
const MAX_FILES = 20;
|
|
22
|
+
|
|
23
|
+
function getExt(name: string): string {
|
|
24
|
+
const idx = name.lastIndexOf('.');
|
|
25
|
+
return idx >= 0 ? name.slice(idx).toLowerCase() : '';
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function formatSize(bytes: number): string {
|
|
29
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
30
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
31
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function useFileImport() {
|
|
35
|
+
const [files, setFiles] = useState<ImportFile[]>([]);
|
|
36
|
+
const [step, setStep] = useState<ImportStep>('select');
|
|
37
|
+
const [intent, setIntent] = useState<ImportIntent>('archive');
|
|
38
|
+
const [targetSpace, setTargetSpace] = useState('');
|
|
39
|
+
const [conflict, setConflict] = useState<ConflictMode>('rename');
|
|
40
|
+
const [importing, setImporting] = useState(false);
|
|
41
|
+
const [result, setResult] = useState<{
|
|
42
|
+
created: Array<{ original: string; path: string }>;
|
|
43
|
+
skipped: Array<{ name: string; reason: string }>;
|
|
44
|
+
errors: Array<{ name: string; error: string }>;
|
|
45
|
+
updatedFiles: string[];
|
|
46
|
+
} | null>(null);
|
|
47
|
+
const inputRef = useRef<HTMLInputElement>(null);
|
|
48
|
+
|
|
49
|
+
const addFiles = useCallback(async (fileList: FileList | File[]) => {
|
|
50
|
+
const incoming = Array.from(fileList).slice(0, MAX_FILES);
|
|
51
|
+
const newFiles: ImportFile[] = [];
|
|
52
|
+
|
|
53
|
+
for (const file of incoming) {
|
|
54
|
+
const ext = getExt(file.name);
|
|
55
|
+
const maxSize = ext === '.pdf' ? MAX_PDF_SIZE : MAX_FILE_SIZE;
|
|
56
|
+
|
|
57
|
+
let error: string | null = null;
|
|
58
|
+
if (!ALLOWED_IMPORT_EXTENSIONS.has(ext)) {
|
|
59
|
+
error = 'unsupported';
|
|
60
|
+
} else if (file.size > maxSize) {
|
|
61
|
+
error = 'tooLarge';
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
newFiles.push({
|
|
65
|
+
file,
|
|
66
|
+
name: file.name,
|
|
67
|
+
size: file.size,
|
|
68
|
+
content: null,
|
|
69
|
+
loading: !error,
|
|
70
|
+
error,
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
setFiles(prev => {
|
|
75
|
+
const merged = [...prev];
|
|
76
|
+
for (const f of newFiles) {
|
|
77
|
+
const isDup = merged.some(m =>
|
|
78
|
+
m.name === f.name && m.size === f.size
|
|
79
|
+
);
|
|
80
|
+
if (!isDup && merged.length < MAX_FILES) merged.push(f);
|
|
81
|
+
}
|
|
82
|
+
return merged;
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
for (const f of newFiles) {
|
|
86
|
+
if (f.error) continue;
|
|
87
|
+
try {
|
|
88
|
+
const text = await f.file.text();
|
|
89
|
+
setFiles(prev => prev.map(p =>
|
|
90
|
+
p.name === f.name && p.size === f.size
|
|
91
|
+
? { ...p, content: text, loading: false }
|
|
92
|
+
: p
|
|
93
|
+
));
|
|
94
|
+
} catch {
|
|
95
|
+
setFiles(prev => prev.map(p =>
|
|
96
|
+
p.name === f.name && p.size === f.size
|
|
97
|
+
? { ...p, loading: false, error: 'readFailed' }
|
|
98
|
+
: p
|
|
99
|
+
));
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}, []);
|
|
103
|
+
|
|
104
|
+
const removeFile = useCallback((index: number) => {
|
|
105
|
+
setFiles(prev => prev.filter((_, i) => i !== index));
|
|
106
|
+
}, []);
|
|
107
|
+
|
|
108
|
+
const clearFiles = useCallback(() => {
|
|
109
|
+
setFiles([]);
|
|
110
|
+
setStep('select');
|
|
111
|
+
setResult(null);
|
|
112
|
+
}, []);
|
|
113
|
+
|
|
114
|
+
const validFiles = files.filter(f => !f.error && f.content !== null);
|
|
115
|
+
const allReady = files.length > 0 && files.every(f => !f.loading);
|
|
116
|
+
const hasErrors = files.some(f => f.error);
|
|
117
|
+
|
|
118
|
+
const doArchive = useCallback(async () => {
|
|
119
|
+
if (validFiles.length === 0) return;
|
|
120
|
+
setImporting(true);
|
|
121
|
+
setStep('importing');
|
|
122
|
+
|
|
123
|
+
try {
|
|
124
|
+
const payload = {
|
|
125
|
+
files: validFiles.map(f => ({
|
|
126
|
+
name: f.name,
|
|
127
|
+
content: f.content!,
|
|
128
|
+
})),
|
|
129
|
+
targetSpace: targetSpace || undefined,
|
|
130
|
+
conflict,
|
|
131
|
+
organize: true,
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
const res = await fetch('/api/file/import', {
|
|
135
|
+
method: 'POST',
|
|
136
|
+
headers: { 'Content-Type': 'application/json' },
|
|
137
|
+
body: JSON.stringify(payload),
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
const data = await res.json();
|
|
141
|
+
if (!res.ok) throw new Error(data.error || `HTTP ${res.status}`);
|
|
142
|
+
|
|
143
|
+
setResult(data);
|
|
144
|
+
setStep('done');
|
|
145
|
+
} catch (err) {
|
|
146
|
+
setResult({
|
|
147
|
+
created: [],
|
|
148
|
+
skipped: [],
|
|
149
|
+
errors: [{ name: '*', error: (err as Error).message }],
|
|
150
|
+
updatedFiles: [],
|
|
151
|
+
});
|
|
152
|
+
setStep('done');
|
|
153
|
+
} finally {
|
|
154
|
+
setImporting(false);
|
|
155
|
+
}
|
|
156
|
+
}, [validFiles, targetSpace, conflict]);
|
|
157
|
+
|
|
158
|
+
const reset = useCallback(() => {
|
|
159
|
+
setFiles([]);
|
|
160
|
+
setStep('select');
|
|
161
|
+
setIntent('archive');
|
|
162
|
+
setTargetSpace('');
|
|
163
|
+
setConflict('rename');
|
|
164
|
+
setImporting(false);
|
|
165
|
+
setResult(null);
|
|
166
|
+
}, []);
|
|
167
|
+
|
|
168
|
+
return {
|
|
169
|
+
files,
|
|
170
|
+
step,
|
|
171
|
+
intent,
|
|
172
|
+
targetSpace,
|
|
173
|
+
conflict,
|
|
174
|
+
importing,
|
|
175
|
+
result,
|
|
176
|
+
inputRef,
|
|
177
|
+
validFiles,
|
|
178
|
+
allReady,
|
|
179
|
+
hasErrors,
|
|
180
|
+
addFiles,
|
|
181
|
+
removeFile,
|
|
182
|
+
clearFiles,
|
|
183
|
+
setStep,
|
|
184
|
+
setIntent,
|
|
185
|
+
setTargetSpace,
|
|
186
|
+
setConflict,
|
|
187
|
+
doArchive,
|
|
188
|
+
reset,
|
|
189
|
+
formatSize,
|
|
190
|
+
};
|
|
191
|
+
}
|
|
@@ -115,6 +115,16 @@ export function useFileUpload() {
|
|
|
115
115
|
setUploadError('');
|
|
116
116
|
}, []);
|
|
117
117
|
|
|
118
|
+
const injectFiles = useCallback((files: LocalAttachment[]) => {
|
|
119
|
+
setLocalAttachments(prev => {
|
|
120
|
+
const merged = [...prev];
|
|
121
|
+
for (const item of files) {
|
|
122
|
+
if (!merged.some(m => m.name === item.name)) merged.push(item);
|
|
123
|
+
}
|
|
124
|
+
return merged;
|
|
125
|
+
});
|
|
126
|
+
}, []);
|
|
127
|
+
|
|
118
128
|
return {
|
|
119
129
|
localAttachments,
|
|
120
130
|
uploadError,
|
|
@@ -122,5 +132,6 @@ export function useFileUpload() {
|
|
|
122
132
|
pickFiles,
|
|
123
133
|
removeAttachment,
|
|
124
134
|
clearAttachments,
|
|
135
|
+
injectFiles,
|
|
125
136
|
};
|
|
126
137
|
}
|
package/app/lib/agent/context.ts
CHANGED
|
@@ -227,7 +227,7 @@ export async function compactMessages(
|
|
|
227
227
|
|
|
228
228
|
console.log(`[ask] Compacted ${earlyMessages.length} early messages into summary (${summaryText.length} chars)`);
|
|
229
229
|
|
|
230
|
-
const summaryContent = `[
|
|
230
|
+
const summaryContent = `[System Note: Older conversation history has been truncated due to context length limits, but here is an AI-generated summary of what was discussed so far.]\n\n${summaryText}`;
|
|
231
231
|
|
|
232
232
|
// If first recent message is also 'user', merge summary into it to avoid
|
|
233
233
|
// consecutive user messages (Anthropic rejects user→user sequences).
|
|
@@ -316,10 +316,15 @@ export function hardPrune(
|
|
|
316
316
|
console.log(`[ask] Hard pruned ${cutIdx} messages, injecting synthetic user message (${messages.length} → ${pruned.length + 1})`);
|
|
317
317
|
const syntheticUser: UserMessage = {
|
|
318
318
|
role: 'user',
|
|
319
|
-
content: '[
|
|
319
|
+
content: '[System Note: Older conversation history has been truncated due to context length limits. The user may refer to things you can no longer see. If so, kindly ask them to repeat the context.]',
|
|
320
320
|
timestamp: Date.now(),
|
|
321
321
|
};
|
|
322
322
|
return [syntheticUser as AgentMessage, ...pruned];
|
|
323
|
+
} else if (cutIdx > 0 && pruned.length > 0 && (pruned[0] as any).role === 'user') {
|
|
324
|
+
// If we pruned and the first message IS a user message, prepend the warning to it
|
|
325
|
+
const firstMsg = { ...pruned[0] } as UserMessage;
|
|
326
|
+
firstMsg.content = `[System Note: Older conversation history has been truncated due to context length limits. The user may refer to things you can no longer see. If so, kindly ask them to repeat the context.]\n\n` + firstMsg.content;
|
|
327
|
+
pruned[0] = firstMsg as AgentMessage;
|
|
323
328
|
}
|
|
324
329
|
|
|
325
330
|
if (cutIdx > 0) {
|