@code4bug/jarvis-agent 1.0.3 → 1.1.4

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 (77) hide show
  1. package/README.md +63 -0
  2. package/dist/agents/jarvis.md +11 -0
  3. package/dist/cli.js +2 -2
  4. package/dist/commands/index.js +2 -2
  5. package/dist/commands/init.js +1 -1
  6. package/dist/components/MessageItem.d.ts +1 -1
  7. package/dist/components/MessageItem.js +10 -6
  8. package/dist/components/SlashCommandMenu.d.ts +1 -1
  9. package/dist/components/StatusBar.d.ts +2 -1
  10. package/dist/components/StatusBar.js +6 -5
  11. package/dist/components/StreamingText.js +1 -1
  12. package/dist/components/WelcomeHeader.js +1 -1
  13. package/dist/config/constants.js +3 -3
  14. package/dist/core/AgentMessageBus.d.ts +63 -0
  15. package/dist/core/AgentMessageBus.js +107 -0
  16. package/dist/core/AgentRegistry.d.ts +22 -0
  17. package/dist/core/AgentRegistry.js +16 -0
  18. package/dist/core/QueryEngine.d.ts +7 -1
  19. package/dist/core/QueryEngine.js +18 -7
  20. package/dist/core/SubAgentBridge.d.ts +20 -0
  21. package/dist/core/SubAgentBridge.js +191 -0
  22. package/dist/core/WorkerBridge.d.ts +2 -2
  23. package/dist/core/WorkerBridge.js +68 -0
  24. package/dist/core/busAccess.d.ts +9 -0
  25. package/dist/core/busAccess.js +32 -0
  26. package/dist/core/hint.js +4 -4
  27. package/dist/core/query.d.ts +4 -0
  28. package/dist/core/query.js +91 -5
  29. package/dist/core/queryWorker.d.ts +62 -0
  30. package/dist/core/queryWorker.js +35 -0
  31. package/dist/core/spawnRegistry.d.ts +16 -0
  32. package/dist/core/spawnRegistry.js +32 -0
  33. package/dist/core/subAgentWorker.d.ts +89 -0
  34. package/dist/core/subAgentWorker.js +107 -0
  35. package/dist/core/workerBusProxy.d.ts +10 -0
  36. package/dist/core/workerBusProxy.js +57 -0
  37. package/dist/hooks/useSlashMenu.d.ts +7 -5
  38. package/dist/hooks/useSlashMenu.js +9 -5
  39. package/dist/hooks/useStreamThrottle.d.ts +1 -1
  40. package/dist/hooks/useTokenDisplay.d.ts +1 -0
  41. package/dist/hooks/useTokenDisplay.js +5 -0
  42. package/dist/index.js +1 -1
  43. package/dist/screens/repl.js +52 -34
  44. package/dist/screens/slashCommands.d.ts +1 -1
  45. package/dist/screens/slashCommands.js +7 -6
  46. package/dist/services/api/llm.d.ts +4 -2
  47. package/dist/services/api/llm.js +20 -6
  48. package/dist/services/api/mock.d.ts +1 -1
  49. package/dist/skills/index.d.ts +2 -2
  50. package/dist/skills/index.js +2 -2
  51. package/dist/tools/createSkill.d.ts +1 -1
  52. package/dist/tools/createSkill.js +3 -3
  53. package/dist/tools/index.d.ts +15 -9
  54. package/dist/tools/index.js +21 -10
  55. package/dist/tools/listDirectory.d.ts +1 -1
  56. package/dist/tools/publishMessage.d.ts +8 -0
  57. package/dist/tools/publishMessage.js +41 -0
  58. package/dist/tools/readChannel.d.ts +8 -0
  59. package/dist/tools/readChannel.js +44 -0
  60. package/dist/tools/readFile.d.ts +1 -1
  61. package/dist/tools/runAgent.d.ts +11 -0
  62. package/dist/tools/runAgent.js +111 -0
  63. package/dist/tools/runCommand.d.ts +1 -1
  64. package/dist/tools/runCommand.js +1 -1
  65. package/dist/tools/searchFiles.d.ts +1 -1
  66. package/dist/tools/semanticSearch.d.ts +1 -1
  67. package/dist/tools/semanticSearch.js +1 -1
  68. package/dist/tools/sendToAgent.d.ts +11 -0
  69. package/dist/tools/sendToAgent.js +35 -0
  70. package/dist/tools/spawnAgent.d.ts +6 -0
  71. package/dist/tools/spawnAgent.js +163 -0
  72. package/dist/tools/subscribeMessage.d.ts +8 -0
  73. package/dist/tools/subscribeMessage.js +59 -0
  74. package/dist/tools/writeFile.d.ts +1 -1
  75. package/dist/tools/writeFile.js +1 -1
  76. package/dist/types/index.d.ts +49 -1
  77. package/package.json +1 -1
