glmcode 0.1.12 → 0.1.13

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 +10 -0
  2. package/package.json +1 -1
  3. package/src/cli.js +46 -26
package/CHANGELOG.md CHANGED
@@ -1,5 +1,15 @@
1
1
  # CHANGELOG
2
2
 
3
+ ## 0.1.13 (2026-08-28)
4
+
5
+ - **修复手机 Termux 窄屏 TUI 渲染故障**:状态栏/菜单原按「字符数」截断,CJK 宽字符实际占 2 列,超宽后触发终端自动换行+滚动,导致旧菜单擦不掉、`/exit` 叠影、状态栏混乱
6
+ - 新增 `strW` / `truncW` 列宽工具:所有文本按「显示列宽」截断(CJK 等宽字符计 2 列),从根上杜绝超宽换行
7
+ - 状态栏、菜单、输入行、光标定位全部改用列宽计算
8
+ - 修复菜单绘制顺序:改为**底部对齐正序**绘制,此前第一项画在最底行导致菜单整体倒序
9
+ - 修复滚动指示符 `⋯`/`⋮`:改为行首绘制,不再压盖菜单文本;同时修复「上滚指示永不显示」的 bug
10
+ - 40x20 PTY + pyte 仿真验证全阶段无超宽行,菜单/过滤/退格/滚动/超长输入全部正常
11
+ - 版本升级至 0.1.13
12
+
3
13
  ## 0.1.12 (2026-08-28)
4
14
 
5
15
  - **新增 `COMPATIBILITY.md` 跨平台兼容性说明**:覆盖 Windows / macOS / Linux / Android(Termux) / FreeBSD 等全部支持系统,含功能支持矩阵、各平台安装方式、跨平台自动适配机制、已知限制与自测清单
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "glmcode",
3
- "version": "0.1.12",
3
+ "version": "0.1.13",
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.12";
10
+ const VERSION="0.1.13";
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();
@@ -227,14 +227,25 @@ function handleCommand(line){
227
227
  }
228
228
 
229
229
  function logo(){
230
- const rows=[" ██████╗ ██╗ ███╗ ███╗","██╔════╝ ██║ ████╗ ████║","██║ ███╗ ██║ ██╔████╔██║","██║ ██║ ██║ ██║╚██╔╝██║","╚██████╔╝ ███████╗ ██║ ╚═╝ ██║"," ╚═════╝ ╚══════╝ ╚═╝ ╚═╝"];
231
- const W=44;console.log(color("".padStart(W),C.cyan));console.log(color("✧ GLM ✧".padStart((W+"✧ GLM ✧".length)/2),C.cyanB));for(const r of rows)console.log(color(r.padStart((W+r.length)/2),C.cyanB));console.log(color("GLM Code · Coding Agent",C.cyanB));console.log(color(`v${VERSION}`,C.gray));console.log(color("输入 / 弹菜单 · ↑↓ 选择 · 回车执行 · Ctrl+C 退出",C.gray));console.log("");
230
+ const w=tw();
231
+ console.log(color(` GLM Code · Coding Agent v${VERSION} `.slice(0,w),C.cyanB));
232
+ console.log(color(" 输入 / 弹菜单 · ↑↓ 选择 · 回车执行 · Ctrl+C 退出".slice(0,w),C.gray));
233
+ console.log("");
232
234
  }
233
235
 
234
236
  /* =============== 真 TUI:悬浮菜单 + 上下键 + 实时过滤 + 底部状态栏 =============== */
235
- const PROMPT=" 输入( / 菜单 · ↑↓选择 · Enter执行) ┃ ";
237
+ const PROMPT="glm> ";
236
238
  const TUI={input:"",cursor:0,items:[],sel:0,scroll:0,history:[],histIdx:-1};
237
- function statusText(){return ` 思考:${thinkingEnabled?"开":"关"} 模型:${config.model} 技能:${skills.size} 目录:${cwd} `;}
239
+ function tw(){return (term.width>0&&isFinite(term.width))?term.width:40;}
240
+ function th(){return (term.height>0&&isFinite(term.height))?term.height:24;}
241
+ const MENU_MAX=5;
242
+ /* ---- 显示列宽工具:CJK 等宽字符占 2 列,按列截断,杜绝超宽触发终端换行滚动 ---- */
243
+ function strW(s){let w=0;for(const ch of String(s)){w+=ch.codePointAt(0)>0x2e7f?2:1;}return w;}
244
+ 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;}
245
+ function statusText(){
246
+ const dir=cwd.split(/[\\/]/).pop()||cwd;
247
+ return ` 思考:${thinkingEnabled?"开":"关"} 模型:${config.model} 技能:${skills.size} 目录:${dir} `;
248
+ }
238
249
  function filterItems(input){
239
250
  const s=(input||"").trim();
240
251
  if(!s.startsWith("/"))return [];
@@ -246,43 +257,47 @@ function filterItems(input){
246
257
  return n.startsWith(name)||n.includes(name)||d.toLowerCase().includes(name);
247
258
  }).map(([c,d])=>`${c} ${d}`);
248
259
  }
