Update status bar and track progress
This commit is contained in:
parent
136a503846
commit
6d2d4857f3
@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"id": "1042413d-b4ee-4586-85e5-eb80879d9fbe",
|
||||||
|
"title": "新对话",
|
||||||
|
"created_at": "2026-02-28T04:33:13.020Z",
|
||||||
|
"updated_at": "2026-02-28T04:33:13.020Z",
|
||||||
|
"metadata": {
|
||||||
|
"model_key": "kimi-k2.5",
|
||||||
|
"model_id": "kimi-k2.5",
|
||||||
|
"thinking_mode": true,
|
||||||
|
"allow_mode": "full_access",
|
||||||
|
"token_usage": {
|
||||||
|
"prompt": 0,
|
||||||
|
"completion": 0,
|
||||||
|
"total": 0
|
||||||
|
},
|
||||||
|
"cwd": "/Users/jojo/Desktop/easyagent"
|
||||||
|
},
|
||||||
|
"messages": []
|
||||||
|
}
|
||||||
@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"id": "1be5b6a9-769a-4276-96b6-185627fc308a",
|
||||||
|
"title": "新对话",
|
||||||
|
"created_at": "2026-02-28T04:35:06.703Z",
|
||||||
|
"updated_at": "2026-02-28T04:35:10.259Z",
|
||||||
|
"metadata": {
|
||||||
|
"model_key": "kimi-k2.5",
|
||||||
|
"model_id": "kimi-k2.5",
|
||||||
|
"thinking_mode": true,
|
||||||
|
"allow_mode": "read_only",
|
||||||
|
"token_usage": {
|
||||||
|
"prompt": 0,
|
||||||
|
"completion": 0,
|
||||||
|
"total": 0
|
||||||
|
},
|
||||||
|
"cwd": "/Users/jojo/Desktop/easyagent"
|
||||||
|
},
|
||||||
|
"messages": []
|
||||||
|
}
|
||||||
@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"id": "3a4b4cce-feac-49e7-8aee-66fe2881d076",
|
||||||
|
"title": "新对话",
|
||||||
|
"created_at": "2026-02-28T04:16:47.305Z",
|
||||||
|
"updated_at": "2026-02-28T04:16:50.825Z",
|
||||||
|
"metadata": {
|
||||||
|
"model_key": "kimi-k2.5",
|
||||||
|
"model_id": "kimi-k2.5",
|
||||||
|
"thinking_mode": true,
|
||||||
|
"allow_mode": "read_only",
|
||||||
|
"token_usage": {
|
||||||
|
"prompt": 0,
|
||||||
|
"completion": 0,
|
||||||
|
"total": 0
|
||||||
|
},
|
||||||
|
"cwd": "/Users/jojo/Desktop/easyagent"
|
||||||
|
},
|
||||||
|
"messages": []
|
||||||
|
}
|
||||||
@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"id": "cb1e445c-e7f5-4b70-bbf9-6114c441af47",
|
||||||
|
"title": "新对话",
|
||||||
|
"created_at": "2026-02-28T04:25:19.837Z",
|
||||||
|
"updated_at": "2026-02-28T04:25:22.770Z",
|
||||||
|
"metadata": {
|
||||||
|
"model_key": "kimi-k2.5",
|
||||||
|
"model_id": "kimi-k2.5",
|
||||||
|
"thinking_mode": true,
|
||||||
|
"allow_mode": "read_only",
|
||||||
|
"token_usage": {
|
||||||
|
"prompt": 0,
|
||||||
|
"completion": 0,
|
||||||
|
"total": 0
|
||||||
|
},
|
||||||
|
"cwd": "/Users/jojo/Desktop/easyagent"
|
||||||
|
},
|
||||||
|
"messages": []
|
||||||
|
}
|
||||||
@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"id": "e24b0466-9fed-4da5-b8b8-627154fe1d76",
|
||||||
|
"title": "新对话",
|
||||||
|
"created_at": "2026-02-28T04:32:28.441Z",
|
||||||
|
"updated_at": "2026-02-28T04:32:31.209Z",
|
||||||
|
"metadata": {
|
||||||
|
"model_key": "kimi-k2.5",
|
||||||
|
"model_id": "kimi-k2.5",
|
||||||
|
"thinking_mode": true,
|
||||||
|
"allow_mode": "read_only",
|
||||||
|
"token_usage": {
|
||||||
|
"prompt": 0,
|
||||||
|
"completion": 0,
|
||||||
|
"total": 0
|
||||||
|
},
|
||||||
|
"cwd": "/Users/jojo/Desktop/easyagent"
|
||||||
|
},
|
||||||
|
"messages": []
|
||||||
|
}
|
||||||
52
.easyagent/index.json
Normal file
52
.easyagent/index.json
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
{
|
||||||
|
"3a4b4cce-feac-49e7-8aee-66fe2881d076": {
|
||||||
|
"title": "新对话",
|
||||||
|
"created_at": "2026-02-28T04:16:47.305Z",
|
||||||
|
"updated_at": "2026-02-28T04:16:50.825Z",
|
||||||
|
"total_messages": 0,
|
||||||
|
"total_tools": 0,
|
||||||
|
"thinking_mode": true,
|
||||||
|
"run_mode": "thinking",
|
||||||
|
"model_key": "kimi-k2.5"
|
||||||
|
},
|
||||||
|
"cb1e445c-e7f5-4b70-bbf9-6114c441af47": {
|
||||||
|
"title": "新对话",
|
||||||
|
"created_at": "2026-02-28T04:25:19.837Z",
|
||||||
|
"updated_at": "2026-02-28T04:25:22.770Z",
|
||||||
|
"total_messages": 0,
|
||||||
|
"total_tools": 0,
|
||||||
|
"thinking_mode": true,
|
||||||
|
"run_mode": "thinking",
|
||||||
|
"model_key": "kimi-k2.5"
|
||||||
|
},
|
||||||
|
"e24b0466-9fed-4da5-b8b8-627154fe1d76": {
|
||||||
|
"title": "新对话",
|
||||||
|
"created_at": "2026-02-28T04:32:28.441Z",
|
||||||
|
"updated_at": "2026-02-28T04:32:31.209Z",
|
||||||
|
"total_messages": 0,
|
||||||
|
"total_tools": 0,
|
||||||
|
"thinking_mode": true,
|
||||||
|
"run_mode": "thinking",
|
||||||
|
"model_key": "kimi-k2.5"
|
||||||
|
},
|
||||||
|
"1042413d-b4ee-4586-85e5-eb80879d9fbe": {
|
||||||
|
"title": "新对话",
|
||||||
|
"created_at": "2026-02-28T04:33:13.020Z",
|
||||||
|
"updated_at": "2026-02-28T04:33:13.020Z",
|
||||||
|
"total_messages": 0,
|
||||||
|
"total_tools": 0,
|
||||||
|
"thinking_mode": true,
|
||||||
|
"run_mode": "thinking",
|
||||||
|
"model_key": "kimi-k2.5"
|
||||||
|
},
|
||||||
|
"1be5b6a9-769a-4276-96b6-185627fc308a": {
|
||||||
|
"title": "新对话",
|
||||||
|
"created_at": "2026-02-28T04:35:06.703Z",
|
||||||
|
"updated_at": "2026-02-28T04:35:10.259Z",
|
||||||
|
"total_messages": 0,
|
||||||
|
"total_tools": 0,
|
||||||
|
"thinking_mode": true,
|
||||||
|
"run_mode": "thinking",
|
||||||
|
"model_key": "kimi-k2.5"
|
||||||
|
}
|
||||||
|
}
|
||||||
87
2.txt
Normal file
87
2.txt
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
收到,我先不再改代码。下面是根据本次对话记录整理出来的修改清单(仅基于聊天内容,不查看当前文件;如果某条在中途被回滚/覆盖,已在备注里说明)。
|
||||||
|
|
||||||
|
整体新增/变更汇总(按功能模块)
|
||||||
|
|
||||||
|
1. 底部状态栏 + 保留空行 ✓
|
||||||
|
|
||||||
|
- 新增“永久底部状态栏”概念:最底行显示提示信息,上一行留空;输入行位于其上方。
|
||||||
|
- 左侧文案在输入时为“输入/查看所有指令”,运行时变为“按下Esc停止”。
|
||||||
|
- 右侧显示“当前上下文 0/256k”,且数字>1000时显示 1.1k 格式。
|
||||||
|
- 需要窗口缩放时自适应,不增加新底层显示行。
|
||||||
|
|
||||||
|
2. ESC 取消行为与记录落盘时机
|
||||||
|
|
||||||
|
- 按 Esc 需要立即中断运行:
|
||||||
|
- 若模型思考中:显示“∙ 停止思考”,并在思考内容下方打印红色“已取消本次响应”,上下各留一行空行。
|
||||||
|
- 若工具执行中:立即停止工具执行,工具结果为红色“任务被用户取消”,且不再继续执行工具结果处理。
|
||||||
|
- 数据落盘更细:用户输入后、模型完整输出后(不含工具)、工具调用后、工具结果后都应写入内存+落盘。
|
||||||
|
- 取消时如果输出未完成,未完成内容不入记录。
|
||||||
|
|
||||||
|
3. 工具输出显示
|
||||||
|
|
||||||
|
- “任务被用户取消”不带“└”或“失败:”前缀;左侧对齐。
|
||||||
|
- 工具执行完后输出与下一行 prompt 之间空一行。
|
||||||
|
- “取消响应”也需红色且左侧对齐,上一行留空。
|
||||||
|
|
||||||
|
4. /new 与 /resume token 复位
|
||||||
|
|
||||||
|
- /new 需要把右下角“当前上下文”重置(逻辑同 /resume)。
|
||||||
|
- 修复了 /new 未被识别的问题(与命令菜单注入有关)。
|
||||||
|
|
||||||
|
5. 只读权限提示文案
|
||||||
|
|
||||||
|
- 只读模式提示文案最后一句改为“请告知当前无权限需要用户输入 /allow 切换权限”。
|
||||||
|
- 只读模式下工具结果文案也需更新。
|
||||||
|
|
||||||
|
6. /allow、/model 等“Update”提示清理
|
||||||
|
|
||||||
|
- /allow /model /thinking 等菜单里不显示“Update …”。
|
||||||
|
|
||||||
|
7. /model 交互逻辑
|
||||||
|
|
||||||
|
- 需求最终定为:
|
||||||
|
- 选择模型后,隔一行显示“模型已切换为: xxx”;
|
||||||
|
- 隔一行再显示思考模式选择;
|
||||||
|
- 选择思考模式后,在选项下面隔一行显示“思考模式: xxx”;
|
||||||
|
- 不再要求清除菜单残留(因为清理和状态栏冲突)。
|
||||||
|
|
||||||
|
8. 命令菜单 / 无效指令
|
||||||
|
|
||||||
|
- 当 / 输入无匹配结果:
|
||||||
|
- 菜单显示“无效的指令”(替换 No results found)。
|
||||||
|
- 按 Enter 后不发送给模型,只显示“无效的命令“/app””。
|
||||||
|
|
||||||
|
9. 多模态(图片/视频)
|
||||||
|
|
||||||
|
- 输入路径或拖拽可自动插入蓝色 token [图片 #1] / [视频 #1]。
|
||||||
|
- 删除时 token 作为整体删除。
|
||||||
|
- 最多 9 张图片、1 个视频。
|
||||||
|
- 发送时:文本里包含 [图片 #n] 显示;实际请求使用 image_url/video_url base64 发送。
|
||||||
|
- 识别单引号路径 '/path/to/file.png'。
|
||||||
|
- 修复“只发送文本而未发送真实图片”的问题,最终要求按示例把图片作为 content list 的一部分发送。
|
||||||
|
|
||||||
|
10. 文件编辑工具创建文件
|
||||||
|
|
||||||
|
- 编辑文件时自动创建不存在的文件与目录,不再报错 ENOENT。
|
||||||
|
|
||||||
|
11. 提示信息空行规范
|
||||||
|
|
||||||
|
- 所有提示信息(如“已创建新对话/权限切换/模型切换/思考模式切换”)上下各空一行。
|
||||||
|
- Banner 与 prompt 之间需保持空行。
|
||||||
|
|
||||||
|
12. 思考完成标记
|
||||||
|
|
||||||
|
- 思考完成行使用 ∙ 而不是 ○。
|
||||||
|
- ∙ 行与思考内容整体左移一个空格以对齐工具调用的 • 前缀。
|
||||||
|
|
||||||
|
13. /system.txt 增加模型变量
|
||||||
|
|
||||||
|
- system prompt 里加入当前使用模型(用请求模型名称)。
|
||||||
|
|
||||||
|
14. 取消时错误显示
|
||||||
|
|
||||||
|
- 在思考时按 Esc,不能重复显示思考内容;只显示“停止思考 + 已取消本次响应”。
|
||||||
|
|
||||||
|
15. 运行指令取消空行
|
||||||
|
|
||||||
|
- 工具取消输出行与后续 prompt 之间需空一行。
|
||||||
4094
edited_records.txt
Normal file
4094
edited_records.txt
Normal file
File diff suppressed because it is too large
Load Diff
@ -1,8 +1,9 @@
|
|||||||
你是 easyagent,一个极简终端智能体。
|
你是 EasyAgent,一个极简终端智能体。
|
||||||
目标:帮助用户完成开发任务,优先高信息密度输出。
|
目标:帮助用户完成开发任务,优先高信息密度输出。
|
||||||
输出限制:禁止使用 Markdown(md)格式,内容无法渲染,必须使用纯文字格式输出。
|
输出限制:禁止使用 Markdown(md)格式,内容无法渲染,必须使用纯文字格式输出。
|
||||||
|
|
||||||
- 当前时间:{current_time}
|
- 当前时间:{current_time}
|
||||||
|
- 当前模型:{model_id}
|
||||||
- 工作区路径:{path}
|
- 工作区路径:{path}
|
||||||
- 系统信息:{system}
|
- 系统信息:{system}
|
||||||
- 终端类型:{terminal}
|
- 终端类型:{terminal}
|
||||||
|
|||||||
BIN
src/.DS_Store
vendored
BIN
src/.DS_Store
vendored
Binary file not shown.
@ -144,7 +144,7 @@ async function handleCommand(input, ctx) {
|
|||||||
value: 'full_access',
|
value: 'full_access',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
const selected = await runSelect({ rl, message: 'Update Permissions', choices, pageSize: 6 });
|
const selected = await runSelect({ rl, message: '', choices, pageSize: 6 });
|
||||||
if (selected) {
|
if (selected) {
|
||||||
state.allowMode = selected;
|
state.allowMode = selected;
|
||||||
console.log(`运行模式已切换为: ${state.allowMode}`);
|
console.log(`运行模式已切换为: ${state.allowMode}`);
|
||||||
@ -157,19 +157,24 @@ async function handleCommand(input, ctx) {
|
|||||||
const modelChoices = [
|
const modelChoices = [
|
||||||
{ name: `1. Kimi${state.modelKey === 'kimi-k2.5' ? ' (current)' : ''}`, value: 'kimi-k2.5' },
|
{ name: `1. Kimi${state.modelKey === 'kimi-k2.5' ? ' (current)' : ''}`, value: 'kimi-k2.5' },
|
||||||
];
|
];
|
||||||
const model = await runSelect({ rl, message: 'Update Model', choices: modelChoices, pageSize: 6 });
|
const model = await runSelect({ rl, message: '', choices: modelChoices, pageSize: 6 });
|
||||||
if (!model) return { exit: false };
|
if (!model) return { exit: false };
|
||||||
state.modelKey = model;
|
state.modelKey = model;
|
||||||
state.modelId = config.model_id || 'kimi-k2.5';
|
state.modelId = config.model_id || 'kimi-k2.5';
|
||||||
|
console.log('');
|
||||||
|
console.log(`模型已切换为: ${state.modelKey}`);
|
||||||
|
console.log('');
|
||||||
|
|
||||||
const thinkingChoices = [
|
const thinkingChoices = [
|
||||||
{ name: `1. Fast${!state.thinkingMode ? ' (current)' : ''}`, value: 'fast' },
|
{ name: `1. Fast${!state.thinkingMode ? ' (current)' : ''}`, value: 'fast' },
|
||||||
{ name: `2. Thinking${state.thinkingMode ? ' (current)' : ''}`, value: 'thinking' },
|
{ name: `2. Thinking${state.thinkingMode ? ' (current)' : ''}`, value: 'thinking' },
|
||||||
];
|
];
|
||||||
const mode = await runSelect({ rl, message: 'Update Thinking Mode', choices: thinkingChoices, pageSize: 6 });
|
const mode = await runSelect({ rl, message: '', choices: thinkingChoices, pageSize: 6 });
|
||||||
if (mode) {
|
if (mode) {
|
||||||
state.thinkingMode = mode === 'thinking';
|
state.thinkingMode = mode === 'thinking';
|
||||||
console.log(`模型已切换为: ${state.modelKey} | 思考模式: ${mode}`);
|
console.log('');
|
||||||
|
console.log(`思考模式: ${mode}`);
|
||||||
|
console.log('');
|
||||||
persist();
|
persist();
|
||||||
}
|
}
|
||||||
return { exit: false };
|
return { exit: false };
|
||||||
|
|||||||
@ -20,6 +20,7 @@ const { createConversation, updateConversation } = require('../storage/conversat
|
|||||||
const { applyUsage, normalizeTokenUsage } = require('../utils/token_usage');
|
const { applyUsage, normalizeTokenUsage } = require('../utils/token_usage');
|
||||||
const { gray, cyan, green } = require('../utils/colors');
|
const { gray, cyan, green } = require('../utils/colors');
|
||||||
const { createIndentedWriter } = require('../ui/indented_writer');
|
const { createIndentedWriter } = require('../ui/indented_writer');
|
||||||
|
const { createStatusBar } = require('../ui/status_bar');
|
||||||
|
|
||||||
const WORKSPACE = process.cwd();
|
const WORKSPACE = process.cwd();
|
||||||
const WORKSPACE_NAME = path.basename(WORKSPACE);
|
const WORKSPACE_NAME = path.basename(WORKSPACE);
|
||||||
@ -46,8 +47,11 @@ renderBanner({
|
|||||||
workspace: WORKSPACE,
|
workspace: WORKSPACE,
|
||||||
conversationId: state.conversation?.id,
|
conversationId: state.conversation?.id,
|
||||||
});
|
});
|
||||||
|
console.log('');
|
||||||
|
console.log('');
|
||||||
|
|
||||||
let rl = null;
|
let rl = null;
|
||||||
|
let statusBar = null;
|
||||||
let commandMenuActive = false;
|
let commandMenuActive = false;
|
||||||
let menuSearchTerm = '';
|
let menuSearchTerm = '';
|
||||||
let menuLastSearchTerm = '';
|
let menuLastSearchTerm = '';
|
||||||
@ -60,6 +64,12 @@ if (process.stdin.isTTY) process.stdin.setRawMode(true);
|
|||||||
|
|
||||||
initReadline();
|
initReadline();
|
||||||
|
|
||||||
|
statusBar = createStatusBar({
|
||||||
|
getTokens: () => normalizeTokenUsage(state.tokenUsage).total || 0,
|
||||||
|
maxTokens: '256k',
|
||||||
|
});
|
||||||
|
statusBar.render();
|
||||||
|
|
||||||
process.stdin.on('keypress', (str, key) => {
|
process.stdin.on('keypress', (str, key) => {
|
||||||
if (commandMenuActive) {
|
if (commandMenuActive) {
|
||||||
if (key && key.name === 'backspace' && menuSearchTerm === '') {
|
if (key && key.name === 'backspace' && menuSearchTerm === '') {
|
||||||
@ -119,7 +129,7 @@ process.stdin.on('keypress', (str, key) => {
|
|||||||
readline.clearLine(process.stdout, 0);
|
readline.clearLine(process.stdout, 0);
|
||||||
readline.cursorTo(process.stdout, 0);
|
readline.cursorTo(process.stdout, 0);
|
||||||
}
|
}
|
||||||
rl.prompt(true);
|
promptWithStatus(true);
|
||||||
if (menuInjectedCommand) {
|
if (menuInjectedCommand) {
|
||||||
const injected = menuInjectedCommand;
|
const injected = menuInjectedCommand;
|
||||||
menuInjectedCommand = null;
|
menuInjectedCommand = null;
|
||||||
@ -143,7 +153,7 @@ function initReadline() {
|
|||||||
|
|
||||||
process.stdin.resume();
|
process.stdin.resume();
|
||||||
rl.setPrompt(PROMPT);
|
rl.setPrompt(PROMPT);
|
||||||
rl.prompt();
|
promptWithStatus();
|
||||||
|
|
||||||
rl.on('line', async (line) => {
|
rl.on('line', async (line) => {
|
||||||
if (rl) {
|
if (rl) {
|
||||||
@ -173,14 +183,14 @@ function initReadline() {
|
|||||||
menuLastSearchTerm = '';
|
menuLastSearchTerm = '';
|
||||||
}
|
}
|
||||||
if (!input) {
|
if (!input) {
|
||||||
rl.prompt();
|
promptWithStatus();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (input.startsWith('/')) {
|
if (input.startsWith('/')) {
|
||||||
const result = await handleCommand(input, { rl, state, config, workspace: WORKSPACE });
|
const result = await handleCommand(input, { rl, state, config, workspace: WORKSPACE });
|
||||||
if (result && result.exit) return;
|
if (result && result.exit) return;
|
||||||
rl.prompt();
|
promptWithStatus();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -190,10 +200,11 @@ function initReadline() {
|
|||||||
state.messages.push({ role: 'user', content: line });
|
state.messages.push({ role: 'user', content: line });
|
||||||
persistConversation();
|
persistConversation();
|
||||||
await runAssistantLoop();
|
await runAssistantLoop();
|
||||||
rl.prompt();
|
promptWithStatus();
|
||||||
});
|
});
|
||||||
|
|
||||||
rl.on('close', () => {
|
rl.on('close', () => {
|
||||||
|
if (statusBar) statusBar.destroy();
|
||||||
process.stdout.write('\n');
|
process.stdout.write('\n');
|
||||||
process.exit(0);
|
process.exit(0);
|
||||||
});
|
});
|
||||||
@ -224,6 +235,19 @@ function persistConversation() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function promptWithStatus(force = false) {
|
||||||
|
if (!rl) return;
|
||||||
|
if (process.stdout.isTTY) {
|
||||||
|
const rows = process.stdout.rows || 24;
|
||||||
|
if (rows >= 3) {
|
||||||
|
readline.cursorTo(process.stdout, 0, rows - 3);
|
||||||
|
readline.clearLine(process.stdout, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
rl.prompt(force);
|
||||||
|
if (statusBar) statusBar.render();
|
||||||
|
}
|
||||||
|
|
||||||
function buildApiMessages() {
|
function buildApiMessages() {
|
||||||
const system = buildSystemPrompt(systemPrompt, { workspace: WORKSPACE, allowMode: state.allowMode });
|
const system = buildSystemPrompt(systemPrompt, { workspace: WORKSPACE, allowMode: state.allowMode });
|
||||||
const messages = [{ role: 'system', content: system }];
|
const messages = [{ role: 'system', content: system }];
|
||||||
@ -256,6 +280,7 @@ async function runAssistantLoop() {
|
|||||||
let firstLoop = true;
|
let firstLoop = true;
|
||||||
const hideCursor = () => process.stdout.write('\x1b[?25l');
|
const hideCursor = () => process.stdout.write('\x1b[?25l');
|
||||||
const showCursor = () => process.stdout.write('\x1b[?25h');
|
const showCursor = () => process.stdout.write('\x1b[?25h');
|
||||||
|
if (statusBar) statusBar.setMode('running');
|
||||||
while (continueLoop) {
|
while (continueLoop) {
|
||||||
hideCursor();
|
hideCursor();
|
||||||
const spinner = new Spinner(' ', firstLoop ? 1 : 0);
|
const spinner = new Spinner(' ', firstLoop ? 1 : 0);
|
||||||
@ -418,4 +443,5 @@ async function runAssistantLoop() {
|
|||||||
continueLoop = false;
|
continueLoop = false;
|
||||||
}
|
}
|
||||||
showCursor();
|
showCursor();
|
||||||
|
if (statusBar) statusBar.setMode('input');
|
||||||
}
|
}
|
||||||
|
|||||||
@ -37,7 +37,7 @@ function buildSystemPrompt(basePrompt, opts) {
|
|||||||
const systemName = platform === 'darwin' ? 'macos' : platform === 'win32' ? 'windows' : 'linux';
|
const systemName = platform === 'darwin' ? 'macos' : platform === 'win32' ? 'windows' : 'linux';
|
||||||
let prompt = basePrompt;
|
let prompt = basePrompt;
|
||||||
const allowModeValue = opts.allowMode === 'read_only'
|
const allowModeValue = opts.allowMode === 'read_only'
|
||||||
? `${opts.allowMode}\n 已禁用 edit_file、run_command。若用户要求使用,请告知当前无权限需要请求切换权限。`
|
? `${opts.allowMode}\n 已禁用 edit_file、run_command。若用户要求使用,请告知当前无权限需要用户输入 /allow 切换权限。`
|
||||||
: opts.allowMode;
|
: opts.allowMode;
|
||||||
const replacements = {
|
const replacements = {
|
||||||
current_time: localIso,
|
current_time: localIso,
|
||||||
@ -48,6 +48,7 @@ function buildSystemPrompt(basePrompt, opts) {
|
|||||||
allow_mode: allowModeValue,
|
allow_mode: allowModeValue,
|
||||||
permissions: allowModeValue,
|
permissions: allowModeValue,
|
||||||
git: getGitInfo(opts.workspace),
|
git: getGitInfo(opts.workspace),
|
||||||
|
model_id: opts.modelId || '',
|
||||||
};
|
};
|
||||||
for (const [key, value] of Object.entries(replacements)) {
|
for (const [key, value] of Object.entries(replacements)) {
|
||||||
const token = `{${key}}`;
|
const token = `{${key}}`;
|
||||||
|
|||||||
@ -7,7 +7,7 @@ function createState(config, workspace) {
|
|||||||
modelKey: config.default_model_key || 'kimi-k2.5',
|
modelKey: config.default_model_key || 'kimi-k2.5',
|
||||||
modelId: config.model_id || 'kimi-k2.5',
|
modelId: config.model_id || 'kimi-k2.5',
|
||||||
thinkingMode: true,
|
thinkingMode: true,
|
||||||
tokenUsage: 0,
|
tokenUsage: { prompt: 0, completion: 0, total: 0 },
|
||||||
conversation: null,
|
conversation: null,
|
||||||
messages: [],
|
messages: [],
|
||||||
};
|
};
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
const { getModelProfile } = require('./model_profiles');
|
const { getModelProfile } = require('./model_profiles');
|
||||||
|
|
||||||
async function* streamChat({ config, messages, tools, thinkingMode }) {
|
async function* streamChat({ config, messages, tools, thinkingMode, abortSignal }) {
|
||||||
const profile = getModelProfile(config);
|
const profile = getModelProfile(config);
|
||||||
const url = `${profile.base_url}/chat/completions`;
|
const url = `${profile.base_url}/chat/completions`;
|
||||||
const headers = {
|
const headers = {
|
||||||
@ -31,8 +31,14 @@ async function* streamChat({ config, messages, tools, thinkingMode }) {
|
|||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers,
|
headers,
|
||||||
body: JSON.stringify(payload),
|
body: JSON.stringify(payload),
|
||||||
|
signal: abortSignal,
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
if (abortSignal && abortSignal.aborted) {
|
||||||
|
const error = new Error('请求已取消');
|
||||||
|
error.code = 'aborted';
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
const cause = err && err.cause ? ` (${err.cause.code || err.cause.message || err.cause})` : '';
|
const cause = err && err.cause ? ` (${err.cause.code || err.cause.message || err.cause})` : '';
|
||||||
throw new Error(`请求失败: ${err.message || err}${cause}`);
|
throw new Error(`请求失败: ${err.message || err}${cause}`);
|
||||||
}
|
}
|
||||||
@ -47,6 +53,11 @@ async function* streamChat({ config, messages, tools, thinkingMode }) {
|
|||||||
let buffer = '';
|
let buffer = '';
|
||||||
|
|
||||||
while (true) {
|
while (true) {
|
||||||
|
if (abortSignal && abortSignal.aborted) {
|
||||||
|
const error = new Error('请求已取消');
|
||||||
|
error.code = 'aborted';
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
const { value, done } = await reader.read();
|
const { value, done } = await reader.read();
|
||||||
if (done) break;
|
if (done) break;
|
||||||
buffer += decoder.decode(value, { stream: true });
|
buffer += decoder.decode(value, { stream: true });
|
||||||
|
|||||||
@ -4,6 +4,7 @@ const fs = require('fs');
|
|||||||
const path = require('path');
|
const path = require('path');
|
||||||
const crypto = require('crypto');
|
const crypto = require('crypto');
|
||||||
const { toISO } = require('../utils/time');
|
const { toISO } = require('../utils/time');
|
||||||
|
const { normalizeTokenUsage } = require('../utils/token_usage');
|
||||||
|
|
||||||
function getWorkspaceStore(workspace) {
|
function getWorkspaceStore(workspace) {
|
||||||
const base = path.join(workspace, '.easyagent');
|
const base = path.join(workspace, '.easyagent');
|
||||||
@ -33,9 +34,18 @@ function saveIndex(indexFile, index) {
|
|||||||
|
|
||||||
function extractTitle(messages) {
|
function extractTitle(messages) {
|
||||||
for (const msg of messages) {
|
for (const msg of messages) {
|
||||||
if (msg.role === 'user' && typeof msg.content === 'string' && msg.content.trim()) {
|
if (msg.role === 'user') {
|
||||||
const content = msg.content.trim();
|
let content = '';
|
||||||
return content.length > 50 ? `${content.slice(0, 50)}...` : content;
|
if (typeof msg.content === 'string') {
|
||||||
|
content = msg.content.trim();
|
||||||
|
} else if (Array.isArray(msg.content)) {
|
||||||
|
content = msg.content
|
||||||
|
.filter((part) => part && part.type === 'text' && typeof part.text === 'string')
|
||||||
|
.map((part) => part.text)
|
||||||
|
.join(' ')
|
||||||
|
.trim();
|
||||||
|
}
|
||||||
|
if (content) return content.length > 50 ? `${content.slice(0, 50)}...` : content;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return '新对话';
|
return '新对话';
|
||||||
@ -82,7 +92,7 @@ function createConversation(workspace, metadata = {}) {
|
|||||||
model_id: metadata.model_id || 'kimi-k2.5',
|
model_id: metadata.model_id || 'kimi-k2.5',
|
||||||
thinking_mode: !!metadata.thinking_mode,
|
thinking_mode: !!metadata.thinking_mode,
|
||||||
allow_mode: metadata.allow_mode || 'full_access',
|
allow_mode: metadata.allow_mode || 'full_access',
|
||||||
token_usage: metadata.token_usage || 0,
|
token_usage: normalizeTokenUsage(metadata.token_usage),
|
||||||
cwd: metadata.cwd || '',
|
cwd: metadata.cwd || '',
|
||||||
},
|
},
|
||||||
messages: [],
|
messages: [],
|
||||||
@ -123,6 +133,9 @@ function updateConversation(workspace, conversation, messages, metadataUpdates =
|
|||||||
updated.metadata = {
|
updated.metadata = {
|
||||||
...conversation.metadata,
|
...conversation.metadata,
|
||||||
...metadataUpdates,
|
...metadataUpdates,
|
||||||
|
token_usage: metadataUpdates && Object.prototype.hasOwnProperty.call(metadataUpdates, 'token_usage')
|
||||||
|
? normalizeTokenUsage(metadataUpdates.token_usage)
|
||||||
|
: normalizeTokenUsage(conversation.metadata?.token_usage),
|
||||||
};
|
};
|
||||||
saveConversation(workspace, updated);
|
saveConversation(workspace, updated);
|
||||||
return updated;
|
return updated;
|
||||||
|
|||||||
@ -138,7 +138,7 @@ function buildToolContent(result) {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function executeTool({ workspace, config, allowMode, toolCall }) {
|
async function executeTool({ workspace, config, allowMode, toolCall, abortSignal }) {
|
||||||
const name = toolCall.function.name;
|
const name = toolCall.function.name;
|
||||||
let args = {};
|
let args = {};
|
||||||
try {
|
try {
|
||||||
@ -153,7 +153,7 @@ async function executeTool({ workspace, config, allowMode, toolCall }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (allowMode === 'read_only' && (name === 'edit_file' || name === 'run_command')) {
|
if (allowMode === 'read_only' && (name === 'edit_file' || name === 'run_command')) {
|
||||||
const note = '当前为只读模式,已禁用 edit_file、run_command。若需使用,请先请求切换权限。';
|
const note = '当前为只读模式,已禁用 edit_file、run_command。若需使用,请告知当前无权限需要用户输入 /allow 切换权限。';
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
tool: name,
|
tool: name,
|
||||||
@ -162,11 +162,21 @@ async function executeTool({ workspace, config, allowMode, toolCall }) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (abortSignal && abortSignal.aborted) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
tool: name,
|
||||||
|
error: '任务被用户取消',
|
||||||
|
formatted: '任务被用户取消',
|
||||||
|
raw: { success: false, error: '任务被用户取消', cancelled: true },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
let raw;
|
let raw;
|
||||||
if (name === 'read_file') raw = readFileTool(workspace, args);
|
if (name === 'read_file') raw = readFileTool(workspace, args);
|
||||||
else if (name === 'edit_file') raw = editFileTool(workspace, args);
|
else if (name === 'edit_file') raw = editFileTool(workspace, args);
|
||||||
else if (name === 'run_command') raw = await runCommandTool(workspace, args);
|
else if (name === 'run_command') raw = await runCommandTool(workspace, args, abortSignal);
|
||||||
else if (name === 'web_search') raw = await webSearchTool(config, args);
|
else if (name === 'web_search') raw = await webSearchTool(config, args, abortSignal);
|
||||||
else if (name === 'extract_webpage') {
|
else if (name === 'extract_webpage') {
|
||||||
if (!args.mode) args.mode = 'read';
|
if (!args.mode) args.mode = 'read';
|
||||||
const targetPath = args.target_path ? (path.isAbsolute(args.target_path) ? args.target_path : path.join(workspace, args.target_path)) : null;
|
const targetPath = args.target_path ? (path.isAbsolute(args.target_path) ? args.target_path : path.join(workspace, args.target_path)) : null;
|
||||||
@ -174,10 +184,10 @@ async function executeTool({ workspace, config, allowMode, toolCall }) {
|
|||||||
if (!targetPath || !targetPath.toLowerCase().endsWith('.md')) {
|
if (!targetPath || !targetPath.toLowerCase().endsWith('.md')) {
|
||||||
raw = { success: false, error: 'target_path 必须是 .md 文件' };
|
raw = { success: false, error: 'target_path 必须是 .md 文件' };
|
||||||
} else {
|
} else {
|
||||||
raw = await extractWebpageTool(config, args, targetPath);
|
raw = await extractWebpageTool(config, args, targetPath, abortSignal);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
raw = await extractWebpageTool(config, args, targetPath);
|
raw = await extractWebpageTool(config, args, targetPath, abortSignal);
|
||||||
}
|
}
|
||||||
} else if (name === 'search_workspace') raw = await searchWorkspaceTool(workspace, args);
|
} else if (name === 'search_workspace') raw = await searchWorkspaceTool(workspace, args);
|
||||||
else if (name === 'read_mediafile') raw = readMediafileTool(workspace, args);
|
else if (name === 'read_mediafile') raw = readMediafileTool(workspace, args);
|
||||||
|
|||||||
@ -45,19 +45,26 @@ function editFileTool(workspace, args) {
|
|||||||
const oldString = args.old_string ?? '';
|
const oldString = args.old_string ?? '';
|
||||||
const newString = args.new_string ?? '';
|
const newString = args.new_string ?? '';
|
||||||
try {
|
try {
|
||||||
|
let creating = false;
|
||||||
if (!fs.existsSync(target)) {
|
if (!fs.existsSync(target)) {
|
||||||
return { success: false, error: '文件不存在' };
|
creating = true;
|
||||||
|
fs.mkdirSync(path.dirname(target), { recursive: true });
|
||||||
|
fs.writeFileSync(target, '', 'utf8');
|
||||||
}
|
}
|
||||||
const stat = fs.statSync(target);
|
const stat = fs.statSync(target);
|
||||||
if (!stat.isFile()) {
|
if (!stat.isFile()) {
|
||||||
return { success: false, error: '目标不是文件' };
|
return { success: false, error: '目标不是文件' };
|
||||||
}
|
}
|
||||||
const original = fs.readFileSync(target, 'utf8');
|
const original = fs.readFileSync(target, 'utf8');
|
||||||
if (!original.includes(oldString)) {
|
if (!creating && oldString === '') {
|
||||||
|
return { success: false, error: 'old_string 不能为空,请从 read_file 内容中精确复制' };
|
||||||
|
}
|
||||||
|
if (!creating && !original.includes(oldString)) {
|
||||||
return { success: false, error: 'old_string 未匹配到内容' };
|
return { success: false, error: 'old_string 未匹配到内容' };
|
||||||
}
|
}
|
||||||
const updated = original.split(oldString).join(newString);
|
const updated = creating ? newString : original.split(oldString).join(newString);
|
||||||
const replacements = original.split(oldString).length - 1;
|
let replacements = creating ? 0 : original.split(oldString).length - 1;
|
||||||
|
if (creating && newString) replacements = 1;
|
||||||
fs.writeFileSync(target, updated, 'utf8');
|
fs.writeFileSync(target, updated, 'utf8');
|
||||||
const diff = diffSummary(original, updated);
|
const diff = diffSummary(original, updated);
|
||||||
return {
|
return {
|
||||||
|
|||||||
@ -9,14 +9,21 @@ function resolvePath(workspace, p) {
|
|||||||
return path.join(workspace, p);
|
return path.join(workspace, p);
|
||||||
}
|
}
|
||||||
|
|
||||||
function runCommandTool(workspace, args) {
|
function runCommandTool(workspace, args, abortSignal) {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
const cmd = args.command;
|
const cmd = args.command;
|
||||||
const timeoutSec = Number(args.timeout || 0);
|
const timeoutSec = Number(args.timeout || 0);
|
||||||
const cwd = resolvePath(workspace, args.working_dir || '.');
|
const cwd = resolvePath(workspace, args.working_dir || '.');
|
||||||
if (!cmd) return resolve({ success: false, error: 'command 不能为空' });
|
if (!cmd) return resolve({ success: false, error: 'command 不能为空' });
|
||||||
|
if (abortSignal && abortSignal.aborted) {
|
||||||
|
return resolve({ success: false, error: '任务被用户取消', cancelled: true });
|
||||||
|
}
|
||||||
const timeoutMs = Math.max(0, timeoutSec) * 1000;
|
const timeoutMs = Math.max(0, timeoutSec) * 1000;
|
||||||
exec(cmd, { cwd, timeout: timeoutMs, maxBuffer: 10 * 1024 * 1024, shell: true }, (err, stdout, stderr) => {
|
let finished = false;
|
||||||
|
const child = exec(cmd, { cwd, timeout: timeoutMs, maxBuffer: 10 * 1024 * 1024, shell: true }, (err, stdout, stderr) => {
|
||||||
|
if (finished) return;
|
||||||
|
finished = true;
|
||||||
|
if (abortSignal) abortSignal.removeEventListener('abort', onAbort);
|
||||||
const output = [stdout, stderr].filter(Boolean).join('');
|
const output = [stdout, stderr].filter(Boolean).join('');
|
||||||
if (err) {
|
if (err) {
|
||||||
const isTimeout = err.killed && err.signal === 'SIGTERM';
|
const isTimeout = err.killed && err.signal === 'SIGTERM';
|
||||||
@ -31,6 +38,15 @@ function runCommandTool(workspace, args) {
|
|||||||
}
|
}
|
||||||
resolve({ success: true, status: 'ok', output });
|
resolve({ success: true, status: 'ok', output });
|
||||||
});
|
});
|
||||||
|
const onAbort = () => {
|
||||||
|
if (finished) return;
|
||||||
|
finished = true;
|
||||||
|
try {
|
||||||
|
child.kill('SIGTERM');
|
||||||
|
} catch (_) {}
|
||||||
|
return resolve({ success: false, error: '任务被用户取消', cancelled: true });
|
||||||
|
};
|
||||||
|
if (abortSignal) abortSignal.addEventListener('abort', onAbort, { once: true });
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,9 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
async function webSearchTool(config, args) {
|
async function webSearchTool(config, args, abortSignal) {
|
||||||
|
if (abortSignal && abortSignal.aborted) {
|
||||||
|
return { success: false, error: '任务被用户取消', cancelled: true };
|
||||||
|
}
|
||||||
const body = {
|
const body = {
|
||||||
api_key: config.tavily_api_key,
|
api_key: config.tavily_api_key,
|
||||||
query: args.query,
|
query: args.query,
|
||||||
@ -18,6 +21,7 @@ async function webSearchTool(config, args) {
|
|||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify(body),
|
body: JSON.stringify(body),
|
||||||
|
signal: abortSignal,
|
||||||
});
|
});
|
||||||
const json = await res.json();
|
const json = await res.json();
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
@ -25,11 +29,17 @@ async function webSearchTool(config, args) {
|
|||||||
}
|
}
|
||||||
return { success: true, ...json, query: args.query, searched_at: new Date().toISOString() };
|
return { success: true, ...json, query: args.query, searched_at: new Date().toISOString() };
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
if (err && err.name === 'AbortError') {
|
||||||
|
return { success: false, error: '任务被用户取消', cancelled: true };
|
||||||
|
}
|
||||||
return { success: false, error: err.message || String(err) };
|
return { success: false, error: err.message || String(err) };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function extractWebpageTool(config, args, savePath) {
|
async function extractWebpageTool(config, args, savePath, abortSignal) {
|
||||||
|
if (abortSignal && abortSignal.aborted) {
|
||||||
|
return { success: false, error: '任务被用户取消', cancelled: true };
|
||||||
|
}
|
||||||
const body = {
|
const body = {
|
||||||
api_key: config.tavily_api_key,
|
api_key: config.tavily_api_key,
|
||||||
urls: [args.url],
|
urls: [args.url],
|
||||||
@ -40,6 +50,7 @@ async function extractWebpageTool(config, args, savePath) {
|
|||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify(body),
|
body: JSON.stringify(body),
|
||||||
|
signal: abortSignal,
|
||||||
});
|
});
|
||||||
const json = await res.json();
|
const json = await res.json();
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
@ -53,6 +64,9 @@ async function extractWebpageTool(config, args, savePath) {
|
|||||||
}
|
}
|
||||||
return { success: true, content, url: args.url };
|
return { success: true, content, url: args.url };
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
if (err && err.name === 'AbortError') {
|
||||||
|
return { success: false, error: '任务被用户取消', cancelled: true };
|
||||||
|
}
|
||||||
return { success: false, error: err.message || String(err) };
|
return { success: false, error: err.message || String(err) };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -14,6 +14,14 @@ const COMMAND_CHOICES = [
|
|||||||
{ value: '/exit', desc: '退出程序' },
|
{ value: '/exit', desc: '退出程序' },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
function hasCommandMatch(term) {
|
||||||
|
if (!term) return false;
|
||||||
|
const t = term.toLowerCase();
|
||||||
|
return COMMAND_CHOICES.some(
|
||||||
|
(c) => c.value.toLowerCase().includes(t) || c.desc.toLowerCase().includes(t)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
async function openCommandMenu(options) {
|
async function openCommandMenu(options) {
|
||||||
const { rl, prompt, pageSize, colorEnabled, resetAnsi, onInput, abortSignal } = options;
|
const { rl, prompt, pageSize, colorEnabled, resetAnsi, onInput, abortSignal } = options;
|
||||||
let latestInput = '';
|
let latestInput = '';
|
||||||
@ -37,6 +45,7 @@ async function openCommandMenu(options) {
|
|||||||
message: (text) => text,
|
message: (text) => text,
|
||||||
searchTerm: (text) => `/${(text || '').replace(/^\/+/, '')}`,
|
searchTerm: (text) => `/${(text || '').replace(/^\/+/, '')}`,
|
||||||
highlight: (text) => (colorEnabled ? `\x1b[94m${text}${resetAnsi}` : text),
|
highlight: (text) => (colorEnabled ? `\x1b[94m${text}${resetAnsi}` : text),
|
||||||
|
error: () => '无效的指令',
|
||||||
keysHelpTip: () => '',
|
keysHelpTip: () => '',
|
||||||
description: (text) => text,
|
description: (text) => text,
|
||||||
answer: () => '',
|
answer: () => '',
|
||||||
@ -77,4 +86,4 @@ async function openCommandMenu(options) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = { openCommandMenu };
|
module.exports = { openCommandMenu, hasCommandMatch };
|
||||||
|
|||||||
103
src/ui/status_bar.js
Normal file
103
src/ui/status_bar.js
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
const readline = require('readline');
|
||||||
|
const { visibleWidth, truncatePlain } = require('../utils/text_width');
|
||||||
|
|
||||||
|
function truncateNoEllipsis(text, maxCols) {
|
||||||
|
const out = truncatePlain(text, maxCols);
|
||||||
|
if (out.endsWith('...') && visibleWidth(text) > maxCols) {
|
||||||
|
return out.slice(0, -3);
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
function createStatusBar({ getTokens, maxTokens }) {
|
||||||
|
let mode = 'input';
|
||||||
|
let enabled = true;
|
||||||
|
let scrollApplied = false;
|
||||||
|
let lastRows = null;
|
||||||
|
|
||||||
|
const render = () => {
|
||||||
|
if (!enabled || !process.stdout.isTTY) return;
|
||||||
|
const cols = process.stdout.columns || 80;
|
||||||
|
const rows = process.stdout.rows || 24;
|
||||||
|
if (rows < 3) return;
|
||||||
|
const left = mode === 'running' ? '按下Esc停止' : '输入/查看所有指令';
|
||||||
|
const total = typeof getTokens === 'function' ? getTokens() : 0;
|
||||||
|
const formatCount = (value) => {
|
||||||
|
const num = Number(value) || 0;
|
||||||
|
if (num < 1000) return String(num);
|
||||||
|
const k = Math.round((num / 1000) * 10) / 10;
|
||||||
|
return `${k % 1 === 0 ? k.toFixed(0) : k.toFixed(1)}k`;
|
||||||
|
};
|
||||||
|
const right = `当前上下文 ${formatCount(total)}/${maxTokens}`;
|
||||||
|
const rightTail = ` ${formatCount(total)}/${maxTokens}`;
|
||||||
|
const leftWidth = visibleWidth(left);
|
||||||
|
const rightWidth = visibleWidth(right);
|
||||||
|
const usableCols = Math.max(1, cols - 1);
|
||||||
|
let safeLine = '';
|
||||||
|
|
||||||
|
const availableLeft = usableCols - rightWidth - 1;
|
||||||
|
if (availableLeft >= 1) {
|
||||||
|
const displayLeft = truncateNoEllipsis(left, availableLeft);
|
||||||
|
const gap = usableCols - visibleWidth(displayLeft) - rightWidth;
|
||||||
|
safeLine = `${displayLeft}${' '.repeat(Math.max(0, gap))}${right}`;
|
||||||
|
} else {
|
||||||
|
safeLine = truncateNoEllipsis(right, usableCols);
|
||||||
|
}
|
||||||
|
|
||||||
|
process.stdout.write('\x1b7'); // save cursor
|
||||||
|
if (!scrollApplied || lastRows !== rows) {
|
||||||
|
process.stdout.write('\x1b[r'); // reset scroll region
|
||||||
|
process.stdout.write(`\x1b[1;${rows - 2}r`); // reserve last 2 lines
|
||||||
|
scrollApplied = true;
|
||||||
|
lastRows = rows;
|
||||||
|
}
|
||||||
|
// keep one blank spacer line above status line
|
||||||
|
readline.cursorTo(process.stdout, 0, rows - 2);
|
||||||
|
readline.clearLine(process.stdout, 0);
|
||||||
|
// render status line on last row
|
||||||
|
readline.cursorTo(process.stdout, 0, rows - 1);
|
||||||
|
readline.clearLine(process.stdout, 0);
|
||||||
|
process.stdout.write('\x1b[?7l'); // disable auto-wrap
|
||||||
|
process.stdout.write(safeLine);
|
||||||
|
process.stdout.write('\x1b[?7h'); // re-enable auto-wrap
|
||||||
|
process.stdout.write('\x1b8'); // restore cursor
|
||||||
|
};
|
||||||
|
|
||||||
|
const setMode = (nextMode) => {
|
||||||
|
if (mode === nextMode) return;
|
||||||
|
mode = nextMode;
|
||||||
|
render();
|
||||||
|
};
|
||||||
|
|
||||||
|
const setEnabled = (nextEnabled) => {
|
||||||
|
enabled = nextEnabled;
|
||||||
|
if (enabled) render();
|
||||||
|
};
|
||||||
|
|
||||||
|
const destroy = () => {
|
||||||
|
if (!process.stdout.isTTY) return;
|
||||||
|
if (scrollApplied) {
|
||||||
|
process.stdout.write('\x1b[r'); // reset scroll region
|
||||||
|
scrollApplied = false;
|
||||||
|
}
|
||||||
|
const rows = process.stdout.rows || 24;
|
||||||
|
if (rows >= 2) {
|
||||||
|
readline.cursorTo(process.stdout, 0, rows - 2);
|
||||||
|
readline.clearLine(process.stdout, 0);
|
||||||
|
}
|
||||||
|
if (rows >= 1) {
|
||||||
|
readline.cursorTo(process.stdout, 0, rows - 1);
|
||||||
|
readline.clearLine(process.stdout, 0);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (process.stdout.isTTY) {
|
||||||
|
process.stdout.on('resize', () => render());
|
||||||
|
}
|
||||||
|
|
||||||
|
return { render, setMode, setEnabled, destroy };
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { createStatusBar };
|
||||||
@ -130,6 +130,7 @@ function startToolDisplay(line) {
|
|||||||
function formatResultLines(name, args, raw) {
|
function formatResultLines(name, args, raw) {
|
||||||
if (!raw || raw.success === false) {
|
if (!raw || raw.success === false) {
|
||||||
const msg = raw && raw.error ? raw.error : '执行失败';
|
const msg = raw && raw.error ? raw.error : '执行失败';
|
||||||
|
if (msg === '任务被用户取消') return [red(msg)];
|
||||||
return [red(`失败: ${msg}`)];
|
return [red(`失败: ${msg}`)];
|
||||||
}
|
}
|
||||||
if (name === 'run_command') {
|
if (name === 'run_command') {
|
||||||
@ -216,6 +217,7 @@ function printResultLines(lines) {
|
|||||||
process.stdout.write(` ${line}\n`);
|
process.stdout.write(` ${line}\n`);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
process.stdout.write('\n');
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = { buildStartLine, buildFinalLine, startToolDisplay, formatResultLines, printResultLines };
|
module.exports = { buildStartLine, buildFinalLine, startToolDisplay, formatResultLines, printResultLines };
|
||||||
|
|||||||
28
src/utils/token_usage.js
Normal file
28
src/utils/token_usage.js
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
function normalizeTokenUsage(value) {
|
||||||
|
if (value && typeof value === 'object') {
|
||||||
|
return {
|
||||||
|
prompt: Number(value.prompt) || 0,
|
||||||
|
completion: Number(value.completion) || 0,
|
||||||
|
total: Number(value.total) || 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const num = Number(value);
|
||||||
|
return {
|
||||||
|
prompt: 0,
|
||||||
|
completion: 0,
|
||||||
|
total: Number.isFinite(num) ? num : 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyUsage(base, usage) {
|
||||||
|
const normalized = normalizeTokenUsage(base);
|
||||||
|
if (!usage) return normalized;
|
||||||
|
if (Number.isFinite(usage.prompt_tokens)) normalized.prompt += usage.prompt_tokens;
|
||||||
|
if (Number.isFinite(usage.completion_tokens)) normalized.completion += usage.completion_tokens;
|
||||||
|
if (Number.isFinite(usage.total_tokens)) normalized.total = usage.total_tokens;
|
||||||
|
return normalized;
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { normalizeTokenUsage, applyUsage };
|
||||||
Loading…
Reference in New Issue
Block a user