@code4bug/jarvis-agent 1.1.8 → 1.3.1

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 (65) hide show
  1. package/README.md +1 -1
  2. package/dist/agents/jarvis.md +1 -1
  3. package/dist/commands/init.js +4 -4
  4. package/dist/components/AnimatedStatusText.d.ts +10 -0
  5. package/dist/components/AnimatedStatusText.js +17 -0
  6. package/dist/components/ComposerPane.d.ts +25 -0
  7. package/dist/components/ComposerPane.js +10 -0
  8. package/dist/components/FooterPane.d.ts +9 -0
  9. package/dist/components/FooterPane.js +22 -0
  10. package/dist/components/InputTextView.d.ts +11 -0
  11. package/dist/components/InputTextView.js +44 -0
  12. package/dist/components/MarkdownText.d.ts +4 -0
  13. package/dist/components/MarkdownText.js +10 -3
  14. package/dist/components/MessageItem.js +4 -1
  15. package/dist/components/MessageList.d.ts +9 -0
  16. package/dist/components/MessageList.js +8 -0
  17. package/dist/components/MessageViewport.d.ts +21 -0
  18. package/dist/components/MessageViewport.js +11 -0
  19. package/dist/components/MultilineInput.js +62 -344
  20. package/dist/components/StatusBar.js +9 -6
  21. package/dist/components/StreamingDraft.d.ts +11 -0
  22. package/dist/components/StreamingDraft.js +14 -0
  23. package/dist/components/WelcomeHeader.js +4 -2
  24. package/dist/components/inputEditing.d.ts +20 -0
  25. package/dist/components/inputEditing.js +48 -0
  26. package/dist/components/setup/SetupConfirmStep.d.ts +8 -0
  27. package/dist/components/setup/SetupConfirmStep.js +12 -0
  28. package/dist/components/setup/SetupDoneStep.d.ts +7 -0
  29. package/dist/components/setup/SetupDoneStep.js +5 -0
  30. package/dist/components/setup/SetupFormStep.d.ts +11 -0
  31. package/dist/components/setup/SetupFormStep.js +44 -0
  32. package/dist/components/setup/SetupHeader.d.ts +9 -0
  33. package/dist/components/setup/SetupHeader.js +25 -0
  34. package/dist/components/setup/SetupProviderStep.d.ts +6 -0
  35. package/dist/components/setup/SetupProviderStep.js +20 -0
  36. package/dist/components/setup/SetupWelcomeStep.d.ts +5 -0
  37. package/dist/components/setup/SetupWelcomeStep.js +5 -0
  38. package/dist/config/bootstrap.d.ts +38 -0
  39. package/dist/config/bootstrap.js +155 -0
  40. package/dist/config/constants.d.ts +7 -6
  41. package/dist/config/constants.js +29 -16
  42. package/dist/config/loader.d.ts +2 -0
  43. package/dist/config/loader.js +4 -0
  44. package/dist/core/hint.js +3 -3
  45. package/dist/core/query.js +3 -2
  46. package/dist/hooks/useMultilineInputStream.d.ts +17 -0
  47. package/dist/hooks/useMultilineInputStream.js +141 -0
  48. package/dist/hooks/useTerminalCursorSync.d.ts +8 -0
  49. package/dist/hooks/useTerminalCursorSync.js +44 -0
  50. package/dist/hooks/useTerminalSize.d.ts +7 -0
  51. package/dist/hooks/useTerminalSize.js +21 -0
  52. package/dist/index.js +2 -2
  53. package/dist/screens/AppBootstrap.d.ts +1 -0
  54. package/dist/screens/AppBootstrap.js +14 -0
  55. package/dist/screens/repl.js +39 -28
  56. package/dist/screens/setup/SetupWizard.d.ts +7 -0
  57. package/dist/screens/setup/SetupWizard.js +198 -0
  58. package/dist/services/api/llm.js +5 -3
  59. package/dist/skills/index.js +10 -3
  60. package/dist/terminal/cursor.d.ts +6 -0
  61. package/dist/terminal/cursor.js +21 -0
  62. package/dist/tools/createSkill.js +59 -1
  63. package/dist/tools/readFile.js +28 -3
  64. package/dist/tools/writeFile.js +63 -2
  65. package/package.json +1 -1