@@ -0,0 +1,32 @@
1
+ /**
2
+ * spawnRegistry — spawn_subagent IPC 挂起请求注册表
3
+ *
4
+ * 独立模块,避免 spawnAgent.ts ↔ queryWorker.ts 循环依赖。
5
+ * queryWorker.ts 和 spawnAgent.ts 都可以安全 import 此模块。
6
+ */
7
+ /** 等待主线程完成 SubAgent 启动的挂起 Promise,key = requestId */
8
+ export const pendingSpawnRequests = new Map();
9
+ // ===== 运行中 SubAgent 计数器 =====
10
+ /** 当前运行中的 SubAgent 数量 */
11
+ let _activeAgentCount = 0;
12
+ /** 计数变更订阅者列表 */
13
+ const _countListeners = new Set();
14
+ /** 增加运行中 SubAgent 计数 */
15
+ export function incrementActiveAgents() {
16
+ _activeAgentCount++;
17
+ _countListeners.forEach((fn) => fn(_activeAgentCount));
18
+ }
19
+ /** 减少运行中 SubAgent 计数 */
20
+ export function decrementActiveAgents() {
21
+ _activeAgentCount = Math.max(0, _activeAgentCount - 1);
22
+ _countListeners.forEach((fn) => fn(_activeAgentCount));
23
+ }
24
+ /** 获取当前运行中 SubAgent 数量 */
25
+ export function getActiveAgentCount() {
26
+ return _activeAgentCount;
27
+ }
28
+ /** 订阅计数变更,返回取消订阅函数 */
29
+ export function subscribeAgentCount(listener) {
30
+ _countListeners.add(listener);
31
+ return () => _countListeners.delete(listener);
32
+ }
@@ -0,0 +1,89 @@
1
+ import { DangerConfirmResult } from './query.js';
2
+ import { TranscriptMessage, Message, LoopState, SubAgentTask } from '../types/index.js';
3
+ import { BusMessage } from './AgentMessageBus.js';
4
+ export type SubAgentInbound = {
5
+ type: 'run';
6
+ task: SubAgentTask;
7
+ } | {
8
+ type: 'abort';
9
+ } | {
10
+ type: 'danger_confirm_result';
11
+ requestId: string;
12
+ choice: DangerConfirmResult;
13
+ } | {
14
+ type: 'bus_publish_ack';
15
+ requestId: string;
16
+ } | {
17
+ type: 'bus_subscribe_result';
18
+ requestId: string;
19
+ message: BusMessage | null;
20
+ } | {
21
+ type: 'bus_read_history_result';
22
+ requestId: string;
23
+ messages: BusMessage[];
24
+ } | {
25
+ type: 'bus_get_offset_result';
26
+ requestId: string;
27
+ offset: number;
28
+ } | {
29
+ type: 'bus_list_channels_result';
30
+ requestId: string;
31
+ channels: string[];
32
+ };
33
+ export type SubAgentOutbound = {
34
+ type: 'message';
35
+ taskId: string;
36
+ msg: Message;
37
+ } | {
38
+ type: 'update_message';
39
+ taskId: string;
40
+ id: string;
41
+ updates: Partial<Message>;
42
+ } | {
43
+ type: 'stream_text';
44
+ taskId: string;
45
+ text: string;
46
+ } | {
47
+ type: 'loop_state';
48
+ taskId: string;
49
+ state: LoopState;
50
+ } | {
51
+ type: 'danger_confirm_request';
52
+ taskId: string;
53
+ requestId: string;
54
+ command: string;
55
+ reason: string;
56
+ ruleName: string;
57
+ } | {
58
+ type: 'bus_publish';
59
+ requestId: string;
60
+ from: string;
61
+ channel: string;
62
+ payload: string;
63
+ } | {
64
+ type: 'bus_subscribe';
65
+ requestId: string;
66
+ channel: string;
67
+ timeoutMs: number;
68
+ fromOffset?: number;
69
+ } | {
70
+ type: 'bus_read_history';
71
+ requestId: string;
72
+ channel: string;
73
+ limit?: number;
74
+ } | {
75
+ type: 'bus_get_offset';
76
+ requestId: string;
77
+ channel: string;
78
+ } | {
79
+ type: 'bus_list_channels';
80
+ requestId: string;
81
+ } | {
82
+ type: 'done';
83
+ taskId: string;
84
+ transcript: TranscriptMessage[];
85
+ } | {
86
+ type: 'error';
87
+ taskId: string;
88
+ message: string;
89
+ };
@@ -0,0 +1,107 @@
1
+ /**
2
+ * SubAgent Worker 线程入口
3
+ *
4
+ * 每个 SubAgent 运行在独立线程中,拥有完整的 react_loop 能力。
5
+ * 通过 parentPort 与主线程(SubAgentBridge)双向通信。
6
+ *
7
+ * 消息协议:
8
+ * 主线程 → Worker:SubAgentInbound
9
+ * Worker → 主线程:SubAgentOutbound
10
+ */
11
+ import { parentPort } from 'worker_threads';
12
+ import { executeQuery } from './query.js';
13
+ import { getAllTools } from '../tools/index.js';
14
+ import { LLMServiceImpl, fromModelConfig } from '../services/api/llm.js';
15
+ import { MockService } from '../services/api/mock.js';
16
+ import { loadConfig, getActiveModel } from '../config/loader.js';
17
+ import { pendingBusRequests } from './workerBusProxy.js';
18
+ if (!parentPort)
19
+ throw new Error('subAgentWorker must run inside worker_threads');
20
+ // ===== 初始化 LLM 服务 =====
21
+ const config = loadConfig();
22
+ const activeModel = getActiveModel(config);
23
+ function buildSubAgentService(role, customSystemPrompt) {
24
+ const systemPrompt = customSystemPrompt
25
+ ?? (role
26
+ ? `${role}\n\n你是一个专注的子任务执行助手。请严格按照任务指令完成工作,输出结构化结果。`
27
+ : '你是一个专注的子任务执行助手。请严格按照任务指令完成工作,输出结构化结果。');
28
+ if (activeModel) {
29
+ try {
30
+ return new LLMServiceImpl({ ...fromModelConfig(activeModel), systemPrompt });
31
+ }
32
+ catch {
33
+ return new MockService();
34
+ }
35
+ }
36
+ return new MockService();
37
+ }
38
+ // ===== 中断信号 =====
39
+ const abortSignal = { aborted: false };
40
+ // ===== 危险命令确认:挂起 Promise,等待主线程回复 =====
41
+ const pendingConfirms = new Map();
42
+ // ===== 监听主线程消息 =====
43
+ parentPort.on('message', async (msg) => {
44
+ if (msg.type === 'abort') {
45
+ abortSignal.aborted = true;
46
+ return;
47
+ }
48
+ if (msg.type === 'danger_confirm_result') {
49
+ const resolve = pendingConfirms.get(msg.requestId);
50
+ if (resolve) {
51
+ pendingConfirms.delete(msg.requestId);
52
+ resolve(msg.choice);
53
+ }
54
+ return;
55
+ }
56
+ // MessageBus IPC 回复分发 → 转发给 workerBusProxy 的 pending map
57
+ if (msg.type === 'bus_publish_ack' ||
58
+ msg.type === 'bus_subscribe_result' ||
59
+ msg.type === 'bus_read_history_result' ||
60
+ msg.type === 'bus_get_offset_result' ||
61
+ msg.type === 'bus_list_channels_result') {
62
+ const resolve = pendingBusRequests.get(msg.requestId);
63
+ if (resolve) {
64
+ pendingBusRequests.delete(msg.requestId);
65
+ if (msg.type === 'bus_publish_ack')
66
+ resolve(undefined);
67
+ else if (msg.type === 'bus_subscribe_result')
68
+ resolve(msg.message);
69
+ else if (msg.type === 'bus_read_history_result')
70
+ resolve(msg.messages);
71
+ else if (msg.type === 'bus_get_offset_result')
72
+ resolve(msg.offset);
73
+ else if (msg.type === 'bus_list_channels_result')
74
+ resolve(msg.channels);
75
+ }
76
+ return;
77
+ }
78
+ if (msg.type === 'run') {
79
+ abortSignal.aborted = false;
80
+ const { task } = msg;
81
+ const { taskId, instruction, allowedTools, contextTranscript, role, systemPrompt } = task;
82
+ const send = (out) => parentPort.postMessage(out);
83
+ const service = buildSubAgentService(role, systemPrompt);
84
+ const tools = getAllTools().filter((t) => !allowedTools || allowedTools.length === 0 || allowedTools.includes(t.name));
85
+ const callbacks = {
86
+ onMessage: (m) => send({ type: 'message', taskId, msg: m }),
87
+ onUpdateMessage: (id, updates) => send({ type: 'update_message', taskId, id, updates }),
88
+ onStreamText: (text) => send({ type: 'stream_text', taskId, text }),
89
+ onClearStreamText: () => { },
90
+ onLoopStateChange: (state) => send({ type: 'loop_state', taskId, state }),
91
+ onConfirmDangerousCommand: (command, reason, ruleName) => {
92
+ return new Promise((resolve) => {
93
+ const requestId = `${Date.now()}-${Math.random()}`;
94
+ pendingConfirms.set(requestId, resolve);
95
+ send({ type: 'danger_confirm_request', taskId, requestId, command, reason, ruleName });
96
+ });
97
+ },
98
+ };
99
+ try {
100
+ const newTranscript = await executeQuery(instruction, contextTranscript ?? [], tools, service, callbacks, abortSignal);
101
+ send({ type: 'done', taskId, transcript: newTranscript });
102
+ }
103
+ catch (err) {
104
+ send({ type: 'error', taskId, message: err.message ?? '未知错误' });
105
+ }
106
+ }
107
+ });
@@ -0,0 +1,10 @@
1
+ import type { BusMessage } from './AgentMessageBus.js';
2
+ /** 等待中的 bus 请求,key = requestId */
3
+ export declare const pendingBusRequests: Map<string, (result: any) => void>;
4
+ export declare const workerBusProxy: {
5
+ publish(from: string, channel: string, payload: string): Promise<void>;
6
+ subscribe(channel: string, timeoutMs?: number, fromOffset?: number): Promise<BusMessage | null>;
7
+ getHistory(channel: string, limit?: number): Promise<BusMessage[]>;
8
+ getOffset(channel: string): Promise<number>;
9
+ listChannels(): Promise<string[]>;
10
+ };
@@ -0,0 +1,57 @@
1
+ /**
2
+ * workerBusProxy — Worker 线程侧 MessageBus IPC 代理
3
+ *
4
+ * 在 SubAgent Worker 线程中,通过 parentPort 消息将 bus 操作
5
+ * 转发给主线程(SubAgentBridge)代理执行,绕过线程内存隔离。
6
+ *
7
+ * 此文件独立于 subAgentWorker.ts,避免循环依赖。
8
+ * subAgentWorker.ts 负责注册 pendingBusRequests 的回复分发。
9
+ */
10
+ import { parentPort } from 'worker_threads';
11
+ /** 等待中的 bus 请求,key = requestId */
12
+ export const pendingBusRequests = new Map();
13
+ function requestId() {
14
+ return `bus-${Date.now()}-${Math.random().toString(36).slice(2)}`;
15
+ }
16
+ function send(msg) {
17
+ if (!parentPort)
18
+ throw new Error('workerBusProxy: 不在 Worker 线程中');
19
+ parentPort.postMessage(msg);
20
+ }
21
+ export const workerBusProxy = {
22
+ publish(from, channel, payload) {
23
+ return new Promise((resolve) => {
24
+ const reqId = requestId();
25
+ pendingBusRequests.set(reqId, () => resolve());
26
+ send({ type: 'bus_publish', requestId: reqId, from, channel, payload });
27
+ });
28
+ },
29
+ subscribe(channel, timeoutMs = 30_000, fromOffset) {
30
+ return new Promise((resolve) => {
31
+ const reqId = requestId();
32
+ pendingBusRequests.set(reqId, (msg) => resolve(msg));
33
+ send({ type: 'bus_subscribe', requestId: reqId, channel, timeoutMs, fromOffset });
34
+ });
35
+ },
36
+ getHistory(channel, limit) {
37
+ return new Promise((resolve) => {
38
+ const reqId = requestId();
39
+ pendingBusRequests.set(reqId, (msgs) => resolve(msgs));
40
+ send({ type: 'bus_read_history', requestId: reqId, channel, limit });
41
+ });
42
+ },
43
+ getOffset(channel) {
44
+ return new Promise((resolve) => {
45
+ const reqId = requestId();
46
+ pendingBusRequests.set(reqId, (offset) => resolve(offset));
47
+ send({ type: 'bus_get_offset', requestId: reqId, channel });
48
+ });
49
+ },
50
+ listChannels() {
51
+ return new Promise((resolve) => {
52
+ const reqId = requestId();
53
+ pendingBusRequests.set(reqId, (channels) => resolve(channels));
54
+ send({ type: 'bus_list_channels', requestId: reqId });
55
+ });
56
+ },
57
+ };
@@ -1,18 +1,20 @@
1
- import { Message } from '../types/index';
2
- import { QueryEngine } from '../core/QueryEngine';
3
- import { SlashCommand } from '../commands/index';
1
+ import { Message } from '../types/index.js';
2
+ import { QueryEngine } from '../core/QueryEngine.js';
3
+ import { SlashCommand } from '../commands/index.js';
4
4
  interface UseSlashMenuOptions {
5
5
  engineRef: React.RefObject<QueryEngine | null>;
6
- sessionRef: React.MutableRefObject<import('../types/index').Session>;
6
+ sessionRef: React.MutableRefObject<import('../types/index.js').Session>;
7
7
  tokenCountRef: React.MutableRefObject<number>;
8
8
  setMessages: React.Dispatch<React.SetStateAction<Message[]>>;
9
9
  setDisplayTokens: (n: number) => void;
10
- setLoopState: React.Dispatch<React.SetStateAction<import('../types/index').LoopState | null>>;
10
+ setLoopState: React.Dispatch<React.SetStateAction<import('../types/index.js').LoopState | null>>;
11
11
  setIsProcessing: React.Dispatch<React.SetStateAction<boolean>>;
12
12
  setShowWelcome: React.Dispatch<React.SetStateAction<boolean>>;
13
13
  setInput: React.Dispatch<React.SetStateAction<string>>;
14
14
  /** resume 时重置所有流式状态(含 thinkingIdRef) */
15
15
  stopAll: () => void;
16
+ /** /new 命令:开启新会话 */
17
+ onNewSession: () => void;
16
18
  }
