@code4bug/jarvis-agent 1.1.5 → 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.
Files changed (38) hide show
  1. package/README.md +171 -215
  2. package/dist/commands/index.d.ts +2 -0
  3. package/dist/commands/index.js +17 -15
  4. package/dist/components/MultilineInput.js +2 -2
  5. package/dist/components/WelcomeHeader.js +1 -1
  6. package/dist/config/dream.d.ts +10 -0
  7. package/dist/config/dream.js +60 -0
  8. package/dist/config/memory.d.ts +7 -0
  9. package/dist/config/memory.js +55 -0
  10. package/dist/config/userProfile.d.ts +5 -1
  11. package/dist/config/userProfile.js +15 -2
  12. package/dist/core/QueryEngine.d.ts +11 -0
  13. package/dist/core/QueryEngine.js +104 -8
  14. package/dist/core/WorkerBridge.d.ts +3 -1
  15. package/dist/core/WorkerBridge.js +2 -2
  16. package/dist/core/query.d.ts +5 -1
  17. package/dist/core/query.js +4 -4
  18. package/dist/core/queryWorker.d.ts +3 -0
  19. package/dist/core/queryWorker.js +1 -1
  20. package/dist/hooks/useSlashMenu.d.ts +3 -1
  21. package/dist/hooks/useSlashMenu.js +58 -71
  22. package/dist/screens/repl.js +63 -34
  23. package/dist/services/api/llm.d.ts +5 -2
  24. package/dist/services/api/llm.js +28 -7
  25. package/dist/services/api/mock.d.ts +3 -1
  26. package/dist/services/api/mock.js +1 -1
  27. package/dist/services/dream.d.ts +7 -0
  28. package/dist/services/dream.js +171 -0
  29. package/dist/services/persistentMemory.d.ts +8 -0
  30. package/dist/services/persistentMemory.js +178 -0
  31. package/dist/services/userProfile.d.ts +1 -0
  32. package/dist/services/userProfile.js +15 -0
  33. package/dist/tools/index.d.ts +2 -1
  34. package/dist/tools/index.js +3 -1
  35. package/dist/tools/manageMemory.d.ts +2 -0
  36. package/dist/tools/manageMemory.js +46 -0
  37. package/dist/types/index.d.ts +3 -1
  38. package/package.json +3 -3
@@ -0,0 +1,55 @@
1
+ import fs from 'fs';
2
+ import os from 'os';
3
+ import path from 'path';
4
+ export const MEMORY_FILE_PATH = path.join(os.homedir(), '.jarvis', 'MEMORY.md');
5
+ const MEMORY_FILE_HEADER = [
6
+ '# Jarvis 长期记忆',
7
+ '',
8
+ '> 这里沉淀可复用的经验、技能、偏好、约束与稳定环境事实。',
9
+ '> 避免写入一次性闲聊、临时输出、密钥或其它敏感信息。',
10
+ '',
11
+ ].join('\n');
12
+ export function ensureMemoryHomeDir() {
13
+ const dir = path.dirname(MEMORY_FILE_PATH);
14
+ if (!fs.existsSync(dir)) {
15
+ fs.mkdirSync(dir, { recursive: true });
16
+ }
17
+ return dir;
18
+ }
19
+ export function ensureMemoryFile() {
20
+ ensureMemoryHomeDir();
21
+ if (!fs.existsSync(MEMORY_FILE_PATH)) {
22
+ fs.writeFileSync(MEMORY_FILE_PATH, MEMORY_FILE_HEADER, 'utf-8');
23
+ }
24
+ return MEMORY_FILE_PATH;
25
+ }
26
+ export function readPersistentMemory() {
27
+ try {
28
+ ensureMemoryFile();
29
+ return fs.readFileSync(MEMORY_FILE_PATH, 'utf-8').trim();
30
+ }
31
+ catch {
32
+ return '';
33
+ }
34
+ }
35
+ export function readPersistentMemoryForPrompt(maxChars = 12000) {
36
+ const content = readPersistentMemory();
37
+ if (!content)
38
+ return '';
39
+ if (content.length <= maxChars)
40
+ return content;
41
+ return `...[已截断,仅保留最近 ${maxChars} 字符]\n${content.slice(-maxChars)}`;
42
+ }
43
+ export function appendPersistentMemory(content) {
44
+ const normalized = content.trim();
45
+ if (!normalized)
46
+ return;
47
+ ensureMemoryFile();
48
+ const current = fs.readFileSync(MEMORY_FILE_PATH, 'utf-8');
49
+ const suffix = current.endsWith('\n\n') ? '' : (current.endsWith('\n') ? '\n' : '\n\n');
50
+ fs.appendFileSync(MEMORY_FILE_PATH, `${suffix}${normalized}\n`, 'utf-8');
51
+ }
52
+ export function replacePersistentMemory(content) {
53
+ ensureMemoryFile();
54
+ fs.writeFileSync(MEMORY_FILE_PATH, `${content.trimEnd()}\n`, 'utf-8');
55
+ }
@@ -1,4 +1,8 @@
1
1
  export declare const USER_PROFILE_PATH: string;