@@ -0,0 +1,141 @@
1
+ import { useEffect, useRef } from 'react';
2
+ export function useMultilineInputStream({ stdin, isActive, pasteDetectWindowMs, imeComposeWindowMs, isAsciiLowerAlpha, hasNonAscii, insertPaste, handleNormalInput, commitImeBuffer, flushImeBuffer, appendImeBuffer, getImeBuffer, clearImeBufferWithInsertedLenRollback, }) {
3
+ const pasteBufferRef = useRef(null);
4
+ const batchBufferRef = useRef('');
5
+ const batchTimerRef = useRef(null);
6
+ const imeTimerRef = useRef(null);
7
+ const escTimerRef = useRef(null);
8
+ const ESC_WAIT_MS = 50;
9
+ useEffect(() => {
10
+ if (!stdin || !isActive)
11
+ return;
12
+ const flushBatchBuffer = () => {
13
+ const buf = batchBufferRef.current;
14
+ batchBufferRef.current = '';
15
+ batchTimerRef.current = null;
16
+ if (!buf)
17
+ return;
18
+ const cleaned = buf.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
19
+ if (cleaned.includes('\n') && cleaned.length > 1) {
20
+ insertPaste(cleaned);
21
+ return;
22
+ }
23
+ if (isAsciiLowerAlpha(buf)) {
24
+ appendImeBuffer(buf);
25
+ if (imeTimerRef.current !== null) {
26
+ clearTimeout(imeTimerRef.current);
27
+ }
28
+ imeTimerRef.current = setTimeout(flushImeBuffer, imeComposeWindowMs);
29
+ return;
30
+ }
31
+ if (hasNonAscii(buf) && getImeBuffer().length > 0) {
32
+ if (imeTimerRef.current !== null) {
33
+ clearTimeout(imeTimerRef.current);
34
+ imeTimerRef.current = null;
35
+ }
36
+ clearImeBufferWithInsertedLenRollback();
37
+ handleNormalInput(buf);
38
+ return;
39
+ }
40
+ if (getImeBuffer().length > 0) {
41
+ commitImeBuffer();
42
+ }
43
+ handleNormalInput(buf);
44
+ };
45
+ const onData = (data) => {
46
+ const raw = data.toString('utf-8');
47
+ if (raw.includes('\x1B[200~')) {
48
+ const startIdx = raw.indexOf('\x1B[200~') + 6;
49
+ const endIdx = raw.indexOf('\x1B[201~');
50
+ if (endIdx !== -1) {
51
+ insertPaste(raw.slice(startIdx, endIdx));
52
+ }
53
+ else {
54
+ pasteBufferRef.current = raw.slice(startIdx);
55
+ }
56
+ return;
57
+ }
58
+ if (pasteBufferRef.current !== null) {
59
+ const endIdx = raw.indexOf('\x1B[201~');
60
+ if (endIdx !== -1) {
61
+ pasteBufferRef.current += raw.slice(0, endIdx);
62
+ insertPaste(pasteBufferRef.current);
63
+ pasteBufferRef.current = null;
64
+ }
65
+ else {
66
+ pasteBufferRef.current += raw;
67
+ }
68
+ return;
69
+ }
70
+ if (escTimerRef.current !== null) {
71
+ clearTimeout(escTimerRef.current);
72
+ escTimerRef.current = null;
73
+ if (raw === '\r' || raw === '\n') {
74
+ handleNormalInput('\x1B\r');
75
+ return;
76
+ }
77
+ handleNormalInput('\x1B');
78
+ }
79
+ const isSingleControl = raw === '\r' || raw === '\n' ||
80
+ raw === '\x7F' || raw === '\x08' ||
81
+ raw === '\t' || raw === '\x1B' ||
82
+ raw === '\x1B\r' || raw === '\x1B\n' ||
83
+ raw.startsWith('\x1B[') || raw.startsWith('\x1B]') || raw.startsWith('\x1BO');
84
+ if (raw === '\x03' || raw === '\x0F' || raw === '\x0C')
85
+ return;
86
+ if (raw === '\x1B') {
87
+ escTimerRef.current = setTimeout(() => {
88
+ escTimerRef.current = null;
89
+ handleNormalInput('\x1B');
90
+ }, ESC_WAIT_MS);
91
+ return;
92
+ }
93
+ if (isSingleControl && batchBufferRef.current === '') {
94
+ if (getImeBuffer().length > 0) {
95
+ commitImeBuffer();
96
+ }
97
+ handleNormalInput(raw);
98
+ return;
99
+ }
100
+ if (isSingleControl && batchBufferRef.current !== '') {
101
+ batchBufferRef.current += raw;
102
+ return;
103
+ }
104
+ batchBufferRef.current += raw;
105
+ if (batchTimerRef.current !== null) {
106
+ clearTimeout(batchTimerRef.current);
107
+ }
108
+ batchTimerRef.current = setTimeout(flushBatchBuffer, pasteDetectWindowMs);
109
+ };
110
+ stdin.prependListener('data', onData);
111
+ return () => {
112
+ stdin.off('data', onData);
113
+ if (batchTimerRef.current !== null) {
114
+ clearTimeout(batchTimerRef.current);
115
+ batchTimerRef.current = null;
116
+ }
117
+ if (escTimerRef.current !== null) {
118
+ clearTimeout(escTimerRef.current);
119
+ escTimerRef.current = null;
120
+ }
121
+ if (imeTimerRef.current !== null) {
122
+ clearTimeout(imeTimerRef.current);
123
+ imeTimerRef.current = null;
124
+ }
125
+ };
126
+ }, [
127
+ stdin,
128
+ isActive,
129
+ pasteDetectWindowMs,
130
+ imeComposeWindowMs,
131
+ isAsciiLowerAlpha,
132
+ hasNonAscii,
133
+ insertPaste,
134
+ handleNormalInput,
135
+ commitImeBuffer,
136
+ flushImeBuffer,
137
+ appendImeBuffer,
138
+ getImeBuffer,
139
+ clearImeBufferWithInsertedLenRollback,
140
+ ]);
141
+ }
@@ -0,0 +1,8 @@
1
+ interface UseTerminalCursorSyncOptions {
2
+ showCursor: boolean;
3
+ isActive: boolean;
4
+ rowsBelow: number;
5
+ delayMs?: number;
6
+ }
7
+ export declare function useTerminalCursorSync({ showCursor, isActive, rowsBelow, delayMs, }: UseTerminalCursorSyncOptions): void;
8
+ export {};
@@ -0,0 +1,44 @@
1
+ import { useEffect, useRef } from 'react';
2
+ import { moveCursorToColumn, relocateCursorToInputLine } from '../terminal/cursor.js';
3
+ export function useTerminalCursorSync({ showCursor, isActive, rowsBelow, delayMs = 80, }) {
4
+ const cursorRelocTimerRef = useRef(null);
5
+ const lastCursorCommandRef = useRef('');
6
+ useEffect(() => {
7
+ return () => {
8
+ if (cursorRelocTimerRef.current !== null) {
9
+ clearTimeout(cursorRelocTimerRef.current);
10
+ }
11
+ };
12
+ }, []);
13
+ useEffect(() => {
14
+ if (cursorRelocTimerRef.current !== null) {
15
+ clearTimeout(cursorRelocTimerRef.current);
16
+ }
17
+ if (!showCursor || !isActive) {
18
+ cursorRelocTimerRef.current = setTimeout(() => {
19
+ cursorRelocTimerRef.current = null;
20
+ const commandKey = 'col:1';
21
+ if (lastCursorCommandRef.current === commandKey)
22
+ return;
23
+ lastCursorCommandRef.current = commandKey;
24
+ moveCursorToColumn(1);
25
+ }, delayMs);
26
+ }
27
+ else {
28
+ cursorRelocTimerRef.current = setTimeout(() => {
29
+ cursorRelocTimerRef.current = null;
30
+ const commandKey = `input:${rowsBelow}:3`;
31
+ if (lastCursorCommandRef.current === commandKey)
32
+ return;
33
+ lastCursorCommandRef.current = commandKey;
34
+ relocateCursorToInputLine(rowsBelow, 3);
35
+ }, delayMs);
36
+ }
37
+ return () => {
38
+ if (cursorRelocTimerRef.current !== null) {
39
+ clearTimeout(cursorRelocTimerRef.current);
40
+ cursorRelocTimerRef.current = null;
41
+ }
42
+ };
43
+ }, [showCursor, isActive, rowsBelow, delayMs]);
44
+ }
@@ -0,0 +1,7 @@
1
+ interface TerminalSize {
2
+ width: number;
3
+ height: number;
4
+ }
5
+ /** 响应式终端尺寸 */
6
+ export declare function useTerminalSize(): TerminalSize;
7
+ export {};
@@ -0,0 +1,21 @@
1
+ import { useState, useEffect } from 'react';
2
+ /** 响应式终端尺寸 */
3
+ export function useTerminalSize() {
4
+ const [size, setSize] = useState(() => ({
5
+ width: process.stdout.columns || 80,
6
+ height: process.stdout.rows || 24,
7
+ }));
8
+ useEffect(() => {
9
+ const onResize = () => {
10
+ setSize({
11
+ width: process.stdout.columns || 80,
12
+ height: process.stdout.rows || 24,
13
+ });
14
+ };
15
+ process.stdout.on('resize', onResize);
16
+ return () => {
17
+ process.stdout.off('resize', onResize);
18
+ };
19
+ }, []);
20
+ return size;
21
+ }
package/dist/index.js CHANGED
@@ -1,8 +1,8 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { render } from 'ink';
3
3
  import { logInfo } from './core/logger.js';
