termux-dev 1.6.0 → 1.8.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.
- package/README.md +214 -166
- package/assets/banner.svg +23 -28
- package/assets/preview.png +0 -0
- package/assets/preview.svg +101 -10
- package/dist/cli/banner.js +3 -4
- package/dist/cli/clipboard-write.js +55 -0
- package/dist/cli/clipboard.js +182 -106
- package/dist/cli/colors.js +67 -0
- package/dist/cli/confirm.js +50 -0
- package/dist/cli/doctor.js +4 -4
- package/dist/cli/effort.js +80 -0
- package/dist/cli/export.js +3 -3
- package/dist/cli/files.js +19 -5
- package/dist/cli/finder.js +241 -0
- package/dist/cli/headless.js +23 -11
- package/dist/cli/index.js +1557 -744
- package/dist/cli/keys.js +69 -0
- package/dist/cli/liveblock.js +45 -0
- package/dist/cli/log.js +19 -0
- package/dist/cli/logo.js +30 -0
- package/dist/cli/markdown.js +174 -37
- package/dist/cli/mcp-panel.js +432 -0
- package/dist/cli/permission.js +84 -0
- package/dist/cli/picker.js +143 -0
- package/dist/cli/prompt.js +256 -56
- package/dist/cli/resume.js +15 -0
- package/dist/cli/rewind.js +51 -0
- package/dist/cli/runtime_errors.js +6 -0
- package/dist/cli/server.js +85 -24
- package/dist/cli/settings.js +307 -0
- package/dist/cli/statusline.js +1 -1
- package/dist/cli/theme.js +126 -108
- package/dist/cli/transcript.js +134 -0
- package/dist/cli/typeahead.js +371 -0
- package/dist/cli/ui.js +1022 -0
- package/dist/cli/updater.js +23 -21
- package/dist/cli/usage-panel.js +242 -0
- package/dist/cli/welcome.js +56 -0
- package/dist/cli/width.js +178 -0
- package/dist/core/capabilities.js +433 -0
- package/dist/core/catalog-util.js +27 -0
- package/dist/core/checkpoint.js +90 -0
- package/dist/core/context-stats.js +29 -0
- package/dist/core/cost.js +191 -0
- package/dist/core/formats.js +199 -0
- package/dist/core/history.js +51 -1
- package/dist/core/hooks.js +79 -0
- package/dist/core/i18n.js +42 -0
- package/dist/core/local-config.js +69 -0
- package/dist/core/locales/de.js +265 -0
- package/dist/core/locales/en.js +276 -0
- package/dist/core/locales/es.js +265 -0
- package/dist/core/locales/fr.js +265 -0
- package/dist/core/locales/ja.js +265 -0
- package/dist/core/locales/pt.js +265 -0
- package/dist/core/locales/ru.js +265 -0
- package/dist/core/locales/uk.js +265 -0
- package/dist/core/locales/zh.js +265 -0
- package/dist/core/loop.js +125 -49
- package/dist/core/memory.js +39 -12
- package/dist/core/models.js +33 -20
- package/dist/core/network.js +12 -11
- package/dist/core/notify.js +54 -0
- package/dist/core/planfile.js +43 -0
- package/dist/core/price-catalog.json +1 -0
- package/dist/core/prices.js +249 -0
- package/dist/core/reasoning-catalog.json +1 -0
- package/dist/core/reasoning.js +393 -0
- package/dist/core/redact.js +41 -0
- package/dist/core/secure-file.js +22 -0
- package/dist/core/session.js +16 -5
- package/dist/core/snapshot.js +14 -2
- package/dist/core/title.js +61 -0
- package/dist/core/types.js +1 -1
- package/dist/core/usage.js +43 -7
- package/dist/core/vision-catalog.json +1 -0
- package/dist/core/vision.js +123 -0
- package/dist/mcp/client.js +36 -5
- package/dist/mcp/http-client.js +174 -0
- package/dist/mcp/manager.js +330 -93
- package/dist/permissions/guard.js +385 -117
- package/dist/prompts/builder.js +46 -38
- package/dist/providers/anthropic.js +517 -0
- package/dist/providers/index.js +18 -1
- package/dist/providers/openai.js +138 -125
- package/dist/providers/shared.js +116 -0
- package/dist/providers/toolguard.js +101 -0
- package/dist/tools/bash.js +141 -51
- package/dist/tools/diagnostics.js +76 -48
- package/dist/tools/fs.js +77 -5
- package/dist/tools/index.js +2 -1
- package/dist/tools/mcp.js +72 -0
- package/dist/tools/packages.js +16 -1
- package/dist/tools/plan.js +8 -8
- package/dist/tools/questions.js +50 -54
- package/dist/tools/server.js +6 -0
- package/dist/tools/todo.js +20 -18
- package/dist/tools/web.js +143 -8
- package/package.json +1 -1
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
export const zh = {
|
|
2
|
+
'net.unknown': '未知网络错误',
|
|
3
|
+
'net.rateLimit': '已超出 API 请求频率限制 (429)',
|
|
4
|
+
'net.badGateway': '服务商网关暂时不可用 (502)',
|
|
5
|
+
'net.unavailable': '服务商服务器过载 (503)',
|
|
6
|
+
'net.gatewayTimeout': '服务商网关超时 (504)',
|
|
7
|
+
'net.offline': '无网络连接或 DNS 故障(离线)',
|
|
8
|
+
'net.reset': '与服务器的连接被重置',
|
|
9
|
+
'net.timeout': '网络连接超时',
|
|
10
|
+
'net.refused': '服务器拒绝了连接',
|
|
11
|
+
'net.fetchFailed': '网络请求失败(连接中断)',
|
|
12
|
+
'net.generic': '互联网连接故障',
|
|
13
|
+
'net.failedAfter': '网络错误:尝试 {n} 次后仍无法恢复连接({reason})。',
|
|
14
|
+
'conn.unstable': '连接不稳定({reason})',
|
|
15
|
+
'conn.reconnecting': '正在重连 {attempt}/{max} · {delay} 秒后重试',
|
|
16
|
+
'conn.restored': '连接已恢复',
|
|
17
|
+
'conn.classicUnstable': '连接不稳定({reason})',
|
|
18
|
+
'conn.classicRetry': '正在重连 [{attempt}/{max}](暂停 {delay} 秒)...',
|
|
19
|
+
'conn.classicWaiting': '等待网络 [{attempt}/{max}]...',
|
|
20
|
+
'conn.classicRestored': '✓ 连接已恢复!继续生成...',
|
|
21
|
+
'stop.connection': '连接已取消',
|
|
22
|
+
'stop.stopped': '已停止',
|
|
23
|
+
'stop.interrupted': '已中断,正在发送你的消息',
|
|
24
|
+
'stop.classicConnection': '连接已取消 (Ctrl+C)。',
|
|
25
|
+
'stop.classicStopped': '生成已停止 (Ctrl+C)。',
|
|
26
|
+
'stop.classicInterrupted': '已中断:正在发送你的消息。',
|
|
27
|
+
'status.waiting': '等待模型',
|
|
28
|
+
'status.thinking': '思考中',
|
|
29
|
+
'status.writing': '撰写中',
|
|
30
|
+
'status.running': '运行中',
|
|
31
|
+
'status.reconnecting': '重连中',
|
|
32
|
+
'status.stop': 'ctrl+c 停止',
|
|
33
|
+
'status.thinkingLine': '思考',
|
|
34
|
+
'status.thoughtFor': '思考了 {sec} 秒',
|
|
35
|
+
'composer.placeholder': '描述任务或提出问题…',
|
|
36
|
+
'composer.queued': '已排队',
|
|
37
|
+
'composer.queuedMany': '{n} 条已排队',
|
|
38
|
+
'composer.queuedHint': 'ctrl+enter 立即发送 · ↓ 编辑',
|
|
39
|
+
'composer.hint1': 'enter 排队 · ctrl+c 停止',
|
|
40
|
+
'composer.hint2': 'enter 排队',
|
|
41
|
+
'composer.hint3': 'enter 排队 · ctrl+c 停止',
|
|
42
|
+
'composer.hint4': 'enter 排队',
|
|
43
|
+
'summary.toolOne': '{n} 个工具',
|
|
44
|
+
'summary.toolMany': '{n} 个工具',
|
|
45
|
+
'summary.in': '输入 {n}',
|
|
46
|
+
'summary.out': '输出 {n}',
|
|
47
|
+
'summary.stopped': '已停止',
|
|
48
|
+
'settings.title': '设置',
|
|
49
|
+
'settings.section.appearance': '外观',
|
|
50
|
+
'settings.section.agent': '智能体',
|
|
51
|
+
'settings.section.general': '通用',
|
|
52
|
+
'settings.section.about': '关于',
|
|
53
|
+
'settings.row.theme': '主题',
|
|
54
|
+
'settings.row.layout': '界面',
|
|
55
|
+
'settings.row.pureBlack': '黑色背景',
|
|
56
|
+
'settings.row.autoApprove': '自动批准 (YOLO)',
|
|
57
|
+
'settings.row.maxIterations': '最大步数',
|
|
58
|
+
'settings.row.memory': '项目记忆',
|
|
59
|
+
'settings.row.updates': '检查更新',
|
|
60
|
+
'settings.row.language': '语言',
|
|
61
|
+
'settings.row.about': '关于 devx',
|
|
62
|
+
'settings.short.theme': '主题',
|
|
63
|
+
'settings.short.layout': '界面',
|
|
64
|
+
'settings.short.pureBlack': '黑背景',
|
|
65
|
+
'settings.short.autoApprove': 'YOLO',
|
|
66
|
+
'settings.short.maxIterations': '步数',
|
|
67
|
+
'settings.short.memory': '记忆',
|
|
68
|
+
'settings.short.updates': '更新',
|
|
69
|
+
'settings.short.language': '语言',
|
|
70
|
+
'settings.short.about': '关于',
|
|
71
|
+
'settings.value.on': '开',
|
|
72
|
+
'settings.value.off': '关',
|
|
73
|
+
'settings.value.clean': '简洁',
|
|
74
|
+
'settings.value.classic': '经典',
|
|
75
|
+
'settings.value.unlimited': '不限',
|
|
76
|
+
'settings.value.steps': '{n} 步',
|
|
77
|
+
'settings.hint.theme': '整个界面的配色。左右键切换主题,面板会立即变色。',
|
|
78
|
+
'settings.hint.layout': '简洁:带边框的输入框、卡片和安静的工具行。经典:原来的 ASCII 布局。',
|
|
79
|
+
'settings.hint.pureBlack': '将终端背景设为深邃的 OLED 黑 (#0a0a0c)。关闭则保留你自己的背景。',
|
|
80
|
+
'settings.hint.autoApprove': '跳过常规的权限确认。危险命令和凭据文件仍会先询问。仅在本次会话有效:下次启动 devx 时会重新关闭。',
|
|
81
|
+
'settings.hint.maxIterations': '智能体处理一次请求最多可执行多少个工具步骤(编辑文件、运行命令)。',
|
|
82
|
+
'settings.hint.memory': '每次会话都会加载 .devx/memory.md 中的规则。',
|
|
83
|
+
'settings.hint.updates': '启动时在 GitHub 上查找 devx 新版本。无论开关,/update 都可用。',
|
|
84
|
+
'settings.hint.language': '界面语言:消息、设置和提示。代码和工具名称保持英文。',
|
|
85
|
+
'settings.hint.about': 'ApvCode 出品的终端原生 AI 编程智能体。MIT 许可证。github.com/apvcode/Termux-Dev',
|
|
86
|
+
'settings.warnYolo': '已开启:智能体将不经询问直接运行命令并编辑文件。',
|
|
87
|
+
'settings.keys.move': '移动',
|
|
88
|
+
'settings.keys.change': '更改',
|
|
89
|
+
'settings.keys.toggle': '切换',
|
|
90
|
+
'settings.keys.close': '关闭',
|
|
91
|
+
// ── /mcp panel
|
|
92
|
+
'mcp.title': 'MCP 服务器',
|
|
93
|
+
'mcp.count': '服务器:{n}',
|
|
94
|
+
'mcp.scope.user': '用户服务器',
|
|
95
|
+
'mcp.scope.project': '项目服务器',
|
|
96
|
+
'mcp.toolsCount': '工具:{n}',
|
|
97
|
+
'mcp.status.connected': '已连接',
|
|
98
|
+
'mcp.status.connecting': '连接中…',
|
|
99
|
+
'mcp.status.failed': '失败',
|
|
100
|
+
'mcp.status.disabled': '已禁用',
|
|
101
|
+
'mcp.empty': '尚未配置 MCP 服务器。',
|
|
102
|
+
'mcp.emptyHint': '运行 /mcp import 导入 Claude Code 的服务器,或用 /mcp add <名称> <命令> [参数] 添加。',
|
|
103
|
+
'mcp.detail.status': '状态',
|
|
104
|
+
'mcp.detail.command': '命令',
|
|
105
|
+
'mcp.detail.config': '配置',
|
|
106
|
+
'mcp.detail.capabilities': '能力',
|
|
107
|
+
'mcp.detail.tools': '工具',
|
|
108
|
+
'mcp.detail.error': '错误',
|
|
109
|
+
'mcp.action.tools': '查看工具',
|
|
110
|
+
'mcp.action.reconnect': '重新连接',
|
|
111
|
+
'mcp.action.disable': '禁用',
|
|
112
|
+
'mcp.action.enable': '启用',
|
|
113
|
+
'mcp.toolsTitle': '{name} 的工具',
|
|
114
|
+
'mcp.noTools': '该服务器没有提供工具。',
|
|
115
|
+
'mcp.tag.readOnly': '只读',
|
|
116
|
+
'mcp.tag.destructive': '破坏性',
|
|
117
|
+
'mcp.tag.openWorld': '开放世界',
|
|
118
|
+
'mcp.params': '参数',
|
|
119
|
+
'mcp.noParams': '没有参数。',
|
|
120
|
+
'mcp.required': '必填',
|
|
121
|
+
'mcp.more': '还有 {n} 个',
|
|
122
|
+
'mcp.keys.open': '打开',
|
|
123
|
+
'mcp.keys.back': '返回',
|
|
124
|
+
'mcp.keys.reload': '全部重载',
|
|
125
|
+
'mcp.keys.close': '关闭',
|
|
126
|
+
// ── questions from the agent
|
|
127
|
+
'questions.title': '问题',
|
|
128
|
+
'questions.custom': '输入自己的答案…',
|
|
129
|
+
'questions.skip': '跳过',
|
|
130
|
+
'questions.skipped': '(已跳过)',
|
|
131
|
+
'questions.yourAnswer': '你的答案:',
|
|
132
|
+
'questions.empty': '答案不能为空',
|
|
133
|
+
// ── confirmations
|
|
134
|
+
'confirm.yes': '是',
|
|
135
|
+
'confirm.no': '否',
|
|
136
|
+
'confirm.exitTitle': '退出',
|
|
137
|
+
'confirm.exit': '退出 devx?',
|
|
138
|
+
'confirm.goodbye': '再见',
|
|
139
|
+
'confirm.deleteAll': '删除全部 {n} 个已保存的会话?',
|
|
140
|
+
'confirm.update': '现在将 devx 更新到 v{version}?',
|
|
141
|
+
'confirm.commit': '用此信息提交更改?',
|
|
142
|
+
'trust.title': '工作区信任',
|
|
143
|
+
'trust.question': '首次进入此文件夹。信任它并允许智能体在这里工作吗?',
|
|
144
|
+
'trust.yes': '是,信任此文件夹',
|
|
145
|
+
'trust.no': '否,退出',
|
|
146
|
+
'trust.wantsYolo': '项目配置要求自动批准 (YOLO)',
|
|
147
|
+
'trust.wantsAllowlist': '项目配置允许这些命令:{list}',
|
|
148
|
+
'trust.exiting': '未信任该文件夹,devx 将退出',
|
|
149
|
+
'trust.mcpQuestion': '此项目想启动 MCP 服务器(本地程序,见上方列表)。允许吗?',
|
|
150
|
+
// ── view switches
|
|
151
|
+
'view.expandThought': 'ctrl+p 展开',
|
|
152
|
+
'view.expandOutput': 'ctrl+o 展开',
|
|
153
|
+
'view.moreLines': '… 还有 {n} 行',
|
|
154
|
+
// ── plan approval
|
|
155
|
+
'plan.cardTitle': '计划',
|
|
156
|
+
'plan.files': '文件',
|
|
157
|
+
'plan.ready': '计划已就绪。开始执行吗?',
|
|
158
|
+
'plan.go': 'Go:切换到 AGENT 模式并开始',
|
|
159
|
+
'plan.edit': '修改计划(留在 PLAN 模式)',
|
|
160
|
+
'plan.editHint': '说明要改什么:计划保持在 PLAN 模式并会重做。',
|
|
161
|
+
'plan.started': 'AGENT 模式:按已批准的计划执行',
|
|
162
|
+
// ── undo
|
|
163
|
+
'undo.question': '将 {n} 个文件恢复到上一轮之前的状态?',
|
|
164
|
+
'undo.note': '包括 shell 命令所做的更改。被 git 忽略的文件不受影响。',
|
|
165
|
+
'undo.done': '已恢复 {n} 个文件',
|
|
166
|
+
// ── permissions
|
|
167
|
+
'permissions.title': '无需询问即可执行',
|
|
168
|
+
'permissions.none': '目前没有无需询问的项目:在批准卡片上选择 Session 或 Always 即可添加。',
|
|
169
|
+
'permissions.hint': '/permissions clear 会清除全部(包括仅本次会话的)。',
|
|
170
|
+
'permissions.cleared': '已清除所有无需询问的许可',
|
|
171
|
+
// ── cost limit
|
|
172
|
+
'settings.row.costLimit': '费用上限',
|
|
173
|
+
'settings.short.costLimit': '费用上限',
|
|
174
|
+
'settings.hint.costLimit': '单次会话最多可花费的金额。达到 80% 时 devx 会提醒,达到上限时会停止任务,并在继续花费前询问。',
|
|
175
|
+
'cost.title': '费用上限',
|
|
176
|
+
'cost.warn': '已花费 {spent}:本次会话 {limit} 上限的 80%',
|
|
177
|
+
'cost.stopped': '已达到 {limit} 的费用上限:任务已停止',
|
|
178
|
+
'cost.reached': '本次会话已花费 {spent},上限为 {limit}。继续花费吗?',
|
|
179
|
+
'cost.raise': '继续将把本次会话的上限提高到 {limit}。',
|
|
180
|
+
'cost.continue': '是,继续',
|
|
181
|
+
'cost.cancel': '否,暂不',
|
|
182
|
+
'cost.cancelled': '未发送任何内容。你的消息已回到输入行。',
|
|
183
|
+
// ── /copy and /rename
|
|
184
|
+
'copy.none': '还没有可复制的回答。',
|
|
185
|
+
'copy.done': '已复制最后一条回答({n} 个字符)',
|
|
186
|
+
'copy.failed': '无法访问剪贴板(没有 wl-copy、xclip、xsel,且回答对终端来说太长)。',
|
|
187
|
+
'rename.done': '聊天已重命名:{name}',
|
|
188
|
+
'rename.usage': '用法:/rename <名称>。不带名称时,有了第一条回答后由 AI 重新命名。',
|
|
189
|
+
'rename.failed': 'AI 没有想出名称。请重试或自己指定:/rename <名称>。',
|
|
190
|
+
// ── memory
|
|
191
|
+
'memory.capped': '记忆太长:最旧的 {n} 条不会发送给模型(只能放下最新的)。请从 .devx/memory.md 中删除旧的。',
|
|
192
|
+
'mcp.removeUsage': '用法:/mcp remove <名称>',
|
|
193
|
+
'mcp.removeNotFound': '"{name}" 不在 ~/.devx/mcp.json 中。其他文件(项目、~/.devxrc.json)里的服务器需要手动从该文件删除。',
|
|
194
|
+
'mcp.removed': '已删除服务器 "{name}"',
|
|
195
|
+
// ── /rewind
|
|
196
|
+
'rewind.title': '回退',
|
|
197
|
+
'rewind.count': '{n} 条消息',
|
|
198
|
+
'rewind.current': '(当前:留在这里)',
|
|
199
|
+
'rewind.none': '还没有可以回到的你的消息。',
|
|
200
|
+
'rewind.later': '之后 {n} 条',
|
|
201
|
+
'rewind.ago.now': '刚刚',
|
|
202
|
+
'rewind.ago.min': '{n} 分钟前',
|
|
203
|
+
'rewind.ago.hour': '{n} 小时前',
|
|
204
|
+
'rewind.ago.day': '{n} 天前',
|
|
205
|
+
'rewind.filesQuestion': '也把文件({n} 个)恢复到当时的样子吗?',
|
|
206
|
+
'rewind.filesYes': '是,文件也恢复',
|
|
207
|
+
'rewind.filesNo': '否,只回退对话',
|
|
208
|
+
'rewind.done': '已回到你的消息。之后的内容已被清除,模型也看不到了。修改消息后发送。',
|
|
209
|
+
'rewind.doneImages': '已回到你的消息(其中的图片未保留,请重新添加)。之后的内容已被清除,模型也看不到了。',
|
|
210
|
+
// ── first run and the prompts around it
|
|
211
|
+
'welcome.tagline': '终端里的 AI 编程助手',
|
|
212
|
+
'welcome.intro': '我们来完成设置,大约只需一分钟。',
|
|
213
|
+
'welcome.step.language': '语言',
|
|
214
|
+
'welcome.step.provider': '服务商',
|
|
215
|
+
'welcome.step.key': 'API 密钥',
|
|
216
|
+
'welcome.step.model': '模型',
|
|
217
|
+
'welcome.keyNote': '你的密钥只保存在这台电脑上,位于 ~/.devxrc.json。',
|
|
218
|
+
'onboard.language.title': '语言',
|
|
219
|
+
'onboard.language.note': '之后可以在 /settings 中更改。',
|
|
220
|
+
'onboard.provider.title': '服务商',
|
|
221
|
+
'onboard.provider.placeholder': '输入以查找服务商',
|
|
222
|
+
'onboard.provider.note': '提供模型的服务。没有找到?请选择自定义地址。',
|
|
223
|
+
'onboard.custom': '自定义地址',
|
|
224
|
+
'onboard.customDetail': '任何兼容 OpenAI 的地址',
|
|
225
|
+
'onboard.baseUrl.title': '地址',
|
|
226
|
+
'onboard.baseUrl.label': 'API 的基础 URL',
|
|
227
|
+
'onboard.baseUrl.placeholder': 'https://api.example.com/v1',
|
|
228
|
+
'onboard.key.title': 'api 密钥',
|
|
229
|
+
'onboard.key.label': '粘贴 {provider} 的 API 密钥',
|
|
230
|
+
'onboard.key.note': '输入的内容不会显示。按 Esc 返回。',
|
|
231
|
+
'onboard.key.useSaved': '使用已保存的密钥',
|
|
232
|
+
'onboard.key.enterNew': '输入新密钥',
|
|
233
|
+
'onboard.key.savedTitle': '已保存 {provider} 的密钥',
|
|
234
|
+
'onboard.model.title': '模型',
|
|
235
|
+
'onboard.model.placeholder': '输入以查找模型',
|
|
236
|
+
'onboard.model.custom': '输入模型名称',
|
|
237
|
+
'onboard.model.customDetail': '任意模型 ID',
|
|
238
|
+
'onboard.model.label': '模型名称(例如 gpt-4o)',
|
|
239
|
+
'onboard.model.fetching': '正在获取模型列表...',
|
|
240
|
+
'onboard.model.manual': '无法获取模型列表,请输入模型名称。',
|
|
241
|
+
'onboard.model.context': '{n} 上下文',
|
|
242
|
+
'onboard.model.anthropic': 'Anthropic 格式',
|
|
243
|
+
'onboard.back': '返回',
|
|
244
|
+
'onboard.back.detail': '保持当前设置',
|
|
245
|
+
'onboard.done': '完成。你的选择已保存到 ~/.devxrc.json。',
|
|
246
|
+
'finder.placeholder': '输入以筛选',
|
|
247
|
+
'finder.empty': '没有匹配项',
|
|
248
|
+
'finder.hint.filter': '筛选',
|
|
249
|
+
'picker.hint.move': '移动',
|
|
250
|
+
'picker.hint.select': '选择',
|
|
251
|
+
'picker.hint.cancel': '取消',
|
|
252
|
+
'picker.hint.confirm': '确认',
|
|
253
|
+
'welcome.step.theme': '主题',
|
|
254
|
+
'onboard.theme.title': '主题',
|
|
255
|
+
'onboard.theme.note': '界面的配色。随时可以用 /theme 更改。',
|
|
256
|
+
'resume.title': '恢复会话',
|
|
257
|
+
'resume.placeholder': '输入以筛选会话',
|
|
258
|
+
'resume.msgs': '{n} 条消息',
|
|
259
|
+
'sessions.deleteTitle': '删除会话',
|
|
260
|
+
'sessions.deleteAll': '删除全部 {n} 个会话',
|
|
261
|
+
'commands.title': '命令',
|
|
262
|
+
'commands.placeholder': '输入以查找命令',
|
|
263
|
+
'compact.done': '上下文已压缩,摘要已保留',
|
|
264
|
+
'compact.summary': '模型写的内容',
|
|
265
|
+
};
|
package/dist/core/loop.js
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
|
+
import { CONTEXT_USAGE_RATIO, toolSchemaTokens } from './context-stats.js';
|
|
2
|
+
import { t } from './i18n.js';
|
|
3
|
+
import { redactSecrets } from './redact.js';
|
|
4
|
+
import { runHooks } from './hooks.js';
|
|
1
5
|
import { isNetworkOrRetryableError, getHumanReadableNetworkError, waitDelay } from './network.js';
|
|
2
|
-
function getActionDescription(toolName, args) {
|
|
6
|
+
export function getActionDescription(toolName, args) {
|
|
3
7
|
const t = toolName.toLowerCase();
|
|
4
8
|
if (t === 'read_file' || t === 'view_file' || t === 'read') {
|
|
5
9
|
return `→ Read ${args.path || args.filePath || args.targetFile || ''}`;
|
|
@@ -31,11 +35,27 @@ function getActionDescription(toolName, args) {
|
|
|
31
35
|
if (t === 'todo_list' || t === 'update_todos') {
|
|
32
36
|
return `→ Update Plan & Tasks`;
|
|
33
37
|
}
|
|
38
|
+
if (t.startsWith('mcp__')) {
|
|
39
|
+
const [, server = 'mcp', ...rest] = toolName.split('__');
|
|
40
|
+
let brief = '';
|
|
41
|
+
try {
|
|
42
|
+
brief = args && Object.keys(args).length ? JSON.stringify(args) : '';
|
|
43
|
+
}
|
|
44
|
+
catch { }
|
|
45
|
+
return `→ MCP: ${server} · ${rest.join('__')}${brief ? ' ' + (brief.length > 120 ? brief.slice(0, 117) + '...' : brief) : ''}`;
|
|
46
|
+
}
|
|
34
47
|
if (t === 'plan_ready') {
|
|
35
48
|
return `→ Finalize Plan`;
|
|
36
49
|
}
|
|
37
50
|
return `→ Executing ${toolName}...`;
|
|
38
51
|
}
|
|
52
|
+
const CONTEXT_LENGTH_ERROR = /context[_ ]length|maximum context|context window|too many tokens|prompt is too long|reduce the length|input is too long|exceeds? the (?:model|maximum|max)/i;
|
|
53
|
+
function isContextLengthError(err) {
|
|
54
|
+
const status = err?.status;
|
|
55
|
+
if (status !== undefined && status !== 400 && status !== 413 && status !== 422)
|
|
56
|
+
return false;
|
|
57
|
+
return CONTEXT_LENGTH_ERROR.test(String(err?.message || ''));
|
|
58
|
+
}
|
|
39
59
|
export class Agent {
|
|
40
60
|
config;
|
|
41
61
|
provider;
|
|
@@ -49,19 +69,31 @@ export class Agent {
|
|
|
49
69
|
this.history = history;
|
|
50
70
|
this.guard = guard;
|
|
51
71
|
}
|
|
52
|
-
async *run(signal) {
|
|
72
|
+
async *run(signal, hooks) {
|
|
53
73
|
let iterations = 0;
|
|
54
|
-
|
|
74
|
+
// A config without maxIterations used to make the loop exit immediately and silently.
|
|
75
|
+
const maxIterations = this.config.maxIterations > 0 ? this.config.maxIterations : 100;
|
|
76
|
+
while (iterations < maxIterations) {
|
|
55
77
|
if (signal?.aborted) {
|
|
56
78
|
return;
|
|
57
79
|
}
|
|
58
|
-
|
|
80
|
+
// Messages the user typed while the previous step ran join the conversation here, between
|
|
81
|
+
// the tool results and the next request, so the model sees them before it decides what to do.
|
|
82
|
+
for (const message of (await hooks?.takePending?.()) ?? []) {
|
|
83
|
+
this.history.addMessage(message);
|
|
84
|
+
yield { type: 'user_message', content: message.content };
|
|
85
|
+
}
|
|
86
|
+
const toolDefinitions = Array.from(this.tools.values()).map((t) => t.definition);
|
|
87
|
+
// Tool schemas are sent with every request and count against the context window.
|
|
88
|
+
const schemaTokens = toolSchemaTokens(toolDefinitions);
|
|
89
|
+
const promptBudget = Math.max(1000, Math.floor(this.config.maxContextTokens * CONTEXT_USAGE_RATIO) - schemaTokens);
|
|
90
|
+
const wasCompacted = this.history.pruneToLimit(promptBudget);
|
|
59
91
|
if (wasCompacted) {
|
|
60
92
|
yield { type: 'system', message: 'Context compacted to fit model context window.' };
|
|
61
93
|
}
|
|
62
94
|
const request = {
|
|
63
95
|
messages: this.history.getMessages(),
|
|
64
|
-
tools:
|
|
96
|
+
tools: toolDefinitions,
|
|
65
97
|
signal,
|
|
66
98
|
};
|
|
67
99
|
if (request.tools.length === 0) {
|
|
@@ -69,6 +101,7 @@ export class Agent {
|
|
|
69
101
|
}
|
|
70
102
|
let response;
|
|
71
103
|
let attempt = 0;
|
|
104
|
+
let contextRetries = 0;
|
|
72
105
|
const maxRetries = 4;
|
|
73
106
|
while (true) {
|
|
74
107
|
if (signal?.aborted)
|
|
@@ -111,6 +144,16 @@ export class Agent {
|
|
|
111
144
|
if (signal?.aborted || err.name === 'AbortError' || err.message?.includes('aborted')) {
|
|
112
145
|
return;
|
|
113
146
|
}
|
|
147
|
+
// The provider says the prompt is too long (our estimate was too optimistic):
|
|
148
|
+
// shrink the history harder and retry instead of failing the whole session.
|
|
149
|
+
if (contextRetries < 2 && isContextLengthError(err)) {
|
|
150
|
+
contextRetries++;
|
|
151
|
+
const target = Math.floor(this.history.getTotalTokens() * (contextRetries === 1 ? 0.6 : 0.35));
|
|
152
|
+
this.history.pruneToLimit(Math.max(500, target));
|
|
153
|
+
request.messages = this.history.getMessages();
|
|
154
|
+
yield { type: 'system', message: 'Provider reported the context is too long. Compacted history, retrying.' };
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
114
157
|
const isNetwork = isNetworkOrRetryableError(err);
|
|
115
158
|
if (isNetwork && attempt < maxRetries) {
|
|
116
159
|
attempt++;
|
|
@@ -129,7 +172,7 @@ export class Agent {
|
|
|
129
172
|
continue;
|
|
130
173
|
}
|
|
131
174
|
const errMsg = isNetwork
|
|
132
|
-
?
|
|
175
|
+
? t('net.failedAfter', { n: attempt, reason: err.message })
|
|
133
176
|
: `API Error: ${err.message}`;
|
|
134
177
|
yield { type: 'error', message: errMsg, isFatal: true };
|
|
135
178
|
return;
|
|
@@ -138,6 +181,9 @@ export class Agent {
|
|
|
138
181
|
if (signal?.aborted) {
|
|
139
182
|
return;
|
|
140
183
|
}
|
|
184
|
+
for (const notice of this.provider.drainNotices?.() ?? []) {
|
|
185
|
+
yield { type: 'system', message: notice };
|
|
186
|
+
}
|
|
141
187
|
if (!response) {
|
|
142
188
|
response = { content: '' };
|
|
143
189
|
}
|
|
@@ -148,6 +194,9 @@ export class Agent {
|
|
|
148
194
|
if (response.toolCalls && response.toolCalls.length > 0) {
|
|
149
195
|
assistantMsg.tool_calls = response.toolCalls;
|
|
150
196
|
}
|
|
197
|
+
if (response.thinkingBlocks && response.thinkingBlocks.length > 0) {
|
|
198
|
+
assistantMsg.thinking_blocks = response.thinkingBlocks;
|
|
199
|
+
}
|
|
151
200
|
this.history.addMessage(assistantMsg);
|
|
152
201
|
if (response.usage) {
|
|
153
202
|
yield { type: 'usage', usage: response.usage };
|
|
@@ -157,64 +206,91 @@ export class Agent {
|
|
|
157
206
|
yield { type: 'text', content: assistantMsg.content };
|
|
158
207
|
}
|
|
159
208
|
if (!response.toolCalls || response.toolCalls.length === 0) {
|
|
209
|
+
// The user wrote something while the model was finishing: answer it instead of stopping.
|
|
210
|
+
if (hooks?.hasPending?.()) {
|
|
211
|
+
iterations++;
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
160
214
|
break;
|
|
161
215
|
}
|
|
162
216
|
const executedToolCallIds = new Set();
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
}
|
|
217
|
+
try {
|
|
218
|
+
for (const call of response.toolCalls) {
|
|
219
|
+
if (signal?.aborted)
|
|
220
|
+
return;
|
|
221
|
+
const actionDesc = getActionDescription(call.name, call.arguments);
|
|
222
|
+
yield { type: 'tool_start', id: call.id, name: call.name, argsRaw: JSON.stringify(call.arguments), actionDesc };
|
|
223
|
+
if (signal?.aborted)
|
|
224
|
+
return;
|
|
225
|
+
let result = '';
|
|
226
|
+
const tool = this.tools.get(call.name);
|
|
227
|
+
if (!tool) {
|
|
228
|
+
result = `Error: Tool '${call.name}' not found.`;
|
|
175
229
|
}
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
tool.validateArgs(call.arguments);
|
|
188
|
-
if (this.guard.check(tool.name, call.arguments)) {
|
|
189
|
-
const allowed = await this.guard.askUser(tool.name, call.arguments);
|
|
190
|
-
if (allowed) {
|
|
191
|
-
result = await tool.execute(call.arguments, this.config);
|
|
230
|
+
else if (call.argsError) {
|
|
231
|
+
// Never run a tool with arguments we could not parse ({} could mean something else).
|
|
232
|
+
result = `Error: ${call.argsError}`;
|
|
233
|
+
}
|
|
234
|
+
else {
|
|
235
|
+
try {
|
|
236
|
+
tool.validateArgs(call.arguments);
|
|
237
|
+
// A hook of the user may say no before the user is even asked.
|
|
238
|
+
const pre = await runHooks(this.config.hooks, 'PreToolUse', tool.name, call.arguments);
|
|
239
|
+
if (pre.blocked) {
|
|
240
|
+
result = `Blocked by a hook: ${pre.blocked}`;
|
|
192
241
|
}
|
|
193
242
|
else {
|
|
194
|
-
|
|
243
|
+
const allowed = !this.guard.check(tool.name, call.arguments)
|
|
244
|
+
|| await this.guard.askUser(tool.name, call.arguments);
|
|
245
|
+
// Cancellation may arrive while waiting for confirmation.
|
|
246
|
+
if (signal?.aborted)
|
|
247
|
+
return;
|
|
248
|
+
result = allowed
|
|
249
|
+
? await tool.execute(call.arguments, this.config, { signal })
|
|
250
|
+
: 'Action denied by user.';
|
|
251
|
+
if (allowed) {
|
|
252
|
+
const post = await runHooks(this.config.hooks, 'PostToolUse', tool.name, call.arguments, result);
|
|
253
|
+
if (post.notes.length > 0)
|
|
254
|
+
result += `\n[hook] ${post.notes.join('\n[hook] ')}`;
|
|
255
|
+
}
|
|
195
256
|
}
|
|
196
257
|
}
|
|
197
|
-
|
|
198
|
-
result =
|
|
258
|
+
catch (err) {
|
|
259
|
+
result = signal?.aborted
|
|
260
|
+
? '[Operation cancelled by user]'
|
|
261
|
+
: `Error executing tool: ${err.message}`;
|
|
199
262
|
}
|
|
200
263
|
}
|
|
201
|
-
|
|
202
|
-
|
|
264
|
+
// Keys and tokens in the output must not reach the model, the chat or the saved session.
|
|
265
|
+
result = redactSecrets(result);
|
|
266
|
+
// Save the result before yielding: the UI can stop consuming at any event.
|
|
267
|
+
this.history.addMessage({
|
|
268
|
+
role: 'tool',
|
|
269
|
+
content: result,
|
|
270
|
+
tool_call_id: call.id,
|
|
271
|
+
name: call.name,
|
|
272
|
+
});
|
|
273
|
+
executedToolCallIds.add(call.id);
|
|
274
|
+
yield { type: 'tool_end', id: call.id, name: call.name, result };
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
finally {
|
|
278
|
+
// Keep every tool call paired with a response, even if the consumer exits early.
|
|
279
|
+
for (const call of response.toolCalls) {
|
|
280
|
+
if (!executedToolCallIds.has(call.id)) {
|
|
281
|
+
this.history.addMessage({
|
|
282
|
+
role: 'tool',
|
|
283
|
+
content: '[Operation cancelled by user]',
|
|
284
|
+
tool_call_id: call.id,
|
|
285
|
+
name: call.name,
|
|
286
|
+
});
|
|
203
287
|
}
|
|
204
288
|
}
|
|
205
|
-
yield { type: 'tool_end', id: call.id, name: call.name, result };
|
|
206
|
-
this.history.addMessage({
|
|
207
|
-
role: 'tool',
|
|
208
|
-
content: result,
|
|
209
|
-
tool_call_id: call.id,
|
|
210
|
-
name: call.name,
|
|
211
|
-
});
|
|
212
|
-
executedToolCallIds.add(call.id);
|
|
213
289
|
}
|
|
214
290
|
iterations++;
|
|
215
291
|
}
|
|
216
|
-
if (iterations >=
|
|
217
|
-
yield { type: 'system', message: `Reached max iterations (${
|
|
292
|
+
if (iterations >= maxIterations) {
|
|
293
|
+
yield { type: 'system', message: `Reached max iterations (${maxIterations}).` };
|
|
218
294
|
}
|
|
219
295
|
}
|
|
220
296
|
}
|
package/dist/core/memory.js
CHANGED
|
@@ -1,27 +1,54 @@
|
|
|
1
1
|
import fs from 'fs/promises';
|
|
2
2
|
import fsSync from 'fs';
|
|
3
3
|
import path from 'path';
|
|
4
|
-
|
|
5
|
-
const
|
|
4
|
+
// Looked up when used, not when the module loads: the memory belongs to the folder devx works in.
|
|
5
|
+
const memoryDir = () => path.join(process.cwd(), '.devx');
|
|
6
|
+
const memoryFile = () => path.join(memoryDir(), 'memory.md');
|
|
7
|
+
export const MEMORY_PROMPT_MAX_CHARS = 4000;
|
|
8
|
+
/**
|
|
9
|
+
* The memory for the system prompt: all of it when it is small, else the newest facts that fit
|
|
10
|
+
* (facts are appended, so the end is the newest) and a line that says how many were left out.
|
|
11
|
+
*/
|
|
12
|
+
export function memoryForPrompt(text, max = MEMORY_PROMPT_MAX_CHARS) {
|
|
13
|
+
if (text.length <= max)
|
|
14
|
+
return { text, omitted: 0 };
|
|
15
|
+
const lines = text.split('\n');
|
|
16
|
+
const firstFact = lines.findIndex((l) => l.startsWith('•'));
|
|
17
|
+
const header = firstFact === -1 ? [] : lines.slice(0, firstFact);
|
|
18
|
+
const facts = (firstFact === -1 ? lines : lines.slice(firstFact)).filter((l) => l.trim().length > 0);
|
|
19
|
+
const note = (n) => `(${n} older facts left out: /memory shows all of them)`;
|
|
20
|
+
const kept = [];
|
|
21
|
+
let used = header.join('\n').length + note(facts.length).length + 2;
|
|
22
|
+
for (let i = facts.length - 1; i >= 0; i--) {
|
|
23
|
+
if (used + facts[i].length + 1 > max && kept.length > 0)
|
|
24
|
+
break;
|
|
25
|
+
kept.unshift(facts[i]);
|
|
26
|
+
used += facts[i].length + 1;
|
|
27
|
+
}
|
|
28
|
+
const omitted = facts.length - kept.length;
|
|
29
|
+
return { text: [...header, ...(omitted > 0 ? [note(omitted)] : []), ...kept].join('\n'), omitted };
|
|
30
|
+
}
|
|
6
31
|
export class MemoryManager {
|
|
32
|
+
/** The `Project memory` switch of the settings: off means the bank is not put into the prompt. */
|
|
33
|
+
static enabled = true;
|
|
7
34
|
static async loadMemory() {
|
|
8
|
-
if (!fsSync.existsSync(
|
|
35
|
+
if (!fsSync.existsSync(memoryFile())) {
|
|
9
36
|
return '';
|
|
10
37
|
}
|
|
11
38
|
try {
|
|
12
|
-
return await fs.readFile(
|
|
39
|
+
return await fs.readFile(memoryFile(), 'utf8');
|
|
13
40
|
}
|
|
14
41
|
catch {
|
|
15
42
|
return '';
|
|
16
43
|
}
|
|
17
44
|
}
|
|
18
45
|
static async addFact(fact) {
|
|
19
|
-
if (!fsSync.existsSync(
|
|
20
|
-
await fs.mkdir(
|
|
46
|
+
if (!fsSync.existsSync(memoryDir())) {
|
|
47
|
+
await fs.mkdir(memoryDir(), { recursive: true });
|
|
21
48
|
}
|
|
22
49
|
let current = '';
|
|
23
|
-
if (fsSync.existsSync(
|
|
24
|
-
current = await fs.readFile(
|
|
50
|
+
if (fsSync.existsSync(memoryFile())) {
|
|
51
|
+
current = await fs.readFile(memoryFile(), 'utf8');
|
|
25
52
|
}
|
|
26
53
|
const trimmedFact = fact.trim();
|
|
27
54
|
if (!trimmedFact)
|
|
@@ -32,16 +59,16 @@ export class MemoryManager {
|
|
|
32
59
|
current = `# Project Memory Bank\n\n## Established Rules & Architecture\n`;
|
|
33
60
|
}
|
|
34
61
|
current += entry;
|
|
35
|
-
await fs.writeFile(
|
|
62
|
+
await fs.writeFile(memoryFile(), current, 'utf8');
|
|
36
63
|
}
|
|
37
64
|
}
|
|
38
65
|
static async clearMemory() {
|
|
39
|
-
if (fsSync.existsSync(
|
|
40
|
-
await fs.unlink(
|
|
66
|
+
if (fsSync.existsSync(memoryFile())) {
|
|
67
|
+
await fs.unlink(memoryFile());
|
|
41
68
|
}
|
|
42
69
|
}
|
|
43
70
|
static isMemoryPresent() {
|
|
44
|
-
return fsSync.existsSync(
|
|
71
|
+
return fsSync.existsSync(memoryFile());
|
|
45
72
|
}
|
|
46
73
|
}
|
|
47
74
|
export const saveMemoryTool = {
|