@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.
Files changed (57) hide show
  1. package/README.md +265 -297
  2. package/dist/core/config.d.ts +29 -11
  3. package/dist/core/config.d.ts.map +1 -1
  4. package/dist/core/config.js +65 -101
  5. package/dist/core/config.js.map +1 -1
  6. package/dist/core/llm.d.ts.map +1 -1
  7. package/dist/core/llm.js +14 -0
  8. package/dist/core/llm.js.map +1 -1
  9. package/dist/core/pricing.d.ts.map +1 -1
  10. package/dist/core/pricing.js +0 -2
  11. package/dist/core/pricing.js.map +1 -1
  12. package/dist/core/session.d.ts +3 -2
  13. package/dist/core/session.d.ts.map +1 -1
  14. package/dist/core/session.js +4 -3
  15. package/dist/core/session.js.map +1 -1
  16. package/dist/core/skills.d.ts +2 -1
  17. package/dist/core/skills.d.ts.map +1 -1
  18. package/dist/core/skills.js +9 -0
  19. package/dist/core/skills.js.map +1 -1
  20. package/dist/core/system-prompt.d.ts.map +1 -1
  21. package/dist/core/system-prompt.js +6 -2
  22. package/dist/core/system-prompt.js.map +1 -1
  23. package/dist/main.d.ts.map +1 -1
  24. package/dist/main.js +66 -124
  25. package/dist/main.js.map +1 -1
  26. package/dist/modes/interactive.d.ts.map +1 -1
  27. package/dist/modes/interactive.js +514 -273
  28. package/dist/modes/interactive.js.map +1 -1
  29. package/dist/tools/browser.d.ts +10 -0
  30. package/dist/tools/browser.d.ts.map +1 -0
  31. package/dist/tools/browser.js +231 -0
  32. package/dist/tools/browser.js.map +1 -0
  33. package/dist/tools/computer.d.ts +3 -0
  34. package/dist/tools/computer.d.ts.map +1 -0
  35. package/dist/tools/computer.js +251 -0
  36. package/dist/tools/computer.js.map +1 -0
  37. package/dist/tools/index.d.ts +5 -2
  38. package/dist/tools/index.d.ts.map +1 -1
  39. package/dist/tools/index.js +11 -2
  40. package/dist/tools/index.js.map +1 -1
  41. package/dist/tools/read.d.ts.map +1 -1
  42. package/dist/tools/read.js +29 -4
  43. package/dist/tools/read.js.map +1 -1
  44. package/dist/tools/screenshot.d.ts +3 -0
  45. package/dist/tools/screenshot.d.ts.map +1 -0
  46. package/dist/tools/screenshot.js +113 -0
  47. package/dist/tools/screenshot.js.map +1 -0
  48. package/dist/utils/file-processor.d.ts +2 -2
  49. package/dist/utils/file-processor.d.ts.map +1 -1
  50. package/dist/utils/file-processor.js +7 -10
  51. package/dist/utils/file-processor.js.map +1 -1
  52. package/package.json +3 -2
  53. package/skills/find-skills/SKILL.md +66 -0
  54. package/skills/playwright-mcp/SKILL.md +90 -0
  55. package/skills/self-improving-agent/SKILL.md +88 -0
  56. package/skills/skill-creator/SKILL.md +93 -0
  57. 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 { getKnownModels, loadConfig, saveConfig, guessProvider } from "../core/config.js";
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": "切换模型(如 /model gpt-4o)",
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 popup menu below the input line using relative cursor movement.
65
- * Unlike \x1b[s / \x1b[u (save/restore absolute position), relative movement
66
- * via \x1b[nA works correctly even when the terminal scrolls during output.
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 renderMenu(menu, cursorCol) {
69
- if (!menu.visible || menu.items.length === 0)
112
+ function renderBelowArea(menu, below, session, cursorCol) {
113
+ if (!below.active)
70
114
  return;
71
- const totalLinesToClear = Math.max(menu.items.length, menu.renderedLines);
72
- const padWidth = menu.type === "file" ? 24 : 16;
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
- for (let i = 0; i < menu.items.length; i++) {
75
- const { label, detail } = menu.items[i];
76
- const isSelected = i === menu.selectedIndex;
77
- if (isSelected) {
78
- output += ` ${chalk.bgCyan.black(` ${label.padEnd(padWidth)}`)} ${chalk.white(detail)}`;
79
- }
80
- else {
81
- output += ` ${chalk.cyan(` ${label.padEnd(padWidth)}`)} ${chalk.dim(detail)}`;
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
- for (let i = menu.items.length; i < menu.renderedLines; i++) {
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
- menu.renderedLines = Math.max(menu.renderedLines, menu.items.length);
89
- const linesToGoUp = 1 + totalLinesToClear;
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 clearMenu(menu, cursorCol = 0) {
97
- if (menu.renderedLines === 0)
158
+ function clearBelowArea(below, cursorCol) {
159
+ if (below.renderedLines === 0)
98
160
  return;
99
161
  let output = "";
100
- const totalLines = menu.renderedLines + 1;
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
- menu.visible = false;
110
- menu.items = [];
111
- menu.selectedIndex = 0;
112
- menu.renderedLines = 0;
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
- if (!firstTokenReceived) {
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
- // Stream raw text for responsiveness
204
- process.stdout.write(chalk.white(event.delta));
205
- streamingRawText += event.delta;
206
- currentOutput += event.delta;
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
- const fullText = assistantMsg.content
332
+ let fullText = assistantMsg.content
214
333
  .filter((c) => c.type === "text")
215
334
  .map((c) => c.text)
216
335
  .join("");
217
- if (fullText.trim() && streamingRawText.trim()) {
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
- try {
225
- const rendered = renderMarkdown(fullText);
226
- process.stdout.write(rendered + "\n");
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
- catch {
229
- process.stdout.write(fullText + "\n");
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
- // Show per-turn usage summary
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 && key.name === "up") {
346
- menu.selectedIndex = Math.max(0, menu.selectedIndex - 1);
347
- renderMenu(menu, cursorCol);
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 && key.name === "down") {
351
- menu.selectedIndex = Math.min(menu.items.length - 1, menu.selectedIndex + 1);
352
- renderMenu(menu, cursorCol);
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 && (key.name === "return" || key.name === "tab")) {
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
- clearMenu(menu, cursorCol);
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 (key.name === "return") {
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 && key.name === "escape") {
396
- clearMenu(menu, cursorCol);
609
+ if (key && keyName === "escape") {
610
+ resetMenu(menu);
611
+ renderBelowArea(menu, below, session, cursorCol);
397
612
  return;
398
613
  }
399
614
  }
400
- originalTtyWrite.call(this, s, key);
401
- const newLine = rl.line ?? "";
402
- const newCursorCol = getCursorCol();
403
- if (newLine.startsWith("/") && !newLine.includes(" ")) {
404
- const filtered = filterEntries(allEntries, newLine);
405
- if (filtered.length > 0) {
406
- menu.visible = true;
407
- menu.items = filtered;
408
- menu.type = "command";
409
- menu.selectedIndex = 0;
410
- renderMenu(menu, newCursorCol);
411
- }
412
- else if (menu.visible) {
413
- clearMenu(menu, newCursorCol);
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
- else {
417
- const atPrefix = extractAtPrefix(newLine);
418
- if (atPrefix !== null) {
419
- const entries = getFileMenuEntries(atPrefix, session.cwd);
420
- if (entries.length > 0) {
421
- menu.visible = true;
422
- menu.items = entries;
423
- menu.type = "file";
424
- menu.selectedIndex = 0;
425
- renderMenu(menu, newCursorCol);
426
- }
427
- else if (menu.visible) {
428
- clearMenu(menu, newCursorCol);
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
- else if (menu.visible) {
432
- clearMenu(menu, newCursorCol);
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
- clearMenu(menu);
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
- clearMenu(menu);
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 ? fileContent + "\n" + expandedText : expandedText;
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 modelId = args[0];
509
- if (!modelId) {
510
- console.log(chalk.yellow(`当前模型: ${session.model.id}`));
511
- console.log(chalk.dim("用法: /model <model-id>\n"));
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
- try {
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(`已切换到: ${modelId}\n`));
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(`切换失败: ${err.message}\n`));
865
+ console.log(chalk.red(`\n 切换失败: ${err.message}\n`));
520
866
  }
521
867
  break;
522
868
  }
523
- case "/models": {
524
- const cfg = await loadConfig();
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 (!sub) {
577
- console.log(chalk.bold("\n当前配置:\n"));
578
- console.log(` 模型: ${chalk.cyan(session.model.id)}`);
579
- console.log(` Provider: ${chalk.cyan(session.model.provider)}`);
580
- console.log(` Base URL: ${chalk.cyan(session.model.baseUrl || "(默认)")}`);
581
- console.log(` API Key: ${session.model.apiKey ? chalk.green("已配置") : chalk.red("未设置")}`);
582
- console.log(` Session: ${chalk.cyan(session.sessionManager.shortId)}`);
583
- if (session.model.headers && Object.keys(session.model.headers).length > 0) {
584
- console.log(` Headers: ${chalk.dim(Object.keys(session.model.headers).join(", "))}`);
585
- }
586
- console.log(chalk.bold("\n 所有 Provider:"));
587
- for (const [name, prov] of Object.entries(config.providers)) {
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 用法: /config set model|key|url|add-model (查看 /config help)\n"));
604
- break;
605
- }
606
- if (sub === "help") {
607
- console.log(chalk.bold("\n/config 子命令:\n"));
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
- else {
665
- console.log(chalk.red(`\n 未知字段: ${field}。支持: model, key, url`));
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
- if (sub === "add") {
671
- const what = args[1];
672
- if (what === "model") {
673
- const modelId = args[2];
674
- const provider = args[3];
675
- if (!modelId || !provider) {
676
- console.log(chalk.red("\n 用法: /config add model <model-id> <provider>\n"));
677
- console.log(chalk.dim(" provider 可选: anthropic, openai, openai-compatible, google\n"));
678
- break;
679
- }
680
- if (!config.customModels)
681
- config.customModels = {};
682
- config.customModels[modelId] = {
683
- provider,
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 (sub === "rm") {
698
- const what = args[1];
699
- if (what === "model") {
700
- const modelId = args[2];
701
- if (!modelId) {
702
- console.log(chalk.red("\n 用法: /config rm model <model-id>\n"));
703
- break;
704
- }
705
- if (!config.customModels?.[modelId]) {
706
- console.log(chalk.red(`\n 自定义模型 "${modelId}" 不存在 (内置模型不可删除)\n`));
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.red(`\n 未知子命令: ${sub}`));
720
- console.log(chalk.dim(" 查看帮助: /config help\n"));
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
- console.log(` ${chalk.cyan(s.name.padEnd(24))} ${chalk.dim(s.description.slice(0, 60))}`);
732
- console.log(` ${chalk.dim(s.filePath)}`);
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:<名称> 查看技能详情。\n"));
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.replace(process.env.HOME || "", "~");
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 tips = [
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