4
- import REPL from './screens/repl.js';
4
+ import AppBootstrap from './screens/AppBootstrap.js';
5
5
  export function startJarvis() {
6
6
  logInfo('app.render.start');
7
- render(_jsx(REPL, {}), { exitOnCtrlC: false });
7
+ render(_jsx(AppBootstrap, {}), { exitOnCtrlC: false });
8
8
  }
@@ -0,0 +1 @@
1
+ export default function AppBootstrap(): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,14 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { useState } from 'react';
3
+ import REPL from './repl.js';
4
+ import SetupWizard from './setup/SetupWizard.js';
5
+ import { checkBootstrapStatus } from '../config/bootstrap.js';
6
+ export default function AppBootstrap() {
7
+ const [status, setStatus] = useState(() => checkBootstrapStatus());
8
+ if (status.ok) {
9
+ return _jsx(REPL, {});
10
+ }
11
+ return (_jsx(SetupWizard, { initialStatus: status, onCompleted: () => {
12
+ setStatus(checkBootstrapStatus());
13
+ } }));
14
+ }
@@ -1,20 +1,15 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useState, useCallback, useRef, useEffect } from 'react';
3
- import { Box, Text, useInput, useApp } from 'ink';
4
- import Spinner from 'ink-spinner';
5
- import MultilineInput from '../components/MultilineInput.js';
3
+ import { Box, useInput, useApp } from 'ink';
6
4
  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';