2
2
  export declare function ensureJarvisHomeDir(): string;
3
3
  export declare function readUserProfile(): string;
4
- export declare function writeUserProfile(content: string): void;
4
+ export declare function initializeUserProfileCache(): string;
5
+ export declare function getCachedUserProfile(): string;
6
+ export declare function writeUserProfile(content: string, options?: {
7
+ updateCache?: boolean;
8
+ }): void;
@@ -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 writeUserProfile(content) {
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
- fs.writeFileSync(USER_PROFILE_PATH, content.trimEnd() + '\n', 'utf-8');
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();
@@ -20,6 +20,12 @@ export declare class QueryEngine {
20
20
  private session;
21
21
  private transcript;
22
22
  private workerBridge;
23
+ private memoryUpdateQueue;
24
+ private userProfileUpdateQueue;
25
+ private dreamUpdateQueue;
26
+ private dreamTimer;
27
+ private lastActivityAt;
28
+ private isQueryRunning;
23
29
  constructor();
24
30
  /** 注册持久 UI 回调,供后台 spawn_agent 子 Agent 推送消息 */
25
31
  registerUIBus(onMessage: (msg: Message) => void, onUpdateMessage: (id: string, updates: Partial<Message>) => void): void;
@@ -38,6 +44,11 @@ export declare class QueryEngine {
38
44
  switchAgent(agentName: string): void;
39
45
  /** 保存会话到文件 */
40
46
  private saveSession;
47
+ private schedulePersistentMemoryUpdate;
48
+ private scheduleUserProfileUpdate;
49
+ private touchActivity;
50
+ private scheduleDreamTimer;
51
+ private runDreamCycle;
41
52
  /** 列出所有历史会话(按更新时间倒序),返回摘要信息 */
42
53
  static listSessions(): {
43
54
  id: string;
@@ -10,16 +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
+ 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;
14
18
  export class QueryEngine {
15
19
  service;
16
20
  session;
17
21
  transcript = [];
18
22
  workerBridge = new WorkerBridge();
23
+ memoryUpdateQueue = Promise.resolve();
24
+ userProfileUpdateQueue = Promise.resolve();
25
+ dreamUpdateQueue = Promise.resolve();
26
+ dreamTimer = null;
27
+ lastActivityAt = Date.now();
28
+ isQueryRunning = false;
19
29
  constructor() {
20
30
  this.service = this.createService();
21
31
  this.session = this.createSession();
22
32
  this.ensureSessionDir();
33
+ this.scheduleDreamTimer();
23
34
  logInfo('engine.created', {
24
35
  sessionId: this.session.id,
25
36
  service: this.service.constructor.name,
@@ -60,6 +71,9 @@ export class QueryEngine {
60
71
  }
61
72
  /** 处理用户输入(在独立 Worker 线程中执行) */
62
73
  async handleQuery(userInput, callbacks) {
74
+ this.touchActivity();
75
+ this.isQueryRunning = true;
76
+ const previousTranscriptLength = this.transcript.length;
63
77
  logInfo('query.received', {
64
78
  sessionId: this.session.id,
65
79
  inputLength: userInput.length,
@@ -74,12 +88,8 @@ export class QueryEngine {
74
88
  };
75
89
  callbacks.onMessage(userMsg);
76
90
  this.session.messages.push(userMsg);
77
- if (this.transcript.length === 0) {
78
- const updated = await updateUserProfileFromInput(userInput);
79
- if (updated) {
80
- this.service = this.createService();
81
- }
82
- }
91
+ const includeUserProfile = shouldIncludeUserProfile(userInput);
92
+ this.scheduleUserProfileUpdate(userInput);
83
93
  // 将回调包装后传给 WorkerBridge,Worker 事件会映射回这里
84
94
  const bridgeCallbacks = {
85
95
  onMessage: (msg) => {
@@ -104,7 +114,9 @@ export class QueryEngine {
104
114
  onSubAgentUpdateMessage: callbacks.onSubAgentUpdateMessage,
105
115
  };
106
116
  try {
107
- this.transcript = await this.workerBridge.run(userInput, this.transcript, bridgeCallbacks);
117
+ this.transcript = await this.workerBridge.run(userInput, this.transcript, bridgeCallbacks, { includeUserProfile });
118
+ const recentTranscript = this.transcript.slice(previousTranscriptLength);
119
+ this.schedulePersistentMemoryUpdate(userInput, recentTranscript);
108
120
  logInfo('query.completed', {
109
121
  sessionId: this.session.id,
110
122
  transcriptLength: this.transcript.length,
@@ -122,6 +134,10 @@ export class QueryEngine {
122
134
  };
123
135
  callbacks.onMessage(errMsg);
124
136
  }
137
+ finally {
138
+ this.isQueryRunning = false;
139
+ this.scheduleDreamTimer();
140
+ }
125
141
  this.session.updatedAt = Date.now();
126
142
  callbacks.onSessionUpdate(this.session);
127
143
  this.saveSession();
@@ -137,6 +153,10 @@ export class QueryEngine {
137
153
  this.saveSession();
138
154
  this.session = this.createSession();
139
155
  this.transcript = [];
156
+ this.memoryUpdateQueue = Promise.resolve();
157
+ this.userProfileUpdateQueue = Promise.resolve();
158
+ this.dreamUpdateQueue = Promise.resolve();
159
+ this.touchActivity();
140
160
  clearAuthorizations();
141
161
  }
142
162
  /**
@@ -154,6 +174,10 @@ export class QueryEngine {
154
174
  this.saveSession();
155
175
  this.session = this.createSession();
156
176
  this.transcript = [];
177
+ this.memoryUpdateQueue = Promise.resolve();
178
+ this.userProfileUpdateQueue = Promise.resolve();
179
+ this.dreamUpdateQueue = Promise.resolve();
180
+ this.touchActivity();
157
181
  logInfo('agent.switch.completed', {
158
182
  sessionId: this.session.id,
159
183
  agentName,
@@ -182,6 +206,77 @@ export class QueryEngine {
182
206
  logError('session.save_failed', error, { sessionId: this.session.id });
183
207
  }
184
208
  }
209
+ schedulePersistentMemoryUpdate(userInput, recentTranscript) {
210
+ if (!userInput.trim() || recentTranscript.length === 0)
211
+ return;
212
+ const sessionId = this.session.id;
213
+ this.memoryUpdateQueue = this.memoryUpdateQueue
214
+ .catch(() => { })
215
+ .then(async () => {
216
+ await updatePersistentMemoryFromConversation({
217
+ sessionId,
218
+ userInput,
219
+ recentTranscript,
220
+ });
221
+ });
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
+ }
185
280
  /** 列出所有历史会话(按更新时间倒序),返回摘要信息 */
186
281
  static listSessions() {
187
282
  try {
@@ -256,6 +351,7 @@ export class QueryEngine {
256
351
  messageCount: cleanedMessages.length,
257
352
  transcriptLength: this.transcript.length,
258
353
  });
354
+ this.touchActivity();
259
355
  return { session: this.session, messages: cleanedMessages };
260
356
  }
261
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): Promise<TranscriptMessage[]>;
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
  }
@@ -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 {};
@@ -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;
@@ -5,6 +5,9 @@ export type WorkerInbound = {
5
5
  type: 'run';
6
6
  userInput: string;
7
7
  transcript: TranscriptMessage[];
8
+ options?: {
9
+ includeUserProfile?: boolean;
10
+ };
8
11
  } | {
9
12
  type: 'abort';
10
13
  } | {
@@ -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, onNewSession, } = opts;
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
- const cmd = slashMenuItems[slashMenuIndex];
81
- if (!cmd)
82
- return;
83
- // 二级 agent 菜单
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
- return;
87
+ setSlashMenuVisible(matched.length > 0);
88
+ return '/agent ';
116
89
  }
117
- // 一级菜单选中 /resume -> 进入二级菜单
118
- if (cmd.name === 'resume') {
119
- setInput('/resume ');
120
- const sessions = QueryEngine.listSessions().slice(0, 20);
121
- const items = sessions.map((s) => {
122
- const date = new Date(s.updatedAt);
123
- const dateStr = `${date.getMonth() + 1}/${date.getDate()} ${date.getHours().toString().padStart(2, '0')}:${date.getMinutes().toString().padStart(2, '0')}`;
124
- return {
125
- name: s.id,
126
- description: `[${dateStr}] ${s.summary}`,
127
- category: 'builtin',
128
- };
129
- });
130
- setSlashMenuItems(items);
131
- setSlashMenuIndex(0);
132
- setSlashMenuVisible(items.length > 0);
133
- setResumeMenuMode(true);
134
- setAgentMenuMode(false);
135
- return;
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 (cmd.category === 'builtin') {
139
- setInput('');
120
+ // 二级 resume 菜单
121
+ if (resumeMenuMode) {
122
+ const nextInput = `/resume ${cmd.name}`;
123
+ setInput(nextInput);
140
124
  setSlashMenuVisible(false);
141
- if (cmd.name === 'new') {
142
- onNewSession();
143
- return;
144
- }
145
- const msg = executeSlashCommand(cmd.name);
146
- if (msg)
147
- setMessages((prev) => [...prev, msg]);
148
- return;
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
- setInput(`/${cmd.name} `);
152
- setSlashMenuVisible(false);
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,