chatccc 0.2.223 → 0.2.225

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.
@@ -9,6 +9,9 @@
9
9
  * 实现要点:
10
10
  * - 隐藏光标(\x1b[?25l)→ 每帧整块重绘(\x1b[{n}A 上移 + \x1b[2K 清行 + \x1b[J 清下方)
11
11
  * - 帧节流合并重绘,避免高频文本增量导致闪烁
12
+ * - 每行按显示宽度截断(CJK/emoji 算 2 列),保证永不折行:一旦折行,
13
+ * 实际占用的屏幕行数会超过 blockLines 计数,下次重绘上移行数不足,
14
+ * 会清掉区块上方的历史内容(用户输入的问题被"吃掉")。
12
15
  * - end() 时把终态区块定型留在屏幕上(与飞书完成卡片留在消息流语义一致),恢复光标
13
16
  */
14
17
 
@@ -17,6 +20,7 @@ import { progressView, type ProgressToolCall, type ProgressView } from "./view.t
17
20
 
18
21
  const RESET = "\x1b[0m";
19
22
  const DIM = "\x1b[2m";
23
+ const BOLD = "\x1b[1m";
20
24
  const GREEN = "\x1b[32m";
21
25
  const YELLOW = "\x1b[33m";
22
26
  const RED = "\x1b[31m";
@@ -24,11 +28,72 @@ const RED = "\x1b[31m";
24
28
  const HIDE_CURSOR = "\x1b[?25l";
25
29
  const SHOW_CURSOR = "\x1b[?25h";
26
30
 
31
+ /** 生成中标题后的心跳点号动画帧(1 → 2 → 3 → 2 循环,避免跳变感) */
32
+ const ANIM_FRAMES = ["·", "··", "···", "··"];
33
+
34
+ const ANSI_TOKEN = /\x1b\[[0-9;]*m/;
35
+
36
+ /** 单个字符在终端里的显示列宽:CJK/emoji 占 2 列,控制符/ZWJ/变体选择符占 0 列 */
37
+ export function charWidth(ch: string): number {
38
+ const code = ch.codePointAt(0)!;
39
+ if (code < 32 || (code >= 0x7f && code <= 0xa0)) return 0; // 控制字符
40
+ if (code === 0x200d || code === 0xfe0e || code === 0xfe0f) return 0; // ZWJ / VS15 / VS16
41
+ if (
42
+ (code >= 0x1100 && code <= 0x115f) || // 谚文 Jamo
43
+ (code >= 0x2e80 && code <= 0x303f) || // CJK 部首 / 标点
44
+ (code >= 0x3040 && code <= 0xa4cf) || // 假名 / 汉字 / 谚文 / 彝文
45
+ (code >= 0xac00 && code <= 0xd7a3) || // 谚文音节
46
+ (code >= 0xf900 && code <= 0xfaff) || // CJK 兼容汉字
47
+ (code >= 0xfe30 && code <= 0xfe6f) || // CJK 兼容形式
48
+ (code >= 0xff00 && code <= 0xff60) || // 全角形式
49
+ (code >= 0xffe0 && code <= 0xffe6) || // 全角符号
50
+ (code >= 0x2600 && code <= 0x27bf) || // 杂项符号 + Dingbats(emoji 呈现 2 列)
51
+ (code >= 0x1f000 && code <= 0x1faff) // emoji 补充区
52
+ ) {
53
+ return 2;
54
+ }
55
+ return 1;
56
+ }
57
+
58
+ /**
59
+ * 按终端显示宽度截断一行(CJK/emoji 计 2 列),ANSI 序列零宽且永不截断。
60
+ * 截断位置若处于未闭合的颜色状态,自动补 \x1b[0m,避免颜色泄漏到后续输出。
61
+ */
62
+ export function clipToWidth(s: string, maxCols: number): string {
63
+ if (maxCols <= 0) return "";
64
+ let out = "";
65
+ let visible = 0;
66
+ let colorOpen = false;
67
+ let i = 0;
68
+ while (i < s.length) {
69
+ if (s.charCodeAt(i) === 0x1b) {
70
+ const m = ANSI_TOKEN.exec(s.slice(i));
71
+ if (m && m.index === 0) {
72
+ out += m[0];
73
+ colorOpen = m[0] !== "\x1b[0m";
74
+ i += m[0].length;
75
+ continue;
76
+ }
77
+ }
78
+ const cp = s.codePointAt(i)!;
79
+ const ch = String.fromCodePoint(cp);
80
+ const w = charWidth(ch);
81
+ if (visible + w > maxCols) break;
82
+ out += ch;
83
+ visible += w;
84
+ i += ch.length;
85
+ }
86
+ if (colorOpen) out += "\x1b[0m";
87
+ return out;
88
+ }
89
+
27
90
  export interface TerminalRendererOptions {
28
91
  /** 输出流,默认 process.stdout */
29
92
  out?: NodeJS.WritableStream & { columns?: number };
30
93
  /** 帧节流毫秒数,默认 66(约 15fps) */
31
94
  frameMs?: number;
95
+ /** 生成中心跳动画毫秒数,默认 300;<=0 禁用动画 */
96
+ animMs?: number;
32
97
  /** 正文最大行数(超出截断,保留首行+末段) */
33
98
  maxBodyLines?: number;
34
99
  /** 正文最大字符数 */
@@ -38,7 +103,7 @@ export interface TerminalRendererOptions {
38
103
  function buildStatusLine(view: ProgressView): string {
39
104
  switch (view.status) {
40
105
  case "done":
41
- return `${GREEN}✅ 完成${RESET}`;
106
+ return `${BOLD}${GREEN}✅ 完成${RESET}`;
42
107
  case "stopped":
43
108
  return `${YELLOW}⏹ 已停止${RESET}`;
44
109
  case "error":
@@ -54,12 +119,13 @@ function buildToolLine(tool: ProgressToolCall): string {
54
119
  const emoji = getToolEmoji(tool.name);
55
120
  const mark =
56
121
  tool.status === "running"
57
- ? `${DIM}…${RESET}`
122
+ ? "…"
58
123
  : tool.status === "ok"
59
124
  ? `${GREEN}✓${RESET}`
60
125
  : `${RED}✗${RESET}`;
61
126
  const info = tool.status === "running" ? (tool.detail ?? "") : (tool.summary ?? "");
62
- return ` ${emoji} ${tool.name} ${mark} ${DIM}${info}${RESET}`;
127
+ // 摘要/详情用正常色:最终区块里不出现浅色文字(可读性优先)
128
+ return ` ${emoji} ${tool.name} ${mark} ${info}`;
63
129
  }
64
130
 
65
131
  /**
@@ -72,7 +138,7 @@ export function buildBlockLines(
72
138
  maxBodyLines = 30,
73
139
  maxBodyChars = 12000,
74
140
  ): string[] {
75
- const clip = (s: string) => (s.length > width ? s.slice(0, Math.max(0, width - 1)) : s);
141
+ const clip = (s: string) => clipToWidth(s, width);
76
142
  const lines: string[] = [];
77
143
  lines.push(clip(buildStatusLine(view)));
78
144
  for (const tool of view.tools) {
@@ -92,6 +158,7 @@ export function buildBlockLines(
92
158
  export class TerminalProgressRenderer {
93
159
  private readonly out: NodeJS.WritableStream & { columns?: number };
94
160
  private readonly frameMs: number;
161
+ private readonly animMs: number;
95
162
  private readonly maxBodyLines: number;
96
163
  private readonly maxBodyChars: number;
97
164
  private view: ProgressView;
@@ -99,20 +166,24 @@ export class TerminalProgressRenderer {
99
166
  private dirty = false;
100
167
  private timer: NodeJS.Timeout | null = null;
101
168
  private ended = false;
169
+ private animFrame = 0;
170
+ private animTimer: NodeJS.Timeout | null = null;
102
171
 
103
172
  constructor(opts: TerminalRendererOptions = {}) {
104
173
  this.out = opts.out ?? process.stdout;
105
174
  this.frameMs = opts.frameMs ?? 66;
175
+ this.animMs = opts.animMs ?? 300;
106
176
  this.maxBodyLines = opts.maxBodyLines ?? 30;
107
177
  this.maxBodyChars = opts.maxBodyChars ?? 12000;
108
178
  this.view = progressView();
109
179
  }
110
180
 
111
- /** 开始一轮区块:隐藏光标并渲染首帧 */
181
+ /** 开始一轮区块:隐藏光标并渲染首帧,随后启动生成中心跳动画 */
112
182
  begin(view: ProgressView): void {
113
183
  this.view = view;
114
184
  this.out.write(HIDE_CURSOR);
115
185
  this.renderNow(view);
186
+ this.startAnimation();
116
187
  }
117
188
 
118
189
  /** 标记视图已更新,按帧节流合并重绘(高频增量不闪烁) */
@@ -141,12 +212,13 @@ export class TerminalProgressRenderer {
141
212
  this.renderNow(this.view);
142
213
  }
143
214
 
144
- /** 结束一轮:定型终态区块(留在屏幕上),恢复光标 */
215
+ /** 结束一轮:定型终态区块(留在屏幕上),恢复光标,停止动画 */
145
216
  end(view: ProgressView): void {
146
217
  if (this.timer) {
147
218
  clearTimeout(this.timer);
148
219
  this.timer = null;
149
220
  }
221
+ this.stopAnimation();
150
222
  this.ended = true;
151
223
  this.renderNow(view);
152
224
  this.out.write(SHOW_CURSOR);
@@ -158,18 +230,42 @@ export class TerminalProgressRenderer {
158
230
  clearTimeout(this.timer);
159
231
  this.timer = null;
160
232
  }
233
+ this.stopAnimation();
161
234
  if (!this.ended) {
162
235
  this.out.write(SHOW_CURSOR);
163
236
  }
164
237
  }
165
238
 
239
+ private startAnimation(): void {
240
+ if (this.animMs <= 0 || this.animTimer) return;
241
+ this.animTimer = setInterval(() => {
242
+ this.animFrame++;
243
+ // 终态(done/stopped/error)由 end() 停止;此处兜底跳过,避免结束后还在重绘
244
+ if (this.ended || this.view.status !== "generating") return;
245
+ // 无新事件也持续重绘,点号动画让静止期(思考/工具运行中)也有动态感
246
+ this.renderNow(this.view);
247
+ }, this.animMs);
248
+ this.animTimer.unref?.();
249
+ }
250
+
251
+ private stopAnimation(): void {
252
+ if (this.animTimer) {
253
+ clearInterval(this.animTimer);
254
+ this.animTimer = null;
255
+ }
256
+ }
257
+
166
258
  private renderNow(view: ProgressView): void {
167
259
  const width = this.out.columns && this.out.columns > 0 ? this.out.columns : 80;
168
- const lines = buildBlockLines(view, width, this.maxBodyLines, this.maxBodyChars);
260
+ const lines = buildBlockLines(this.applyAnimation(view), width, this.maxBodyLines, this.maxBodyChars);
169
261
  const newLines = lines.length;
170
262
 
171
263
  if (this.blockLines > 0) {
172
- this.out.write(`\x1b[${this.blockLines}A`);
264
+ // 关键:光标此刻停在区块【最后一行】行尾,回到区块第一行只需上移
265
+ // blockLines - 1 行。上移 blockLines 行会每帧多上移 1 行,把区块上方
266
+ // 的历史内容逐行"吃掉"(用户看到的"一行一行往上吃")。
267
+ this.out.write(`\x1b[${this.blockLines - 1}A`);
268
+ this.out.write("\r");
173
269
  }
174
270
  for (let i = 0; i < lines.length; i++) {
175
271
  this.out.write(`\r\x1b[2K${lines[i]}`);
@@ -187,4 +283,12 @@ export class TerminalProgressRenderer {
187
283
  this.out.write("\x1b[J");
188
284
  this.blockLines = newLines;
189
285
  }
286
+
287
+ /** 生成中阶段在标题后叠加心跳点号动画;终态原样返回(buildBlockLines 保持纯函数可测) */
288
+ private applyAnimation(view: ProgressView): ProgressView {
289
+ if (view.status !== "generating") return view;
290
+ const base = view.headerTitle.replace(/\.\.\.\s*$/, "").trim();
291
+ const dots = ANIM_FRAMES[this.animFrame % ANIM_FRAMES.length];
292
+ return { ...view, headerTitle: `${base} ${dots}` };
293
+ }
190
294
  }
package/src/session.ts CHANGED
@@ -526,7 +526,7 @@ export function getEffectiveEffortForTool(tool: string, sessionId?: string): str
526
526
  const override = sessionEffortOverrides.get(sessionId);
527
527
  if (override) return override;
528
528
  }
529
- if (tool === "claude" || tool === "codex") {
529
+ if (tool === "claude" || tool === "codex" || tool === "ccc") {
530
530
  return getDefaultEffortForTool(tool);
531
531
  }
532
532
  return "";
@@ -597,7 +597,10 @@ export function getAdapterForTool(tool: string, sessionId?: string): ToolAdapter
597
597
  fastMode: effectiveFastMode,
598
598
  });
599
599
  } else if (tool === "ccc") {
600
- adapter = createCccAdapter({ model: effectiveModel || undefined });
600
+ adapter = createCccAdapter({
601
+ model: effectiveModel || undefined,
602
+ effort: effectiveEffort || undefined,
603
+ });
601
604
  } else {
602
605
  adapter = createClaudeAdapter({
603
606
  model: effectiveModel,