@antaif3ng/til-work 0.1.2 → 0.3.0
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 +265 -297
- package/dist/core/config.d.ts +29 -11
- package/dist/core/config.d.ts.map +1 -1
- package/dist/core/config.js +65 -101
- package/dist/core/config.js.map +1 -1
- package/dist/core/llm.d.ts.map +1 -1
- package/dist/core/llm.js +14 -0
- package/dist/core/llm.js.map +1 -1
- package/dist/core/pricing.d.ts.map +1 -1
- package/dist/core/pricing.js +0 -2
- package/dist/core/pricing.js.map +1 -1
- package/dist/core/session.d.ts +3 -2
- package/dist/core/session.d.ts.map +1 -1
- package/dist/core/session.js +4 -3
- package/dist/core/session.js.map +1 -1
- package/dist/core/skills.d.ts +2 -1
- package/dist/core/skills.d.ts.map +1 -1
- package/dist/core/skills.js +9 -0
- package/dist/core/skills.js.map +1 -1
- package/dist/core/system-prompt.d.ts.map +1 -1
- package/dist/core/system-prompt.js +6 -2
- package/dist/core/system-prompt.js.map +1 -1
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +66 -124
- package/dist/main.js.map +1 -1
- package/dist/modes/interactive.d.ts.map +1 -1
- package/dist/modes/interactive.js +514 -273
- package/dist/modes/interactive.js.map +1 -1
- package/dist/tools/browser.d.ts +10 -0
- package/dist/tools/browser.d.ts.map +1 -0
- package/dist/tools/browser.js +231 -0
- package/dist/tools/browser.js.map +1 -0
- package/dist/tools/computer.d.ts +3 -0
- package/dist/tools/computer.d.ts.map +1 -0
- package/dist/tools/computer.js +251 -0
- package/dist/tools/computer.js.map +1 -0
- package/dist/tools/index.d.ts +5 -2
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +11 -2
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/read.d.ts.map +1 -1
- package/dist/tools/read.js +29 -4
- package/dist/tools/read.js.map +1 -1
- package/dist/tools/screenshot.d.ts +3 -0
- package/dist/tools/screenshot.d.ts.map +1 -0
- package/dist/tools/screenshot.js +113 -0
- package/dist/tools/screenshot.js.map +1 -0
- package/dist/utils/file-processor.d.ts +2 -2
- package/dist/utils/file-processor.d.ts.map +1 -1
- package/dist/utils/file-processor.js +7 -10
- package/dist/utils/file-processor.js.map +1 -1
- package/package.json +3 -2
- package/skills/find-skills/SKILL.md +66 -0
- package/skills/playwright-mcp/SKILL.md +90 -0
- package/skills/self-improving-agent/SKILL.md +88 -0
- package/skills/skill-creator/SKILL.md +93 -0
- package/skills/summarize/SKILL.md +55 -0
|
@@ -5,7 +5,7 @@ import * as readline from "node:readline";
|
|
|
5
5
|
import { readFileSync } from "node:fs";
|
|
6
6
|
import chalk from "chalk";
|
|
7
7
|
import { TilSession } from "../core/session.js";
|
|
8
|
-
import {
|
|
8
|
+
import { getConfigPath, loadConfig, saveConfig } from "../core/config.js";
|
|
9
9
|
import { VERSION } from "../version.js";
|
|
10
10
|
import { createToolCallRequestHandler } from "../core/tool-permissions.js";
|
|
11
11
|
import { formatTokenCount } from "../core/pricing.js";
|
|
@@ -15,8 +15,7 @@ import { extractAtReferences } from "../utils/file-processor.js";
|
|
|
15
15
|
const COMMANDS = {
|
|
16
16
|
"/help": "显示帮助信息",
|
|
17
17
|
"/clear": "清空对话历史",
|
|
18
|
-
"/model": "
|
|
19
|
-
"/models": "查看可用模型列表",
|
|
18
|
+
"/model": "查看/切换/管理模型",
|
|
20
19
|
"/skills": "查看已加载的技能",
|
|
21
20
|
"/mcp": "查看已连接的 MCP 服务和工具",
|
|
22
21
|
"/extensions": "查看已加载的扩展和 MCP 服务",
|
|
@@ -26,6 +25,15 @@ const COMMANDS = {
|
|
|
26
25
|
"/config": "查看当前配置",
|
|
27
26
|
"/exit": "退出 TIL",
|
|
28
27
|
};
|
|
28
|
+
const TIPS = [
|
|
29
|
+
"输入 / 查看所有可用命令,方向键选择",
|
|
30
|
+
"使用 /model 切换模型",
|
|
31
|
+
"按 Ctrl+C 或 Esc 中止正在执行的请求",
|
|
32
|
+
"使用 /memory 查看记忆内容",
|
|
33
|
+
"使用 /sessions 查看历史会话",
|
|
34
|
+
"输入 @ 引用文件内容(支持搜索和方向键选择)",
|
|
35
|
+
"换行: 行尾输入 \\ 再回车,或 Ctrl+J / Alt+Enter",
|
|
36
|
+
];
|
|
29
37
|
// ── UI helpers ──
|
|
30
38
|
function getBorderWidth() {
|
|
31
39
|
return Math.min(process.stdout.columns || 80, 80);
|
|
@@ -33,8 +41,16 @@ function getBorderWidth() {
|
|
|
33
41
|
function printBorder(ch = "─") {
|
|
34
42
|
console.log(chalk.dim(ch.repeat(getBorderWidth())));
|
|
35
43
|
}
|
|
44
|
+
function shortenDir(cwd) {
|
|
45
|
+
return cwd.replace(process.env.HOME || "", "~");
|
|
46
|
+
}
|
|
36
47
|
// ── Interactive Popup Menu ──
|
|
37
|
-
const PROMPT_WIDTH = 3; // visible width of " › "
|
|
48
|
+
const PROMPT_WIDTH = 3; // visible width of " › " or " … "
|
|
49
|
+
function resetMenu(menu) {
|
|
50
|
+
menu.visible = false;
|
|
51
|
+
menu.items = [];
|
|
52
|
+
menu.selectedIndex = 0;
|
|
53
|
+
}
|
|
38
54
|
function getCommandEntries(session) {
|
|
39
55
|
const allCmds = Object.keys(COMMANDS);
|
|
40
56
|
const skillCmds = session.getSkillNames().map((n) => `/skill:${n}`);
|
|
@@ -52,6 +68,33 @@ function filterEntries(entries, filter) {
|
|
|
52
68
|
return entries;
|
|
53
69
|
return entries.filter((e) => e.label.startsWith(filter));
|
|
54
70
|
}
|
|
71
|
+
function getModelMenuEntries(filter, session) {
|
|
72
|
+
const entries = [];
|
|
73
|
+
const currentId = session.model.id;
|
|
74
|
+
entries.push({
|
|
75
|
+
label: currentId,
|
|
76
|
+
detail: "当前",
|
|
77
|
+
value: `/model ${currentId}`,
|
|
78
|
+
});
|
|
79
|
+
const cfg = session.config;
|
|
80
|
+
if (cfg.models) {
|
|
81
|
+
for (const [id, m] of Object.entries(cfg.models)) {
|
|
82
|
+
if (id === currentId)
|
|
83
|
+
continue;
|
|
84
|
+
const parts = [];
|
|
85
|
+
if (m.baseUrl)
|
|
86
|
+
parts.push(m.baseUrl.replace(/https?:\/\//, "").slice(0, 30));
|
|
87
|
+
entries.push({
|
|
88
|
+
label: id,
|
|
89
|
+
detail: parts.join(" ") || "",
|
|
90
|
+
value: `/model ${id}`,
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
if (!filter)
|
|
95
|
+
return entries;
|
|
96
|
+
return entries.filter((e) => e.label.includes(filter));
|
|
97
|
+
}
|
|
55
98
|
function getFileMenuEntries(prefix, cwd) {
|
|
56
99
|
const suggestions = getFileSuggestions(prefix, cwd);
|
|
57
100
|
return suggestions.slice(0, 10).map((s) => ({
|
|
@@ -60,44 +103,63 @@ function getFileMenuEntries(prefix, cwd) {
|
|
|
60
103
|
value: s.value,
|
|
61
104
|
}));
|
|
62
105
|
}
|
|
106
|
+
const PLACEHOLDER = "Ctrl+J 或 \\+Enter 换行";
|
|
63
107
|
/**
|
|
64
|
-
* Render the
|
|
65
|
-
*
|
|
66
|
-
*
|
|
108
|
+
* Render the area below the input cursor: popup menu (if any) +
|
|
109
|
+
* underline + status line. Only called at prompt start and on menu
|
|
110
|
+
* state changes — NEVER on every keystroke — so CJK IME is not disturbed.
|
|
67
111
|
*/
|
|
68
|
-
function
|
|
69
|
-
if (!
|
|
112
|
+
function renderBelowArea(menu, below, session, cursorCol) {
|
|
113
|
+
if (!below.active)
|
|
70
114
|
return;
|
|
71
|
-
const
|
|
72
|
-
const
|
|
115
|
+
const menuItemCount = menu.visible && menu.items.length > 0 ? menu.items.length : 0;
|
|
116
|
+
const fixedLines = 2; // underline + status
|
|
117
|
+
const contentLines = menuItemCount + fixedLines;
|
|
118
|
+
const totalLinesToProcess = Math.max(contentLines, below.renderedLines);
|
|
119
|
+
// Pre-scroll: ensure enough vertical space below cursor so that
|
|
120
|
+
// writing \n never causes unexpected scrolling after rendering.
|
|
121
|
+
let preScroll = "";
|
|
122
|
+
for (let i = 0; i < totalLinesToProcess + 1; i++)
|
|
123
|
+
preScroll += "\n";
|
|
124
|
+
preScroll += `\x1b[${totalLinesToProcess + 1}A`;
|
|
73
125
|
let output = "\n";
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
const
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
126
|
+
// Menu items
|
|
127
|
+
if (menuItemCount > 0) {
|
|
128
|
+
const padWidth = menu.type === "file" ? 24 : 16;
|
|
129
|
+
for (let i = 0; i < menu.items.length; i++) {
|
|
130
|
+
const { label, detail } = menu.items[i];
|
|
131
|
+
const isSelected = i === menu.selectedIndex;
|
|
132
|
+
if (isSelected) {
|
|
133
|
+
output += ` ${chalk.bgCyan.black(` ${label.padEnd(padWidth)}`)} ${chalk.white(detail)}`;
|
|
134
|
+
}
|
|
135
|
+
else {
|
|
136
|
+
output += ` ${chalk.cyan(` ${label.padEnd(padWidth)}`)} ${chalk.dim(detail)}`;
|
|
137
|
+
}
|
|
138
|
+
output += "\x1b[K\n";
|
|
82
139
|
}
|
|
83
|
-
output += "\x1b[K\n";
|
|
84
140
|
}
|
|
85
|
-
|
|
141
|
+
// Underline
|
|
142
|
+
output += chalk.dim("─".repeat(getBorderWidth())) + "\x1b[K\n";
|
|
143
|
+
// Status line
|
|
144
|
+
const dir = shortenDir(session.cwd);
|
|
145
|
+
output += chalk.dim(` ${dir} · Tip: ${below.tip}`) + "\x1b[K\n";
|
|
146
|
+
// Clear stale lines from previous render
|
|
147
|
+
for (let i = contentLines; i < below.renderedLines; i++) {
|
|
86
148
|
output += "\x1b[K\n";
|
|
87
149
|
}
|
|
88
|
-
|
|
89
|
-
const linesToGoUp = 1 +
|
|
150
|
+
below.renderedLines = contentLines;
|
|
151
|
+
const linesToGoUp = 1 + totalLinesToProcess;
|
|
90
152
|
output += `\x1b[${linesToGoUp}A\r`;
|
|
91
153
|
if (cursorCol > 0) {
|
|
92
154
|
output += `\x1b[${cursorCol}C`;
|
|
93
155
|
}
|
|
94
|
-
process.stdout.write(output);
|
|
156
|
+
process.stdout.write(preScroll + output);
|
|
95
157
|
}
|
|
96
|
-
function
|
|
97
|
-
if (
|
|
158
|
+
function clearBelowArea(below, cursorCol) {
|
|
159
|
+
if (below.renderedLines === 0)
|
|
98
160
|
return;
|
|
99
161
|
let output = "";
|
|
100
|
-
const totalLines =
|
|
162
|
+
const totalLines = below.renderedLines + 1;
|
|
101
163
|
for (let i = 0; i < totalLines; i++) {
|
|
102
164
|
output += "\n\x1b[K";
|
|
103
165
|
}
|
|
@@ -106,10 +168,11 @@ function clearMenu(menu, cursorCol = 0) {
|
|
|
106
168
|
output += `\x1b[${cursorCol}C`;
|
|
107
169
|
}
|
|
108
170
|
process.stdout.write(output);
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
171
|
+
below.renderedLines = 0;
|
|
172
|
+
}
|
|
173
|
+
/** Show grey placeholder text when input is empty, using cursor save/restore. */
|
|
174
|
+
function showPlaceholder() {
|
|
175
|
+
process.stdout.write(`\x1b[s${chalk.dim(PLACEHOLDER)}\x1b[u`);
|
|
113
176
|
}
|
|
114
177
|
// ── Spinner ──
|
|
115
178
|
const SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
@@ -157,9 +220,18 @@ async function printExitMessage(session) {
|
|
|
157
220
|
console.log(chalk.dim(`继续最近会话: ${chalk.cyan("til --continue")}`));
|
|
158
221
|
await session.shutdown();
|
|
159
222
|
}
|
|
223
|
+
// ── Tips helper ──
|
|
224
|
+
function getRandomTip(session) {
|
|
225
|
+
const tips = [...TIPS];
|
|
226
|
+
if (session.skills.length > 0)
|
|
227
|
+
tips.push("使用 /skills 查看已加载的技能");
|
|
228
|
+
const extInfo = session.getExtensionInfo();
|
|
229
|
+
if (extInfo.length > 0)
|
|
230
|
+
tips.push("使用 /extensions 查看已加载的扩展");
|
|
231
|
+
return tips[Math.floor(Math.random() * tips.length)];
|
|
232
|
+
}
|
|
160
233
|
// ── Main interactive mode ──
|
|
161
234
|
export async function runInteractiveMode(options) {
|
|
162
|
-
// Pending permission prompts queue — resolved by the readline prompt
|
|
163
235
|
let pendingPermissionResolve = null;
|
|
164
236
|
const permissionPrompt = (question) => {
|
|
165
237
|
return new Promise((resolve) => {
|
|
@@ -180,6 +252,12 @@ export async function runInteractiveMode(options) {
|
|
|
180
252
|
let isStreaming = false;
|
|
181
253
|
let spinner = null;
|
|
182
254
|
let firstTokenReceived = false;
|
|
255
|
+
let insideThinkBlock = false;
|
|
256
|
+
let thinkTagBuffer = "";
|
|
257
|
+
// Multi-line input state
|
|
258
|
+
const multiLineBuffer = [];
|
|
259
|
+
// Below-area state: menu + underline + status
|
|
260
|
+
const below = { renderedLines: 0, active: false, tip: "" };
|
|
183
261
|
session.subscribe((event) => {
|
|
184
262
|
switch (event.type) {
|
|
185
263
|
case "agent_start":
|
|
@@ -187,12 +265,49 @@ export async function runInteractiveMode(options) {
|
|
|
187
265
|
streamingRawText = "";
|
|
188
266
|
isStreaming = true;
|
|
189
267
|
firstTokenReceived = false;
|
|
268
|
+
insideThinkBlock = false;
|
|
269
|
+
thinkTagBuffer = "";
|
|
190
270
|
spinner = new Spinner("思考中...");
|
|
191
271
|
spinner.start();
|
|
192
272
|
break;
|
|
193
273
|
case "message_update": {
|
|
194
274
|
if (event.delta) {
|
|
195
|
-
|
|
275
|
+
currentOutput += event.delta;
|
|
276
|
+
let text = thinkTagBuffer + event.delta;
|
|
277
|
+
thinkTagBuffer = "";
|
|
278
|
+
let normalText = "";
|
|
279
|
+
let thinkText = "";
|
|
280
|
+
while (text.length > 0) {
|
|
281
|
+
if (insideThinkBlock) {
|
|
282
|
+
const closeIdx = text.indexOf("</think>");
|
|
283
|
+
if (closeIdx === -1) {
|
|
284
|
+
thinkText += text;
|
|
285
|
+
break;
|
|
286
|
+
}
|
|
287
|
+
thinkText += text.slice(0, closeIdx);
|
|
288
|
+
text = text.slice(closeIdx + 8);
|
|
289
|
+
insideThinkBlock = false;
|
|
290
|
+
}
|
|
291
|
+
else {
|
|
292
|
+
const openIdx = text.indexOf("<think>");
|
|
293
|
+
if (openIdx === -1) {
|
|
294
|
+
const partialIdx = text.lastIndexOf("<");
|
|
295
|
+
if (partialIdx !== -1 && partialIdx > text.length - 8 && "<think>".startsWith(text.slice(partialIdx))) {
|
|
296
|
+
thinkTagBuffer = text.slice(partialIdx);
|
|
297
|
+
normalText += text.slice(0, partialIdx);
|
|
298
|
+
}
|
|
299
|
+
else {
|
|
300
|
+
normalText += text;
|
|
301
|
+
}
|
|
302
|
+
break;
|
|
303
|
+
}
|
|
304
|
+
normalText += text.slice(0, openIdx);
|
|
305
|
+
text = text.slice(openIdx + 7);
|
|
306
|
+
insideThinkBlock = true;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
const hasOutput = normalText || thinkText;
|
|
310
|
+
if (hasOutput && !firstTokenReceived) {
|
|
196
311
|
firstTokenReceived = true;
|
|
197
312
|
if (spinner) {
|
|
198
313
|
spinner.stop();
|
|
@@ -200,33 +315,47 @@ export async function runInteractiveMode(options) {
|
|
|
200
315
|
}
|
|
201
316
|
process.stdout.write("\n");
|
|
202
317
|
}
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
318
|
+
if (thinkText) {
|
|
319
|
+
process.stdout.write(chalk.dim.italic(thinkText));
|
|
320
|
+
streamingRawText += thinkText;
|
|
321
|
+
}
|
|
322
|
+
if (normalText) {
|
|
323
|
+
process.stdout.write(chalk.white(normalText));
|
|
324
|
+
streamingRawText += normalText;
|
|
325
|
+
}
|
|
207
326
|
}
|
|
208
327
|
break;
|
|
209
328
|
}
|
|
210
329
|
case "message_end": {
|
|
211
330
|
if (event.message.role === "assistant") {
|
|
212
331
|
const assistantMsg = event.message;
|
|
213
|
-
|
|
332
|
+
let fullText = assistantMsg.content
|
|
214
333
|
.filter((c) => c.type === "text")
|
|
215
334
|
.map((c) => c.text)
|
|
216
335
|
.join("");
|
|
217
|
-
|
|
336
|
+
const thinkBlocks = extractThinkBlocks(fullText);
|
|
337
|
+
const cleanText = stripThinkBlocks(fullText);
|
|
338
|
+
if ((cleanText.trim() || thinkBlocks.length > 0) && streamingRawText.trim()) {
|
|
218
339
|
const termWidth = process.stdout.columns || 80;
|
|
219
340
|
const visualLines = countVisualLines(streamingRawText, termWidth);
|
|
220
341
|
for (let i = 0; i < visualLines; i++) {
|
|
221
342
|
process.stdout.write("\x1b[2K\x1b[1A");
|
|
222
343
|
}
|
|
223
344
|
process.stdout.write("\x1b[2K\r");
|
|
224
|
-
|
|
225
|
-
const
|
|
226
|
-
|
|
345
|
+
if (thinkBlocks.length > 0) {
|
|
346
|
+
const thinkContent = thinkBlocks.join("\n").trim();
|
|
347
|
+
if (thinkContent) {
|
|
348
|
+
process.stdout.write(chalk.dim.italic(thinkContent) + "\n\n");
|
|
349
|
+
}
|
|
227
350
|
}
|
|
228
|
-
|
|
229
|
-
|
|
351
|
+
if (cleanText.trim()) {
|
|
352
|
+
try {
|
|
353
|
+
const rendered = renderMarkdown(cleanText);
|
|
354
|
+
process.stdout.write(rendered + "\n");
|
|
355
|
+
}
|
|
356
|
+
catch {
|
|
357
|
+
process.stdout.write(cleanText + "\n");
|
|
358
|
+
}
|
|
230
359
|
}
|
|
231
360
|
}
|
|
232
361
|
streamingRawText = "";
|
|
@@ -284,7 +413,11 @@ export async function runInteractiveMode(options) {
|
|
|
284
413
|
spinner = null;
|
|
285
414
|
}
|
|
286
415
|
isStreaming = false;
|
|
287
|
-
|
|
416
|
+
const lastMsg = event.messages?.[event.messages.length - 1];
|
|
417
|
+
if (lastMsg && "stopReason" in lastMsg && lastMsg.stopReason === "error") {
|
|
418
|
+
const errText = lastMsg.errorMessage || "未知错误";
|
|
419
|
+
process.stdout.write(chalk.red(`\n ✗ 错误: ${errText}\n`));
|
|
420
|
+
}
|
|
288
421
|
const turnUsage = session.getLastTurnUsageSummary();
|
|
289
422
|
const ctxInfo = session.getContextUsagePercent();
|
|
290
423
|
if (turnUsage) {
|
|
@@ -295,7 +428,7 @@ export async function runInteractiveMode(options) {
|
|
|
295
428
|
ctxColor = chalk.yellow;
|
|
296
429
|
process.stdout.write(chalk.dim(`\n ${turnUsage} ctx:${ctxColor(ctxInfo.display)}`));
|
|
297
430
|
}
|
|
298
|
-
process.stdout.write("\n");
|
|
431
|
+
process.stdout.write("\n\n");
|
|
299
432
|
break;
|
|
300
433
|
}
|
|
301
434
|
}
|
|
@@ -304,7 +437,6 @@ export async function runInteractiveMode(options) {
|
|
|
304
437
|
visible: false,
|
|
305
438
|
items: [],
|
|
306
439
|
selectedIndex: 0,
|
|
307
|
-
renderedLines: 0,
|
|
308
440
|
type: "command",
|
|
309
441
|
};
|
|
310
442
|
const completer = (line) => {
|
|
@@ -319,7 +451,59 @@ export async function runInteractiveMode(options) {
|
|
|
319
451
|
const getCursorCol = () => PROMPT_WIDTH + (rl.cursor ?? 0);
|
|
320
452
|
const allEntries = getCommandEntries(session);
|
|
321
453
|
const originalTtyWrite = rl._ttyWrite;
|
|
454
|
+
// Helper: update menu visibility based on current line content
|
|
455
|
+
function updateMenuForLine(line, session) {
|
|
456
|
+
if (line.startsWith("/model ")) {
|
|
457
|
+
const modelFilter = line.slice("/model ".length);
|
|
458
|
+
if (!["add ", "rm ", "default "].some((sub) => modelFilter.startsWith(sub))) {
|
|
459
|
+
const entries = getModelMenuEntries(modelFilter, session);
|
|
460
|
+
if (entries.length > 0) {
|
|
461
|
+
menu.visible = true;
|
|
462
|
+
menu.items = entries;
|
|
463
|
+
menu.type = "command";
|
|
464
|
+
menu.selectedIndex = 0;
|
|
465
|
+
}
|
|
466
|
+
else {
|
|
467
|
+
resetMenu(menu);
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
else {
|
|
471
|
+
resetMenu(menu);
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
else if (line.startsWith("/") && !line.includes(" ")) {
|
|
475
|
+
const filtered = filterEntries(allEntries, line);
|
|
476
|
+
if (filtered.length > 0) {
|
|
477
|
+
menu.visible = true;
|
|
478
|
+
menu.items = filtered;
|
|
479
|
+
menu.type = "command";
|
|
480
|
+
menu.selectedIndex = 0;
|
|
481
|
+
}
|
|
482
|
+
else {
|
|
483
|
+
resetMenu(menu);
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
else {
|
|
487
|
+
const atPrefix = extractAtPrefix(line);
|
|
488
|
+
if (atPrefix !== null) {
|
|
489
|
+
const entries = getFileMenuEntries(atPrefix, session.cwd);
|
|
490
|
+
if (entries.length > 0) {
|
|
491
|
+
menu.visible = true;
|
|
492
|
+
menu.items = entries;
|
|
493
|
+
menu.type = "file";
|
|
494
|
+
menu.selectedIndex = 0;
|
|
495
|
+
}
|
|
496
|
+
else {
|
|
497
|
+
resetMenu(menu);
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
else {
|
|
501
|
+
resetMenu(menu);
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
}
|
|
322
505
|
rl._ttyWrite = function (s, key) {
|
|
506
|
+
// ── Permission prompt passthrough ──
|
|
323
507
|
if (pendingPermissionResolve && s) {
|
|
324
508
|
const resolve = pendingPermissionResolve;
|
|
325
509
|
pendingPermissionResolve = null;
|
|
@@ -327,6 +511,7 @@ export async function runInteractiveMode(options) {
|
|
|
327
511
|
resolve(s);
|
|
328
512
|
return;
|
|
329
513
|
}
|
|
514
|
+
// ── Streaming: only Esc to abort ──
|
|
330
515
|
if (isStreaming) {
|
|
331
516
|
if (key && key.name === "escape") {
|
|
332
517
|
if (spinner) {
|
|
@@ -341,28 +526,55 @@ export async function runInteractiveMode(options) {
|
|
|
341
526
|
return originalTtyWrite.call(this, s, key);
|
|
342
527
|
}
|
|
343
528
|
const cursorCol = getCursorCol();
|
|
529
|
+
const keyName = key?.name;
|
|
530
|
+
const isReturnKey = keyName === "return" || keyName === "enter";
|
|
531
|
+
// ── Multi-line newline insertion ──
|
|
532
|
+
// 1. Shift+Enter / Alt+Enter (terminals that report modifier flags)
|
|
533
|
+
// 2. Ctrl+J: raw char is always \n (0x0A), distinct from Enter's \r (0x0D)
|
|
534
|
+
// 3. Raw \x1b\r from Kitty shift+enter mapping or legacy Alt+Enter
|
|
535
|
+
const isNewlineInsert = (key && isReturnKey && (key.shift === true || key.meta === true)) ||
|
|
536
|
+
s === "\n" ||
|
|
537
|
+
s === "\x1b\r";
|
|
538
|
+
if (isNewlineInsert) {
|
|
539
|
+
clearBelowArea(below, cursorCol);
|
|
540
|
+
multiLineBuffer.push(rl.line ?? "");
|
|
541
|
+
process.stdout.write("\n");
|
|
542
|
+
rl.line = "";
|
|
543
|
+
rl.cursor = 0;
|
|
544
|
+
rl._prompt = chalk.cyan(" … ");
|
|
545
|
+
process.stdout.write(chalk.cyan(" … "));
|
|
546
|
+
resetMenu(menu);
|
|
547
|
+
renderBelowArea(menu, below, session, PROMPT_WIDTH);
|
|
548
|
+
return;
|
|
549
|
+
}
|
|
550
|
+
// ── Menu visible: handle navigation & selection ──
|
|
344
551
|
if (menu.visible) {
|
|
345
|
-
if (key &&
|
|
346
|
-
menu.selectedIndex =
|
|
347
|
-
|
|
552
|
+
if (key && keyName === "up") {
|
|
553
|
+
menu.selectedIndex = menu.selectedIndex <= 0
|
|
554
|
+
? menu.items.length - 1
|
|
555
|
+
: menu.selectedIndex - 1;
|
|
556
|
+
renderBelowArea(menu, below, session, cursorCol);
|
|
348
557
|
return;
|
|
349
558
|
}
|
|
350
|
-
if (key &&
|
|
351
|
-
menu.selectedIndex =
|
|
352
|
-
|
|
559
|
+
if (key && keyName === "down") {
|
|
560
|
+
menu.selectedIndex = menu.selectedIndex >= menu.items.length - 1
|
|
561
|
+
? 0
|
|
562
|
+
: menu.selectedIndex + 1;
|
|
563
|
+
renderBelowArea(menu, below, session, cursorCol);
|
|
353
564
|
return;
|
|
354
565
|
}
|
|
355
|
-
if (key && (
|
|
566
|
+
if (key && (isReturnKey || keyName === "tab")) {
|
|
356
567
|
const selected = menu.items[menu.selectedIndex];
|
|
357
568
|
const menuType = menu.type;
|
|
358
569
|
if (selected) {
|
|
359
|
-
|
|
570
|
+
resetMenu(menu);
|
|
360
571
|
if (menuType === "command") {
|
|
361
572
|
rl.line = "";
|
|
362
573
|
rl.cursor = 0;
|
|
363
574
|
process.stdout.write("\r\x1b[K");
|
|
364
575
|
process.stdout.write(chalk.cyan(" › "));
|
|
365
|
-
if (
|
|
576
|
+
if (isReturnKey) {
|
|
577
|
+
clearBelowArea(below, PROMPT_WIDTH);
|
|
366
578
|
rl.line = selected.value;
|
|
367
579
|
rl.cursor = selected.value.length;
|
|
368
580
|
process.stdout.write(selected.value);
|
|
@@ -372,6 +584,7 @@ export async function runInteractiveMode(options) {
|
|
|
372
584
|
rl.line = selected.value;
|
|
373
585
|
rl.cursor = selected.value.length;
|
|
374
586
|
process.stdout.write(selected.value);
|
|
587
|
+
renderBelowArea(menu, below, session, getCursorCol());
|
|
375
588
|
return;
|
|
376
589
|
}
|
|
377
590
|
}
|
|
@@ -388,49 +601,87 @@ export async function runInteractiveMode(options) {
|
|
|
388
601
|
process.stdout.write(chalk.cyan(" › "));
|
|
389
602
|
process.stdout.write(newLine);
|
|
390
603
|
}
|
|
604
|
+
renderBelowArea(menu, below, session, getCursorCol());
|
|
391
605
|
return;
|
|
392
606
|
}
|
|
393
607
|
}
|
|
394
608
|
}
|
|
395
|
-
if (key &&
|
|
396
|
-
|
|
609
|
+
if (key && keyName === "escape") {
|
|
610
|
+
resetMenu(menu);
|
|
611
|
+
renderBelowArea(menu, below, session, cursorCol);
|
|
397
612
|
return;
|
|
398
613
|
}
|
|
399
614
|
}
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
615
|
+
// ── Escape: cancel multi-line or ignore ──
|
|
616
|
+
if (key && keyName === "escape") {
|
|
617
|
+
if (multiLineBuffer.length > 0) {
|
|
618
|
+
clearBelowArea(below, cursorCol);
|
|
619
|
+
for (let i = 0; i < multiLineBuffer.length; i++) {
|
|
620
|
+
process.stdout.write("\x1b[1A\x1b[2K");
|
|
621
|
+
}
|
|
622
|
+
process.stdout.write("\r\x1b[K");
|
|
623
|
+
multiLineBuffer.length = 0;
|
|
624
|
+
rl.line = "";
|
|
625
|
+
rl.cursor = 0;
|
|
626
|
+
rl._prompt = chalk.cyan(" › ");
|
|
627
|
+
process.stdout.write(chalk.cyan(" › "));
|
|
628
|
+
renderBelowArea(menu, below, session, PROMPT_WIDTH);
|
|
629
|
+
showPlaceholder();
|
|
630
|
+
return;
|
|
414
631
|
}
|
|
632
|
+
return;
|
|
415
633
|
}
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
634
|
+
// ── Enter: submit or backslash-newline ──
|
|
635
|
+
if (key && isReturnKey) {
|
|
636
|
+
const currentLine = rl.line ?? "";
|
|
637
|
+
const cursor = rl.cursor ?? 0;
|
|
638
|
+
// Backslash+Enter → newline (pi-mono style, works in ALL terminals)
|
|
639
|
+
if (cursor > 0 && currentLine[cursor - 1] === "\\") {
|
|
640
|
+
clearBelowArea(below, cursorCol);
|
|
641
|
+
const before = currentLine.slice(0, cursor - 1);
|
|
642
|
+
const after = currentLine.slice(cursor);
|
|
643
|
+
multiLineBuffer.push(before + after);
|
|
644
|
+
process.stdout.write("\n");
|
|
645
|
+
rl.line = "";
|
|
646
|
+
rl.cursor = 0;
|
|
647
|
+
rl._prompt = chalk.cyan(" … ");
|
|
648
|
+
process.stdout.write(chalk.cyan(" … "));
|
|
649
|
+
resetMenu(menu);
|
|
650
|
+
renderBelowArea(menu, below, session, PROMPT_WIDTH);
|
|
651
|
+
return;
|
|
430
652
|
}
|
|
431
|
-
|
|
432
|
-
|
|
653
|
+
// Normal submit
|
|
654
|
+
clearBelowArea(below, cursorCol);
|
|
655
|
+
below.active = false;
|
|
656
|
+
if (multiLineBuffer.length > 0) {
|
|
657
|
+
const fullInput = [...multiLineBuffer, currentLine].join("\n");
|
|
658
|
+
multiLineBuffer.length = 0;
|
|
659
|
+
rl.line = fullInput;
|
|
660
|
+
rl.cursor = fullInput.length;
|
|
661
|
+
rl._prompt = chalk.cyan(" › ");
|
|
433
662
|
}
|
|
663
|
+
originalTtyWrite.call(this, s, key);
|
|
664
|
+
return;
|
|
665
|
+
}
|
|
666
|
+
// ── Normal key processing ──
|
|
667
|
+
const prevMenuVisible = menu.visible;
|
|
668
|
+
const prevMenuItemCount = menu.items.length;
|
|
669
|
+
const prevLine = rl.line ?? "";
|
|
670
|
+
originalTtyWrite.call(this, s, key);
|
|
671
|
+
const newLine = rl.line ?? "";
|
|
672
|
+
updateMenuForLine(newLine, session);
|
|
673
|
+
// Clear placeholder remnants when first character is typed
|
|
674
|
+
if (prevLine.length === 0 && newLine.length > 0) {
|
|
675
|
+
process.stdout.write("\x1b[K");
|
|
676
|
+
}
|
|
677
|
+
const menuChanged = menu.visible !== prevMenuVisible ||
|
|
678
|
+
(menu.visible && menu.items.length !== prevMenuItemCount);
|
|
679
|
+
if (menuChanged) {
|
|
680
|
+
renderBelowArea(menu, below, session, getCursorCol());
|
|
681
|
+
}
|
|
682
|
+
// Re-show placeholder when input is empty
|
|
683
|
+
if (newLine.length === 0 && multiLineBuffer.length === 0 && !menu.visible) {
|
|
684
|
+
showPlaceholder();
|
|
434
685
|
}
|
|
435
686
|
};
|
|
436
687
|
if (process.stdin.isTTY) {
|
|
@@ -438,13 +689,22 @@ export async function runInteractiveMode(options) {
|
|
|
438
689
|
}
|
|
439
690
|
const promptUser = () => {
|
|
440
691
|
return new Promise((resolve) => {
|
|
441
|
-
|
|
692
|
+
clearBelowArea(below, 0);
|
|
693
|
+
resetMenu(menu);
|
|
694
|
+
multiLineBuffer.length = 0;
|
|
695
|
+
below.active = true;
|
|
696
|
+
below.tip = getRandomTip(session);
|
|
697
|
+
console.log();
|
|
442
698
|
printBorder();
|
|
443
699
|
rl.question(chalk.cyan(" › "), (answer) => {
|
|
444
|
-
|
|
700
|
+
clearBelowArea(below, getCursorCol());
|
|
701
|
+
below.active = false;
|
|
445
702
|
printBorder();
|
|
446
703
|
resolve(answer);
|
|
447
704
|
});
|
|
705
|
+
// Render placeholder + underline + status below input
|
|
706
|
+
showPlaceholder();
|
|
707
|
+
renderBelowArea(menu, below, session, PROMPT_WIDTH);
|
|
448
708
|
});
|
|
449
709
|
};
|
|
450
710
|
process.on("SIGINT", () => {
|
|
@@ -457,6 +717,20 @@ export async function runInteractiveMode(options) {
|
|
|
457
717
|
process.stdout.write(chalk.yellow("\n⚡ 已中止 (Ctrl+C)\n"));
|
|
458
718
|
printSessionTip(session);
|
|
459
719
|
}
|
|
720
|
+
else if (multiLineBuffer.length > 0) {
|
|
721
|
+
clearBelowArea(below, getCursorCol());
|
|
722
|
+
for (let i = 0; i < multiLineBuffer.length; i++) {
|
|
723
|
+
process.stdout.write("\x1b[1A\x1b[2K");
|
|
724
|
+
}
|
|
725
|
+
process.stdout.write("\r\x1b[K");
|
|
726
|
+
multiLineBuffer.length = 0;
|
|
727
|
+
rl.line = "";
|
|
728
|
+
rl.cursor = 0;
|
|
729
|
+
rl._prompt = chalk.cyan(" › ");
|
|
730
|
+
process.stdout.write(chalk.cyan(" › "));
|
|
731
|
+
showPlaceholder();
|
|
732
|
+
renderBelowArea(menu, below, session, PROMPT_WIDTH);
|
|
733
|
+
}
|
|
460
734
|
else {
|
|
461
735
|
console.log(chalk.dim("\n(输入 /exit 或 Ctrl+D 退出)"));
|
|
462
736
|
}
|
|
@@ -479,9 +753,10 @@ export async function runInteractiveMode(options) {
|
|
|
479
753
|
continue;
|
|
480
754
|
}
|
|
481
755
|
try {
|
|
482
|
-
// Expand @path references into file contents
|
|
483
756
|
const { expandedText, fileContent } = extractAtReferences(trimmed, session.cwd);
|
|
484
|
-
const finalInput = fileContent
|
|
757
|
+
const finalInput = fileContent
|
|
758
|
+
? `${expandedText}\n\n<referenced_files>\n${fileContent}</referenced_files>`
|
|
759
|
+
: expandedText;
|
|
485
760
|
await session.prompt(finalInput);
|
|
486
761
|
}
|
|
487
762
|
catch (err) {
|
|
@@ -505,41 +780,95 @@ async function handleCommand(input, session) {
|
|
|
505
780
|
console.log(chalk.dim("对话已清空。\n"));
|
|
506
781
|
break;
|
|
507
782
|
case "/model": {
|
|
508
|
-
const
|
|
509
|
-
if (!
|
|
510
|
-
|
|
511
|
-
|
|
783
|
+
const sub = args[0];
|
|
784
|
+
if (!sub) {
|
|
785
|
+
const cfg = await loadConfig();
|
|
786
|
+
const configuredModels = cfg.models ?? {};
|
|
787
|
+
const modelIds = Object.keys(configuredModels);
|
|
788
|
+
console.log(chalk.bold(`\n当前模型: ${chalk.cyan(session.model.id)}\n`));
|
|
789
|
+
if (modelIds.length > 0) {
|
|
790
|
+
console.log(chalk.bold("已配置模型:\n"));
|
|
791
|
+
for (const [id, m] of Object.entries(configuredModels)) {
|
|
792
|
+
const marker = id === session.model.id ? chalk.green(" ●") : " ";
|
|
793
|
+
const parts = [];
|
|
794
|
+
if (m.baseUrl)
|
|
795
|
+
parts.push(m.baseUrl);
|
|
796
|
+
if (m.apiKey)
|
|
797
|
+
parts.push(chalk.green("key ✓"));
|
|
798
|
+
console.log(`${marker} ${chalk.white(id)}${parts.length > 0 ? chalk.dim(` ${parts.join(" ")}`) : ""}`);
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
else {
|
|
802
|
+
console.log(chalk.dim(" 暂无配置模型。使用 /model add 添加。"));
|
|
803
|
+
}
|
|
804
|
+
console.log(chalk.dim(`\n /model <id> 切换模型`));
|
|
805
|
+
console.log(chalk.dim(` /model add <id> [baseUrl] [apiKey] 添加模型`));
|
|
806
|
+
console.log(chalk.dim(` /model default <id> 设为默认(持久化)`));
|
|
807
|
+
console.log(chalk.dim(` /model rm <id> 删除模型\n`));
|
|
512
808
|
break;
|
|
513
809
|
}
|
|
514
|
-
|
|
810
|
+
if (sub === "default") {
|
|
811
|
+
const modelId = args[1];
|
|
812
|
+
if (!modelId) {
|
|
813
|
+
console.log(chalk.red("\n 用法: /model default <model-id>\n"));
|
|
814
|
+
break;
|
|
815
|
+
}
|
|
816
|
+
const cfg = await loadConfig();
|
|
817
|
+
cfg.model = modelId;
|
|
818
|
+
await saveConfig(cfg);
|
|
515
819
|
session.switchModel(modelId);
|
|
516
|
-
console.log(chalk.green(
|
|
820
|
+
console.log(chalk.green(`\n 默认模型已设为: ${modelId} (已同步切换)\n`));
|
|
821
|
+
break;
|
|
822
|
+
}
|
|
823
|
+
if (sub === "add") {
|
|
824
|
+
const modelId = args[1];
|
|
825
|
+
if (!modelId) {
|
|
826
|
+
console.log(chalk.red("\n 用法: /model add <model-id> [baseUrl] [apiKey]\n"));
|
|
827
|
+
console.log(chalk.dim(" 示例: /model add deepseek-chat https://api.deepseek.com/v1 sk-xxx\n"));
|
|
828
|
+
break;
|
|
829
|
+
}
|
|
830
|
+
const cfg = await loadConfig();
|
|
831
|
+
if (!cfg.models)
|
|
832
|
+
cfg.models = {};
|
|
833
|
+
const entry = {};
|
|
834
|
+
if (args[2])
|
|
835
|
+
entry.baseUrl = args[2];
|
|
836
|
+
if (args[3])
|
|
837
|
+
entry.apiKey = args[3];
|
|
838
|
+
cfg.models[modelId] = entry;
|
|
839
|
+
await saveConfig(cfg);
|
|
840
|
+
console.log(chalk.green(`\n 已添加: ${modelId}`));
|
|
841
|
+
console.log(chalk.dim(` 切换使用: /model ${modelId}\n`));
|
|
842
|
+
break;
|
|
843
|
+
}
|
|
844
|
+
if (sub === "rm") {
|
|
845
|
+
const modelId = args[1];
|
|
846
|
+
if (!modelId) {
|
|
847
|
+
console.log(chalk.red("\n 用法: /model rm <model-id>\n"));
|
|
848
|
+
break;
|
|
849
|
+
}
|
|
850
|
+
const cfg = await loadConfig();
|
|
851
|
+
if (!cfg.models?.[modelId]) {
|
|
852
|
+
console.log(chalk.red(`\n 自定义模型 "${modelId}" 不存在\n`));
|
|
853
|
+
break;
|
|
854
|
+
}
|
|
855
|
+
delete cfg.models[modelId];
|
|
856
|
+
await saveConfig(cfg);
|
|
857
|
+
console.log(chalk.green(`\n 已删除: ${modelId}\n`));
|
|
858
|
+
break;
|
|
859
|
+
}
|
|
860
|
+
try {
|
|
861
|
+
session.switchModel(sub);
|
|
862
|
+
console.log(chalk.green(`\n 已切换到: ${sub}\n`));
|
|
517
863
|
}
|
|
518
864
|
catch (err) {
|
|
519
|
-
console.log(chalk.red(
|
|
865
|
+
console.log(chalk.red(`\n 切换失败: ${err.message}\n`));
|
|
520
866
|
}
|
|
521
867
|
break;
|
|
522
868
|
}
|
|
523
|
-
case "/models":
|
|
524
|
-
|
|
525
|
-
const builtinModels = getKnownModels();
|
|
526
|
-
console.log(chalk.bold("\n内置模型:\n"));
|
|
527
|
-
for (const [id, m] of Object.entries(builtinModels)) {
|
|
528
|
-
const marker = id === session.model.id ? chalk.green(" ●") : " ";
|
|
529
|
-
const ctx = m.contextWindow ? chalk.dim(` ${formatTokenCount(m.contextWindow)} ctx`) : "";
|
|
530
|
-
console.log(`${marker} ${id.padEnd(35)} ${chalk.dim(m.provider)}${ctx}`);
|
|
531
|
-
}
|
|
532
|
-
if (cfg.customModels && Object.keys(cfg.customModels).length > 0) {
|
|
533
|
-
console.log(chalk.bold("\n自定义模型:\n"));
|
|
534
|
-
for (const [id, m] of Object.entries(cfg.customModels)) {
|
|
535
|
-
const marker = id === session.model.id ? chalk.green(" ●") : " ";
|
|
536
|
-
const ctx = m.contextWindow ? chalk.dim(` ${formatTokenCount(m.contextWindow)} ctx`) : "";
|
|
537
|
-
console.log(`${marker} ${id.padEnd(35)} ${chalk.dim(m.provider)}${ctx}`);
|
|
538
|
-
}
|
|
539
|
-
}
|
|
540
|
-
console.log(chalk.dim(`\n 当前: ${session.model.id} | 切换: /model <id> | 添加: /config add model <id> <provider>\n`));
|
|
869
|
+
case "/models":
|
|
870
|
+
console.log(chalk.dim(" 已合并到 /model 命令,请直接使用 /model\n"));
|
|
541
871
|
break;
|
|
542
|
-
}
|
|
543
872
|
case "/memory": {
|
|
544
873
|
const mem = session.getMemoryContent();
|
|
545
874
|
if (!mem) {
|
|
@@ -573,151 +902,62 @@ async function handleCommand(input, session) {
|
|
|
573
902
|
case "/config": {
|
|
574
903
|
const config = await loadConfig();
|
|
575
904
|
const sub = args[0];
|
|
576
|
-
if (
|
|
577
|
-
console.log(chalk.bold("\n
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
console.log(`
|
|
582
|
-
console.log(`
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
const parts = [];
|
|
589
|
-
if (prov.apiKey)
|
|
590
|
-
parts.push(chalk.green("key ✓"));
|
|
591
|
-
else
|
|
592
|
-
parts.push(chalk.dim("无 key"));
|
|
593
|
-
if (prov.baseUrl)
|
|
594
|
-
parts.push(prov.baseUrl);
|
|
595
|
-
console.log(` ${chalk.cyan(name.padEnd(20))} ${parts.join(" ")}`);
|
|
596
|
-
}
|
|
597
|
-
if (config.customModels && Object.keys(config.customModels).length > 0) {
|
|
598
|
-
console.log(chalk.bold("\n 自定义模型:"));
|
|
599
|
-
for (const [id, m] of Object.entries(config.customModels)) {
|
|
600
|
-
console.log(` ${chalk.cyan(id.padEnd(30))} ${chalk.dim(m.provider)}${m.contextWindow ? ` ctx:${formatTokenCount(m.contextWindow)}` : ""}`);
|
|
601
|
-
}
|
|
905
|
+
if (sub === "check") {
|
|
906
|
+
console.log(chalk.bold("\n配置检测:\n"));
|
|
907
|
+
const m = session.model;
|
|
908
|
+
const provider = m.provider;
|
|
909
|
+
const maskedKey = m.apiKey ? m.apiKey.slice(0, 8) + "..." + m.apiKey.slice(-4) : chalk.red("未设置");
|
|
910
|
+
console.log(` 模型: ${chalk.cyan(m.id)}`);
|
|
911
|
+
console.log(` Provider: ${chalk.cyan(provider)}`);
|
|
912
|
+
console.log(` Base URL: ${chalk.cyan(m.baseUrl || "(默认)")}`);
|
|
913
|
+
console.log(` API Key: ${maskedKey}`);
|
|
914
|
+
if (!m.apiKey) {
|
|
915
|
+
console.log(chalk.red("\n ✗ API Key 未设置,无法连接\n"));
|
|
916
|
+
break;
|
|
602
917
|
}
|
|
603
|
-
console.log(chalk.dim("\n
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
console.log(` ${chalk.cyan("/config")} 查看当前配置`);
|
|
609
|
-
console.log(` ${chalk.cyan("/config set model <id>")} 设置默认模型 (持久化)`);
|
|
610
|
-
console.log(` ${chalk.cyan("/config set key <provider> <key>")} 设置 Provider API Key`);
|
|
611
|
-
console.log(` ${chalk.cyan("/config set url <provider> <url>")} 设置 Provider Base URL`);
|
|
612
|
-
console.log(` ${chalk.cyan("/config add model <id> <provider>")} 添加自定义模型`);
|
|
613
|
-
console.log(` ${chalk.cyan("/config rm model <id>")} 删除自定义模型`);
|
|
614
|
-
console.log();
|
|
615
|
-
console.log(chalk.dim(" 示例:"));
|
|
616
|
-
console.log(chalk.dim(" /config set model gpt-4o"));
|
|
617
|
-
console.log(chalk.dim(" /config set key openai sk-xxx"));
|
|
618
|
-
console.log(chalk.dim(" /config set url openai https://my-proxy.com/v1"));
|
|
619
|
-
console.log(chalk.dim(" /config add model deepseek-chat openai-compatible"));
|
|
620
|
-
console.log();
|
|
621
|
-
break;
|
|
622
|
-
}
|
|
623
|
-
if (sub === "set") {
|
|
624
|
-
const field = args[1];
|
|
625
|
-
if (field === "model") {
|
|
626
|
-
const modelId = args[2];
|
|
627
|
-
if (!modelId) {
|
|
628
|
-
console.log(chalk.red("\n 用法: /config set model <model-id>\n"));
|
|
629
|
-
break;
|
|
630
|
-
}
|
|
631
|
-
const provider = guessProvider(modelId);
|
|
632
|
-
config.defaultModel = { provider, id: modelId };
|
|
633
|
-
await saveConfig(config);
|
|
634
|
-
session.switchModel(modelId);
|
|
635
|
-
console.log(chalk.green(`\n 默认模型已设为: ${modelId} (provider: ${provider})`));
|
|
636
|
-
console.log(chalk.dim(` 已同步切换当前会话模型\n`));
|
|
637
|
-
}
|
|
638
|
-
else if (field === "key") {
|
|
639
|
-
const provider = args[2];
|
|
640
|
-
const key = args[3];
|
|
641
|
-
if (!provider || !key) {
|
|
642
|
-
console.log(chalk.red("\n 用法: /config set key <provider> <api-key>\n"));
|
|
643
|
-
break;
|
|
644
|
-
}
|
|
645
|
-
if (!config.providers[provider])
|
|
646
|
-
config.providers[provider] = {};
|
|
647
|
-
config.providers[provider].apiKey = key;
|
|
648
|
-
await saveConfig(config);
|
|
649
|
-
console.log(chalk.green(`\n 已设置 ${provider} 的 API Key\n`));
|
|
650
|
-
}
|
|
651
|
-
else if (field === "url") {
|
|
652
|
-
const provider = args[2];
|
|
653
|
-
const url = args[3];
|
|
654
|
-
if (!provider || !url) {
|
|
655
|
-
console.log(chalk.red("\n 用法: /config set url <provider> <base-url>\n"));
|
|
656
|
-
break;
|
|
657
|
-
}
|
|
658
|
-
if (!config.providers[provider])
|
|
659
|
-
config.providers[provider] = {};
|
|
660
|
-
config.providers[provider].baseUrl = url;
|
|
661
|
-
await saveConfig(config);
|
|
662
|
-
console.log(chalk.green(`\n 已设置 ${provider} 的 Base URL: ${url}\n`));
|
|
918
|
+
console.log(chalk.dim("\n 正在测试 API 连接..."));
|
|
919
|
+
try {
|
|
920
|
+
const { completeLLM } = await import("../core/llm.js");
|
|
921
|
+
const text = await completeLLM(m, "You are a test.", "Say OK", 32);
|
|
922
|
+
console.log(chalk.green(` ✓ 连接成功! 模型响应: "${text.slice(0, 80)}"`));
|
|
663
923
|
}
|
|
664
|
-
|
|
665
|
-
console.log(chalk.red(
|
|
666
|
-
console.log(chalk.dim(" 查看帮助: /config help\n"));
|
|
924
|
+
catch (err) {
|
|
925
|
+
console.log(chalk.red(` ✗ 连接失败: ${err.message}`));
|
|
667
926
|
}
|
|
927
|
+
console.log();
|
|
668
928
|
break;
|
|
669
929
|
}
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
name: modelId,
|
|
685
|
-
maxTokens: 8192,
|
|
686
|
-
};
|
|
687
|
-
await saveConfig(config);
|
|
688
|
-
console.log(chalk.green(`\n 已添加自定义模型: ${modelId} (provider: ${provider})`));
|
|
689
|
-
console.log(chalk.dim(` 切换使用: /model ${modelId}\n`));
|
|
690
|
-
}
|
|
691
|
-
else {
|
|
692
|
-
console.log(chalk.red(`\n 未知类型: ${what}。支持: model`));
|
|
693
|
-
console.log(chalk.dim(" 查看帮助: /config help\n"));
|
|
930
|
+
console.log(chalk.bold("\n当前配置:\n"));
|
|
931
|
+
console.log(` 模型: ${chalk.cyan(session.model.id)}`);
|
|
932
|
+
console.log(` Base URL: ${chalk.cyan(session.model.baseUrl || "(默认)")}`);
|
|
933
|
+
console.log(` API Key: ${session.model.apiKey ? chalk.green("已配置") : chalk.red("未设置")}`);
|
|
934
|
+
console.log(` Session: ${chalk.cyan(session.sessionManager.shortId)}`);
|
|
935
|
+
if (config.models && Object.keys(config.models).length > 0) {
|
|
936
|
+
console.log(chalk.bold("\n 自定义模型:"));
|
|
937
|
+
for (const [id, m] of Object.entries(config.models)) {
|
|
938
|
+
const parts = [];
|
|
939
|
+
if (m.apiKey)
|
|
940
|
+
parts.push(chalk.green("key ✓"));
|
|
941
|
+
if (m.baseUrl)
|
|
942
|
+
parts.push(m.baseUrl);
|
|
943
|
+
console.log(` ${chalk.cyan(id.padEnd(25))} ${parts.join(" ") || chalk.dim("(继承全局)")}`);
|
|
694
944
|
}
|
|
695
|
-
break;
|
|
696
945
|
}
|
|
697
|
-
if (
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
const
|
|
701
|
-
if (
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
if (
|
|
706
|
-
console.log(chalk.
|
|
707
|
-
break;
|
|
946
|
+
if (Object.keys(config.providers).length > 0) {
|
|
947
|
+
console.log(chalk.bold("\n Provider 凭证:"));
|
|
948
|
+
for (const [name, prov] of Object.entries(config.providers)) {
|
|
949
|
+
const parts = [];
|
|
950
|
+
if (prov.apiKey)
|
|
951
|
+
parts.push(chalk.green("key ✓"));
|
|
952
|
+
if (prov.baseUrl)
|
|
953
|
+
parts.push(prov.baseUrl);
|
|
954
|
+
if (parts.length > 0) {
|
|
955
|
+
console.log(` ${chalk.cyan(name.padEnd(20))} ${parts.join(" ")}`);
|
|
708
956
|
}
|
|
709
|
-
delete config.customModels[modelId];
|
|
710
|
-
await saveConfig(config);
|
|
711
|
-
console.log(chalk.green(`\n 已删除自定义模型: ${modelId}\n`));
|
|
712
|
-
}
|
|
713
|
-
else {
|
|
714
|
-
console.log(chalk.red(`\n 未知类型: ${what}。支持: model`));
|
|
715
|
-
console.log(chalk.dim(" 查看帮助: /config help\n"));
|
|
716
957
|
}
|
|
717
|
-
break;
|
|
718
958
|
}
|
|
719
|
-
console.log(chalk.
|
|
720
|
-
console.log(chalk.dim(
|
|
959
|
+
console.log(chalk.dim(`\n 配置文件: ${getConfigPath()}`));
|
|
960
|
+
console.log(chalk.dim(` /config check 测试连接 | /model 管理模型 | til --setup 重新配置\n`));
|
|
721
961
|
break;
|
|
722
962
|
}
|
|
723
963
|
case "/skills": {
|
|
@@ -726,12 +966,14 @@ async function handleCommand(input, session) {
|
|
|
726
966
|
console.log(chalk.dim("\n 暂无技能。将 SKILL.md 文件放入 ~/.til/skills/ 或 .til/skills/ 即可加载。\n"));
|
|
727
967
|
}
|
|
728
968
|
else {
|
|
969
|
+
const sourceLabel = { builtin: "内置", user: "用户", project: "项目", path: "路径" };
|
|
729
970
|
console.log(chalk.bold(`\n已加载技能 (${skills.length}):\n`));
|
|
730
971
|
for (const s of skills) {
|
|
731
|
-
|
|
732
|
-
console.log(` ${chalk.dim(s.
|
|
972
|
+
const tag = chalk.dim(`[${sourceLabel[s.source] || s.source}]`);
|
|
973
|
+
console.log(` ${chalk.cyan(s.name.padEnd(24))} ${tag} ${chalk.dim(s.description.slice(0, 50))}`);
|
|
733
974
|
}
|
|
734
|
-
console.log(chalk.dim("\n 使用 /skill:<名称>
|
|
975
|
+
console.log(chalk.dim("\n 使用 /skill:<名称> 查看技能详情。"));
|
|
976
|
+
console.log(chalk.dim(" 自定义: ~/.til/skills/ 或 .til/skills/\n"));
|
|
735
977
|
}
|
|
736
978
|
break;
|
|
737
979
|
}
|
|
@@ -835,7 +1077,6 @@ async function handleCommand(input, session) {
|
|
|
835
1077
|
}
|
|
836
1078
|
break;
|
|
837
1079
|
}
|
|
838
|
-
// Try extension-registered commands
|
|
839
1080
|
const extCmd = cmd.slice(1);
|
|
840
1081
|
const extCommands = session.getExtensionCommands();
|
|
841
1082
|
if (extCommands.has(extCmd)) {
|
|
@@ -909,6 +1150,20 @@ function getVisualWidth(str) {
|
|
|
909
1150
|
}
|
|
910
1151
|
return w;
|
|
911
1152
|
}
|
|
1153
|
+
function extractThinkBlocks(text) {
|
|
1154
|
+
const blocks = [];
|
|
1155
|
+
const re = /<think>([\s\S]*?)<\/think>/g;
|
|
1156
|
+
let m;
|
|
1157
|
+
while ((m = re.exec(text)) !== null) {
|
|
1158
|
+
const content = m[1].trim();
|
|
1159
|
+
if (content)
|
|
1160
|
+
blocks.push(content);
|
|
1161
|
+
}
|
|
1162
|
+
return blocks;
|
|
1163
|
+
}
|
|
1164
|
+
function stripThinkBlocks(text) {
|
|
1165
|
+
return text.replace(/<think>[\s\S]*?<\/think>\s*/g, "").replace(/\n{3,}/g, "\n\n").trim();
|
|
1166
|
+
}
|
|
912
1167
|
function countVisualLines(text, termWidth) {
|
|
913
1168
|
if (termWidth <= 0)
|
|
914
1169
|
return text.split("\n").length;
|
|
@@ -922,7 +1177,7 @@ function countVisualLines(text, termWidth) {
|
|
|
922
1177
|
}
|
|
923
1178
|
function printBanner(session) {
|
|
924
1179
|
const modelId = session.model.name || session.model.id;
|
|
925
|
-
const dir = session.cwd
|
|
1180
|
+
const dir = shortenDir(session.cwd);
|
|
926
1181
|
const sm = session.sessionManager;
|
|
927
1182
|
console.log();
|
|
928
1183
|
console.log(chalk.bold.cyan(">_") + " " + chalk.bold("欢迎使用千岛湖 Til-Work") + " " + chalk.dim(`v${VERSION}`));
|
|
@@ -944,21 +1199,7 @@ function printBanner(session) {
|
|
|
944
1199
|
const totalTools = extInfo.reduce((sum, e) => sum + e.toolCount, 0);
|
|
945
1200
|
console.log(` ${chalk.dim("extensions")} ${chalk.white(String(extInfo.length))} 个已加载 (${totalTools} 工具)`);
|
|
946
1201
|
}
|
|
947
|
-
const
|
|
948
|
-
"输入 / 查看所有可用命令,方向键选择",
|
|
949
|
-
"使用 /model 切换模型",
|
|
950
|
-
"按 Ctrl+C 或 Esc 中止正在执行的请求",
|
|
951
|
-
"使用 /memory 查看记忆内容",
|
|
952
|
-
"使用 /sessions 查看历史会话",
|
|
953
|
-
"输入 @ 引用文件内容(支持搜索和方向键选择)",
|
|
954
|
-
];
|
|
955
|
-
if (session.skills.length > 0) {
|
|
956
|
-
tips.push("使用 /skills 查看已加载的技能");
|
|
957
|
-
}
|
|
958
|
-
if (extInfo.length > 0) {
|
|
959
|
-
tips.push("使用 /extensions 查看已加载的扩展");
|
|
960
|
-
}
|
|
961
|
-
const tip = tips[Math.floor(Math.random() * tips.length)];
|
|
1202
|
+
const tip = getRandomTip(session);
|
|
962
1203
|
console.log(`\n ${chalk.bold("Tip:")} ${chalk.italic(tip)}\n`);
|
|
963
1204
|
}
|
|
964
1205
|
//# sourceMappingURL=interactive.js.map
|