skyloom 1.13.0 → 1.13.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/src/cli/main.ts CHANGED
@@ -12,7 +12,7 @@ import { listProviders, modelsFor, providerLabel, validateModel } from "../core/
12
12
  import { agentTheme } from "../core/theme";
13
13
  import { classify } from "../core/router";
14
14
  import { InteractiveMode, ModeController } from "./mode";
15
- import { readInput, type TUIContext } from "./tui";
15
+ import { readLine, renderPalette, StreamRenderer } from "./tui";
16
16
 
17
17
  const MODE = new ModeController();
18
18
  const VERSION = (() => { try { return require("../../package.json").version; } catch { return "1.5.2"; } })();
@@ -137,52 +137,59 @@ function formatCost(c: number): string {
137
137
  async function streamResponse(agent: any, input: string): Promise<void> {
138
138
  const theme = agentTheme(agent.name);
139
139
  const pigment = chalk.hex(theme.hex);
140
+ const out = process.stdout;
141
+
142
+ // ── Thinking spinner (animates until the first token lands; TTY only) ──
143
+ const isTTY = !!out.isTTY;
144
+ const frames = ["· ", "·· ", " ··", " ·"];
145
+ let fi = 0, spinning = true;
146
+ const draw = () => { if (spinning && isTTY) out.write(`\r ${pigment(theme.symbol)} ${chalk.dim("思忖 " + frames[fi++ % frames.length])}`); };
147
+ const timer = isTTY ? setInterval(draw, 140) : null; draw();
148
+ const stopSpinner = () => { if (spinning) { spinning = false; if (timer) clearInterval(timer); if (isTTY) out.write("\r" + " ".repeat(20) + "\r"); } };
149
+
150
+ let headerShown = false;
140
151
  let mode: "none" | "reasoning" | "content" = "none";
141
- let atLineStart = true;
152
+ let renderer: StreamRenderer | null = null;
153
+ const header = () => { if (!headerShown) { out.write("\n " + chalk.bold.hex(theme.hex)(`${theme.symbol} ${theme.kanji}`) + chalk.hex(theme.hex)(` ${theme.name}`) + "\n\n"); headerShown = true; } };
154
+ const endBlock = () => { if (renderer) { renderer.flush(); renderer = null; out.write("\n"); } };
142
155
 
143
- const writeContent = (text: string) => {
144
- for (const ch of text) {
145
- if (atLineStart) { process.stdout.write(" "); atLineStart = false; }
146
- process.stdout.write(ch);
147
- if (ch === "\n") atLineStart = true;
148
- }
149
- };
150
-
151
- // Thinking indicator until the first event lands
152
- process.stdout.write(chalk.dim(` ${theme.symbol} ${theme.pigment} …\r`));
153
- let cleared = false;
154
- const clearThinking = () => { if (!cleared) { process.stdout.write("\r" + " ".repeat(40) + "\r"); cleared = true; } };
155
-
156
- for await (const ev of agent.chatStream(input)) {
157
- switch (ev.type) {
158
- case "reasoning":
159
- clearThinking();
160
- if (mode !== "reasoning") { process.stdout.write(chalk.dim("\n ")); mode = "reasoning"; }
161
- process.stdout.write(chalk.dim.italic(String(ev.text).replace(/\n/g, " ")));
162
- break;
163
- case "content":
164
- clearThinking();
165
- if (mode !== "content") { process.stdout.write(mode === "reasoning" ? "\n\n" : "\n"); atLineStart = true; mode = "content"; }
166
- writeContent(String(ev.text));
167
- break;
168
- case "tool_status":
169
- clearThinking();
170
- process.stdout.write("\n" + pigment(` ${theme.symbol} ${ev.tool_name}`) + chalk.dim(` ${ev.label || ""} …`) + "\n");
171
- atLineStart = true; mode = "none";
172
- break;
173
- case "tool_done":
174
- process.stdout.write((ev.success ? chalk.green(" ✓ ") : chalk.red(" ✗ ")) + chalk.dim(String(ev.tool_name)) + "\n");
175
- atLineStart = true; mode = "none";
176
- break;
177
- case "truncated":
178
- process.stdout.write(chalk.yellow(`\n ⚠ 截断: ${ev.reason}\n`));
179
- break;
180
- case "done":
181
- break;
156
+ try {
157
+ for await (const ev of agent.chatStream(input)) {
158
+ switch (ev.type) {
159
+ case "reasoning":
160
+ stopSpinner();
161
+ if (mode !== "reasoning") { out.write(chalk.dim(" ◦ 思考 ")); mode = "reasoning"; }
162
+ out.write(chalk.dim.italic(String(ev.text).replace(/\s+/g, " ")));
163
+ break;
164
+ case "content":
165
+ stopSpinner();
166
+ if (mode === "reasoning") out.write("\n");
167
+ if (mode !== "content") { header(); renderer = new StreamRenderer(out, { gutter: " " }); mode = "content"; }
168
+ renderer!.write(String(ev.text));
169
+ break;
170
+ case "tool_status":
171
+ stopSpinner();
172
+ endBlock();
173
+ out.write("\n " + pigment(`${theme.symbol} ${ev.tool_name}`) + (ev.label ? chalk.dim(` ${ev.label}`) : "") + chalk.dim(" ") + "\n");
174
+ mode = "none";
175
+ break;
176
+ case "tool_done":
177
+ out.write(" " + (ev.success ? chalk.hex("#3a7a6e")("✓") : chalk.hex("#b3342d")("✗")) + " " + chalk.dim(String(ev.tool_name)) + "\n");
178
+ mode = "none";
179
+ break;
180
+ case "truncated":
181
+ endBlock();
182
+ out.write(chalk.yellow(`\n ⚠ ${ev.reason}\n`));
183
+ break;
184
+ case "done":
185
+ break;
186
+ }
182
187
  }
188
+ } finally {
189
+ stopSpinner();
190
+ endBlock();
183
191
  }
184
- clearThinking();
185
- process.stdout.write("\n\n");
192
+ out.write("\n");
186
193
  }
187
194
 
188
195
  /* ═══════════════════════════════════════
@@ -319,15 +326,15 @@ async function chat(agentName: string, modelOverride?: string): Promise<void> {
319
326
  let currentAgent = agent; // mutable for agent switching
320
327
  welcome(agent);
321
328
 
322
- process.stdout.write(chalk.dim(" Key: " + haveKey + "\n\n"));
323
-
324
- // ── TUI loop ──
325
- const ctx_: TUIContext = { agent: currentAgent, agents: ctx.agentMap, model: "default", cost: "$0", width: 80, height: 24 };
329
+ process.stdout.write(chalk.dim(" · 输入 / 看命令(Tab 补全)· ↑↓ 翻历史 · Ctrl-C 退出\n\n"));
326
330
 
327
331
  while (true) {
328
- const inp = await readInput(process.stdin, process.stdout, ctx_);
332
+ const inp = await readLine(currentAgent.name);
329
333
  if (!inp) continue;
330
334
 
335
+ // Bare "/" → show the inline command palette
336
+ if (inp === "/") { process.stdout.write("\n" + renderPalette("") + "\n"); continue; }
337
+
331
338
  const cmdL = inp.toLowerCase();
332
339
 
333
340
  // Agent switch — stamp a mineral seal on change
@@ -336,7 +343,7 @@ async function chat(agentName: string, modelOverride?: string): Promise<void> {
336
343
  if (cmdL === "/" + n) {
337
344
  const a = ctx.agentMap.get(n);
338
345
  if (a) {
339
- await a.init(); currentAgent = a; ctx_.agent = a;
346
+ await a.init(); currentAgent = a;
340
347
  const t = agentTheme(n);
341
348
  process.stdout.write("\n " + chalk.bold.hex(t.hex)(`▣ ${t.kanji} ${t.pigment}`) + chalk.dim(` · ${t.specialty}`) + "\n");
342
349
  process.stdout.write(" " + chalk.dim.italic(t.poem) + "\n\n");
@@ -347,7 +354,7 @@ async function chat(agentName: string, modelOverride?: string): Promise<void> {
347
354
  if (switched) continue;
348
355
  if (cmdL === "/quit" || cmdL === "/exit") break;
349
356
  if (cmdL === "/clear") { console.clear(); continue; }
350
- if (cmdL === "/help") { process.stdout.write(helpText()); continue; }
357
+ if (cmdL === "/help") { process.stdout.write("\n" + renderPalette("") + "\n"); continue; }
351
358
  if (cmdL === "/version") { process.stdout.write(" Skyloom v" + VERSION + "\n"); continue; }
352
359
  if (cmdL === "/status") { process.stdout.write(chalk.bold("\n " + currentAgent.displayName + " (" + currentAgent.name + ")\n") + chalk.dim(" State: " + currentAgent.state + " · Memory: " + currentAgent.memory.shortTerm.length + " msgs\n\n")); continue; }
353
360
  if (cmdL === "/cost") { process.stdout.write(chalk.bold("\n Total: " + formatCost(ctx.llm.getTotalCost()) + "\n\n")); continue; }
@@ -363,7 +370,7 @@ async function chat(agentName: string, modelOverride?: string): Promise<void> {
363
370
  if (cmdL.startsWith("/task ")) { const g = inp.slice(6); process.stdout.write(chalk.cyan("\n ✦ " + g + "\n\n")); await runTask(g); continue; }
364
371
  if (cmdL === "/setup") { const r = await setupWizard(); if (r) process.stdout.write(chalk.green(` ${r.provider} · ${r.model} — Ready!\n`)); continue; }
365
372
  if (cmdL.startsWith("/model")) { process.stdout.write(chalk.dim(" Run /setup to reconfigure models\n")); continue; }
366
- if (inp.startsWith("/")) { process.stdout.write(helpText()); continue; }
373
+ if (inp.startsWith("/")) { process.stdout.write("\n" + chalk.dim(` 未知命令 ${inp.split(" ")[0]}\n`) + renderPalette(cmdL.split(" ")[0]) + "\n"); continue; }
367
374
 
368
375
  // ── Chat (real streaming) ──
369
376
  try {
@@ -391,21 +398,6 @@ async function runTask(goal: string): Promise<void> {
391
398
  await ctx.closeAll();
392
399
  }
393
400
 
394
- function helpText(): string {
395
- const groups: [string, [string, string][]][] = [
396
- ["Agent", [["/fog /rain /frost", "Switch agents"], ["/snow /dew /fair", "Switch agents"]]],
397
- ["Chat", [["/help", "Commands"], ["/clear", "Clear"], ["/compact", "Compress"], ["/retry", "Resend"]]],
398
- ["Info", [["/status", "Status"], ["/cost", "Cost"], ["/memory", "Memory"], ["/sessions", "Sessions"], ["/workspace", "Workspace"], ["/version", "Version"]]],
399
- ["Orch.", [["/task <goal>", "Multi-agent"]]],
400
- ];
401
- let s = "";
402
- for (const [title, cmds] of groups) {
403
- s += chalk.cyan(` ${title}\n`);
404
- for (const [c, d] of cmds) s += ` ${chalk.cyan(c.padEnd(18))}${chalk.dim(d)}\n`;
405
- }
406
- s += "\n";
407
- return s;
408
- }
409
401
 
410
402
  /* ═══════════════════════════════════════
411
403
  Entry