17
19
  /**
18
20
  * 斜杠命令菜单状态管理 hook
@@ -1,15 +1,15 @@
1
1
  import { useState, useCallback } from 'react';
2
- import { QueryEngine } from '../core/QueryEngine';
3
- import { filterCommands, filterAgentCommands } from '../commands/index';
4
- import { setActiveAgent } from '../config/agentState';
5
- import { executeSlashCommand } from '../screens/slashCommands';
2
+ import { QueryEngine } from '../core/QueryEngine.js';
3
+ import { filterCommands, filterAgentCommands } from '../commands/index.js';
4
+ import { setActiveAgent } from '../config/agentState.js';
5
+ import { executeSlashCommand } from '../screens/slashCommands.js';
6
6
  /**
7
7
  * 斜杠命令菜单状态管理 hook
8
8
  *
9
9
  * 管理菜单可见性、选中项、二级菜单(agent / resume)等。
10
10
  */
11
11
  export function useSlashMenu(opts) {
12
- const { engineRef, sessionRef, tokenCountRef, setMessages, setDisplayTokens, setLoopState, setIsProcessing, setShowWelcome, setInput, stopAll, } = opts;
12
+ const { engineRef, sessionRef, tokenCountRef, setMessages, setDisplayTokens, setLoopState, setIsProcessing, setShowWelcome, setInput, stopAll, onNewSession, } = opts;
13
13
  const [slashMenuVisible, setSlashMenuVisible] = useState(false);
14
14
  const [slashMenuItems, setSlashMenuItems] = useState([]);
15
15
  const [slashMenuIndex, setSlashMenuIndex] = useState(0);
@@ -138,6 +138,10 @@ export function useSlashMenu(opts) {
138
138
  if (cmd.category === 'builtin') {
139
139
  setInput('');
140
140
  setSlashMenuVisible(false);
141
+ if (cmd.name === 'new') {
142
+ onNewSession();
143
+ return;
144
+ }
141
145
  const msg = executeSlashCommand(cmd.name);
142
146
  if (msg)
143
147
  setMessages((prev) => [...prev, msg]);
@@ -1,4 +1,4 @@
1
- import { Message } from '../types/index';
1
+ import { Message } from '../types/index.js';
2
2
  /**
3
3
  * 流式文本 + thinking 文本节流 hook
4
4
  *
@@ -9,5 +9,6 @@ export declare function useTokenDisplay(): {
9
9
  startTokenTimer: () => void;
10
10
  stopTokenTimer: () => void;
11
11
  updateTokenCount: (count: number) => void;
12
+ syncTokenDisplay: (count: number) => void;
12
13
  resetTokens: () => void;
13
14
  };
@@ -25,6 +25,10 @@ export function useTokenDisplay() {
25
25
  const updateTokenCount = useCallback((count) => {
26
26
  tokenCountRef.current = count;
27
27
  }, []);
28
+ const syncTokenDisplay = useCallback((count) => {
29
+ tokenCountRef.current = count;
30
+ setDisplayTokens(count);
31
+ }, []);
28
32
  const resetTokens = useCallback(() => {
29
33
  tokenCountRef.current = 0;
30
34
  setDisplayTokens(0);
@@ -40,6 +44,7 @@ export function useTokenDisplay() {
40
44
  startTokenTimer,
41
45
  stopTokenTimer,
42
46
  updateTokenCount,
47
+ syncTokenDisplay,
43
48
  resetTokens,
44
49
  };
45
50
  }
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { render } from 'ink';
3
- import REPL from './screens/repl';
3
+ import REPL from './screens/repl.js';
4
4
  export function startJarvis() {
5
5
  render(_jsx(REPL, {}), { exitOnCtrlC: false });
6
6
  }
@@ -2,24 +2,25 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useState, useCallback, useRef, useEffect } from 'react';
3
3
  import { Box, Text, useInput, useApp } from 'ink';
4
4
  import Spinner from 'ink-spinner';
5
- import MultilineInput from '../components/MultilineInput';
6
- import WelcomeHeader from '../components/WelcomeHeader';
7
- import MessageItem from '../components/MessageItem';
8
- import StreamingText from '../components/StreamingText';
9
- import StatusBar from '../components/StatusBar';
10
- import SlashCommandMenu from '../components/SlashCommandMenu';
11
- import DangerConfirm from '../components/DangerConfirm';
12
- import { useWindowFocus } from '../hooks/useFocus';
13
- import { useInputHistory } from '../hooks/useInputHistory';
14
- import { useDoubleCtrlCExit } from '../hooks/useDoubleCtrlCExit';
15
- import { useTerminalWidth } from '../hooks/useTerminalWidth';
16
- import { useStreamThrottle } from '../hooks/useStreamThrottle';
17
- import { useTokenDisplay } from '../hooks/useTokenDisplay';
18
- import { useSlashMenu } from '../hooks/useSlashMenu';
19
- import { executeSlashCommand } from './slashCommands';
20
- import { QueryEngine } from '../core/QueryEngine';
21
- import { HIDE_WELCOME_AFTER_INPUT } from '../config/constants';
22
- import { generateAgentHint } from '../core/hint';
5
+ import MultilineInput from '../components/MultilineInput.js';
6
+ import WelcomeHeader from '../components/WelcomeHeader.js';
7
+ import MessageItem from '../components/MessageItem.js';
8
+ import StreamingText from '../components/StreamingText.js';
9
+ import StatusBar from '../components/StatusBar.js';
10
+ import SlashCommandMenu from '../components/SlashCommandMenu.js';
11
+ import DangerConfirm from '../components/DangerConfirm.js';
12
+ import { useWindowFocus } from '../hooks/useFocus.js';
13
+ import { useInputHistory } from '../hooks/useInputHistory.js';
14
+ import { useDoubleCtrlCExit } from '../hooks/useDoubleCtrlCExit.js';
15
+ import { useTerminalWidth } from '../hooks/useTerminalWidth.js';
16
+ import { useStreamThrottle } from '../hooks/useStreamThrottle.js';
17
+ import { useTokenDisplay } from '../hooks/useTokenDisplay.js';
18
+ import { useSlashMenu } from '../hooks/useSlashMenu.js';
19
+ import { executeSlashCommand } from './slashCommands.js';
20
+ import { QueryEngine } from '../core/QueryEngine.js';
21
+ import { HIDE_WELCOME_AFTER_INPUT } from '../config/constants.js';
22
+ import { generateAgentHint } from '../core/hint.js';
23
+ import { subscribeAgentCount, getActiveAgentCount } from '../core/spawnRegistry.js';
23
24
  export default function REPL() {
24
25
  const { exit } = useApp();
25
26
  const width = useTerminalWidth();
@@ -33,6 +34,7 @@ export default function REPL() {
33
34
  const [showWelcome, setShowWelcome] = useState(true);
34
35
  const [showDetails, setShowDetails] = useState(false);
35
36
  const [placeholder, setPlaceholder] = useState('');
37
+ const [activeAgents, setActiveAgents] = useState(getActiveAgentCount());
36
38
  const lastEscRef = useRef(0);
37
39
  const sessionRef = useRef({
38
40
  id: '', messages: [], createdAt: 0, updatedAt: 0, totalTokens: 0, totalCost: 0,
@@ -40,24 +42,45 @@ export default function REPL() {
40
42
  const engineRef = useRef(null);
41
43
  // 节流 hooks
42
44
  const { streamText, streamBufferRef, startStreamTimer, stopStreamTimer, appendStreamChunk, clearStream, handleThinkingUpdate, finishThinking, thinkingIdRef, stopAll, } = useStreamThrottle(setMessages);
43
- const { displayTokens, tokenCountRef, startTokenTimer, stopTokenTimer, updateTokenCount, resetTokens, } = useTokenDisplay();
45
+ const { displayTokens, tokenCountRef, startTokenTimer, stopTokenTimer, updateTokenCount, syncTokenDisplay, resetTokens, } = useTokenDisplay();
46
+ // ===== 新会话逻辑 =====
47
+ const handleNewSession = useCallback(() => {
48
+ if (engineRef.current) {
49
+ engineRef.current.reset();
50
+ sessionRef.current = engineRef.current.getSession();
51
+ }
52
+ setMessages([]);
53
+ clearStream();
54
+ setLoopState(null);
55
+ setIsProcessing(false);
56
+ setShowWelcome(true);
57
+ resetTokens();
58
+ generateAgentHint().then((hint) => setPlaceholder(hint)).catch(() => { });
59
+ }, [clearStream, resetTokens]);
44
60
  // 斜杠菜单
45
61
  const slashMenu = useSlashMenu({
46
62
  engineRef, sessionRef, tokenCountRef,
47
63
  setMessages,
48
- setDisplayTokens: (n) => updateTokenCount(n),
64
+ setDisplayTokens: (n) => syncTokenDisplay(n),
49
65
  setLoopState, setIsProcessing, setShowWelcome, setInput,
50
66
  stopAll,
67
+ onNewSession: handleNewSession,
51
68
  });
52
69
  // 危险命令确认
53
70
  const [dangerConfirm, setDangerConfirm] = useState(null);
54
71
  useEffect(() => {
55
72
  engineRef.current = new QueryEngine();
56
73
  sessionRef.current = engineRef.current.getSession();
74
+ // 注册持久 UI 回调,供 spawn_agent 后台子 Agent 跨轮次推送消息
75
+ engineRef.current.registerUIBus((msg) => setMessages((prev) => [...prev, msg]), (id, updates) => setMessages((prev) => prev.map((m) => (m.id === id ? { ...m, ...updates } : m))));
57
76
  generateAgentHint().then((hint) => setPlaceholder(hint)).catch((err) => {
58
77
  console.error('[hint] 初始化提示失败:', err);
59
78
  });
60
79
  }, []);
80
+ // 订阅后台 SubAgent 计数变化
81
+ useEffect(() => {
82
+ return subscribeAgentCount((count) => setActiveAgents(count));
83
+ }, []);
61
84
  // ===== Engine Callbacks =====
62
85
  const callbacks = {
63
86
  onMessage: (msg) => {
@@ -106,6 +129,12 @@ export default function REPL() {
106
129
  setDangerConfirm({ command, reason, ruleName, resolve });
107
130
  });
108
131
  },
132
+ onSubAgentMessage: (msg) => {
133
+ setMessages((prev) => [...prev, msg]);
134
+ },
135
+ onSubAgentUpdateMessage: (id, updates) => {
136
+ setMessages((prev) => prev.map((msg) => (msg.id === id ? { ...msg, ...updates } : msg)));
137
+ },
109
138
  };
110
139
  // ===== 提交处理 =====
111
140
  const handleSubmit = useCallback(async (value) => {
@@ -121,18 +150,7 @@ export default function REPL() {
121
150
  setInput('');
122
151
  slashMenu.setSlashMenuVisible(false);
123
152
  if (cmdName === 'new') {
124
- // 新会话:重置引擎 + 清空所有状态
125
- if (engineRef.current) {
126
- engineRef.current.reset();
127
- sessionRef.current = engineRef.current.getSession();
128
- }
129
- setMessages([]);
130
- clearStream();
131
- setLoopState(null);
132
- setIsProcessing(false);
133
- setShowWelcome(true);
134
- resetTokens();
135
- generateAgentHint().then((hint) => setPlaceholder(hint)).catch(() => { });
153
+ handleNewSession();
136
154
  }
137
155
  else if (cmdName === 'session_clear') {
138
156
  if (engineRef.current) {
@@ -228,7 +246,7 @@ export default function REPL() {
228
246
  setInput('');
229
247
  clearStream();
230
248
  await engineRef.current.handleQuery(trimmed, callbacks);
231
- }, [isProcessing, pushHistory, clearStream, resetTokens, slashMenu]);
249
+ }, [isProcessing, pushHistory, clearStream, resetTokens, slashMenu, handleNewSession]);
232
250
  // ===== 输入处理 =====
233
251
  const handleUpArrow = useCallback(() => {
234
252
  const result = navigateUp(input);
@@ -366,5 +384,5 @@ export default function REPL() {
366
384
  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) => {
367
385
  dangerConfirm.resolve(choice);
368
386
  setDangerConfirm(null);
369
- } })), 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 })] })] }));
387
+ } })), 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 })] })] }));
370
388
  }
@@ -1,4 +1,4 @@
1
- import { Message } from '../types/index';
1
+ import { Message } from '../types/index.js';
2
2
  /**
3
3
  * 斜杠命令执行器
4
4
  *
@@ -1,8 +1,8 @@
1
- import { executeInit } from '../commands/init';
2
- import { APP_VERSION } from '../config/constants';
3
- import { allTools } from '../tools/index';
4
- import { listSkills } from '../skills/index';
5
- import { getExternalSkillsDir } from '../skills/loader';
1
+ import { executeInit } from '../commands/init.js';
2
+ import { APP_VERSION } from '../config/constants.js';
3
+ import { allTools } from '../tools/index.js';
4
+ import { listSkills } from '../skills/index.js';
5
+ import { getExternalSkillsDir } from '../skills/loader.js';
6
6
  import { listPermanentAuthorizations, DANGER_RULES, } from '../core/safeguard.js';
7
7
  /**
8
8
  * 斜杠命令执行器
@@ -89,7 +89,8 @@ export function executeSlashCommand(cmdName) {
89
89
  const parts = [];
90
90
  parts.push('### Built-in Tools\n');
91
91
  allTools.forEach((t, i) => {
92
- parts.push(`${i + 1}. \`${t.name}\` - ${t.description.slice(0, 60)}`);
92
+ const summary = t.description.split('\n')[0].slice(0, 80);
93
+ parts.push(`${i + 1}. \`${t.name}\` - ${summary}`);
93
94
  });
94
95
  parts.push('');
95
96
  parts.push(`### External Skills\n`);
@@ -6,8 +6,8 @@
6
6
  * 2. ~/.jarvis/config.json
7
7
  * 3. ./.jarvis/config.json
8
8
  */
9
- import { LLMService, StreamCallbacks, TranscriptMessage, Tool, AbortSignal as AppAbortSignal } from '../../types/index';
10
- import { ModelConfig } from '../../config/loader';
9
+ import { LLMService, StreamCallbacks, TranscriptMessage, Tool, AbortSignal as AppAbortSignal } from '../../types/index.js';
10
+ import { ModelConfig } from '../../config/loader.js';
11
11
  export interface LLMConfig {
12
12
  apiKey: string;
13
13
  model: string;
@@ -16,6 +16,8 @@ export interface LLMConfig {
16
16
  temperature?: number;
17
17
  /** 额外请求体参数,直接合并到 API 请求 body */
18
18
  extraBody?: Record<string, unknown>;
19
+ /** 直接指定 system prompt,跳过 agent 文件加载(SubAgent 场景使用) */
20
+ systemPrompt?: string;
19
21
  }
20
22
  /** 从配置文件构建 LLMConfig,找不到则回退环境变量 */
21
23
  export declare function getDefaultConfig(): LLMConfig;
@@ -6,11 +6,11 @@
6
6
  * 2. ~/.jarvis/config.json
7
7
  * 3. ./.jarvis/config.json
8
8
  */
9
- import { loadConfig, getActiveModel } from '../../config/loader';
10
- import { getAgent } from '../../agents/index';
11
- import { DEFAULT_AGENT } from '../../config/constants';
12
- import { getActiveAgent } from '../../config/agentState';
13
- import { getSystemInfoPrompt } from '../../config/systemInfo';
9
+ import { loadConfig, getActiveModel } from '../../config/loader.js';
10
+ import { getAgent } from '../../agents/index.js';
11
+ import { DEFAULT_AGENT } from '../../config/constants.js';
12
+ import { getActiveAgent } from '../../config/agentState.js';
13
+ import { getSystemInfoPrompt } from '../../config/systemInfo.js';
14
14
  /** 从配置文件构建 LLMConfig,找不到则回退环境变量 */
15
15
  export function getDefaultConfig() {
16
16
  const jarvisCfg = loadConfig();
@@ -142,6 +142,11 @@ export class LLMServiceImpl {
142
142
  if (!this.config.apiKey) {
143
143
  throw new Error('API_KEY 未配置。请在 .jarvis/config.json 或环境变量中设置。');
144
144
  }
145
+ // 若外部直接传入 systemPrompt(SubAgent 场景),直接使用,跳过 agent 文件加载
146
+ if (this.config.systemPrompt) {
147
+ this.systemPrompt = this.config.systemPrompt;
148
+ return;
149
+ }
145
150
  // 从当前激活的智能体加载 system prompt(运行时动态读取)
146
151
  const currentAgent = getActiveAgent(DEFAULT_AGENT);
147
152
  const agent = getAgent(currentAgent);
@@ -157,7 +162,16 @@ export class LLMServiceImpl {
157
162
  '安全围栏会自动处理拦截和用户确认流程。';
158
163
  // 追加系统环境信息,帮助 LLM 感知用户运行环境
159
164
  const systemInfo = getSystemInfoPrompt();
160
- this.systemPrompt = agentPrompt + roleBoundary + systemInfo;
165
+ // 追加当前激活模型信息
166
+ const jarvisCfg = loadConfig();
167
+ const activeModelCfg = getActiveModel(jarvisCfg);
168
+ const activeModelKey = jarvisCfg.system.model ?? 'unknown';
169
+ const modelInfo = '\n\n---\n[当前模型] 以下是本次会话使用的 LLM 模型信息:' +
170
+ `\n- 模型标识: ${activeModelKey}` +
171
+ `\n- 模型名称: ${activeModelCfg?.model ?? 'unknown'}` +
172
+ `\n- API 地址: ${activeModelCfg?.api_url ?? 'unknown'}` +
173
+ `\n- 最大 Token: ${activeModelCfg?.max_tokens ?? 'unknown'}`;
174
+ this.systemPrompt = agentPrompt + roleBoundary + systemInfo + modelInfo;
161
175
  }
162
176
  async streamMessage(transcript, tools, callbacks, abortSignal) {
163
177
  const messages = toOpenAIMessages(transcript, this.systemPrompt);