5
+ import MessageViewport from '../components/MessageViewport.js';
6
+ import ComposerPane from '../components/ComposerPane.js';
7
+ import FooterPane from '../components/FooterPane.js';
12
8
  import { useWindowFocus } from '../hooks/useFocus.js';
13
9
  import { useInputHistory } from '../hooks/useInputHistory.js';
14
10
  import { useDoubleCtrlCExit } from '../hooks/useDoubleCtrlCExit.js';
15
11
  import { useTerminalWidth } from '../hooks/useTerminalWidth.js';
16
12
  import { useStreamThrottle } from '../hooks/useStreamThrottle.js';
17
- import { useTokenDisplay } from '../hooks/useTokenDisplay.js';
18
13
  import { useSlashMenu } from '../hooks/useSlashMenu.js';
19
14
  import { executeSlashCommand } from './slashCommands.js';
20
15
  import { QueryEngine } from '../core/QueryEngine.js';
@@ -24,6 +19,7 @@ import { subscribeAgentCount, getActiveAgentCount } from '../core/spawnRegistry.
24
19
  import { logError, logInfo, logWarn } from '../core/logger.js';
25
20
  import { getAgentSubCommands } from '../commands/index.js';
26
21
  import { setActiveAgent } from '../config/agentState.js';
22
+ import { hideTerminalCursor, showTerminalCursor } from '../terminal/cursor.js';
27
23
  export default function REPL() {
28
24
  const { exit } = useApp();
29
25
  const width = useTerminalWidth();
@@ -43,13 +39,34 @@ export default function REPL() {
43
39
  const [placeholder, setPlaceholder] = useState('');
44
40
  const [activeAgents, setActiveAgents] = useState(getActiveAgentCount());
45
41
  const lastEscRef = useRef(0);
42
+ const lastAbortNoticeRef = useRef(0);
46
43
  const sessionRef = useRef({
47
44
  id: '', messages: [], createdAt: 0, updatedAt: 0, totalTokens: 0, totalCost: 0,
48
45
  });
49
46
  const engineRef = useRef(null);
47
+ const tokenCountRef = useRef(0);
50
48
  // 节流 hooks
51
49
  const { streamText, streamBufferRef, startStreamTimer, stopStreamTimer, appendStreamChunk, clearStream, handleThinkingUpdate, finishThinking, thinkingIdRef, stopAll, } = useStreamThrottle(setMessages);
52
- const { displayTokens, tokenCountRef, startTokenTimer, stopTokenTimer, updateTokenCount, syncTokenDisplay, resetTokens, } = useTokenDisplay();
50
+ const syncTokenDisplay = useCallback((count) => {
51
+ tokenCountRef.current = count;
52
+ }, []);
53
+ const resetTokens = useCallback(() => {
54
+ tokenCountRef.current = 0;
55
+ }, []);
56
+ const appendAbortNotice = useCallback(() => {
57
+ const now = Date.now();
58
+ if (now - lastAbortNoticeRef.current < 800)
59
+ return;
60
+ lastAbortNoticeRef.current = now;
61
+ setMessages((prev) => [...prev, {
62
+ id: `abort-notice-${now}`,
63
+ type: 'system',
64
+ status: 'aborted',
65
+ content: '已中断当前推理',
66
+ timestamp: now,
67
+ abortHint: '推理已中断(ESC)',
68
+ }]);
69
+ }, []);
53
70
  // ===== 新会话逻辑 =====
54
71
  const handleNewSession = useCallback(() => {
55
72
  logInfo('ui.new_session');
@@ -125,17 +142,15 @@ export default function REPL() {
125
142
  setLoopState(state);
126
143
  setIsProcessing(state.isRunning);
127
144
  if (state.isRunning) {
128
- startTokenTimer();
129
145
  startStreamTimer();
130
146
  }
131
147
  else {
132
- stopTokenTimer();
133
148
  stopAll();
134
149
  }
135
150
  },
136
151
  onSessionUpdate: (s) => {
137
152
  sessionRef.current = s;
138
- updateTokenCount(s.totalTokens);
153
+ tokenCountRef.current = s.totalTokens;
139
154
  },
140
155
  onConfirmDangerousCommand: (command, reason, ruleName) => {
141
156
  return new Promise((resolve) => {
@@ -321,19 +336,11 @@ export default function REPL() {
321
336
  }, [placeholder]);
322
337
  // ===== 隐藏系统默认终端光标,使用 ink 渲染的反色光标代替 =====
323
338
  useEffect(() => {
324
- process.stdout.write('\x1B[?25l');
339
+ hideTerminalCursor();
325
340
  return () => {
326
- process.stdout.write('\x1B[?25h');
341
+ showTerminalCursor();
327
342
  };
328
343
  }, []);
329
- // ===== processing/streaming 期间,每次 re-render 后将物理光标移到行首 =====
330
- // ink 每次渲染后光标停在 StatusBar 行末,macOS IME 会在该位置显示 composing 候选框,
331
- // 导致候选框与进度条重叠。将光标移到列 1(行首)让候选框出现在屏幕左侧空白区域。
332
- useEffect(() => {
333
- if (!isProcessing)
334
- return;
335
- process.stdout.write('\x1B[1G');
336
- });
337
344
  // ===== processing 期间隐藏终端光标,阻止 IME composing 显示 =====
338
345
  const isProcessingRef = useRef(isProcessing);
339
346
  isProcessingRef.current = isProcessing;
@@ -341,7 +348,7 @@ export default function REPL() {
341
348
  if (!isProcessing)
342
349
  return;
343
350
  // 隐藏终端光标 — 终端 IME 的 inline composing 仅在光标可见时渲染
344
- process.stdout.write('\x1B[?25l');
351
+ hideTerminalCursor();
345
352
  // 高优先级 stdin 拦截:吞掉 processing 期间的所有输入(Ctrl+C / ESC 除外)
346
353
  // 防止字符积累在缓冲区,processing 结束后污染输入框
347
354
  const drain = (data) => {
@@ -374,6 +381,7 @@ export default function REPL() {
374
381
  return;
375
382
  }
376
383
  if (key.escape && engineRef.current) {
384
+ appendAbortNotice();
377
385
  engineRef.current.abort();
378
386
  return;
379
387
  }
@@ -412,6 +420,7 @@ export default function REPL() {
412
420
  if (key.escape) {
413
421
  if (isProcessing && engineRef.current) {
414
422
  logWarn('ui.abort_by_escape');
423
+ appendAbortNotice();
415
424
  engineRef.current.abort();
416
425
  }
417
426
  else if (input.length > 0) {
@@ -428,8 +437,10 @@ export default function REPL() {
428
437
  }
429
438
  });
430
439
  // ===== 渲染 =====
431
- 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) => {
432
- dangerConfirm.resolve(choice);
433
- setDangerConfirm(null);
434
- } })), 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 })] })] }));
440
+ return (_jsxs(Box, { flexDirection: "column", width: width, children: [showWelcome && _jsx(WelcomeHeader, { width: width }), _jsx(Box, { marginTop: showWelcome ? 0 : 1, children: _jsx(MessageViewport, { messages: messages, streamText: streamText, showDetails: showDetails, dangerConfirm: dangerConfirm, loopState: loopState, onResolveDangerConfirm: (choice) => {
441
+ if (!dangerConfirm)
442
+ return;
443
+ dangerConfirm.resolve(choice);
444
+ setDangerConfirm(null);
445
+ } }) }), _jsx(ComposerPane, { width: width, countdown: countdown, isProcessing: isProcessing, input: input, placeholder: placeholder, windowFocused: windowFocused, slashMenuVisible: slashMenu.slashMenuVisible, slashMenuItems: slashMenu.slashMenuItems, slashMenuIndex: slashMenu.slashMenuIndex, onInputChange: handleInputChange, onSubmit: handleEditorSubmit, onUpArrow: handleUpArrow, onDownArrow: handleDownArrow, onSlashMenuUp: slashMenu.handleSlashMenuUp, onSlashMenuDown: slashMenu.handleSlashMenuDown, onSlashMenuSelect: handleSlashMenuAutocomplete, onSlashMenuClose: slashMenu.handleSlashMenuClose, onTabFillPlaceholder: handleTabFillPlaceholder }), _jsx(FooterPane, { width: width, tokenCountRef: tokenCountRef, activeAgents: activeAgents })] }));
435
446
  }
