@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.
@@ -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();
@@ -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;
@@ -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
- if (this.transcript.length === 0) {
81
- const updated = await updateUserProfileFromInput(userInput);
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): 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,
@@ -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
- const sessions = QueryEngine.listSessions().slice(0, 20);
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, resetTokens, slashMenu, handleNewSession]);
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: handleSubmit, onUpArrow: handleUpArrow, onDownArrow: handleDownArrow, placeholder: placeholder, isActive: !isProcessing, showCursor: windowFocused && !isProcessing, slashMenuActive: slashMenu.slashMenuVisible, onSlashMenuUp: slashMenu.handleSlashMenuUp, onSlashMenuDown: slashMenu.handleSlashMenuDown, onSlashMenuSelect: slashMenu.handleSlashMenuSelect, 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 })] })] }));
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
  }