@geminilight/mindos 0.5.18 → 0.5.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 +5 -4
- package/app/app/api/file/route.ts +35 -11
- package/app/app/api/setup/route.ts +64 -1
- package/app/app/api/skills/route.ts +22 -3
- package/app/app/globals.css +1 -0
- package/app/components/AskFab.tsx +49 -3
- package/app/components/AskModal.tsx +11 -2
- package/app/components/GuideCard.tsx +361 -0
- package/app/components/HomeContent.tsx +2 -2
- package/app/components/Sidebar.tsx +21 -1
- package/app/components/ask/ToolCallBlock.tsx +2 -1
- package/app/components/settings/KnowledgeTab.tsx +64 -2
- package/app/components/settings/McpTab.tsx +286 -56
- package/app/components/setup/StepAI.tsx +9 -1
- package/app/components/setup/index.tsx +4 -0
- package/app/components/setup/types.ts +2 -0
- package/app/hooks/useAskModal.ts +46 -0
- package/app/lib/agent/stream-consumer.ts +4 -2
- package/app/lib/agent/tools.ts +26 -12
- package/app/lib/fs.ts +9 -1
- package/app/lib/i18n.ts +16 -0
- package/app/lib/settings.ts +29 -0
- package/app/next-env.d.ts +1 -1
- package/app/next.config.ts +7 -0
- package/bin/cli.js +135 -9
- package/bin/lib/build.js +2 -7
- package/bin/lib/mcp-spawn.js +2 -13
- package/bin/lib/utils.js +23 -0
- package/package.json +1 -1
- package/scripts/setup.js +13 -0
- package/skills/mindos/SKILL.md +10 -168
- package/skills/mindos-zh/SKILL.md +14 -172
- package/skills/project-wiki/SKILL.md +80 -74
- package/skills/project-wiki/references/file-reference.md +6 -2
- package/templates/skill-rules/en/skill-rules.md +222 -0
- package/templates/skill-rules/en/user-rules.md +20 -0
- package/templates/skill-rules/zh/skill-rules.md +222 -0
- package/templates/skill-rules/zh/user-rules.md +20 -0
package/app/app/api/ask/route.ts
CHANGED
|
@@ -57,11 +57,12 @@ function convertToModelMessages(messages: FrontendMessage[]): ModelMessage[] {
|
|
|
57
57
|
type: 'tool-call',
|
|
58
58
|
toolCallId: part.toolCallId,
|
|
59
59
|
toolName: part.toolName,
|
|
60
|
-
input: part.input,
|
|
60
|
+
input: part.input ?? {},
|
|
61
61
|
});
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
62
|
+
// Always emit a tool result for every tool call. Orphaned tool calls
|
|
63
|
+
// (running/pending from interrupted streams) get an empty result;
|
|
64
|
+
// without one the API rejects the request.
|
|
65
|
+
completedToolCalls.push(part);
|
|
65
66
|
}
|
|
66
67
|
// 'reasoning' parts are display-only; not sent back to model
|
|
67
68
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export const dynamic = 'force-dynamic';
|
|
2
2
|
import { NextRequest, NextResponse } from 'next/server';
|
|
3
|
+
import { revalidatePath } from 'next/cache';
|
|
3
4
|
import {
|
|
4
5
|
getFileContent,
|
|
5
6
|
saveFileContent,
|
|
@@ -37,6 +38,9 @@ export async function GET(req: NextRequest) {
|
|
|
37
38
|
}
|
|
38
39
|
}
|
|
39
40
|
|
|
41
|
+
// Ops that change file tree structure (sidebar needs refresh)
|
|
42
|
+
const TREE_CHANGING_OPS = new Set(['create_file', 'delete_file', 'rename_file', 'move_file']);
|
|
43
|
+
|
|
40
44
|
// POST /api/file body: { op, path, ...params }
|
|
41
45
|
export async function POST(req: NextRequest) {
|
|
42
46
|
let body: Record<string, unknown>;
|
|
@@ -47,20 +51,24 @@ export async function POST(req: NextRequest) {
|
|
|
47
51
|
if (!filePath || typeof filePath !== 'string') return err('missing path');
|
|
48
52
|
|
|
49
53
|
try {
|
|
54
|
+
let resp: NextResponse;
|
|
55
|
+
|
|
50
56
|
switch (op) {
|
|
51
57
|
|
|
52
58
|
case 'save_file': {
|
|
53
59
|
const { content } = params as { content: string };
|
|
54
60
|
if (typeof content !== 'string') return err('missing content');
|
|
55
61
|
saveFileContent(filePath, content);
|
|
56
|
-
|
|
62
|
+
resp = NextResponse.json({ ok: true });
|
|
63
|
+
break;
|
|
57
64
|
}
|
|
58
65
|
|
|
59
66
|
case 'append_to_file': {
|
|
60
67
|
const { content } = params as { content: string };
|
|
61
68
|
if (typeof content !== 'string') return err('missing content');
|
|
62
69
|
appendToFile(filePath, content);
|
|
63
|
-
|
|
70
|
+
resp = NextResponse.json({ ok: true });
|
|
71
|
+
break;
|
|
64
72
|
}
|
|
65
73
|
|
|
66
74
|
case 'insert_lines': {
|
|
@@ -68,7 +76,8 @@ export async function POST(req: NextRequest) {
|
|
|
68
76
|
if (typeof after_index !== 'number') return err('missing after_index');
|
|
69
77
|
if (!Array.isArray(lines)) return err('lines must be array');
|
|
70
78
|
insertLines(filePath, after_index, lines);
|
|
71
|
-
|
|
79
|
+
resp = NextResponse.json({ ok: true });
|
|
80
|
+
break;
|
|
72
81
|
}
|
|
73
82
|
|
|
74
83
|
case 'update_lines': {
|
|
@@ -78,7 +87,8 @@ export async function POST(req: NextRequest) {
|
|
|
78
87
|
if (start < 0 || end < 0) return err('start/end must be >= 0');
|
|
79
88
|
if (start > end) return err('start must be <= end');
|
|
80
89
|
updateLines(filePath, start, end, lines);
|
|
81
|
-
|
|
90
|
+
resp = NextResponse.json({ ok: true });
|
|
91
|
+
break;
|
|
82
92
|
}
|
|
83
93
|
|
|
84
94
|
case 'insert_after_heading': {
|
|
@@ -86,7 +96,8 @@ export async function POST(req: NextRequest) {
|
|
|
86
96
|
if (typeof heading !== 'string') return err('missing heading');
|
|
87
97
|
if (typeof content !== 'string') return err('missing content');
|
|
88
98
|
insertAfterHeading(filePath, heading, content);
|
|
89
|
-
|
|
99
|
+
resp = NextResponse.json({ ok: true });
|
|
100
|
+
break;
|
|
90
101
|
}
|
|
91
102
|
|
|
92
103
|
case 'update_section': {
|
|
@@ -94,44 +105,57 @@ export async function POST(req: NextRequest) {
|
|
|
94
105
|
if (typeof heading !== 'string') return err('missing heading');
|
|
95
106
|
if (typeof content !== 'string') return err('missing content');
|
|
96
107
|
updateSection(filePath, heading, content);
|
|
97
|
-
|
|
108
|
+
resp = NextResponse.json({ ok: true });
|
|
109
|
+
break;
|
|
98
110
|
}
|
|
99
111
|
|
|
100
112
|
case 'delete_file': {
|
|
101
113
|
deleteFile(filePath);
|
|
102
|
-
|
|
114
|
+
resp = NextResponse.json({ ok: true });
|
|
115
|
+
break;
|
|
103
116
|
}
|
|
104
117
|
|
|
105
118
|
case 'rename_file': {
|
|
106
119
|
const { new_name } = params as { new_name: string };
|
|
107
120
|
if (typeof new_name !== 'string' || !new_name) return err('missing new_name');
|
|
108
121
|
const newPath = renameFile(filePath, new_name);
|
|
109
|
-
|
|
122
|
+
resp = NextResponse.json({ ok: true, newPath });
|
|
123
|
+
break;
|
|
110
124
|
}
|
|
111
125
|
|
|
112
126
|
case 'create_file': {
|
|
113
127
|
const { content } = params as { content?: string };
|
|
114
128
|
createFile(filePath, typeof content === 'string' ? content : '');
|
|
115
|
-
|
|
129
|
+
resp = NextResponse.json({ ok: true });
|
|
130
|
+
break;
|
|
116
131
|
}
|
|
117
132
|
|
|
118
133
|
case 'move_file': {
|
|
119
134
|
const { to_path } = params as { to_path: string };
|
|
120
135
|
if (typeof to_path !== 'string' || !to_path) return err('missing to_path');
|
|
121
136
|
const result = moveFile(filePath, to_path);
|
|
122
|
-
|
|
137
|
+
resp = NextResponse.json({ ok: true, ...result });
|
|
138
|
+
break;
|
|
123
139
|
}
|
|
124
140
|
|
|
125
141
|
case 'append_csv': {
|
|
126
142
|
const { row } = params as { row: string[] };
|
|
127
143
|
if (!Array.isArray(row) || row.length === 0) return err('row must be non-empty array');
|
|
128
144
|
const result = appendCsvRow(filePath, row);
|
|
129
|
-
|
|
145
|
+
resp = NextResponse.json({ ok: true, ...result });
|
|
146
|
+
break;
|
|
130
147
|
}
|
|
131
148
|
|
|
132
149
|
default:
|
|
133
150
|
return err(`unknown op: ${op}`);
|
|
134
151
|
}
|
|
152
|
+
|
|
153
|
+
// Invalidate Next.js router cache so sidebar file tree updates
|
|
154
|
+
if (TREE_CHANGING_OPS.has(op)) {
|
|
155
|
+
try { revalidatePath('/', 'layout'); } catch { /* noop in test env */ }
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return resp;
|
|
135
159
|
} catch (e) {
|
|
136
160
|
return err((e as Error).message, 500);
|
|
137
161
|
}
|
|
@@ -30,6 +30,7 @@ export async function GET() {
|
|
|
30
30
|
openaiApiKey: maskApiKey(s.ai.providers.openai.apiKey),
|
|
31
31
|
openaiModel: s.ai.providers.openai.model,
|
|
32
32
|
openaiBaseUrl: s.ai.providers.openai.baseUrl ?? '',
|
|
33
|
+
guideState: s.guideState ?? null,
|
|
33
34
|
});
|
|
34
35
|
} catch (e) {
|
|
35
36
|
return NextResponse.json(
|
|
@@ -95,9 +96,33 @@ export async function POST(req: NextRequest) {
|
|
|
95
96
|
);
|
|
96
97
|
|
|
97
98
|
// Build config
|
|
99
|
+
// Merge AI config: empty apiKey means "keep existing" — never overwrite a
|
|
100
|
+
// configured key with blank just because the user didn't re-enter it.
|
|
101
|
+
let mergedAi = current.ai;
|
|
102
|
+
if (ai) {
|
|
103
|
+
const inAnthropicKey = ai.providers?.anthropic?.apiKey;
|
|
104
|
+
const inOpenaiKey = ai.providers?.openai?.apiKey;
|
|
105
|
+
mergedAi = {
|
|
106
|
+
provider: ai.provider ?? current.ai.provider,
|
|
107
|
+
providers: {
|
|
108
|
+
anthropic: {
|
|
109
|
+
apiKey: inAnthropicKey || current.ai.providers.anthropic.apiKey,
|
|
110
|
+
model: ai.providers?.anthropic?.model || current.ai.providers.anthropic.model,
|
|
111
|
+
},
|
|
112
|
+
openai: {
|
|
113
|
+
apiKey: inOpenaiKey || current.ai.providers.openai.apiKey,
|
|
114
|
+
model: ai.providers?.openai?.model || current.ai.providers.openai.model,
|
|
115
|
+
baseUrl: ai.providers?.openai?.baseUrl ?? current.ai.providers.openai.baseUrl ?? '',
|
|
116
|
+
},
|
|
117
|
+
},
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
98
121
|
const disabledSkills = body.template === 'zh' ? ['mindos'] : ['mindos-zh'];
|
|
122
|
+
// Determine guide template from setup template
|
|
123
|
+
const guideTemplate = body.template === 'zh' ? 'zh' : body.template === 'empty' ? 'empty' : 'en';
|
|
99
124
|
const config: ServerSettings = {
|
|
100
|
-
ai:
|
|
125
|
+
ai: mergedAi,
|
|
101
126
|
mindRoot: resolvedRoot,
|
|
102
127
|
port: webPort,
|
|
103
128
|
mcpPort: mcpPortNum,
|
|
@@ -106,6 +131,14 @@ export async function POST(req: NextRequest) {
|
|
|
106
131
|
startMode: current.startMode,
|
|
107
132
|
setupPending: false, // clear the flag
|
|
108
133
|
disabledSkills,
|
|
134
|
+
guideState: {
|
|
135
|
+
active: true,
|
|
136
|
+
dismissed: false,
|
|
137
|
+
template: guideTemplate as 'en' | 'zh' | 'empty',
|
|
138
|
+
step1Done: false,
|
|
139
|
+
askedAI: false,
|
|
140
|
+
nextStepIndex: 0,
|
|
141
|
+
},
|
|
109
142
|
};
|
|
110
143
|
|
|
111
144
|
writeSettings(config);
|
|
@@ -124,3 +157,33 @@ export async function POST(req: NextRequest) {
|
|
|
124
157
|
);
|
|
125
158
|
}
|
|
126
159
|
}
|
|
160
|
+
|
|
161
|
+
export async function PATCH(req: NextRequest) {
|
|
162
|
+
try {
|
|
163
|
+
const body = await req.json();
|
|
164
|
+
const { guideState: patch } = body;
|
|
165
|
+
if (!patch || typeof patch !== 'object') {
|
|
166
|
+
return NextResponse.json({ error: 'guideState object required' }, { status: 400 });
|
|
167
|
+
}
|
|
168
|
+
const current = readSettings();
|
|
169
|
+
const existing = current.guideState ?? {
|
|
170
|
+
active: false, dismissed: false, template: 'en' as const,
|
|
171
|
+
step1Done: false, askedAI: false, nextStepIndex: 0,
|
|
172
|
+
};
|
|
173
|
+
// Merge only known fields
|
|
174
|
+
const updated = { ...existing };
|
|
175
|
+
if (typeof patch.dismissed === 'boolean') updated.dismissed = patch.dismissed;
|
|
176
|
+
if (typeof patch.step1Done === 'boolean') updated.step1Done = patch.step1Done;
|
|
177
|
+
if (typeof patch.askedAI === 'boolean') updated.askedAI = patch.askedAI;
|
|
178
|
+
if (typeof patch.nextStepIndex === 'number' && patch.nextStepIndex >= 0) updated.nextStepIndex = patch.nextStepIndex;
|
|
179
|
+
if (typeof patch.active === 'boolean') updated.active = patch.active;
|
|
180
|
+
|
|
181
|
+
writeSettings({ ...current, guideState: updated });
|
|
182
|
+
return NextResponse.json({ ok: true, guideState: updated });
|
|
183
|
+
} catch (e) {
|
|
184
|
+
return NextResponse.json(
|
|
185
|
+
{ error: e instanceof Error ? e.message : String(e) },
|
|
186
|
+
{ status: 500 },
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
@@ -129,7 +129,7 @@ export async function POST(req: NextRequest) {
|
|
|
129
129
|
try {
|
|
130
130
|
const body = await req.json();
|
|
131
131
|
const { action, name, description, content, enabled } = body as {
|
|
132
|
-
action: 'create' | 'update' | 'delete' | 'toggle';
|
|
132
|
+
action: 'create' | 'update' | 'delete' | 'toggle' | 'read';
|
|
133
133
|
name?: string;
|
|
134
134
|
description?: string;
|
|
135
135
|
content?: string;
|
|
@@ -172,8 +172,11 @@ export async function POST(req: NextRequest) {
|
|
|
172
172
|
return NextResponse.json({ error: 'A skill with this name already exists' }, { status: 409 });
|
|
173
173
|
}
|
|
174
174
|
fs.mkdirSync(skillDir, { recursive: true });
|
|
175
|
-
|
|
176
|
-
|
|
175
|
+
// If content already has frontmatter, use it as-is; otherwise build frontmatter
|
|
176
|
+
const fileContent = content && content.trimStart().startsWith('---')
|
|
177
|
+
? content
|
|
178
|
+
: `---\nname: ${name}\ndescription: ${description || name}\n---\n\n${content || ''}`;
|
|
179
|
+
fs.writeFileSync(path.join(skillDir, 'SKILL.md'), fileContent, 'utf-8');
|
|
177
180
|
return NextResponse.json({ ok: true });
|
|
178
181
|
}
|
|
179
182
|
|
|
@@ -199,6 +202,22 @@ export async function POST(req: NextRequest) {
|
|
|
199
202
|
return NextResponse.json({ ok: true });
|
|
200
203
|
}
|
|
201
204
|
|
|
205
|
+
case 'read': {
|
|
206
|
+
if (!name) return NextResponse.json({ error: 'name required' }, { status: 400 });
|
|
207
|
+
const dirs = [
|
|
208
|
+
path.join(PROJECT_ROOT, 'app', 'data', 'skills', name),
|
|
209
|
+
path.join(PROJECT_ROOT, 'skills', name),
|
|
210
|
+
path.join(userSkillsDir, name),
|
|
211
|
+
];
|
|
212
|
+
for (const dir of dirs) {
|
|
213
|
+
const file = path.join(dir, 'SKILL.md');
|
|
214
|
+
if (fs.existsSync(file)) {
|
|
215
|
+
return NextResponse.json({ content: fs.readFileSync(file, 'utf-8') });
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
return NextResponse.json({ error: 'Skill not found' }, { status: 404 });
|
|
219
|
+
}
|
|
220
|
+
|
|
202
221
|
default:
|
|
203
222
|
return NextResponse.json({ error: `Unknown action: ${action}` }, { status: 400 });
|
|
204
223
|
}
|
package/app/app/globals.css
CHANGED
|
@@ -288,6 +288,7 @@ body {
|
|
|
288
288
|
/* Micro type scale: text-2xs = 10px (between nothing and text-xs 12px) */
|
|
289
289
|
@layer utilities {
|
|
290
290
|
.text-2xs { font-size: 10px; line-height: 1.4; }
|
|
291
|
+
.animate-spin-slow { animation: spin 3s linear infinite; }
|
|
291
292
|
}
|
|
292
293
|
|
|
293
294
|
/* Hide scrollbar but keep scroll functionality */
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import { useState } from 'react';
|
|
3
|
+
import { useState, useEffect, useCallback } from 'react';
|
|
4
4
|
import { usePathname } from 'next/navigation';
|
|
5
5
|
import { Sparkles } from 'lucide-react';
|
|
6
6
|
import AskModal from './AskModal';
|
|
7
|
+
import { useAskModal } from '@/hooks/useAskModal';
|
|
7
8
|
|
|
8
9
|
export default function AskFab() {
|
|
9
10
|
const [open, setOpen] = useState(false);
|
|
@@ -12,10 +13,49 @@ export default function AskFab() {
|
|
|
12
13
|
? pathname.slice('/view/'.length).split('/').map(decodeURIComponent).join('/')
|
|
13
14
|
: undefined;
|
|
14
15
|
|
|
16
|
+
// Listen to useAskModal store for cross-component open requests (e.g. from GuideCard)
|
|
17
|
+
const askModal = useAskModal();
|
|
18
|
+
const [initialMessage, setInitialMessage] = useState('');
|
|
19
|
+
const [openSource, setOpenSource] = useState<'user' | 'guide' | 'guide-next'>('user');
|
|
20
|
+
|
|
21
|
+
useEffect(() => {
|
|
22
|
+
if (askModal.open) {
|
|
23
|
+
setInitialMessage(askModal.initialMessage);
|
|
24
|
+
setOpenSource(askModal.source);
|
|
25
|
+
setOpen(true);
|
|
26
|
+
askModal.close(); // Reset store state after consuming
|
|
27
|
+
}
|
|
28
|
+
}, [askModal.open, askModal.initialMessage, askModal.source, askModal.close]);
|
|
29
|
+
|
|
30
|
+
const handleClose = useCallback(() => {
|
|
31
|
+
setOpen(false);
|
|
32
|
+
setInitialMessage('');
|
|
33
|
+
setOpenSource('user');
|
|
34
|
+
}, []);
|
|
35
|
+
|
|
36
|
+
// Dispatch correct PATCH based on how the modal was opened
|
|
37
|
+
const handleFirstMessage = useCallback(() => {
|
|
38
|
+
const notifyGuide = () => window.dispatchEvent(new Event('guide-state-updated'));
|
|
39
|
+
|
|
40
|
+
if (openSource === 'guide') {
|
|
41
|
+
// Task ② completion: mark askedAI
|
|
42
|
+
fetch('/api/setup', {
|
|
43
|
+
method: 'PATCH',
|
|
44
|
+
headers: { 'Content-Type': 'application/json' },
|
|
45
|
+
body: JSON.stringify({ guideState: { askedAI: true } }),
|
|
46
|
+
}).then(notifyGuide).catch(() => {});
|
|
47
|
+
} else if (openSource === 'guide-next') {
|
|
48
|
+
// Next-step advancement: GuideCard already PATCHed nextStepIndex optimistically.
|
|
49
|
+
// Just notify GuideCard to re-fetch for consistency; no additional PATCH needed.
|
|
50
|
+
notifyGuide();
|
|
51
|
+
}
|
|
52
|
+
// For 'user' source: no guide action needed
|
|
53
|
+
}, [openSource]);
|
|
54
|
+
|
|
15
55
|
return (
|
|
16
56
|
<>
|
|
17
57
|
<button
|
|
18
|
-
onClick={() => setOpen(true)}
|
|
58
|
+
onClick={() => { setInitialMessage(''); setOpenSource('user'); setOpen(true); }}
|
|
19
59
|
className="
|
|
20
60
|
group
|
|
21
61
|
fixed z-40
|
|
@@ -53,7 +93,13 @@ export default function AskFab() {
|
|
|
53
93
|
</span>
|
|
54
94
|
</button>
|
|
55
95
|
|
|
56
|
-
<AskModal
|
|
96
|
+
<AskModal
|
|
97
|
+
open={open}
|
|
98
|
+
onClose={handleClose}
|
|
99
|
+
currentFile={currentFile}
|
|
100
|
+
initialMessage={initialMessage}
|
|
101
|
+
onFirstMessage={handleFirstMessage}
|
|
102
|
+
/>
|
|
57
103
|
</>
|
|
58
104
|
);
|
|
59
105
|
}
|
|
@@ -17,11 +17,14 @@ interface AskModalProps {
|
|
|
17
17
|
open: boolean;
|
|
18
18
|
onClose: () => void;
|
|
19
19
|
currentFile?: string;
|
|
20
|
+
initialMessage?: string;
|
|
21
|
+
onFirstMessage?: () => void;
|
|
20
22
|
}
|
|
21
23
|
|
|
22
|
-
export default function AskModal({ open, onClose, currentFile }: AskModalProps) {
|
|
24
|
+
export default function AskModal({ open, onClose, currentFile, initialMessage, onFirstMessage }: AskModalProps) {
|
|
23
25
|
const inputRef = useRef<HTMLInputElement>(null);
|
|
24
26
|
const abortRef = useRef<AbortController | null>(null);
|
|
27
|
+
const firstMessageFired = useRef(false);
|
|
25
28
|
const { t } = useLocale();
|
|
26
29
|
|
|
27
30
|
const [input, setInput] = useState('');
|
|
@@ -43,7 +46,8 @@ export default function AskModal({ open, onClose, currentFile }: AskModalProps)
|
|
|
43
46
|
if (cancelled) return;
|
|
44
47
|
await session.initSessions();
|
|
45
48
|
})();
|
|
46
|
-
setInput('');
|
|
49
|
+
setInput(initialMessage || '');
|
|
50
|
+
firstMessageFired.current = false;
|
|
47
51
|
setAttachedFiles(currentFile ? [currentFile] : []);
|
|
48
52
|
upload.clearAttachments();
|
|
49
53
|
mention.resetMention();
|
|
@@ -119,6 +123,11 @@ export default function AskModal({ open, onClose, currentFile }: AskModalProps)
|
|
|
119
123
|
const requestMessages = [...session.messages, userMsg];
|
|
120
124
|
session.setMessages([...requestMessages, { role: 'assistant', content: '' }]);
|
|
121
125
|
setInput('');
|
|
126
|
+
// Notify guide card on first user message (ref prevents duplicate fires during re-render)
|
|
127
|
+
if (onFirstMessage && !firstMessageFired.current) {
|
|
128
|
+
firstMessageFired.current = true;
|
|
129
|
+
onFirstMessage();
|
|
130
|
+
}
|
|
122
131
|
setAttachedFiles(currentFile ? [currentFile] : []);
|
|
123
132
|
setIsLoading(true);
|
|
124
133
|
setLoadingPhase('connecting');
|