@code4bug/jarvis-agent 1.3.6 → 1.3.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -0
- package/dist/commands/index.d.ts +2 -0
- package/dist/commands/index.js +1 -0
- package/dist/components/SlashCommandMenu.js +2 -1
- package/dist/core/QueryEngine.d.ts +16 -0
- package/dist/core/QueryEngine.js +85 -25
- package/dist/hooks/useSlashMenu.d.ts +2 -1
- package/dist/hooks/useSlashMenu.js +65 -6
- package/dist/screens/repl.js +43 -0
- package/dist/screens/slashCommands.js +1 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -131,6 +131,7 @@ jarvis --version
|
|
|
131
131
|
| `/new` | 开启新会话 |
|
|
132
132
|
| `/resume` | 恢复历史会话 |
|
|
133
133
|
| `/resume <ID>` | 直接恢复指定历史会话 |
|
|
134
|
+
| `/rewind` | 打开当前会话提问列表并回退到指定位置 |
|
|
134
135
|
| `/agent` | 打开智能体切换列表 |
|
|
135
136
|
| `/agent <名称>` | 切换智能体,重启后生效 |
|
|
136
137
|
| `/permissions` | 查看持久化授权 |
|
|
@@ -166,6 +167,8 @@ jarvis --version
|
|
|
166
167
|
- 斜杠菜单中可使用 `↑ / ↓` 切换命令
|
|
167
168
|
- 在斜杠菜单中按 `Tab` 可补全当前命令
|
|
168
169
|
- 在斜杠菜单中按 `Enter` 可提交当前命令
|
|
170
|
+
- 输入 `/rewind` 后可列出当前会话的提问列表,按时间从上到下升序排列
|
|
171
|
+
- 在 `/rewind` 列表中选择某条提问后,会把该提问回填到输入框,并丢弃该位置之后的上下文
|
|
169
172
|
- 对于多行输入,`↑ / ↓` 用于在输入框内移动光标
|
|
170
173
|
- 对于单行输入,`↑ / ↓` 用于切换历史输入
|
|
171
174
|
|
package/dist/commands/index.d.ts
CHANGED
package/dist/commands/index.js
CHANGED
|
@@ -12,6 +12,7 @@ const builtinCommands = [
|
|
|
12
12
|
{ name: 'quit', description: '退出应用程序', category: 'builtin', submitMode: 'action' },
|
|
13
13
|
{ name: 'bye', description: '退出应用程序', category: 'builtin', submitMode: 'action' },
|
|
14
14
|
{ name: 'resume', description: '恢复历史会话上下文', category: 'builtin', submitMode: 'list' },
|
|
15
|
+
{ name: 'rewind', description: '回退当前会话到指定提问位置', category: 'builtin', submitMode: 'list' },
|
|
15
16
|
{ name: 'help', description: '显示帮助信息', category: 'builtin', submitMode: 'action' },
|
|
16
17
|
{ name: 'agent', description: '切换智能体', category: 'builtin', submitMode: 'list' },
|
|
17
18
|
{ name: 'permissions', description: '查看所有持久化授权列表', category: 'builtin', submitMode: 'action' },
|
|
@@ -37,7 +37,8 @@ function SlashCommandMenu({ commands, selectedIndex, maxVisible = 6, }) {
|
|
|
37
37
|
const isSelected = realIndex === selectedIndex;
|
|
38
38
|
const catColor = categoryColor[cmd.category] ?? 'gray';
|
|
39
39
|
const catText = categoryLabel[cmd.category] ?? cmd.category;
|
|
40
|
-
|
|
40
|
+
const displayName = cmd.displayName ?? cmd.name;
|
|
41
|
+
return (_jsxs(Box, { children: [_jsxs(Text, { color: isSelected ? 'cyan' : 'white', children: [' ', "/", displayName, ' '] }), _jsxs(Text, { color: isSelected ? 'cyan' : 'gray', children: ["- ", cmd.description, ' '] }), _jsxs(Text, { color: catColor, dimColor: true, children: ["[", catText, "]"] })] }, `${cmd.name}-${realIndex}`));
|
|
41
42
|
}), start + visible < total && (_jsxs(Text, { color: "gray", dimColor: true, children: [" \u2193 \u8FD8\u6709 ", total - start - visible, " \u9879"] }))] }));
|
|
42
43
|
}
|
|
43
44
|
export default React.memo(SlashCommandMenu);
|
|
@@ -30,6 +30,9 @@ export declare class QueryEngine {
|
|
|
30
30
|
/** 注册持久 UI 回调,供后台 spawn_agent 子 Agent 推送消息 */
|
|
31
31
|
registerUIBus(onMessage: (msg: Message) => void, onUpdateMessage: (id: string, updates: Partial<Message>) => void): void;
|
|
32
32
|
private createSession;
|
|
33
|
+
private rebuildTranscriptFromMessages;
|
|
34
|
+
private recomputeSessionSummary;
|
|
35
|
+
private recomputeSessionTokens;
|
|
33
36
|
private ensureSessionDir;
|
|
34
37
|
private createService;
|
|
35
38
|
/** 处理用户输入(在独立 Worker 线程中执行) */
|
|
@@ -62,6 +65,19 @@ export declare class QueryEngine {
|
|
|
62
65
|
messages: Message[];
|
|
63
66
|
} | null;
|
|
64
67
|
getSession(): Session;
|
|
68
|
+
getCurrentSessionUserTurns(): Array<{
|
|
69
|
+
messageId: string;
|
|
70
|
+
turnIndex: number;
|
|
71
|
+
input: string;
|
|
72
|
+
answerPreview: string;
|
|
73
|
+
timestamp: number;
|
|
74
|
+
}>;
|
|
75
|
+
rewindToUserMessage(messageId: string): {
|
|
76
|
+
session: Session;
|
|
77
|
+
messages: Message[];
|
|
78
|
+
input: string;
|
|
79
|
+
turnIndex: number;
|
|
80
|
+
} | null;
|
|
65
81
|
/**
|
|
66
82
|
* 清理非当前会话的所有历史会话文件
|
|
67
83
|
* @returns 删除的会话数量
|
package/dist/core/QueryEngine.js
CHANGED
|
@@ -51,6 +51,39 @@ export class QueryEngine {
|
|
|
51
51
|
totalCost: 0,
|
|
52
52
|
};
|
|
53
53
|
}
|
|
54
|
+
rebuildTranscriptFromMessages(messages) {
|
|
55
|
+
const transcript = [];
|
|
56
|
+
for (const msg of messages) {
|
|
57
|
+
if (msg.type === 'user') {
|
|
58
|
+
transcript.push({ role: 'user', content: msg.content });
|
|
59
|
+
}
|
|
60
|
+
else if (msg.type === 'reasoning' && msg.status === 'success' && msg.content) {
|
|
61
|
+
transcript.push({
|
|
62
|
+
role: 'assistant',
|
|
63
|
+
content: [{ type: 'text', text: msg.content }],
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
else if (msg.type === 'tool_exec' && msg.toolName && msg.toolResult !== undefined) {
|
|
67
|
+
transcript.push({
|
|
68
|
+
role: 'assistant',
|
|
69
|
+
content: [{ type: 'tool_use', id: msg.id, name: msg.toolName, input: msg.toolArgs ?? {} }],
|
|
70
|
+
});
|
|
71
|
+
transcript.push({
|
|
72
|
+
role: 'tool_result',
|
|
73
|
+
content: msg.toolResult,
|
|
74
|
+
toolUseId: msg.id,
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return transcript;
|
|
79
|
+
}
|
|
80
|
+
recomputeSessionSummary(messages) {
|
|
81
|
+
const firstUser = messages.find((m) => m.type === 'user');
|
|
82
|
+
return firstUser ? firstUser.content.slice(0, 80).replace(/\n/g, ' ') : undefined;
|
|
83
|
+
}
|
|
84
|
+
recomputeSessionTokens(messages) {
|
|
85
|
+
return messages.reduce((sum, msg) => sum + (msg.tokenCount ?? 0), 0);
|
|
86
|
+
}
|
|
54
87
|
ensureSessionDir() {
|
|
55
88
|
if (!fs.existsSync(SESSIONS_DIR)) {
|
|
56
89
|
fs.mkdirSync(SESSIONS_DIR, { recursive: true });
|
|
@@ -321,31 +354,7 @@ export class QueryEngine {
|
|
|
321
354
|
const cleanedMessages = loaded.messages.filter((m) => !(m.type === 'thinking' && m.status === 'pending'));
|
|
322
355
|
this.session.messages = cleanedMessages;
|
|
323
356
|
// 从历史消息重建 transcript
|
|
324
|
-
this.transcript =
|
|
325
|
-
for (const msg of loaded.messages) {
|
|
326
|
-
if (msg.type === 'user') {
|
|
327
|
-
this.transcript.push({ role: 'user', content: msg.content });
|
|
328
|
-
}
|
|
329
|
-
else if (msg.type === 'reasoning' && msg.status === 'success' && msg.content) {
|
|
330
|
-
// assistant 消息的 content 必须是 ContentBlock[],与 query.ts 中的构建方式一致
|
|
331
|
-
this.transcript.push({
|
|
332
|
-
role: 'assistant',
|
|
333
|
-
content: [{ type: 'text', text: msg.content }],
|
|
334
|
-
});
|
|
335
|
-
}
|
|
336
|
-
else if (msg.type === 'tool_exec' && msg.toolName && msg.toolResult !== undefined) {
|
|
337
|
-
// 工具调用:assistant tool_use + tool_result
|
|
338
|
-
this.transcript.push({
|
|
339
|
-
role: 'assistant',
|
|
340
|
-
content: [{ type: 'tool_use', id: msg.id, name: msg.toolName, input: msg.toolArgs ?? {} }],
|
|
341
|
-
});
|
|
342
|
-
this.transcript.push({
|
|
343
|
-
role: 'tool_result',
|
|
344
|
-
content: msg.toolResult,
|
|
345
|
-
toolUseId: msg.id,
|
|
346
|
-
});
|
|
347
|
-
}
|
|
348
|
-
}
|
|
357
|
+
this.transcript = this.rebuildTranscriptFromMessages(cleanedMessages);
|
|
349
358
|
logInfo('session.loaded', {
|
|
350
359
|
sessionId,
|
|
351
360
|
messageCount: cleanedMessages.length,
|
|
@@ -362,6 +371,57 @@ export class QueryEngine {
|
|
|
362
371
|
getSession() {
|
|
363
372
|
return this.session;
|
|
364
373
|
}
|
|
374
|
+
getCurrentSessionUserTurns() {
|
|
375
|
+
const cleanedMessages = this.session.messages.filter((m) => !(m.type === 'thinking' && m.status === 'pending'));
|
|
376
|
+
const userMessages = cleanedMessages.filter((m) => m.type === 'user');
|
|
377
|
+
return userMessages.map((msg, index) => {
|
|
378
|
+
const currentIndex = cleanedMessages.findIndex((item) => item.id === msg.id);
|
|
379
|
+
const nextUserIndex = cleanedMessages.findIndex((item, i) => i > currentIndex && item.type === 'user');
|
|
380
|
+
const endIndex = nextUserIndex >= 0 ? nextUserIndex : cleanedMessages.length;
|
|
381
|
+
const answerPreview = cleanedMessages
|
|
382
|
+
.slice(currentIndex + 1, endIndex)
|
|
383
|
+
.filter((item) => item.type === 'reasoning' && item.content)
|
|
384
|
+
.map((item) => item.content.trim())
|
|
385
|
+
.find(Boolean) ?? '';
|
|
386
|
+
return {
|
|
387
|
+
messageId: msg.id,
|
|
388
|
+
turnIndex: index + 1,
|
|
389
|
+
input: msg.content,
|
|
390
|
+
answerPreview,
|
|
391
|
+
timestamp: msg.timestamp,
|
|
392
|
+
};
|
|
393
|
+
});
|
|
394
|
+
}
|
|
395
|
+
rewindToUserMessage(messageId) {
|
|
396
|
+
const cleanedMessages = this.session.messages.filter((m) => !(m.type === 'thinking' && m.status === 'pending'));
|
|
397
|
+
const targetIndex = cleanedMessages.findIndex((m) => m.id === messageId && m.type === 'user');
|
|
398
|
+
if (targetIndex < 0)
|
|
399
|
+
return null;
|
|
400
|
+
const userTurns = cleanedMessages.filter((m) => m.type === 'user');
|
|
401
|
+
const turnIndex = userTurns.findIndex((m) => m.id === messageId) + 1;
|
|
402
|
+
const targetMessage = cleanedMessages[targetIndex];
|
|
403
|
+
const keptMessages = cleanedMessages.slice(0, targetIndex);
|
|
404
|
+
this.session.messages = keptMessages;
|
|
405
|
+
this.session.summary = this.recomputeSessionSummary(keptMessages);
|
|
406
|
+
this.session.totalTokens = this.recomputeSessionTokens(keptMessages);
|
|
407
|
+
this.session.updatedAt = Date.now();
|
|
408
|
+
this.transcript = this.rebuildTranscriptFromMessages(keptMessages);
|
|
409
|
+
this.touchActivity();
|
|
410
|
+
this.saveSession();
|
|
411
|
+
logInfo('session.rewind', {
|
|
412
|
+
sessionId: this.session.id,
|
|
413
|
+
targetMessageId: messageId,
|
|
414
|
+
turnIndex,
|
|
415
|
+
keptMessageCount: keptMessages.length,
|
|
416
|
+
transcriptLength: this.transcript.length,
|
|
417
|
+
});
|
|
418
|
+
return {
|
|
419
|
+
session: this.session,
|
|
420
|
+
messages: keptMessages,
|
|
421
|
+
input: targetMessage.content,
|
|
422
|
+
turnIndex,
|
|
423
|
+
};
|
|
424
|
+
}
|
|
365
425
|
/**
|
|
366
426
|
* 清理非当前会话的所有历史会话文件
|
|
367
427
|
* @returns 删除的会话数量
|
|
@@ -27,12 +27,13 @@ export declare function useSlashMenu(opts: UseSlashMenuOptions): {
|
|
|
27
27
|
slashMenuIndex: number;
|
|
28
28
|
agentMenuMode: boolean;
|
|
29
29
|
resumeMenuMode: boolean;
|
|
30
|
+
rewindMenuMode: boolean;
|
|
30
31
|
handleSlashMenuUp: () => void;
|
|
31
32
|
handleSlashMenuDown: () => void;
|
|
32
33
|
handleSlashMenuClose: () => void;
|
|
33
34
|
getSelectedCommand: () => SlashCommand | null;
|
|
34
35
|
autocompleteSlashMenuSelection: (currentInput: string) => string;
|
|
35
|
-
openListCommand: (commandName: "agent" | "resume") => "/agent " | "/resume ";
|
|
36
|
+
openListCommand: (commandName: "agent" | "resume" | "rewind") => "/agent " | "/rewind " | "/resume ";
|
|
36
37
|
updateSlashMenu: (val: string) => void;
|
|
37
38
|
setSlashMenuVisible: import("react").Dispatch<import("react").SetStateAction<boolean>>;
|
|
38
39
|
resumeSession: (sessionId: string) => void;
|
|
@@ -13,6 +13,7 @@ export function useSlashMenu(opts) {
|
|
|
13
13
|
const [slashMenuIndex, setSlashMenuIndex] = useState(0);
|
|
14
14
|
const [agentMenuMode, setAgentMenuMode] = useState(false);
|
|
15
15
|
const [resumeMenuMode, setResumeMenuMode] = useState(false);
|
|
16
|
+
const [rewindMenuMode, setRewindMenuMode] = useState(false);
|
|
16
17
|
// 上移
|
|
17
18
|
const handleSlashMenuUp = useCallback(() => {
|
|
18
19
|
setSlashMenuIndex((prev) => (prev > 0 ? prev - 1 : slashMenuItems.length - 1));
|
|
@@ -23,9 +24,10 @@ export function useSlashMenu(opts) {
|
|
|
23
24
|
}, [slashMenuItems.length]);
|
|
24
25
|
// 关闭
|
|
25
26
|
const handleSlashMenuClose = useCallback(() => {
|
|
26
|
-
if (agentMenuMode || resumeMenuMode) {
|
|
27
|
+
if (agentMenuMode || resumeMenuMode || rewindMenuMode) {
|
|
27
28
|
setAgentMenuMode(false);
|
|
28
29
|
setResumeMenuMode(false);
|
|
30
|
+
setRewindMenuMode(false);
|
|
29
31
|
setInput('/');
|
|
30
32
|
const matched = filterCommands('');
|
|
31
33
|
setSlashMenuItems(matched);
|
|
@@ -35,7 +37,7 @@ export function useSlashMenu(opts) {
|
|
|
35
37
|
else {
|
|
36
38
|
setSlashMenuVisible(false);
|
|
37
39
|
}
|
|
38
|
-
}, [agentMenuMode, resumeMenuMode, setInput]);
|
|
40
|
+
}, [agentMenuMode, resumeMenuMode, rewindMenuMode, setInput]);
|
|
39
41
|
// 恢复会话的通用逻辑
|
|
40
42
|
const resumeSession = useCallback((sessionId) => {
|
|
41
43
|
if (!engineRef.current)
|
|
@@ -76,6 +78,25 @@ export function useSlashMenu(opts) {
|
|
|
76
78
|
return null;
|
|
77
79
|
return slashMenuItems[slashMenuIndex] ?? null;
|
|
78
80
|
}, [slashMenuItems, slashMenuIndex]);
|
|
81
|
+
const buildRewindItems = useCallback(() => {
|
|
82
|
+
if (!engineRef.current)
|
|
83
|
+
return [];
|
|
84
|
+
return engineRef.current.getCurrentSessionUserTurns()
|
|
85
|
+
.slice()
|
|
86
|
+
.map((turn) => {
|
|
87
|
+
const date = new Date(turn.timestamp);
|
|
88
|
+
const dateStr = `${date.getMonth() + 1}/${date.getDate()} ${date.getHours().toString().padStart(2, '0')}:${date.getMinutes().toString().padStart(2, '0')}`;
|
|
89
|
+
const question = turn.input.replace(/\s+/g, ' ').slice(0, 32);
|
|
90
|
+
const answer = turn.answerPreview.replace(/\s+/g, ' ').slice(0, 24);
|
|
91
|
+
return {
|
|
92
|
+
name: turn.messageId,
|
|
93
|
+
displayName: `rewind-${turn.turnIndex}`,
|
|
94
|
+
description: `[Q${turn.turnIndex} ${dateStr}] ${question}${answer ? ` | A: ${answer}` : ''}`,
|
|
95
|
+
category: 'builtin',
|
|
96
|
+
submitMode: 'action',
|
|
97
|
+
};
|
|
98
|
+
});
|
|
99
|
+
}, [engineRef]);
|
|
79
100
|
const openListCommand = useCallback((commandName) => {
|
|
80
101
|
if (commandName === 'agent') {
|
|
81
102
|
setInput('/agent ');
|
|
@@ -84,9 +105,21 @@ export function useSlashMenu(opts) {
|
|
|
84
105
|
setSlashMenuIndex(0);
|
|
85
106
|
setAgentMenuMode(true);
|
|
86
107
|
setResumeMenuMode(false);
|
|
108
|
+
setRewindMenuMode(false);
|
|
87
109
|
setSlashMenuVisible(matched.length > 0);
|
|
88
110
|
return '/agent ';
|
|
89
111
|
}
|
|
112
|
+
if (commandName === 'rewind') {
|
|
113
|
+
const items = buildRewindItems();
|
|
114
|
+
setInput('/rewind ');
|
|
115
|
+
setSlashMenuItems(items);
|
|
116
|
+
setSlashMenuIndex(0);
|
|
117
|
+
setSlashMenuVisible(items.length > 0);
|
|
118
|
+
setRewindMenuMode(true);
|
|
119
|
+
setResumeMenuMode(false);
|
|
120
|
+
setAgentMenuMode(false);
|
|
121
|
+
return '/rewind ';
|
|
122
|
+
}
|
|
90
123
|
const sessions = QueryEngine.listSessions().slice(0, 20);
|
|
91
124
|
const items = sessions.map((s) => {
|
|
92
125
|
const date = new Date(s.updatedAt);
|
|
@@ -104,8 +137,9 @@ export function useSlashMenu(opts) {
|
|
|
104
137
|
setSlashMenuVisible(items.length > 0);
|
|
105
138
|
setResumeMenuMode(true);
|
|
106
139
|
setAgentMenuMode(false);
|
|
140
|
+
setRewindMenuMode(false);
|
|
107
141
|
return '/resume ';
|
|
108
|
-
}, [setInput]);
|
|
142
|
+
}, [setInput, buildRewindItems]);
|
|
109
143
|
const autocompleteSlashMenuSelection = useCallback((currentInput) => {
|
|
110
144
|
const cmd = getSelectedCommand();
|
|
111
145
|
if (!cmd)
|
|
@@ -124,17 +158,23 @@ export function useSlashMenu(opts) {
|
|
|
124
158
|
setSlashMenuVisible(false);
|
|
125
159
|
return nextInput;
|
|
126
160
|
}
|
|
161
|
+
if (rewindMenuMode) {
|
|
162
|
+
const nextInput = `/rewind ${cmd.name}`;
|
|
163
|
+
setInput(nextInput);
|
|
164
|
+
setSlashMenuVisible(false);
|
|
165
|
+
return nextInput;
|
|
166
|
+
}
|
|
127
167
|
const match = currentInput.match(/^\/\S*/);
|
|
128
168
|
const suffix = match ? currentInput.slice(match[0].length) : '';
|
|
129
169
|
const needsTrailingSpace = !suffix && (cmd.submitMode === 'context' || cmd.submitMode === 'list');
|
|
130
170
|
const nextInput = `/${cmd.name}${suffix}${needsTrailingSpace ? ' ' : ''}`;
|
|
131
171
|
setInput(nextInput);
|
|
132
|
-
if (cmd.submitMode === 'list' && (cmd.name === 'agent' || cmd.name === 'resume') && !suffix.trim()) {
|
|
172
|
+
if (cmd.submitMode === 'list' && (cmd.name === 'agent' || cmd.name === 'resume' || cmd.name === 'rewind') && !suffix.trim()) {
|
|
133
173
|
return openListCommand(cmd.name);
|
|
134
174
|
}
|
|
135
175
|
updateSlashMenu(nextInput);
|
|
136
176
|
return nextInput;
|
|
137
|
-
}, [agentMenuMode, resumeMenuMode, getSelectedCommand, setInput, openListCommand]);
|
|
177
|
+
}, [agentMenuMode, resumeMenuMode, rewindMenuMode, getSelectedCommand, setInput, openListCommand]);
|
|
138
178
|
// 输入变化时更新菜单
|
|
139
179
|
const updateSlashMenu = useCallback((val) => {
|
|
140
180
|
if (val.startsWith('/') && !val.includes('\n')) {
|
|
@@ -172,6 +212,22 @@ export function useSlashMenu(opts) {
|
|
|
172
212
|
setSlashMenuVisible(matched.length > 0);
|
|
173
213
|
setResumeMenuMode(true);
|
|
174
214
|
setAgentMenuMode(false);
|
|
215
|
+
setRewindMenuMode(false);
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
// /rewind 二级菜单
|
|
219
|
+
if (/^rewind\s/i.test(query)) {
|
|
220
|
+
const subQuery = query.replace(/^rewind\s*/i, '').toLowerCase();
|
|
221
|
+
const items = buildRewindItems();
|
|
222
|
+
const matched = subQuery
|
|
223
|
+
? items.filter((i) => i.name.includes(subQuery) || (i.displayName ?? '').toLowerCase().includes(subQuery) || i.description.toLowerCase().includes(subQuery))
|
|
224
|
+
: items;
|
|
225
|
+
setSlashMenuItems(matched);
|
|
226
|
+
setSlashMenuIndex(0);
|
|
227
|
+
setSlashMenuVisible(matched.length > 0);
|
|
228
|
+
setRewindMenuMode(true);
|
|
229
|
+
setResumeMenuMode(false);
|
|
230
|
+
setAgentMenuMode(false);
|
|
175
231
|
return;
|
|
176
232
|
}
|
|
177
233
|
// 一级菜单
|
|
@@ -181,19 +237,22 @@ export function useSlashMenu(opts) {
|
|
|
181
237
|
setSlashMenuVisible(matched.length > 0);
|
|
182
238
|
setAgentMenuMode(false);
|
|
183
239
|
setResumeMenuMode(false);
|
|
240
|
+
setRewindMenuMode(false);
|
|
184
241
|
}
|
|
185
242
|
else {
|
|
186
243
|
setSlashMenuVisible(false);
|
|
187
244
|
setAgentMenuMode(false);
|
|
188
245
|
setResumeMenuMode(false);
|
|
246
|
+
setRewindMenuMode(false);
|
|
189
247
|
}
|
|
190
|
-
}, []);
|
|
248
|
+
}, [buildRewindItems]);
|
|
191
249
|
return {
|
|
192
250
|
slashMenuVisible,
|
|
193
251
|
slashMenuItems,
|
|
194
252
|
slashMenuIndex,
|
|
195
253
|
agentMenuMode,
|
|
196
254
|
resumeMenuMode,
|
|
255
|
+
rewindMenuMode,
|
|
197
256
|
handleSlashMenuUp,
|
|
198
257
|
handleSlashMenuDown,
|
|
199
258
|
handleSlashMenuClose,
|
package/dist/screens/repl.js
CHANGED
|
@@ -260,6 +260,49 @@ export default function REPL() {
|
|
|
260
260
|
}
|
|
261
261
|
return;
|
|
262
262
|
}
|
|
263
|
+
// /rewind
|
|
264
|
+
if (cmdName === 'rewind') {
|
|
265
|
+
if (hasArgs && engineRef.current) {
|
|
266
|
+
setInput('');
|
|
267
|
+
slashMenu.setSlashMenuVisible(false);
|
|
268
|
+
const messageId = parts.slice(1).join(' ').trim();
|
|
269
|
+
const result = engineRef.current.rewindToUserMessage(messageId);
|
|
270
|
+
if (!result) {
|
|
271
|
+
const errMsg = {
|
|
272
|
+
id: `rewind-err-${Date.now()}`,
|
|
273
|
+
type: 'error',
|
|
274
|
+
status: 'error',
|
|
275
|
+
content: '未找到要回退的会话位置',
|
|
276
|
+
timestamp: Date.now(),
|
|
277
|
+
};
|
|
278
|
+
setMessages((prev) => [...prev, errMsg]);
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
stopAll();
|
|
282
|
+
setMessages([
|
|
283
|
+
...result.messages,
|
|
284
|
+
{
|
|
285
|
+
id: `rewind-${Date.now()}`,
|
|
286
|
+
type: 'system',
|
|
287
|
+
status: 'success',
|
|
288
|
+
content: `已回退到当前会话的第 ${result.turnIndex} 条提问,请确认后重新发送。`,
|
|
289
|
+
timestamp: Date.now(),
|
|
290
|
+
},
|
|
291
|
+
]);
|
|
292
|
+
sessionRef.current = result.session;
|
|
293
|
+
tokenCountRef.current = result.session.totalTokens;
|
|
294
|
+
syncTokenDisplay(result.session.totalTokens);
|
|
295
|
+
setLoopState(null);
|
|
296
|
+
setIsProcessing(false);
|
|
297
|
+
setShowWelcome(false);
|
|
298
|
+
resetNavigation();
|
|
299
|
+
setInput(result.input);
|
|
300
|
+
}
|
|
301
|
+
else {
|
|
302
|
+
slashMenu.openListCommand('rewind');
|
|
303
|
+
}
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
263
306
|
// /create_skill
|
|
264
307
|
if (cmdName === 'create_skill') {
|
|
265
308
|
setInput('');
|