@@ -0,0 +1,7 @@
1
+ import { BootstrapStatus } from '../../config/bootstrap.js';
2
+ interface SetupWizardProps {
3
+ initialStatus: BootstrapStatus;
4
+ onCompleted: () => void;
5
+ }
6
+ export default function SetupWizard({ initialStatus, onCompleted }: SetupWizardProps): import("react/jsx-runtime").JSX.Element;
7
+ export {};
@@ -0,0 +1,198 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useMemo, useState } from 'react';
3
+ import { Box, Text, useApp, useInput } from 'ink';
4
+ import SetupHeader from '../../components/setup/SetupHeader.js';
5
+ import SetupWelcomeStep from '../../components/setup/SetupWelcomeStep.js';
6
+ import SetupProviderStep from '../../components/setup/SetupProviderStep.js';
7
+ import SetupFormStep from '../../components/setup/SetupFormStep.js';
8
+ import SetupConfirmStep from '../../components/setup/SetupConfirmStep.js';
9
+ import SetupDoneStep from '../../components/setup/SetupDoneStep.js';
10
+ import { buildJarvisConfig, getDefaultSetupForm, validateSetupForm, writeBootstrapConfig, } from '../../config/bootstrap.js';
11
+ import { useDoubleCtrlCExit } from '../../hooks/useDoubleCtrlCExit.js';
12
+ import { useTerminalWidth } from '../../hooks/useTerminalWidth.js';
13
+ const PROVIDERS = ['openai-compatible', 'ollama'];
14
+ const FORM_FIELDS = [
15
+ 'profileName',
16
+ 'apiUrl',
17
+ 'apiKey',
18
+ 'model',
19
+ 'temperature',
20
+ 'maxTokens',
21
+ ];
22
+ function findFirstInvalidFieldIndex(errors) {
23
+ const firstField = FORM_FIELDS.findIndex((field) => Boolean(errors[field]));
24
+ return firstField >= 0 ? firstField : 0;
25
+ }
26
+ export default function SetupWizard({ initialStatus, onCompleted }) {
27
+ const width = useTerminalWidth();
28
+ const { exit } = useApp();
29
+ const { countdown, handleCtrlC } = useDoubleCtrlCExit(() => {
30
+ exit();
31
+ setTimeout(() => process.exit(0), 50);
32
+ });
33
+ const [step, setStep] = useState('welcome');
34
+ const [provider, setProvider] = useState('openai-compatible');
35
+ const [form, setForm] = useState(() => getDefaultSetupForm('openai-compatible'));
36
+ const [errors, setErrors] = useState({});
37
+ const [selectedFieldIndex, setSelectedFieldIndex] = useState(0);
38
+ const [editingField, setEditingField] = useState(null);
39
+ const [editBuffer, setEditBuffer] = useState('');
40
+ const [isSubmitting, setIsSubmitting] = useState(false);
41
+ const [result, setResult] = useState(null);
42
+ const selectedField = FORM_FIELDS[selectedFieldIndex];
43
+ const preview = useMemo(() => JSON.stringify(buildJarvisConfig(form), null, 2), [form]);
44
+ function moveProvider(offset) {
45
+ const currentIndex = PROVIDERS.indexOf(provider);
46
+ const nextIndex = (currentIndex + offset + PROVIDERS.length) % PROVIDERS.length;
47
+ const nextProvider = PROVIDERS[nextIndex];
48
+ setProvider(nextProvider);
49
+ setForm(getDefaultSetupForm(nextProvider));
50
+ setErrors({});
51
+ }
52
+ function moveField(offset) {
53
+ setSelectedFieldIndex((current) => {
54
+ const next = current + offset;
55
+ if (next < 0)
56
+ return FORM_FIELDS.length - 1;
57
+ if (next >= FORM_FIELDS.length)
58
+ return 0;
59
+ return next;
60
+ });
61
+ }
62
+ function startEditing() {
63
+ setEditingField(selectedField);
64
+ setEditBuffer(form[selectedField]);
65
+ }
66
+ function startEditingField(field) {
67
+ setEditingField(field);
68
+ setEditBuffer(form[field]);
69
+ }
70
+ function stopEditing(save) {
71
+ if (!editingField)
72
+ return;
73
+ if (save) {
74
+ setForm((current) => ({ ...current, [editingField]: editBuffer }));
75
+ }
76
+ setEditingField(null);
77
+ setEditBuffer('');
78
+ }
79
+ async function submitConfig() {
80
+ const nextErrors = validateSetupForm(form);
81
+ setErrors(nextErrors);
82
+ if (Object.keys(nextErrors).length > 0) {
83
+ setSelectedFieldIndex(findFirstInvalidFieldIndex(nextErrors));
84
+ setStep('form');
85
+ return;
86
+ }
87
+ setIsSubmitting(true);
88
+ const config = buildJarvisConfig(form);
89
+ const writeResult = writeBootstrapConfig(config);
90
+ setIsSubmitting(false);
91
+ if (writeResult.success) {
92
+ setResult({
93
+ success: true,
94
+ message: '基础模型配置已完成,后续可直接启动进入聊天界面。',
95
+ configPath: writeResult.path,
96
+ });
97
+ }
98
+ else {
99
+ setResult({
100
+ success: false,
101
+ message: writeResult.message,
102
+ });
103
+ }
104
+ setStep('done');
105
+ }
106
+ useInput((input, key) => {
107
+ if (key.ctrl && input === 'c') {
108
+ handleCtrlC();
109
+ return;
110
+ }
111
+ if (editingField) {
112
+ if (key.escape) {
113
+ stopEditing(false);
114
+ return;
115
+ }
116
+ if (key.return) {
117
+ stopEditing(true);
118
+ return;
119
+ }
120
+ if (key.backspace || key.delete) {
121
+ setEditBuffer((current) => current.slice(0, -1));
122
+ return;
123
+ }
124
+ if (input) {
125
+ setEditBuffer((current) => current + input);
126
+ }
127
+ return;
128
+ }
129
+ if (step === 'welcome') {
130
+ if (key.return) {
131
+ setStep('provider');
132
+ }
133
+ return;
134
+ }
135
+ if (step === 'provider') {
136
+ if (key.upArrow)
137
+ moveProvider(-1);
138
+ if (key.downArrow)
139
+ moveProvider(1);
140
+ if (key.escape)
141
+ setStep('welcome');
142
+ if (key.return) {
143
+ setErrors({});
144
+ setSelectedFieldIndex(0);
145
+ setStep('form');
146
+ }
147
+ return;
148
+ }
149
+ if (step === 'form') {
150
+ if (key.upArrow)
151
+ moveField(-1);
152
+ if (key.downArrow || key.tab)
153
+ moveField(1);
154
+ if (key.escape) {
155
+ setErrors({});
156
+ setStep('provider');
157
+ }
158
+ if (key.return) {
159
+ const nextErrors = validateSetupForm(form);
160
+ setErrors(nextErrors);
161
+ if (Object.keys(nextErrors).length === 0) {
162
+ setStep('confirm');
163
+ }
164
+ else {
165
+ const invalidIndex = findFirstInvalidFieldIndex(nextErrors);
166
+ const invalidField = FORM_FIELDS[invalidIndex];
167
+ setSelectedFieldIndex(invalidIndex);
168
+ startEditingField(invalidField);
169
+ }
170
+ }
171
+ if (input === 'e') {
172
+ startEditing();
173
+ }
174
+ return;
175
+ }
176
+ if (step === 'confirm') {
177
+ if (key.escape) {
178
+ setStep('form');
179
+ return;
180
+ }
181
+ if (key.return && !isSubmitting) {
182
+ void submitConfig();
183
+ }
184
+ return;
185
+ }
186
+ if (step === 'done' && result) {
187
+ if (key.return) {
188
+ if (result.success) {
189
+ onCompleted();
190
+ }
191
+ else {
192
+ setStep('form');
193
+ }
194
+ }
195
+ }
196
+ });
197
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsx(SetupHeader, { width: width, currentStep: step, status: initialStatus }), step === 'welcome' ? _jsx(SetupWelcomeStep, { reasonText: initialStatus.ok ? '配置状态正常' : initialStatus.message }) : null, step === 'provider' ? _jsx(SetupProviderStep, { provider: provider }) : null, step === 'form' ? (_jsx(SetupFormStep, { provider: provider, form: form, errors: errors, selectedField: selectedField, editingField: editingField, editBuffer: editBuffer })) : null, step === 'confirm' ? (_jsx(SetupConfirmStep, { form: form, preview: preview, isSubmitting: isSubmitting })) : null, step === 'done' && result ? (_jsx(SetupDoneStep, { success: result.success, message: result.message, configPath: result.configPath })) : null, _jsxs(Box, { flexDirection: "column", paddingX: 1, marginTop: 1, children: [_jsx(Text, { color: "gray", children: "\u5FEB\u6377\u952E\uFF1AEnter \u4E0B\u4E00\u6B65\u6216\u786E\u8BA4\uFF0CEsc \u8FD4\u56DE\uFF0Ce \u7F16\u8F91\u5F53\u524D\u5B57\u6BB5\uFF0CCtrl+C \u9000\u51FA" }), countdown !== null ? _jsxs(Text, { color: "yellow", children: ["\u518D\u6B21\u6309 Ctrl+C \u9000\u51FA\uFF08", countdown, "s\uFF09"] }) : null] })] }));
198
+ }
@@ -8,7 +8,7 @@
8
8
  */