249
- function menuMax(){const h=term.height>0?term.height:24;return Math.max(1,Math.min(10,h-3));}
260
+ function menuMax(){return MENU_MAX;}
250
261
  function keepSelVisible(){
251
- const MAX=menuMax();
262
+ const MAX=MENU_MAX;
252
263
  if(TUI.sel<TUI.scroll)TUI.scroll=TUI.sel;
253
264
  else if(TUI.sel>=TUI.scroll+MAX)TUI.scroll=TUI.sel-MAX+1;
254
265
  }
255
266
  function render(){
256
- const h=term.height>0?term.height:24, w=term.width>0?term.width:80;
257
- const inputRow=h-1, MAX=menuMax();
258
- term.saveCursor();
259
- // 底部状态栏(常驻)
267
+ const h=th(), w=tw();
268
+ const inputRow=h-1, MAX=MENU_MAX;
269
+ // 底部状态栏(固定在最后一行;按列宽截断防超宽换行顶乱布局)
260
270
  term.moveTo(1,h);term.eraseLine();
261
- const st=statusText().padEnd(Math.max(1,w-1));
262
- term.inverse(st.slice(0,w));term.styleReset;
263
- // 擦除菜单区
271
+ term.inverse(truncW(statusText(),w));term.styleReset;
272
+ // 先擦除菜单区(输入行上方 MAX 行),保证每次重绘不残留
264
273
  for(let j=0;j<MAX;j++){term.moveTo(1,inputRow-1-j);term.eraseLine();}
265
- // 悬浮菜单
274
+ // 下拉菜单:输入行上方底部对齐展开,最多 MAX 行(第1项在顶部、末项紧贴输入行)
266
275
  if(TUI.items.length){
267
276
  const count=Math.min(TUI.items.length,MAX);
268
277
  for(let j=0;j<count;j++){
269
278
  const idx=TUI.scroll+j;
270
- const row=inputRow-1-j;
271
- term.moveTo(1,row);
272
- const line=" "+TUI.items[idx]+" ";
273
- if(idx===TUI.sel)term.bgBrightBlue.black(line.slice(0,w));
274
- else term.gray(line.slice(0,w));
279
+ term.moveTo(1,inputRow-count+j);
280
+ const mark=(j===0&&TUI.scroll>0)?"⋯":(j===count-1&&TUI.scroll+count<TUI.items.length)?"⋮":" ";
281
+ const line=" "+mark+" "+truncW(TUI.items[idx],w-3);
282
+ if(idx===TUI.sel)term.bgBrightBlue.black(truncW(line,w));
283
+ else term.gray(truncW(line,w));
275
284
  term.styleReset;
276
285
  }
277
286
  }
278
- // 输入行
287
+ // 输入行(固定在倒数第二行,与状态栏互不重叠)
279
288
  term.moveTo(1,inputRow);term.eraseLine();
280
- term.cyan.bold(PROMPT);
281
289
  const before=TUI.input.slice(0,TUI.cursor), after=TUI.input.slice(TUI.cursor);
282
- term(before);term.cyan(after);term.styleReset;
283
- // 光标
284
- term.restoreCursor();
285
- term.moveTo(Math.min(1+PROMPT.length+TUI.cursor,w),inputRow);
290
+ term.cyan.bold(PROMPT);
291
+ if(strW(PROMPT)+strW(before)+strW(after)<=w){
292
+ term(before);term.cyan(after);term.styleReset;
293
+ term.moveTo(Math.min(1+strW(PROMPT)+strW(before),w),inputRow);
294
+ }else{
295
+ // 超宽防护:只显示输入末尾,防止换行把状态栏/菜单顶乱
296
+ const keep=Math.max(1,w-strW(PROMPT)-2);
297
+ const tail=truncW(before+after,keep);
298
+ term("…"+tail);term.styleReset;
299
+ term.moveTo(Math.min(1+strW(PROMPT)+keep+1,w),inputRow);
300
+ }
286
301
  term.hideCursor(false);
287
302
  }
288
303
  function refreshFilter(){
@@ -294,6 +309,8 @@ function tuiInput(){
294
309
  return new Promise(resolve=>{
295
310
  TUI.input="";TUI.cursor=0;TUI.items=[];TUI.sel=0;TUI.scroll=0;TUI.histIdx=-1;
296
311
  term.grabInput(true);
312
+ // 首轮全屏清理一次,避免启动时终端高度/宽度未就绪导致的错位残留
313
+ if(!tuiInput._cleared){term.clear();tuiInput._cleared=true;}
297
314
  render();
298
315
  const onKey=(name,matches,data)=>{
299
316
  if(data&&data.isCharacter&&name.length===1&&name.charCodeAt(0)>=32){
@@ -339,7 +356,10 @@ function tuiInput(){
339
356
  else if(name==="CTRL_C"){term.removeListener("key",onKey);term.grabInput(false);term.hideCursor(true);term("\n");resolve(null);}
340
357
  else if(name==="CTRL_L"){term.clear();term.moveTo(1,1);render();}
341
358
  };
359
+ // 防监听器累积:注册前先移除上一轮的 key 监听,杜绝按键被重复处理
360
+ if(tuiInput._key)term.removeListener("key",tuiInput._key);
342
361
  term.on("key",onKey);
362
+ tuiInput._key=onKey;
343
363
  });
344
364
  }
345
365
  async function runTui(){