glmcode 0.1.17 → 0.1.19

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 (3) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/package.json +1 -1
  3. package/src/cli.js +74 -33
package/CHANGELOG.md CHANGED
@@ -1,5 +1,18 @@
1
1
  # CHANGELOG
2
2
 
3
+ ## 0.1.19 (2026-08-29)
4
+
5
+ - **修复版本号显示旧版**:`src/cli.js` 的 VERSION 常量此前硬编码为 0.1.16 且未随发版更新,导致已装 0.1.18 但启动页/--version 仍显示 0.1.16;现已与 package.json 同步
6
+ - 版本升级至 0.1.19
7
+
8
+ ## 0.1.18 (2026-08-29)
9
+
10
+ - **退出总结页重做为「左 LOGO 右总结」**:左侧直接复用启动页的 GLM 大 LOGO(welcomeBlock 同一数据源,一个符号不改),右侧并排列出会话总结(时长/模型/执行指令/生成文件/Token 消耗),不再有"最终总结""干净不干净"等废话
11
+ - **宽屏并排、窄屏自动上下**:横屏(≥约 60 列)左 LOGO 右总结同屏居中;竖屏窄屏自动降级为 LOGO 在上、总结在下,任何宽度下总结都完整可读
12
+ - **列宽判定修正**:框线/块元素字符(U+2500+)按 2 列计算,根治 truncW 算窄导致的超宽换行
13
+ - 35 项 `--selftest` 全过;@xterm/headless 真实渲染验证 80x24 并排 / 40x36 上下两种布局
14
+ - 版本升级至 0.1.18
15
+
3
16
  ## 0.1.17 (2026-08-29)
4
17
 
5
18
  - **根治"任务栏/输入框复制多份"**:引入终端滚动保护区(DECSTBM),对话内容只在顶部滚动区内滚动,输入行/下拉菜单/状态栏全部画在滚动区外 → 任何输出都永远无法把固定 UI 顶进历史产生副本
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "glmcode",
3
- "version": "0.1.17",
3
+ "version": "0.1.19",
4
4
  "description": "GLM coding agent CLI with shell tools, skills (SKILL.md/zip auto-extract), streaming chat, slash-command menu and bottom status bar. Auto-installs its dependencies.",
5
5
  "type": "module",