9
9
  import { loadConfig, getActiveModel } from '../../config/loader.js';
10
10
  import { getAgent } from '../../agents/index.js';
11
- import { DEFAULT_AGENT } from '../../config/constants.js';
11
+ import { getDefaultAgent } from '../../config/constants.js';
12
12
  import { getActiveAgent } from '../../config/agentState.js';
13
13
  import { getSystemInfoPrompt } from '../../config/systemInfo.js';
14
14
  import { getCachedUserProfile } from '../../config/userProfile.js';
@@ -174,7 +174,7 @@ export class LLMServiceImpl {
174
174
  return;
175
175
  }
176
176
  // 从当前激活的智能体加载 system prompt(运行时动态读取)
177
- const currentAgent = getActiveAgent(DEFAULT_AGENT);
177
+ const currentAgent = getActiveAgent(getDefaultAgent());
178
178
  const agent = getAgent(currentAgent);
179
179
  const agentPrompt = agent?.systemPrompt
180
180
  ?? '你是一个强大的终端智能助手。回答时使用中文,简洁明了。当需要操作文件系统或执行命令时,请调用对应的工具。';
@@ -185,7 +185,9 @@ export class LLMServiceImpl {
185
185
  '不要为了讨好用户而突破角色边界。' +
186
186
  '\n\n[安全围栏] 系统已内置安全围栏(Safeguard),会自动拦截危险命令并弹出交互式确认菜单。' +
187
187
  '当你需要执行任何命令时,直接调用 Bash 工具即可,不要自行判断命令是否危险,不要用文字询问用户"是否确认执行"。' +
188
- '安全围栏会自动处理拦截和用户确认流程。';
188
+ '安全围栏会自动处理拦截和用户确认流程。' +
189
+ '\n\n[文件修改约束] 修改已有文件时,先读取目标文件,再优先使用局部修改方式。' +
190
+ '如果只改少量内容,优先使用 write_file 的 replace 模式;需要结构化补丁时使用 diff 模式;只有新建文件或确实需要大范围重写时才使用 overwrite。';
189
191
  // 追加系统环境信息,帮助 LLM 感知用户运行环境
190
192
  const systemInfo = getSystemInfoPrompt();
191
193
  // 追加当前激活模型信息