glmcode 0.1.16 → 0.1.18
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/CHANGELOG.md +15 -0
- package/package.json +1 -1
- package/src/cli.js +98 -40
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
# CHANGELOG
|
|
2
2
|
|
|
3
|
+
## 0.1.18 (2026-08-29)
|
|
4
|
+
|
|
5
|
+
- **退出总结页重做为「左 LOGO 右总结」**:左侧直接复用启动页的 GLM 大 LOGO(welcomeBlock 同一数据源,一个符号不改),右侧并排列出会话总结(时长/模型/执行指令/生成文件/Token 消耗),不再有"最终总结""干净不干净"等废话
|
|
6
|
+
- **宽屏并排、窄屏自动上下**:横屏(≥约 60 列)左 LOGO 右总结同屏居中;竖屏窄屏自动降级为 LOGO 在上、总结在下,任何宽度下总结都完整可读
|
|
7
|
+
- **列宽判定修正**:框线/块元素字符(U+2500+)按 2 列计算,根治 truncW 算窄导致的超宽换行
|
|
8
|
+
- 35 项 `--selftest` 全过;@xterm/headless 真实渲染验证 80x24 并排 / 40x36 上下两种布局
|
|
9
|
+
- 版本升级至 0.1.18
|
|
10
|
+
|
|
11
|
+
## 0.1.17 (2026-08-29)
|
|
12
|
+
|
|
13
|
+
- **根治"任务栏/输入框复制多份"**:引入终端滚动保护区(DECSTBM),对话内容只在顶部滚动区内滚动,输入行/下拉菜单/状态栏全部画在滚动区外 → 任何输出都永远无法把固定 UI 顶进历史产生副本
|
|
14
|
+
- **任务栏永远固定在可视区最底行**:电脑终端(无键盘)恒定在最底行;手机键盘弹出时终端变矮,状态栏自动上浮到键盘正上方(键盘收起后清屏重绘,无残留)
|
|
15
|
+
- 用 xterm.js 无头渲染器三重验证:多轮对话后状态栏唯一且恒定、键盘弹出上浮、键盘收起零残留;35 项 `--selftest` 全过
|
|
16
|
+
- 版本升级至 0.1.17
|
|
17
|
+
|
|
3
18
|
## 0.1.16 (2026-08-29)
|
|
4
19
|
|
|
5
20
|
- **自适应布局**:监听终端尺寸变化(手机横竖屏切换 / 窗口拉伸),输入态下立即重绘,不再残留错乱;各元素仍按实时列宽/行高计算
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "glmcode",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.18",
|
|
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
|
@@ -227,7 +227,7 @@ async function handleCommand(line){
|
|
|
227
227
|
const [cmd,...rest]=line.trim().split(/\s+/), arg=rest.join(" ");
|
|
228
228
|
if(cmd.slice(1) && skills.has(cmd.slice(1))){const s=skills.get(cmd.slice(1));messages.push({role:"system",content:s.content});console.log(color(`已加载技能:${s.name}`,C.green));console.log(color("技能指令已注入,现在输入你的任务:",C.gray));return;}
|
|
229
229
|
if(cmd==="/help"){showMenu("/");}
|
|
230
|
-
else if(cmd==="/clear"){messages=[];console.log(color("已清空对话",C.green));}
|
|
230
|
+
else if(cmd==="/clear"){messages=[];TUI.input="";TUI.cursor=0;term.clear();term.moveTo(1,1);render();term.moveTo(1,Math.max(1,th()-2));console.log(color("已清空对话",C.green));}
|
|
231
231
|
else if(cmd==="/thinking"){thinkingEnabled=!thinkingEnabled;config.thinking=thinkingEnabled;saveConfig();console.log(color(`思考模式: ${thinkingEnabled?"开启":"关闭"}`,C.yellow));}
|
|
232
232
|
else if(cmd==="/model"){if(arg){config.model=arg;saveConfig();console.log(color(`模型已切换: ${arg}`,C.green));}else console.log(`当前模型: ${config.model}`);}
|
|
233
233
|
else if(cmd==="/pwd")console.log(cwd);
|
|
@@ -286,8 +286,9 @@ async function handleCommand(line){
|
|
|
286
286
|
else console.log(color(`未知命令: ${cmd}(输入 / 查看全部)`,C.red));
|
|
287
287
|
}
|
|
288
288
|
|
|
289
|
-
|
|
290
|
-
|
|
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
|
|
303
|
-
for(const
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
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
|
-
|
|
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
|
|
327
|
-
function
|
|
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 "~";
|
|
@@ -360,12 +376,16 @@ function render(){
|
|
|
360
376
|
const count=TUI.items.length?Math.min(TUI.items.length,MAX):0;
|
|
361
377
|
// 输入行:菜单展开时上移,把下方空间让给菜单(菜单紧贴输入框正下方);缩回时回到 h-1
|
|
362
378
|
const inputRow=h-1-count;
|
|
363
|
-
//
|
|
379
|
+
// ★ 滚动保护区:对话内容区 = 1..inputRow-1。所有对话/命令输出只在此区域内滚动,
|
|
380
|
+
// 输入行、下拉菜单、状态栏全部画在滚动区之外 → 输出永远无法把固定UI顶进历史/复制出多份
|
|
381
|
+
term.scrollingRegion(1, Math.max(2, inputRow-1));
|
|
382
|
+
// 底部状态栏(固定在"可视区域"最后一行而非终端无限底;手机键盘弹出时 term.height 变小,
|
|
383
|
+
// 触发 resize 重绘后整条状态栏上浮到键盘正上方)
|
|
364
384
|
const st=truncW(statusText(),w);const stPad=" ".repeat(Math.max(0,w-strW(st)));
|
|
365
385
|
term.moveTo(1,h);term.eraseLine();
|
|
366
386
|
term.inverse(st+stPad);term.styleReset;
|
|
367
|
-
//
|
|
368
|
-
for(let j=
|
|
387
|
+
// 只擦输入行+菜单区(inputRow..h-1),绝不触碰内容区,避免误删对话末尾
|
|
388
|
+
for(let j=h-1;j>=inputRow;j--){term.moveTo(1,j);term.eraseLine();}
|
|
369
389
|
// 下拉菜单:输入行正下方从上到下展开,最多 MAX 行,灰色反色条(与状态栏一致)
|
|
370
390
|
if(count){
|
|
371
391
|
for(let j=0;j<count;j++){
|
|
@@ -439,7 +459,9 @@ function tuiInput(){
|
|
|
439
459
|
term.grabInput(false);
|
|
440
460
|
term.hideCursor();
|
|
441
461
|
tuiActive=false;
|
|
442
|
-
|
|
462
|
+
// 收起菜单并恢复全宽滚动区,光标移回滚动区底行再换行:后续输出只在对话区滚动
|
|
463
|
+
TUI.items=[];render();
|
|
464
|
+
term.moveTo(1,Math.max(1,th()-2));term("\n");
|
|
443
465
|
if(v.trim()){TUI.history.push(v.trim());SESSION.commands.push(v.trim());}
|
|
444
466
|
resolve(v);
|
|
445
467
|
}
|
|
@@ -455,7 +477,7 @@ function tuiInput(){
|
|
|
455
477
|
else if(name==="RIGHT"){if(TUI.cursor<TUI.input.length){TUI.cursor++;}render();}
|
|
456
478
|
else if(name==="HOME"){TUI.cursor=0;render();}
|
|
457
479
|
else if(name==="END"){TUI.cursor=TUI.input.length;render();}
|
|
458
|
-
else if(name==="CTRL_C"){term.removeListener("key",onKey);term.grabInput(false);term.hideCursor(true);tuiActive=false;term("\n");resolve(null);}
|
|
480
|
+
else if(name==="CTRL_C"){term.removeListener("key",onKey);term.grabInput(false);term.hideCursor(true);tuiActive=false;term.moveTo(1,Math.max(1,th()-2));term("\n");resolve(null);}
|
|
459
481
|
else if(name==="CTRL_L"){term.clear();term.moveTo(1,1);render();}
|
|
460
482
|
};
|
|
461
483
|
// 防监听器累积:注册前先移除上一轮的 key 监听,杜绝按键被重复处理
|
|
@@ -466,34 +488,70 @@ function tuiInput(){
|
|
|
466
488
|
}
|
|
467
489
|
function showSummaryAndExit(){
|
|
468
490
|
term.grabInput(false);term.hideCursor(true);
|
|
469
|
-
//
|
|
491
|
+
// 真正清屏:清掉本会话所有输出,像打开新页面
|
|
470
492
|
term.clear();term.moveTo(1,1);
|
|
493
|
+
const h=th(),w=tw();
|
|
471
494
|
const dur=Math.max(0,Math.round((Date.now()-SESSION.start)/1000));
|
|
472
495
|
const mm=String(Math.floor(dur/60)).padStart(2,"0"),ss=String(dur%60).padStart(2,"0");
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
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]]);
|
|
480
505
|
if(SESSION.commands.length){
|
|
481
|
-
const shown=SESSION.commands.slice(-
|
|
482
|
-
for(const c of shown)
|
|
483
|
-
if(SESSION.commands.length>shown.length)
|
|
484
|
-
}else
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
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
|
+
}
|
|
491
538
|
}
|
|
492
539
|
async function runTui(){
|
|
493
540
|
term.hideCursor();
|
|
494
541
|
term.clear();logo();
|
|
495
|
-
//
|
|
496
|
-
|
|
542
|
+
// 启动即建立滚动保护区+底部固定UI(输入行/状态栏),后续任何输出都不会顶走它们
|
|
543
|
+
render();
|
|
544
|
+
// 自适应:终端尺寸变化(手机横竖屏/键盘弹出/窗口拉伸)时立即重绘。
|
|
545
|
+
// 键盘弹出 → term.height 变小 → 状态栏/输入行自动上浮到键盘正上方
|
|
546
|
+
let lastH=th();
|
|
547
|
+
const onResize=()=>{
|
|
548
|
+
if(!tuiActive)return;
|
|
549
|
+
const h=th();
|
|
550
|
+
// 高度增大(键盘收起/横屏):先清屏再重绘,抹掉放大后露出的旧输入行/状态栏残留
|
|
551
|
+
if(h>lastH){term.clear();term.moveTo(1,1);}
|
|
552
|
+
render();
|
|
553
|
+
lastH=h;
|
|
554
|
+
};
|
|
497
555
|
term.on("resize",onResize);
|
|
498
556
|
while(true){
|
|
499
557
|
const input=await tuiInput();
|