6
6
  "bin": {
package/src/cli.js CHANGED
@@ -7,7 +7,7 @@ import {fileURLToPath} from "node:url";
7
7
  import termkit from "terminal-kit";
8
8
  const term = termkit.terminal;
9
9
 
10
- const VERSION="0.1.16";
10
+ const VERSION="0.1.19";
11
11
  const BASE_URL="https://open.bigmodel.cn/api/paas/v4";
12
12
  const API_KEYS_URL="https://open.bigmodel.cn/usercenter/proj-mgmt/apikeys";
13
13
  const HOME=os.homedir();
@@ -286,8 +286,9 @@ async function handleCommand(line){
286
286
  else console.log(color(`未知命令: ${cmd}(输入 / 查看全部)`,C.red));
287
287
  }
288
288
 
289
- function logo(){
290
- const w=tw(),h=th();
289
+ /* ---- 启动欢迎块(LOGO+提示+信息面板):单一定义,启动页与退出总结页共用,保证一字不差 ---- */
290
+ function welcomeBlock(){
291
+ const w=tw();
291
292
  const art=[
292
293
  " ██████╗ ██╗ ███╗ ███╗",
293
294
  "██╔════╝ ██║ ████╗ ████║",
@@ -299,19 +300,33 @@ function logo(){
299
300
  // 启动信息面板:填充中间空白区域,矮屏自动紧凑
300
301
  const tip=truncW(" Coding Agent · v"+VERSION+" · 输入 / 弹菜单 · ↑↓ 选择 · 回车执行 · Ctrl+C 退出看总结",w);
301
302
  const cmdKeys=[["/help","全部命令"],["/clear","清空对话"],["/skills","技能列表与目录"],["/skill","管理技能(可新建)"],["/model","切换模型"],["/exit","退出"]];
302
- const panel=[""];
303
- for(const [k,d] of cmdKeys)panel.push(" "+color(k,C.cyanB)+" "+color(truncW(d,Math.max(4,w-6)),C.gray));
304
- panel.push("");
305
- panel.push(truncW(" · 技能自动加载: ~/.glmcode/skills/ 或包内 skills/ · /skill new <名字> 新建",w));
306
- panel.push(truncW(" · 全平台兼容 Linux/Android(termux)/Windows/macOS · 底部状态栏常驻",w));
307
- const blockH=art.length+1+panel.length;
303
+ const rows=[];
304
+ for(const a of art)rows.push([[truncW(a,w),"cyanB"]]);
305
+ rows.push([[tip,"gray"]]);
306
+ rows.push([["",null]]);
307
+ for(const [k,d] of cmdKeys)rows.push([[" "+k,"cyanB"],[" "+truncW(d,Math.max(4,w-6)),"gray"]]);
308
+ rows.push([["",null]]);
309
+ rows.push([[truncW(" · 技能自动加载: ~/.glmcode/skills/ 或包内 skills/ · /skill new <名字> 新建",w),null]]);
310
+ rows.push([[truncW(" · 全平台兼容 Linux/Android(termux)/Windows/macOS · 底部状态栏常驻",w),null]]);
311
+ return rows;
312
+ }
313
+ /* 在 (x,y) 渲染一行多片段文本(每个片段 [文本, 颜色样式],样式为空串/undefined/null 用默认色) */
314
+ function drawRow(row,x,y){
315
+ term.moveTo(x,y);
316
+ for(const [txt,st] of row){
317
+ if(!txt)continue;
318
+ if(st)term(color(txt,st==="cyanB"?C.cyanB:st==="gray"?C.gray:st)); else term(txt);
319
+ }
320
+ term.styleReset;
321
+ }
322
+ function logo(){
323
+ const w=tw(),h=th();
324
+ const rows=welcomeBlock();
325
+ const blockH=rows.length;
308
326
  // 欢迎块整体垂直居中(底部恒留2行给输入行+状态栏);过矮/超屏则顶部紧凑
309
327
  let start=Math.max(1,Math.floor((h-blockH-2)/2));
310
328
  if(start+blockH>h-2)start=1;
311
- term.moveTo(1,start);
312
- for(const a of art)console.log(color(truncW(a,w),C.cyanB));
313
- console.log(color(tip,C.gray));
314
- for(const p of panel)console.log(truncW(p,w));
329
+ for(let i=0;i<rows.length;i++)drawRow(rows[i],1,start+i);
315
330
  }
316
331
 
317
332
  /* =============== 真 TUI:悬浮菜单 + 上下键 + 实时过滤 + 底部状态栏 =============== */
@@ -323,8 +338,9 @@ function th(){return (term.height>0&&isFinite(term.height))?term.height:24;}
323
338
  const MENU_MAX=4;
324
339
  /* 菜单最多 4 行(更短,输入框下方紧凑展示)*/
325
340
  /* ---- 显示列宽工具:CJK 等宽字符占 2 列,按列截断,杜绝超宽触发终端换行滚动 ---- */
326
- function strW(s){let w=0;for(const ch of String(s)){w+=ch.codePointAt(0)>0x2e7f?2:1;}return w;}
327
- function truncW(s,maxW){let w=0,o="";for(const ch of String(s)){const c=ch.codePointAt(0),cw=c>0x2e7f?2:1;if(w+cw>maxW)break;w+=cw;o+=ch;}return o;}
341
+ function isWide(c){return c>=0x2500;} // U+2500 起(框线/块元素/CJK 符号/汉字)均按 2 列,避免 truncW 算窄导致超宽换行
342
+ function strW(s){let w=0;for(const ch of String(s)){w+=isWide(ch.codePointAt(0))?2:1;}return w;}
343
+ function truncW(s,maxW){let w=0,o="";for(const ch of String(s)){const c=ch.codePointAt(0),cw=isWide(c)?2:1;if(w+cw>maxW)break;w+=cw;o+=ch;}return o;}
328
344
  function shortDir(){
329
345
  const home=HOME.replace(/[\\/]+$/,"");
330
346
  if(cwd===home)return "~";
@@ -472,28 +488,53 @@ function tuiInput(){
472
488
  }
473
489
  function showSummaryAndExit(){
474
490
  term.grabInput(false);term.hideCursor(true);
475
- // 真正清屏:像打开新页面一样,清掉本会话所有输出
491
+ // 真正清屏:清掉本会话所有输出,像打开新页面
476
492
  term.clear();term.moveTo(1,1);
493
+ const h=th(),w=tw();
477
494
  const dur=Math.max(0,Math.round((Date.now()-SESSION.start)/1000));
478
495
  const mm=String(Math.floor(dur/60)).padStart(2,"0"),ss=String(dur%60).padStart(2,"0");
479
- const bar="=".repeat(Math.max(8,Math.min(tw()-2,42)));
480
- console.log(color(bar,C.cyan));
481
- console.log(color(" GLMCode 会话总结",C.cyanB));
482
- console.log(color(bar,C.cyan));
483
- console.log("");
484
- console.log(` 时长: ${mm}:${ss} 模型: ${config.model}`);
485
- console.log(` 执行指令: ${SESSION.commands.length} 条`);
496
+ // 右侧总结数据:直接列出,无废话
497
+ const right=[];
498
+ right.push([[" 会话总结","cyanB"]]);
499
+ right.push([[" "+"─".repeat(14),"gray"]]);
500
+ right.push([["",null]]);
501
+ right.push([[" 时长: "+mm+":"+ss,null]]);
502
+ right.push([[" 模型: "+config.model,null]]);
503
+ right.push([["",null]]);
504
+ right.push([[" 执行指令: "+SESSION.commands.length+" 条",null]]);
486
505
  if(SESSION.commands.length){
487
- const shown=SESSION.commands.slice(-10);
488
- for(const c of shown)console.log(color(" · "+truncW(c,Math.max(8,tw()-8)),C.gray));
489
- if(SESSION.commands.length>shown.length)console.log(color(` … 还有 ${SESSION.commands.length-shown.length} 条`,C.gray));
490
- }else console.log(color(" (无)",C.gray));
491
- console.log(` 生成文件: ${SESSION.files.length} 个`);
492
- if(SESSION.files.length){for(const f of SESSION.files.slice(-8))console.log(color(" · "+truncW(f,Math.max(8,tw()-8)),C.gray));}
493
- else console.log(color(" ()",C.gray));
494
- console.log(` Token 消耗: ${SESSION.tokens}`);
495
- console.log("");
496
- console.log(color(" 已清屏,像打开新页面一样干净",C.green));
506
+ const shown=SESSION.commands.slice(-8);
507
+ for(const c of shown)right.push([[" · "+truncW(c,30),"gray"]]);
508
+ if(SESSION.commands.length>shown.length)right.push([[" … 还有 "+SESSION.commands.length+" 条","gray"]]);
509
+ }else right.push([[" (无)","gray"]]);
510
+ right.push([["",null]]);
511
+ right.push([[" 生成文件: "+SESSION.files.length+" ",null]]);
512
+ if(SESSION.files.length){for(const f of SESSION.files.slice(-6))right.push([[" · "+truncW(f,30),"gray"]]);}
513
+ else right.push([[" (无)","gray"]]);
514
+ right.push([["",null]]);
515
+ right.push([[" Token 消耗: "+SESSION.tokens,null]]);
516
+ // 左侧 = 启动 LOGO 页(与启动时完全一致,一个符号不改)
517
+ const rows=welcomeBlock();
518
+ // 布局:右栏保底 24 列,左栏宽度 = 剩余;左栏放不下 LOGO(窄屏)则降级为上下布局
519
+ const R_MIN=24;
520
+ const rightX=w-R_MIN+1;
521
+ const L=rightX-3;
522
+ const stacked=L<20;
523
+ const total=stacked?rows.length+1+right.length:Math.max(rows.length,right.length);
524
+ let start=Math.max(1,Math.floor((h-total)/2));
525
+ if(start+total>h-1)start=1;
526
+ if(stacked){
527
+ // 窄屏:左 LOGO 在上(与启动页逐字一致),总结在下,保证总结完整可读
528
+ for(let i=0;i<rows.length;i++)drawRow(rows[i],1,start+i);
529
+ const sy=start+rows.length+1;
530
+ for(let j=0;j<right.length;j++)drawRow(right[j].map(([t,st])=>[truncW(t,w),st]),1,sy+j);
531
+ }else{
532
+ // 宽屏:左 LOGO 右总结,同一行并排
533
+ for(let i=0;i<total;i++){
534
+ if(i<rows.length)drawRow(rows[i].map(([t,st])=>[truncW(t,L),st]),1,start+i);
535
+ if(i<right.length)drawRow(right[i].map(([t,st])=>[truncW(t,R_MIN),st]),rightX,start+i);
536
+ }
537
+ }
497
538
  }
498
539
  async function runTui(){
499
540
  term.hideCursor();