@code4bug/jarvis-agent 1.1.6 → 1.1.7
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/README.md +171 -215
- package/dist/commands/index.d.ts +2 -0
- package/dist/commands/index.js +17 -15
- package/dist/components/MultilineInput.js +2 -2
- package/dist/components/WelcomeHeader.js +1 -1
- package/dist/config/dream.d.ts +10 -0
- package/dist/config/dream.js +60 -0
- package/dist/config/userProfile.d.ts +5 -1
- package/dist/config/userProfile.js +15 -2
- package/dist/core/QueryEngine.d.ts +9 -0
- package/dist/core/QueryEngine.js +83 -8
- package/dist/core/WorkerBridge.d.ts +3 -1
- package/dist/core/WorkerBridge.js +2 -2
- package/dist/core/query.d.ts +5 -1
- package/dist/core/query.js +4 -4
- package/dist/core/queryWorker.d.ts +3 -0
- package/dist/core/queryWorker.js +1 -1
- package/dist/hooks/useSlashMenu.d.ts +3 -1
- package/dist/hooks/useSlashMenu.js +58 -71
- package/dist/screens/repl.js +63 -34
- package/dist/services/api/llm.d.ts +5 -2
- package/dist/services/api/llm.js +20 -7
- package/dist/services/api/mock.d.ts +3 -1
- package/dist/services/api/mock.js +1 -1
- package/dist/services/dream.d.ts +7 -0
- package/dist/services/dream.js +171 -0
- package/dist/services/userProfile.d.ts +1 -0
- package/dist/services/userProfile.js +15 -0
- package/dist/types/index.d.ts +3 -1
- package/package.json +3 -3
|
@@ -2,6 +2,7 @@ import fs from 'fs';
|
|
|
2
2
|
import os from 'os';
|
|
3
3
|
import path from 'path';
|
|
4
4
|
export const USER_PROFILE_PATH = path.join(os.homedir(), '.jarvis', 'USER.md');
|
|
5
|
+
let cachedUserProfile = '';
|
|
5
6
|
export function ensureJarvisHomeDir() {
|
|
6
7
|
const dir = path.dirname(USER_PROFILE_PATH);
|
|
7
8
|
if (!fs.existsSync(dir)) {
|
|
@@ -19,7 +20,19 @@ export function readUserProfile() {
|
|
|
19
20
|
return '';
|
|
20
21
|
}
|
|
21
22
|
}
|
|
22
|
-
export function
|
|
23
|
+
export function initializeUserProfileCache() {
|
|
24
|
+
cachedUserProfile = readUserProfile();
|
|
25
|
+
return cachedUserProfile;
|
|
26
|
+
}
|
|
27
|
+
export function getCachedUserProfile() {
|
|
28
|
+
return cachedUserProfile;
|
|
29
|
+
}
|
|
30
|
+
export function writeUserProfile(content, options) {
|
|
23
31
|
ensureJarvisHomeDir();
|
|
24
|
-
|
|
32
|
+
const normalizedContent = content.trimEnd();
|
|
33
|
+
fs.writeFileSync(USER_PROFILE_PATH, normalizedContent + '\n', 'utf-8');
|
|
34
|
+
if (options?.updateCache) {
|
|
35
|
+
cachedUserProfile = normalizedContent;
|
|
36
|
+
}
|
|
25
37
|
}
|
|
38
|
+
initializeUserProfileCache();
|
|
@@ -21,6 +21,11 @@ export declare class QueryEngine {
|
|
|
21
21
|
private transcript;
|
|
22
22
|
private workerBridge;
|
|
23
23
|
private memoryUpdateQueue;
|
|
24
|
+
private userProfileUpdateQueue;
|
|
25
|
+
private dreamUpdateQueue;
|
|
26
|
+
private dreamTimer;
|
|
27
|
+
private lastActivityAt;
|
|
28
|
+
private isQueryRunning;
|
|
24
29
|
constructor();
|
|
25
30
|
/** 注册持久 UI 回调,供后台 spawn_agent 子 Agent 推送消息 */
|
|
26
31
|
registerUIBus(onMessage: (msg: Message) => void, onUpdateMessage: (id: string, updates: Partial<Message>) => void): void;
|
|
@@ -40,6 +45,10 @@ export declare class QueryEngine {
|
|
|
40
45
|
/** 保存会话到文件 */
|
|
41
46
|
private saveSession;
|
|
42
47
|
private schedulePersistentMemoryUpdate;
|
|
48
|
+
private scheduleUserProfileUpdate;
|
|
49
|
+
private touchActivity;
|
|
50
|
+
private scheduleDreamTimer;
|
|
51
|
+
private runDreamCycle;
|
|
43
52
|
/** 列出所有历史会话(按更新时间倒序),返回摘要信息 */
|
|
44
53
|
static listSessions(): {
|
|
45
54
|
id: string;
|
package/dist/core/QueryEngine.js
CHANGED
|
@@ -10,18 +10,27 @@ import { setActiveAgent } from '../config/agentState.js';
|
|
|
10
10
|
import { clearAuthorizations } from './safeguard.js';
|
|
11
11
|
import { agentUIBus } from './AgentRegistry.js';
|
|
12
12
|
import { logError, logInfo, logWarn } from './logger.js';
|
|
13
|
-
import { updateUserProfileFromInput } from '../services/userProfile.js';
|
|
13
|
+
import { shouldIncludeUserProfile, updateUserProfileFromInput } from '../services/userProfile.js';
|
|
14
14
|
import { updatePersistentMemoryFromConversation } from '../services/persistentMemory.js';
|
|
15
|
+
import { updateDreamFromSession } from '../services/dream.js';
|
|
16
|
+
const DREAM_IDLE_MIN_MS = 5 * 60 * 1000;
|
|
17
|
+
const DREAM_IDLE_JITTER_MS = 5 * 60 * 1000;
|
|
15
18
|
export class QueryEngine {
|
|
16
19
|
service;
|
|
17
20
|
session;
|
|
18
21
|
transcript = [];
|
|
19
22
|
workerBridge = new WorkerBridge();
|
|
20
23
|
memoryUpdateQueue = Promise.resolve();
|
|
24
|
+
userProfileUpdateQueue = Promise.resolve();
|
|
25
|
+
dreamUpdateQueue = Promise.resolve();
|
|
26
|
+
dreamTimer = null;
|
|
27
|
+
lastActivityAt = Date.now();
|
|
28
|
+
isQueryRunning = false;
|
|
21
29
|
constructor() {
|
|
22
30
|
this.service = this.createService();
|
|
23
31
|
this.session = this.createSession();
|
|
24
32
|
this.ensureSessionDir();
|
|
33
|
+
this.scheduleDreamTimer();
|
|
25
34
|
logInfo('engine.created', {
|
|
26
35
|
sessionId: this.session.id,
|
|
27
36
|
service: this.service.constructor.name,
|
|
@@ -62,6 +71,8 @@ export class QueryEngine {
|
|
|
62
71
|
}
|
|
63
72
|
/** 处理用户输入(在独立 Worker 线程中执行) */
|
|
64
73
|
async handleQuery(userInput, callbacks) {
|
|
74
|
+
this.touchActivity();
|
|
75
|
+
this.isQueryRunning = true;
|
|
65
76
|
const previousTranscriptLength = this.transcript.length;
|
|
66
77
|
logInfo('query.received', {
|
|
67
78
|
sessionId: this.session.id,
|
|
@@ -77,12 +88,8 @@ export class QueryEngine {
|
|
|
77
88
|
};
|
|
78
89
|
callbacks.onMessage(userMsg);
|
|
79
90
|
this.session.messages.push(userMsg);
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
if (updated) {
|
|
83
|
-
this.service = this.createService();
|
|
84
|
-
}
|
|
85
|
-
}
|
|
91
|
+
const includeUserProfile = shouldIncludeUserProfile(userInput);
|
|
92
|
+
this.scheduleUserProfileUpdate(userInput);
|
|
86
93
|
// 将回调包装后传给 WorkerBridge,Worker 事件会映射回这里
|
|
87
94
|
const bridgeCallbacks = {
|
|
88
95
|
onMessage: (msg) => {
|
|
@@ -107,7 +114,7 @@ export class QueryEngine {
|
|
|
107
114
|
onSubAgentUpdateMessage: callbacks.onSubAgentUpdateMessage,
|
|
108
115
|
};
|
|
109
116
|
try {
|
|
110
|
-
this.transcript = await this.workerBridge.run(userInput, this.transcript, bridgeCallbacks);
|
|
117
|
+
this.transcript = await this.workerBridge.run(userInput, this.transcript, bridgeCallbacks, { includeUserProfile });
|
|
111
118
|
const recentTranscript = this.transcript.slice(previousTranscriptLength);
|
|
112
119
|
this.schedulePersistentMemoryUpdate(userInput, recentTranscript);
|
|
113
120
|
logInfo('query.completed', {
|
|
@@ -127,6 +134,10 @@ export class QueryEngine {
|
|
|
127
134
|
};
|
|
128
135
|
callbacks.onMessage(errMsg);
|
|
129
136
|
}
|
|
137
|
+
finally {
|
|
138
|
+
this.isQueryRunning = false;
|
|
139
|
+
this.scheduleDreamTimer();
|
|
140
|
+
}
|
|
130
141
|
this.session.updatedAt = Date.now();
|
|
131
142
|
callbacks.onSessionUpdate(this.session);
|
|
132
143
|
this.saveSession();
|
|
@@ -143,6 +154,9 @@ export class QueryEngine {
|
|
|
143
154
|
this.session = this.createSession();
|
|
144
155
|
this.transcript = [];
|
|
145
156
|
this.memoryUpdateQueue = Promise.resolve();
|
|
157
|
+
this.userProfileUpdateQueue = Promise.resolve();
|
|
158
|
+
this.dreamUpdateQueue = Promise.resolve();
|
|
159
|
+
this.touchActivity();
|
|
146
160
|
clearAuthorizations();
|
|
147
161
|
}
|
|
148
162
|
/**
|
|
@@ -161,6 +175,9 @@ export class QueryEngine {
|
|
|
161
175
|
this.session = this.createSession();
|
|
162
176
|
this.transcript = [];
|
|
163
177
|
this.memoryUpdateQueue = Promise.resolve();
|
|
178
|
+
this.userProfileUpdateQueue = Promise.resolve();
|
|
179
|
+
this.dreamUpdateQueue = Promise.resolve();
|
|
180
|
+
this.touchActivity();
|
|
164
181
|
logInfo('agent.switch.completed', {
|
|
165
182
|
sessionId: this.session.id,
|
|
166
183
|
agentName,
|
|
@@ -203,6 +220,63 @@ export class QueryEngine {
|
|
|
203
220
|
});
|
|
204
221
|
});
|
|
205
222
|
}
|
|
223
|
+
scheduleUserProfileUpdate(userInput) {
|
|
224
|
+
if (!userInput.trim())
|
|
225
|
+
return;
|
|
226
|
+
this.userProfileUpdateQueue = this.userProfileUpdateQueue
|
|
227
|
+
.catch(() => { })
|
|
228
|
+
.then(async () => {
|
|
229
|
+
await updateUserProfileFromInput(userInput);
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
touchActivity() {
|
|
233
|
+
this.lastActivityAt = Date.now();
|
|
234
|
+
this.scheduleDreamTimer();
|
|
235
|
+
}
|
|
236
|
+
scheduleDreamTimer() {
|
|
237
|
+
if (this.dreamTimer) {
|
|
238
|
+
clearTimeout(this.dreamTimer);
|
|
239
|
+
this.dreamTimer = null;
|
|
240
|
+
}
|
|
241
|
+
const delay = DREAM_IDLE_MIN_MS + Math.floor(Math.random() * DREAM_IDLE_JITTER_MS);
|
|
242
|
+
this.dreamTimer = setTimeout(() => {
|
|
243
|
+
void this.runDreamCycle();
|
|
244
|
+
}, delay);
|
|
245
|
+
}
|
|
246
|
+
async runDreamCycle() {
|
|
247
|
+
const idleMs = Date.now() - this.lastActivityAt;
|
|
248
|
+
if (this.isQueryRunning || idleMs < DREAM_IDLE_MIN_MS) {
|
|
249
|
+
this.scheduleDreamTimer();
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
const sessionSnapshot = {
|
|
253
|
+
...this.session,
|
|
254
|
+
messages: [...this.session.messages],
|
|
255
|
+
};
|
|
256
|
+
const transcriptSnapshot = [...this.transcript];
|
|
257
|
+
if (transcriptSnapshot.length === 0) {
|
|
258
|
+
this.scheduleDreamTimer();
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
this.dreamUpdateQueue = this.dreamUpdateQueue
|
|
262
|
+
.catch(() => { })
|
|
263
|
+
.then(async () => {
|
|
264
|
+
logInfo('dream.idle_triggered', {
|
|
265
|
+
sessionId: sessionSnapshot.id,
|
|
266
|
+
idleMs,
|
|
267
|
+
transcriptLength: transcriptSnapshot.length,
|
|
268
|
+
});
|
|
269
|
+
await updateDreamFromSession({
|
|
270
|
+
session: sessionSnapshot,
|
|
271
|
+
transcript: transcriptSnapshot,
|
|
272
|
+
});
|
|
273
|
+
})
|
|
274
|
+
.finally(() => {
|
|
275
|
+
if (!this.isQueryRunning && Date.now() - this.lastActivityAt >= DREAM_IDLE_MIN_MS) {
|
|
276
|
+
this.scheduleDreamTimer();
|
|
277
|
+
}
|
|
278
|
+
});
|
|
279
|
+
}
|
|
206
280
|
/** 列出所有历史会话(按更新时间倒序),返回摘要信息 */
|
|
207
281
|
static listSessions() {
|
|
208
282
|
try {
|
|
@@ -277,6 +351,7 @@ export class QueryEngine {
|
|
|
277
351
|
messageCount: cleanedMessages.length,
|
|
278
352
|
transcriptLength: this.transcript.length,
|
|
279
353
|
});
|
|
354
|
+
this.touchActivity();
|
|
280
355
|
return { session: this.session, messages: cleanedMessages };
|
|
281
356
|
}
|
|
282
357
|
catch (error) {
|
|
@@ -3,7 +3,9 @@ import { EngineCallbacks } from './QueryEngine.js';
|
|
|
3
3
|
export declare class WorkerBridge {
|
|
4
4
|
private worker;
|
|
5
5
|
/** 在独立 Worker 线程中执行查询,返回更新后的 transcript */
|
|
6
|
-
run(userInput: string, transcript: TranscriptMessage[], callbacks: EngineCallbacks
|
|
6
|
+
run(userInput: string, transcript: TranscriptMessage[], callbacks: EngineCallbacks, options?: {
|
|
7
|
+
includeUserProfile?: boolean;
|
|
8
|
+
}): Promise<TranscriptMessage[]>;
|
|
7
9
|
/** 向 Worker 发送中断信号 */
|
|
8
10
|
abort(): void;
|
|
9
11
|
}
|
|
@@ -37,7 +37,7 @@ await tsImport(workerData.__workerFile, pathToFileURL(workerData.__workerFile).h
|
|
|
37
37
|
export class WorkerBridge {
|
|
38
38
|
worker = null;
|
|
39
39
|
/** 在独立 Worker 线程中执行查询,返回更新后的 transcript */
|
|
40
|
-
run(userInput, transcript, callbacks) {
|
|
40
|
+
run(userInput, transcript, callbacks, options) {
|
|
41
41
|
return new Promise((resolve, reject) => {
|
|
42
42
|
const workerTsPath = path.join(__dirname, 'queryWorker.ts');
|
|
43
43
|
const worker = createWorker(workerTsPath);
|
|
@@ -174,7 +174,7 @@ export class WorkerBridge {
|
|
|
174
174
|
}
|
|
175
175
|
});
|
|
176
176
|
// 启动执行
|
|
177
|
-
const runMsg = { type: 'run', userInput, transcript };
|
|
177
|
+
const runMsg = { type: 'run', userInput, transcript, options };
|
|
178
178
|
worker.postMessage(runMsg);
|
|
179
179
|
});
|
|
180
180
|
}
|
package/dist/core/query.d.ts
CHANGED
|
@@ -20,12 +20,15 @@ export interface QueryCallbacks {
|
|
|
20
20
|
/** SubAgent 更新已有消息时透传 */
|
|
21
21
|
onSubAgentUpdateMessage?: (id: string, updates: Partial<Message>) => void;
|
|
22
22
|
}
|
|
23
|
+
interface QueryOptions {
|
|
24
|
+
includeUserProfile?: boolean;
|
|
25
|
+
}
|
|
23
26
|
/**
|
|
24
27
|
* 单轮 Agentic Loop:推理 → 工具调用 → 循环
|
|
25
28
|
*/
|
|
26
29
|
export declare function executeQuery(userInput: string, transcript: TranscriptMessage[], _tools: Tool[], service: LLMService, callbacks: QueryCallbacks, abortSignal: {
|
|
27
30
|
aborted: boolean;
|
|
28
|
-
}): Promise<TranscriptMessage[]>;
|
|
31
|
+
}, options?: QueryOptions): Promise<TranscriptMessage[]>;
|
|
29
32
|
/**
|
|
30
33
|
* 直接执行工具(供 Worker 线程调用)
|
|
31
34
|
* 注意:此函数在 Worker 线程中运行,不能使用 callbacks
|
|
@@ -33,3 +36,4 @@ export declare function executeQuery(userInput: string, transcript: TranscriptMe
|
|
|
33
36
|
export declare function runToolDirect(tc: ToolCallInfo, abortSignal: {
|
|
34
37
|
aborted: boolean;
|
|
35
38
|
}): Promise<string>;
|
|
39
|
+
export {};
|
package/dist/core/query.js
CHANGED
|
@@ -74,7 +74,7 @@ function compressTranscript(transcript) {
|
|
|
74
74
|
/**
|
|
75
75
|
* 单轮 Agentic Loop:推理 → 工具调用 → 循环
|
|
76
76
|
*/
|
|
77
|
-
export async function executeQuery(userInput, transcript, _tools, service, callbacks, abortSignal) {
|
|
77
|
+
export async function executeQuery(userInput, transcript, _tools, service, callbacks, abortSignal, options) {
|
|
78
78
|
logInfo('agent_loop.start', {
|
|
79
79
|
inputLength: userInput.length,
|
|
80
80
|
initialTranscriptLength: transcript.length,
|
|
@@ -95,7 +95,7 @@ export async function executeQuery(userInput, transcript, _tools, service, callb
|
|
|
95
95
|
transcriptLength: localTranscript.length,
|
|
96
96
|
});
|
|
97
97
|
callbacks.onLoopStateChange({ ...loopState });
|
|
98
|
-
const result = await runOneIteration(compressTranscript(localTranscript), _tools, service, callbacks, abortSignal);
|
|
98
|
+
const result = await runOneIteration(compressTranscript(localTranscript), _tools, service, callbacks, abortSignal, options);
|
|
99
99
|
logInfo('agent_loop.iteration.result', {
|
|
100
100
|
iteration: loopState.iteration,
|
|
101
101
|
textLength: result.text.length,
|
|
@@ -182,7 +182,7 @@ export async function executeQuery(userInput, transcript, _tools, service, callb
|
|
|
182
182
|
return localTranscript;
|
|
183
183
|
}
|
|
184
184
|
/** 执行一次 LLM 调用 */
|
|
185
|
-
async function runOneIteration(transcript, tools, service, callbacks, abortSignal) {
|
|
185
|
+
async function runOneIteration(transcript, tools, service, callbacks, abortSignal, options) {
|
|
186
186
|
const startTime = Date.now();
|
|
187
187
|
let accumulatedText = '';
|
|
188
188
|
let accumulatedThinking = '';
|
|
@@ -253,7 +253,7 @@ async function runOneIteration(transcript, tools, service, callbacks, abortSigna
|
|
|
253
253
|
},
|
|
254
254
|
onComplete: () => safeResolve(),
|
|
255
255
|
onError: (err) => reject(err),
|
|
256
|
-
}, abortSignal)
|
|
256
|
+
}, abortSignal, options)
|
|
257
257
|
.catch(reject);
|
|
258
258
|
});
|
|
259
259
|
const duration = Date.now() - startTime;
|
package/dist/core/queryWorker.js
CHANGED
|
@@ -98,7 +98,7 @@ parentPort.on('message', async (msg) => {
|
|
|
98
98
|
onSubAgentUpdateMessage: (id, updates) => send({ type: 'subagent_update_message', id, updates }),
|
|
99
99
|
};
|
|
100
100
|
try {
|
|
101
|
-
const newTranscript = await executeQuery(msg.userInput, msg.transcript, getAllTools(), service, callbacks, abortSignal);
|
|
101
|
+
const newTranscript = await executeQuery(msg.userInput, msg.transcript, getAllTools(), service, callbacks, abortSignal, msg.options);
|
|
102
102
|
logInfo('query_worker.run.done', {
|
|
103
103
|
transcriptLength: newTranscript.length,
|
|
104
104
|
});
|
|
@@ -29,8 +29,10 @@ export declare function useSlashMenu(opts: UseSlashMenuOptions): {
|
|
|
29
29
|
resumeMenuMode: boolean;
|
|
30
30
|
handleSlashMenuUp: () => void;
|
|
31
31
|
handleSlashMenuDown: () => void;
|
|
32
|
-
handleSlashMenuSelect: () => void;
|
|
33
32
|
handleSlashMenuClose: () => void;
|
|
33
|
+
getSelectedCommand: () => SlashCommand | null;
|
|
34
|
+
autocompleteSlashMenuSelection: (currentInput: string) => string;
|
|
35
|
+
openListCommand: (commandName: "agent" | "resume") => "/agent " | "/resume ";
|
|
34
36
|
updateSlashMenu: (val: string) => void;
|
|
35
37
|
setSlashMenuVisible: import("react").Dispatch<import("react").SetStateAction<boolean>>;
|
|
36
38
|
resumeSession: (sessionId: string) => void;
|
|
@@ -1,15 +1,13 @@
|
|
|
1
1
|
import { useState, useCallback } from 'react';
|
|
2
2
|
import { QueryEngine } from '../core/QueryEngine.js';
|
|
3
3
|
import { filterCommands, filterAgentCommands } from '../commands/index.js';
|
|
4
|
-
import { setActiveAgent } from '../config/agentState.js';
|
|
5
|
-
import { executeSlashCommand } from '../screens/slashCommands.js';
|
|
6
4
|
/**
|
|
7
5
|
* 斜杠命令菜单状态管理 hook
|
|
8
6
|
*
|
|
9
7
|
* 管理菜单可见性、选中项、二级菜单(agent / resume)等。
|
|
10
8
|
*/
|
|
11
9
|
export function useSlashMenu(opts) {
|
|
12
|
-
const { engineRef, sessionRef, tokenCountRef, setMessages, setDisplayTokens, setLoopState, setIsProcessing, setShowWelcome, setInput, stopAll,
|
|
10
|
+
const { engineRef, sessionRef, tokenCountRef, setMessages, setDisplayTokens, setLoopState, setIsProcessing, setShowWelcome, setInput, stopAll, } = opts;
|
|
13
11
|
const [slashMenuVisible, setSlashMenuVisible] = useState(false);
|
|
14
12
|
const [slashMenuItems, setSlashMenuItems] = useState([]);
|
|
15
13
|
const [slashMenuIndex, setSlashMenuIndex] = useState(0);
|
|
@@ -73,84 +71,70 @@ export function useSlashMenu(opts) {
|
|
|
73
71
|
setMessages((prev) => [...prev, errMsg]);
|
|
74
72
|
}
|
|
75
73
|
}, [engineRef, sessionRef, tokenCountRef, setMessages, setDisplayTokens, stopAll, setLoopState, setIsProcessing, setShowWelcome]);
|
|
76
|
-
|
|
77
|
-
const handleSlashMenuSelect = useCallback(() => {
|
|
74
|
+
const getSelectedCommand = useCallback(() => {
|
|
78
75
|
if (slashMenuItems.length === 0)
|
|
79
|
-
return;
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
if (agentMenuMode) {
|
|
85
|
-
setActiveAgent(cmd.name);
|
|
86
|
-
setInput('');
|
|
87
|
-
setSlashMenuVisible(false);
|
|
88
|
-
setAgentMenuMode(false);
|
|
89
|
-
const switchMsg = {
|
|
90
|
-
id: `switch-${Date.now()}`,
|
|
91
|
-
type: 'system',
|
|
92
|
-
status: 'success',
|
|
93
|
-
content: `已切换智能体为 ${cmd.name},请重启以生效(Ctrl+C 两次退出后重新启动)`,
|
|
94
|
-
timestamp: Date.now(),
|
|
95
|
-
};
|
|
96
|
-
setMessages((prev) => [...prev, switchMsg]);
|
|
97
|
-
return;
|
|
98
|
-
}
|
|
99
|
-
// 二级 resume 菜单
|
|
100
|
-
if (resumeMenuMode) {
|
|
101
|
-
setInput('');
|
|
102
|
-
setSlashMenuVisible(false);
|
|
103
|
-
setResumeMenuMode(false);
|
|
104
|
-
resumeSession(cmd.name);
|
|
105
|
-
return;
|
|
106
|
-
}
|
|
107
|
-
// 一级菜单选中 /agent -> 进入二级菜单
|
|
108
|
-
if (cmd.name === 'agent') {
|
|
76
|
+
return null;
|
|
77
|
+
return slashMenuItems[slashMenuIndex] ?? null;
|
|
78
|
+
}, [slashMenuItems, slashMenuIndex]);
|
|
79
|
+
const openListCommand = useCallback((commandName) => {
|
|
80
|
+
if (commandName === 'agent') {
|
|
109
81
|
setInput('/agent ');
|
|
110
82
|
const matched = filterAgentCommands('');
|
|
111
83
|
setSlashMenuItems(matched);
|
|
112
84
|
setSlashMenuIndex(0);
|
|
113
85
|
setAgentMenuMode(true);
|
|
114
86
|
setResumeMenuMode(false);
|
|
115
|
-
|
|
87
|
+
setSlashMenuVisible(matched.length > 0);
|
|
88
|
+
return '/agent ';
|
|
116
89
|
}
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
const
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
90
|
+
const sessions = QueryEngine.listSessions().slice(0, 20);
|
|
91
|
+
const items = sessions.map((s) => {
|
|
92
|
+
const date = new Date(s.updatedAt);
|
|
93
|
+
const dateStr = `${date.getMonth() + 1}/${date.getDate()} ${date.getHours().toString().padStart(2, '0')}:${date.getMinutes().toString().padStart(2, '0')}`;
|
|
94
|
+
return {
|
|
95
|
+
name: s.id,
|
|
96
|
+
description: `[${dateStr}] ${s.summary}`,
|
|
97
|
+
category: 'builtin',
|
|
98
|
+
submitMode: 'action',
|
|
99
|
+
};
|
|
100
|
+
});
|
|
101
|
+
setInput('/resume ');
|
|
102
|
+
setSlashMenuItems(items);
|
|
103
|
+
setSlashMenuIndex(0);
|
|
104
|
+
setSlashMenuVisible(items.length > 0);
|
|
105
|
+
setResumeMenuMode(true);
|
|
106
|
+
setAgentMenuMode(false);
|
|
107
|
+
return '/resume ';
|
|
108
|
+
}, [setInput]);
|
|
109
|
+
const autocompleteSlashMenuSelection = useCallback((currentInput) => {
|
|
110
|
+
const cmd = getSelectedCommand();
|
|
111
|
+
if (!cmd)
|
|
112
|
+
return currentInput;
|
|
113
|
+
// 二级 agent 菜单
|
|
114
|
+
if (agentMenuMode) {
|
|
115
|
+
const nextInput = `/agent ${cmd.name}`;
|
|
116
|
+
setInput(nextInput);
|
|
117
|
+
setSlashMenuVisible(false);
|
|
118
|
+
return nextInput;
|
|
136
119
|
}
|
|
137
|
-
//
|
|
138
|
-
if (
|
|
139
|
-
|
|
120
|
+
// 二级 resume 菜单
|
|
121
|
+
if (resumeMenuMode) {
|
|
122
|
+
const nextInput = `/resume ${cmd.name}`;
|
|
123
|
+
setInput(nextInput);
|
|
140
124
|
setSlashMenuVisible(false);
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
125
|
+
return nextInput;
|
|
126
|
+
}
|
|
127
|
+
const match = currentInput.match(/^\/\S*/);
|
|
128
|
+
const suffix = match ? currentInput.slice(match[0].length) : '';
|
|
129
|
+
const needsTrailingSpace = !suffix && (cmd.submitMode === 'context' || cmd.submitMode === 'list');
|
|
130
|
+
const nextInput = `/${cmd.name}${suffix}${needsTrailingSpace ? ' ' : ''}`;
|
|
131
|
+
setInput(nextInput);
|
|
132
|
+
if (cmd.submitMode === 'list' && (cmd.name === 'agent' || cmd.name === 'resume') && !suffix.trim()) {
|
|
133
|
+
return openListCommand(cmd.name);
|
|
149
134
|
}
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
}, [slashMenuItems, slashMenuIndex, agentMenuMode, resumeMenuMode, setInput, setMessages, resumeSession]);
|
|
135
|
+
updateSlashMenu(nextInput);
|
|
136
|
+
return nextInput;
|
|
137
|
+
}, [agentMenuMode, resumeMenuMode, getSelectedCommand, setInput, openListCommand]);
|
|
154
138
|
// 输入变化时更新菜单
|
|
155
139
|
const updateSlashMenu = useCallback((val) => {
|
|
156
140
|
if (val.startsWith('/') && !val.includes('\n')) {
|
|
@@ -177,6 +161,7 @@ export function useSlashMenu(opts) {
|
|
|
177
161
|
name: s.id,
|
|
178
162
|
description: `[${dateStr}] ${s.summary}`,
|
|
179
163
|
category: 'builtin',
|
|
164
|
+
submitMode: 'action',
|
|
180
165
|
};
|
|
181
166
|
});
|
|
182
167
|
const matched = subQuery
|
|
@@ -211,8 +196,10 @@ export function useSlashMenu(opts) {
|
|
|
211
196
|
resumeMenuMode,
|
|
212
197
|
handleSlashMenuUp,
|
|
213
198
|
handleSlashMenuDown,
|
|
214
|
-
handleSlashMenuSelect,
|
|
215
199
|
handleSlashMenuClose,
|
|
200
|
+
getSelectedCommand,
|
|
201
|
+
autocompleteSlashMenuSelection,
|
|
202
|
+
openListCommand,
|
|
216
203
|
updateSlashMenu,
|
|
217
204
|
setSlashMenuVisible,
|
|
218
205
|
resumeSession,
|
package/dist/screens/repl.js
CHANGED
|
@@ -22,6 +22,8 @@ import { HIDE_WELCOME_AFTER_INPUT } from '../config/constants.js';
|
|
|
22
22
|
import { generateAgentHint } from '../core/hint.js';
|
|
23
23
|
import { subscribeAgentCount, getActiveAgentCount } from '../core/spawnRegistry.js';
|
|
24
24
|
import { logError, logInfo, logWarn } from '../core/logger.js';
|
|
25
|
+
import { getAgentSubCommands } from '../commands/index.js';
|
|
26
|
+
import { setActiveAgent } from '../config/agentState.js';
|
|
25
27
|
export default function REPL() {
|
|
26
28
|
const { exit } = useApp();
|
|
27
29
|
const width = useTerminalWidth();
|
|
@@ -209,41 +211,49 @@ export default function REPL() {
|
|
|
209
211
|
await engineRef.current.handleQuery(prompt, callbacks);
|
|
210
212
|
return;
|
|
211
213
|
}
|
|
214
|
+
// /agent
|
|
215
|
+
if (cmdName === 'agent') {
|
|
216
|
+
if (hasArgs) {
|
|
217
|
+
const agentName = parts.slice(1).join(' ').trim().toLowerCase();
|
|
218
|
+
const targetAgent = getAgentSubCommands().find((cmd) => cmd.name === agentName);
|
|
219
|
+
setInput('');
|
|
220
|
+
slashMenu.setSlashMenuVisible(false);
|
|
221
|
+
if (!targetAgent) {
|
|
222
|
+
const errMsg = {
|
|
223
|
+
id: `agent-not-found-${Date.now()}`,
|
|
224
|
+
type: 'error',
|
|
225
|
+
status: 'error',
|
|
226
|
+
content: `未找到智能体: ${agentName}`,
|
|
227
|
+
timestamp: Date.now(),
|
|
228
|
+
};
|
|
229
|
+
setMessages((prev) => [...prev, errMsg]);
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
setActiveAgent(targetAgent.name);
|
|
233
|
+
const switchMsg = {
|
|
234
|
+
id: `switch-${Date.now()}`,
|
|
235
|
+
type: 'system',
|
|
236
|
+
status: 'success',
|
|
237
|
+
content: `已切换智能体为 ${targetAgent.name},请重启以生效(Ctrl+C 两次退出后重新启动)`,
|
|
238
|
+
timestamp: Date.now(),
|
|
239
|
+
};
|
|
240
|
+
setMessages((prev) => [...prev, switchMsg]);
|
|
241
|
+
}
|
|
242
|
+
else {
|
|
243
|
+
slashMenu.openListCommand('agent');
|
|
244
|
+
}
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
212
247
|
// /resume
|
|
213
248
|
if (cmdName === 'resume') {
|
|
214
|
-
setInput('');
|
|
215
|
-
slashMenu.setSlashMenuVisible(false);
|
|
216
249
|
if (hasArgs && engineRef.current) {
|
|
250
|
+
setInput('');
|
|
251
|
+
slashMenu.setSlashMenuVisible(false);
|
|
217
252
|
const sessionId = parts.slice(1).join(' ').trim();
|
|
218
253
|
slashMenu.resumeSession(sessionId);
|
|
219
254
|
}
|
|
220
255
|
else {
|
|
221
|
-
|
|
222
|
-
if (sessions.length === 0) {
|
|
223
|
-
const noMsg = {
|
|
224
|
-
id: `resume-empty-${Date.now()}`,
|
|
225
|
-
type: 'system',
|
|
226
|
-
status: 'success',
|
|
227
|
-
content: '暂无历史会话',
|
|
228
|
-
timestamp: Date.now(),
|
|
229
|
-
};
|
|
230
|
-
setMessages((prev) => [...prev, noMsg]);
|
|
231
|
-
}
|
|
232
|
-
else {
|
|
233
|
-
const lines = sessions.map((s, i) => {
|
|
234
|
-
const date = new Date(s.updatedAt);
|
|
235
|
-
const dateStr = `${date.getMonth() + 1}/${date.getDate()} ${date.getHours().toString().padStart(2, '0')}:${date.getMinutes().toString().padStart(2, '0')}`;
|
|
236
|
-
return ` ${i + 1}. [${dateStr}] ${s.summary}\n ID: ${s.id}`;
|
|
237
|
-
});
|
|
238
|
-
const listMsg = {
|
|
239
|
-
id: `resume-list-${Date.now()}`,
|
|
240
|
-
type: 'system',
|
|
241
|
-
status: 'success',
|
|
242
|
-
content: `历史会话(最近 ${sessions.length} 条):\n\n${lines.join('\n\n')}\n\n使用 /resume <ID> 恢复指定会话`,
|
|
243
|
-
timestamp: Date.now(),
|
|
244
|
-
};
|
|
245
|
-
setMessages((prev) => [...prev, listMsg]);
|
|
246
|
-
}
|
|
256
|
+
slashMenu.openListCommand('resume');
|
|
247
257
|
}
|
|
248
258
|
return;
|
|
249
259
|
}
|
|
@@ -257,7 +267,7 @@ export default function REPL() {
|
|
|
257
267
|
setInput('');
|
|
258
268
|
clearStream();
|
|
259
269
|
await engineRef.current.handleQuery(trimmed, callbacks);
|
|
260
|
-
}, [isProcessing, pushHistory, clearStream,
|
|
270
|
+
}, [isProcessing, pushHistory, clearStream, slashMenu, handleNewSession]);
|
|
261
271
|
// ===== 输入处理 =====
|
|
262
272
|
const handleUpArrow = useCallback(() => {
|
|
263
273
|
const result = navigateUp(input);
|
|
@@ -274,6 +284,29 @@ export default function REPL() {
|
|
|
274
284
|
setInput(val);
|
|
275
285
|
slashMenu.updateSlashMenu(val);
|
|
276
286
|
}, [resetNavigation, slashMenu]);
|
|
287
|
+
const handleSlashMenuAutocomplete = useCallback(() => {
|
|
288
|
+
slashMenu.autocompleteSlashMenuSelection(input);
|
|
289
|
+
}, [slashMenu, input]);
|
|
290
|
+
const handleSlashMenuSubmit = useCallback(async () => {
|
|
291
|
+
const selected = slashMenu.getSelectedCommand();
|
|
292
|
+
if (!selected)
|
|
293
|
+
return;
|
|
294
|
+
const completed = slashMenu.autocompleteSlashMenuSelection(input);
|
|
295
|
+
if (selected.submitMode === 'context') {
|
|
296
|
+
const parts = completed.trim().slice(1).split(/\s+/);
|
|
297
|
+
const hasArgs = parts.length > 1 && parts.slice(1).join('').length > 0;
|
|
298
|
+
if (!hasArgs)
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
await handleSubmit(completed);
|
|
302
|
+
}, [slashMenu, input, handleSubmit]);
|
|
303
|
+
const handleEditorSubmit = useCallback(async (value) => {
|
|
304
|
+
if (slashMenu.slashMenuVisible) {
|
|
305
|
+
await handleSlashMenuSubmit();
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
await handleSubmit(value);
|
|
309
|
+
}, [slashMenu.slashMenuVisible, handleSlashMenuSubmit, handleSubmit]);
|
|
277
310
|
// Tab 填入 placeholder
|
|
278
311
|
const handleTabFillPlaceholder = useCallback(() => {
|
|
279
312
|
if (placeholder) {
|
|
@@ -346,10 +379,6 @@ export default function REPL() {
|
|
|
346
379
|
}
|
|
347
380
|
return; // 丢弃其他所有按键
|
|
348
381
|
}
|
|
349
|
-
if (key.tab && slashMenu.slashMenuVisible) {
|
|
350
|
-
slashMenu.handleSlashMenuSelect();
|
|
351
|
-
return;
|
|
352
|
-
}
|
|
353
382
|
if (key.ctrl && ch === 'c') {
|
|
354
383
|
handleCtrlC();
|
|
355
384
|
return;
|
|
@@ -398,5 +427,5 @@ export default function REPL() {
|
|
|
398
427
|
return (_jsxs(Box, { flexDirection: "column", width: width, children: [showWelcome && _jsx(WelcomeHeader, { width: width }), _jsxs(Box, { flexDirection: "column", paddingX: 1, marginTop: showWelcome ? 0 : 1, children: [messages.map((msg) => (_jsx(MessageItem, { msg: msg, showDetails: showDetails }, msg.id))), streamText && _jsx(StreamingText, { text: streamText }), dangerConfirm && (_jsx(DangerConfirm, { command: dangerConfirm.command, reason: dangerConfirm.reason, ruleName: dangerConfirm.ruleName, onSelect: (choice) => {
|
|
399
428
|
dangerConfirm.resolve(choice);
|
|
400
429
|
setDangerConfirm(null);
|
|
401
|
-
} })), loopState?.isRunning && (_jsxs(Box, { children: [_jsx(Text, { color: "yellow", children: _jsx(Spinner, { type: "dots" }) }), _jsxs(Text, { color: "gray", children: [" iteration ", loopState.iteration, "/", loopState.maxIterations] })] }))] }), _jsxs(Box, { flexDirection: "column", paddingX: 1, children: [_jsx(Text, { color: "gray", children: '─'.repeat(Math.max(width - 2, 1)) }), slashMenu.slashMenuVisible && !isProcessing && (_jsx(SlashCommandMenu, { commands: slashMenu.slashMenuItems, selectedIndex: slashMenu.slashMenuIndex })), _jsx(Box, { children: countdown !== null ? (_jsxs(Box, { children: [_jsx(Text, { color: "gray", dimColor: true, children: "\u276F " }), _jsx(Text, { color: "yellow", children: "Press " }), _jsx(Text, { color: "yellow", bold: true, children: "Ctrl+C" }), _jsx(Text, { color: "yellow", children: " again to exit " }), _jsxs(Text, { color: "gray", dimColor: true, children: ["(", countdown, "s)"] })] })) : isProcessing ? (_jsxs(Box, { children: [_jsx(Text, { color: "cyan", bold: true, children: "\u276F " }), _jsx(Text, { color: "yellow", children: _jsx(Spinner, { type: "dots" }) }), _jsx(Text, { color: "gray", italic: true, children: " processing..." })] })) : (_jsxs(Box, { children: [_jsx(Text, { color: "cyan", bold: true, children: "\u276F " }), _jsx(MultilineInput, { value: input, onChange: handleInputChange, onSubmit:
|
|
430
|
+
} })), loopState?.isRunning && (_jsxs(Box, { children: [_jsx(Text, { color: "yellow", children: _jsx(Spinner, { type: "dots" }) }), _jsxs(Text, { color: "gray", children: [" iteration ", loopState.iteration, "/", loopState.maxIterations] })] }))] }), _jsxs(Box, { flexDirection: "column", paddingX: 1, children: [_jsx(Text, { color: "gray", children: '─'.repeat(Math.max(width - 2, 1)) }), slashMenu.slashMenuVisible && !isProcessing && (_jsx(SlashCommandMenu, { commands: slashMenu.slashMenuItems, selectedIndex: slashMenu.slashMenuIndex })), _jsx(Box, { children: countdown !== null ? (_jsxs(Box, { children: [_jsx(Text, { color: "gray", dimColor: true, children: "\u276F " }), _jsx(Text, { color: "yellow", children: "Press " }), _jsx(Text, { color: "yellow", bold: true, children: "Ctrl+C" }), _jsx(Text, { color: "yellow", children: " again to exit " }), _jsxs(Text, { color: "gray", dimColor: true, children: ["(", countdown, "s)"] })] })) : isProcessing ? (_jsxs(Box, { children: [_jsx(Text, { color: "cyan", bold: true, children: "\u276F " }), _jsx(Text, { color: "yellow", children: _jsx(Spinner, { type: "dots" }) }), _jsx(Text, { color: "gray", italic: true, children: " processing..." })] })) : (_jsxs(Box, { children: [_jsx(Text, { color: "cyan", bold: true, children: "\u276F " }), _jsx(MultilineInput, { value: input, onChange: handleInputChange, onSubmit: handleEditorSubmit, onUpArrow: handleUpArrow, onDownArrow: handleDownArrow, placeholder: placeholder, isActive: !isProcessing, showCursor: windowFocused && !isProcessing, slashMenuActive: slashMenu.slashMenuVisible, onSlashMenuUp: slashMenu.handleSlashMenuUp, onSlashMenuDown: slashMenu.handleSlashMenuDown, onSlashMenuSelect: handleSlashMenuAutocomplete, onSlashMenuClose: slashMenu.handleSlashMenuClose, onTabFillPlaceholder: handleTabFillPlaceholder })] })) }), _jsx(Text, { color: "gray", children: '─'.repeat(Math.max(width - 2, 1)) }), _jsx(StatusBar, { width: width - 2, totalTokens: displayTokens, activeAgents: activeAgents })] })] }));
|
|
402
431
|
}
|