dsh-neotui 0.2.1 → 0.2.2

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 CHANGED
@@ -1,5 +1,27 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.2.2 — 2026-08-16
4
+
5
+ ### Added
6
+
7
+ - 底栏在子代理状态后显示“有 x 条命令正在排队 Ctrl+N 查看详情”。
8
+ - Ctrl+N 打开精简排队命令面板;每条命令一行,支持 `dd` 删除。
9
+
10
+ ### Changed
11
+
12
+ - AskUser 窗口按实际内容自适应高度,长内容才启用滚动。
13
+ - 选项标签与描述只显示一次,完整描述按终端宽度换行。
14
+ - 自定义答案常驻在选项下一行并支持最多六行自动换行。
15
+ - 长正文使用 Ctrl+↑/Ctrl+↓、PgUp/PgDn、Home/End 和滚轮翻页。
16
+ - 权限审批窗口保留完整原因和命令,明确显示滚动提示与位置。
17
+
18
+ ### Fixed
19
+
20
+ - AskUser 自定义回答、真实跳过项、精确鼠标 hitbox 和文本光标编辑。
21
+ - 权限申请窗口支持 ↑/↓ 逐行滚动,不改变安全默认“拒绝”。
22
+ - 超长审批命令不再只保留前六行。
23
+ - Queue 面板移除未要求的编辑与 steering 操作,`dd` 同时兼容 text/key 事件。
24
+
3
25
  ## 0.2.1 — 2026-08-16
4
26
 
5
27
  ### Fixed
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-neotui",
3
- "version": "0.2.1",
3
+ "version": "0.2.2",
4
4
  "description": "Neo-TUI: mouse-driven terminal UI client for DeepSeek Harness (B-tier per dsh-tui-design.md)",
5
5
  "type": "module",
6
6
  "bin": {
package/src/panels.js CHANGED
@@ -1751,18 +1751,17 @@ export class QueuePanel extends Popup {
1751
1751
  const items = app.queueItems ?? [];
1752
1752
  const w = Math.max(24, Math.min(84, app.screen.w - 4));
1753
1753
  const h = Math.max(7, Math.min(24, app.screen.h - 4));
1754
- super({ x: Math.max(0, Math.floor((app.screen.w - w) / 2)), y: Math.max(0, Math.floor((app.screen.h - h) / 2)), w, h, title: "消息队列", lines: [], buttons: [{ label: "关闭", action: "close" }], onAction: () => app.closeOverlay(), scrollable: true });
1755
- this.app = app; this.items = items; this.sel = 0; this.pending = false; this.editInput = null; this.rebuild();
1754
+ super({ x: Math.max(0, Math.floor((app.screen.w - w) / 2)), y: Math.max(0, Math.floor((app.screen.h - h) / 2)), w, h, title: "排队命令 · ↑↓ 选择 · dd 删除 · Esc 关闭", lines: [], buttons: [], scrollable: true });
1755
+ this.app = app; this.items = items; this.sel = 0; this.pending = false; this.dArmed=false; this.rebuild();
1756
1756
  }
1757
1757
  rebuild() {
1758
- const lines = [[{ t: ` ↑↓选择 · e 编辑 · s 立即 steering · d 删除 · Esc关闭${this.pending ? " · 操作中…" : ""}`, fg: this.pending ? K.WARN : K.DIM }]];
1758
+ const lines = [];
1759
1759
  for (let i = 0; i < this.items.length; i++) {
1760
1760
  const item = this.items[i];
1761
1761
  const text = partsText(item.message?.content).replace(/\s+/g, " ");
1762
1762
  lines.push([{ t: `${i === this.sel ? "▸" : " "} ${item.placement === "queued" ? "⏳" : item.placement === "steering" ? "↪" : "ℹ"} ${truncate(text || item.id, this.w - 8)}`, fg: i === this.sel ? T.SELFG : K.TXT, bg: i === this.sel ? T.MENUSEL : -1 }]);
1763
1763
  }
1764
1764
  if (!this.items.length) lines.push([{ t: " (队列为空)", fg: K.FAINT }]);
1765
- if (this.editInput) lines.push([{ t: ` 编辑: ${truncate(this.editInput.value, this.w - 8)}`, fg: K.ACCENT }]);
1766
1765
  this.lines = lines;
1767
1766
  }
1768
1767
  syncItems(items) {
@@ -1782,7 +1781,6 @@ export class QueuePanel extends Popup {
1782
1781
  const action = kind === "edit" ? { kind, content: [{ type: "text", text: content }] } : { kind };
1783
1782
  await this.app.api.call("session.updateQueue", { sessionId: this.app.currentSession, itemId: item.id, action });
1784
1783
  if (kind === "remove") this.syncItems(this.items.filter((row) => row.id !== item.id));
1785
- this.editInput = null;
1786
1784
  } catch (e) {
1787
1785
  const code = this.#errorCode(e);
1788
1786
  if (code === "queue-item-not-found" || /queue-item-not-found/.test(e.message ?? "")) {
@@ -1793,31 +1791,19 @@ export class QueuePanel extends Popup {
1793
1791
  } else this.app.toast(`队列操作失败: ${e.message}`);
1794
1792
  } finally { this.pending = false; this.rebuild(); this.app.redraw(); }
1795
1793
  }
1796
- #beginEdit() {
1797
- const item = this.items[this.sel];
1798
- if (!item || item.placement !== "queued") { this.app.toast("只有排队消息可以编辑"); return; }
1799
- this.editInput = new Input({ x: this.x + 2, y: this.y + this.h - 3, w: this.w - 4, h: 1, prompt: "编辑: ", allowEmptyEnter: false, onEnter: (value) => this.#mutate("edit", value) });
1800
- this.editInput.setValue(partsText(item.message?.content));
1801
- this.rebuild();
1802
- }
1803
1794
  onKey(ev) {
1804
- if (this.editInput) {
1805
- if (ev.type === "key" && ev.name === "escape") { this.editInput = null; this.rebuild(); return true; }
1806
- if (this.editInput.onKey(ev)) { this.rebuild(); this.app.redraw(); return true; }
1807
- }
1795
+ const ch=ev.type==="text"?ev.text:ev.type==="key"&&ev.name==="char"?ev.key:null;
1796
+ if(ch==="d"){if(this.dArmed){this.dArmed=false;this.#mutate("remove");}else{this.dArmed=true;this.app.toast("再按 d 删除这条排队命令");}return true;}
1808
1797
  if (ev.type === "key") {
1809
1798
  if (ev.name === "escape") { this.app.closeOverlay(); return true; }
1810
- if (ev.name === "up") { this.sel = Math.max(0, this.sel - 1); this.rebuild(); return true; }
1811
- if (ev.name === "down") { this.sel = Math.min(this.items.length - 1, this.sel + 1); this.rebuild(); return true; }
1812
- if (ev.name === "char" && ev.key === "e" && !ev.ctrl) { this.#beginEdit(); return true; }
1813
- if (ev.name === "char" && ev.key === "s" && !ev.ctrl) { this.#mutate("steer"); return true; }
1814
- if (ev.name === "char" && ev.key === "d" && !ev.ctrl) { this.#mutate("remove"); return true; }
1799
+ if (ev.name === "up") { this.dArmed=false;this.sel = Math.max(0, this.sel - 1); this.rebuild(); return true; }
1800
+ if (ev.name === "down") { this.dArmed=false;this.sel = Math.min(this.items.length - 1, this.sel + 1); this.rebuild(); return true; }
1815
1801
  }
1816
- return super.onKey(ev);
1802
+ this.dArmed=false;return false;
1817
1803
  }
1818
1804
  onMouse(ev) {
1819
1805
  if (ev.kind === "press" && ev.button === 0) {
1820
- const idx = ev.y - this.y - 2;
1806
+ const idx = ev.y - this.y - 1;
1821
1807
  if (idx >= 0 && idx < this.items.length) { this.sel = idx; this.rebuild(); this.app.redraw(); return true; }
1822
1808
  }
1823
1809
  return super.onMouse(ev);
package/src/views.js CHANGED
@@ -2344,14 +2344,31 @@ function truncateText(s, n) {
2344
2344
  return s.length > n ? s.slice(0, n) + "\n…(截断)" : s;
2345
2345
  }
2346
2346
 
2347
+ function wrapDisplayText(value, width) {
2348
+ const out = [];
2349
+ for (const raw of String(value ?? "").split("\n")) {
2350
+ if (!raw) { out.push(""); continue; }
2351
+ let line = "", used = 0;
2352
+ for (const g of graphemes(raw)) {
2353
+ const gw = graphemeWidth(g);
2354
+ if (line && used + gw > width) { out.push(line); line = ""; used = 0; }
2355
+ line += g; used += gw;
2356
+ }
2357
+ out.push(line);
2358
+ }
2359
+ return out;
2360
+ }
2361
+
2347
2362
  // ---- Question popup ----
2348
2363
 
2349
2364
  export class QuestionPopup extends Popup {
2350
2365
  constructor({ app, frame }) {
2351
2366
  const questions = frame.questions ?? [];
2352
2367
  const planReview = questions.length === 1 && questions[0]?.intent?.kind === "plan-review";
2353
- const w = Math.max(12, Math.min(planReview ? 88 : 72, app.screen.w - 2));
2354
- const h = Math.max(5, Math.min(Math.max(5, app.screen.h - 2), planReview ? app.screen.h - 2 : questions.length * 5 + 8));
2368
+ const w = Math.max(12, Math.min(planReview ? 96 : 92, app.screen.w - 2));
2369
+ const wrapCount=(value,width)=>wrapDisplayText(value,width).length;
2370
+ const estimated=questions.reduce((n,q)=>n+1+wrapCount(q.question??"",w-4)+(q.detail?wrapCount(q.detail,w-4):0)+(q.options??[]).reduce((m,o)=>m+1+(o.description?wrapCount(o.description,w-10):0),0)+(q.intent?.kind==="plan-review"?0:4),0)+2;
2371
+ const h=Math.max(10,Math.min(app.screen.h-2,estimated));
2355
2372
  super({
2356
2373
  x: Math.max(0, Math.floor((app.screen.w - w) / 2)), y: Math.max(0, Math.floor((app.screen.h - h) / 2)),
2357
2374
  w, h, title: planReview ? "✎ 计划审阅" : "❓ 需要你的回答",
@@ -2387,44 +2404,44 @@ export class QuestionPopup extends Popup {
2387
2404
  const q = this.questions[this.questionIdx];
2388
2405
  if (!q) return;
2389
2406
  const draft = this.drafts[this.questionIdx];
2390
- let ly = this.y + 1;
2391
- screen.text(this.x + 2, ly++, truncate(`▎ ${q.header ?? `问题 ${this.questionIdx + 1}/${this.questions.length}`}`, this.w - 4), { fg: K.ACCENT, bold: true });
2392
- screen.text(this.x + 2, ly++, truncate(q.question ?? "", this.w - 4), { fg: K.TXT });
2393
- if (this.planReview) screen.text(this.x + 2, ly++, "Enter 执行计划 · ↓ 继续规划 · Esc 取消审阅", { fg: K.FAINT });
2394
2407
  const opts = q.options ?? [];
2395
- if (q.detail) {
2396
- const detailLines = String(q.detail).split("\n");
2397
- const optionRows = opts.reduce((n, option) => n + (option.description ? 2 : 1), 0);
2398
- const room = Math.max(1, this.y + this.h - 4 - ly - optionRows);
2399
- const maxScroll = Math.max(0, detailLines.length - room);
2400
- this.detailScrollY = Math.max(0, Math.min(this.detailScrollY, maxScroll));
2401
- this.detailPage = room;
2402
- const visible = detailLines.slice(this.detailScrollY, this.detailScrollY + room);
2403
- for (const detail of visible) screen.text(this.x + 2, ly++, truncate(detail, this.w - 4), { fg: K.DIM });
2404
- if (this.planReview && detailLines.length > room && ly < this.y + this.h - 3) {
2405
- const start = this.detailScrollY + 1, end = Math.min(detailLines.length, this.detailScrollY + room);
2406
- screen.text(this.x + 2, ly++, `计划 ${start}–${end} / ${detailLines.length} · PgUp/PgDn/Home/End/滚轮`, { fg: K.FAINT });
2407
- }
2408
- }
2408
+ const optionDescriptionRows=opts.reduce((n,o)=>n+(o.description?wrapDisplayText(o.description,this.w-10).length:0),0);
2409
+ const actionRows = opts.length + optionDescriptionRows + (this.planReview ? 0 : Math.min(6,Math.max(3,wrapDisplayText(draft.custom||"在此输入…",this.w-10).length))+2);
2410
+ const actionTop = Math.max(this.y + 5, this.y + this.h - 1 - Math.min(actionRows,this.h-6));
2411
+ const doc = [
2412
+ { text: `▎ ${q.header ?? `问题 ${this.questionIdx + 1}/${this.questions.length}`}`, fg: K.ACCENT, bold: true },
2413
+ ...wrapDisplayText(q.question ?? "", this.w - 4).map((text) => ({ text, fg: K.TXT })),
2414
+ ...(q.detail ? wrapDisplayText(q.detail, this.w - 4).map((text) => ({ text, fg: K.DIM })) : []),
2415
+ ];
2416
+
2417
+ const room = Math.max(1, actionTop - (this.y + 1));
2418
+ const maxScroll = Math.max(0, doc.length - room);
2419
+ this.detailScrollY = Math.max(0, Math.min(this.detailScrollY, maxScroll)); this.detailPage = room; this.detailTotal = doc.length;
2420
+ let ly = this.y + 1;
2421
+ for (const line of doc.slice(this.detailScrollY, this.detailScrollY + room)) screen.text(this.x + 2, ly++, truncate(line.text, this.w - 4), { fg: line.fg, attrs: line.bold ? 1 : 0 });
2422
+ if(maxScroll>0){screen.text(this.x+this.w-26,this.y,`正文 Ctrl+↑↓ 翻页 ${this.detailScrollY+1}-${Math.min(doc.length,this.detailScrollY+room)}/${doc.length}`,{fg:K.ACCENT,bg:T.BG2});}
2423
+ screen.hline(this.x+1,this.x+this.w-2,actionTop-1,"─",{fg:T.BORDER2,bg:T.BG2});screen.text(this.x+3,actionTop-1," 回答(↑/↓ 选择) ",{fg:K.ACCENT,bg:T.BG2});
2424
+ ly=actionTop;
2409
2425
  this.optionRows = []; this.optionHitboxes = [];
2410
- for (let i = 0; i < opts.length && ly < this.y + this.h - 3; i++) {
2426
+ for (let i = 0; i < opts.length; i++) {
2411
2427
  this.optionRows[i] = ly;
2412
2428
  const chosen = draft.selected.includes(opts[i].label);
2413
2429
  const cursor = this.selIdx === i;
2414
2430
  const glyph = q.multiSelect ? (chosen ? "☑" : "☐") : (chosen ? "●" : "○");
2415
2431
  const optionText=truncate(` ${cursor ? "▸" : " "} ${glyph} ${opts[i].label}`, this.w - 6);
2416
- this.optionHitboxes[i]={x1:this.x+2,x2:this.x+2+strWidth(optionText)-1,y1:ly,y2:ly+(opts[i].description?1:0)};
2432
+ const descLines=opts[i].description?wrapDisplayText(opts[i].description,this.w-10):[];
2433
+ this.optionHitboxes[i]={x1:this.x+2,x2:this.x+2+strWidth(optionText)-1,y1:ly,y2:ly+descLines.length};
2417
2434
  screen.text(this.x + 2, ly++, optionText, { fg: cursor ? T.SELFG : K.TXT, bg: cursor ? T.MENUSEL : -1 });
2418
- if (opts[i].description && ly < this.y + this.h - 3) screen.text(this.x + 7, ly++, truncate(opts[i].description, this.w - 10), { fg: K.FAINT });
2435
+ for(const line of descLines)screen.text(this.x+7,ly++,line,{fg:K.FAINT,bg:cursor?T.MENUSEL:-1});
2419
2436
  }
2420
- if (!this.planReview && ly < this.y + this.h - 3) {
2437
+ if (!this.planReview) {
2421
2438
  this.optionRows[opts.length] = ly;
2422
2439
  const cursor = this.selIdx === opts.length;
2423
2440
  const customText=truncate(` ${cursor ? "▸" : " "} ✎ 输入自己的回答`, this.w - 6);
2424
2441
  this.optionHitboxes[opts.length]={x1:this.x+2,x2:this.x+2+strWidth(customText)-1,y1:ly,y2:ly};
2425
2442
  screen.text(this.x + 2, ly++, customText, { fg: cursor ? T.SELFG : K.ACCENT, bg: cursor ? T.MENUSEL : -1 });
2426
- if(ly<this.y+this.h-3){const chars=Array.from(draft.custom),shown=this.customEditing?[...chars.slice(0,this.customCursor),"▏",...chars.slice(this.customCursor)].join(""):draft.custom;const inputText=truncate(` > ${shown||"在此输入…"}`,this.w-8);screen.text(this.x+2,ly++,inputText,{fg:this.customEditing?K.TXT:K.FAINT,bg:this.customEditing?T.BG2:-1});}
2427
- if(ly<this.y+this.h-3){this.optionRows[opts.length+1]=ly;const skip=this.selIdx===opts.length+1,skipText=` ${skip?"▸":" "} ↷ 跳过此问题`;this.optionHitboxes[opts.length+1]={x1:this.x+2,x2:this.x+2+strWidth(skipText)-1,y1:ly,y2:ly};screen.text(this.x+2,ly++,skipText,{fg:skip?T.SELFG:K.FAINT,bg:skip?T.MENUSEL:-1});}
2443
+ {const chars=Array.from(draft.custom),shown=this.customEditing?[...chars.slice(0,this.customCursor),"▏",...chars.slice(this.customCursor)].join(""):draft.custom;const inputLines=wrapDisplayText(shown||"在此输入…",this.w-10).slice(-6);for(let i=0;i<inputLines.length;i++)screen.text(this.x+4,ly++,`${i===0?"> ":" "}${inputLines[i]}`,{fg:this.customEditing?K.TXT:K.FAINT,bg:this.customEditing?T.BG2:-1});}
2444
+ {this.optionRows[opts.length+1]=ly;const skip=this.selIdx===opts.length+1,skipText=` ${skip?"▸":" "} ↷ 跳过此问题`;this.optionHitboxes[opts.length+1]={x1:this.x+2,x2:this.x+2+strWidth(skipText)-1,y1:ly,y2:ly};screen.text(this.x+2,ly++,skipText,{fg:skip?T.SELFG:K.FAINT,bg:skip?T.MENUSEL:-1});}
2428
2445
  }
2429
2446
  }
2430
2447
 
@@ -2441,8 +2458,9 @@ export class QuestionPopup extends Popup {
2441
2458
  }
2442
2459
 
2443
2460
  onMouse(ev) {
2444
- if (this.planReview && (ev.kind === "wheel-up" || ev.kind === "wheel-down")) {
2445
- this.detailScrollY = Math.max(0, this.detailScrollY + (ev.kind === "wheel-up" ? -3 : 3));
2461
+ if (ev.kind === "wheel-up" || ev.kind === "wheel-down") {
2462
+ const max=Math.max(0,(this.detailTotal??0)-(this.detailPage??1));
2463
+ this.detailScrollY = Math.max(0,Math.min(max,this.detailScrollY + (ev.kind === "wheel-up" ? -3 : 3)));
2446
2464
  this.app.redraw(); return true;
2447
2465
  }
2448
2466
  if (ev.kind === "press" && ev.button === 0) {
@@ -2469,14 +2487,16 @@ export class QuestionPopup extends Popup {
2469
2487
  const choices = count + (this.planReview ? 0 : 2);
2470
2488
  if (ev.type === "text") { if(this.customEditing||this.selIdx===count){this.customEditing=true;draft.selected=[];const chars=Array.from(draft.custom);chars.splice(this.customCursor,0,...Array.from(ev.text));draft.custom=chars.join("");this.customCursor+=Array.from(ev.text).length;draft.skipped=false;return true;} return false; }
2471
2489
  if (ev.type !== "key") return false;
2472
- if (this.planReview && ["pageup", "pagedown", "home", "end"].includes(ev.name)) {
2473
- const total = String(q?.detail ?? "").split("\n").length;
2490
+ if (["pageup", "pagedown", "home", "end"].includes(ev.name)) {
2491
+ const total=this.detailTotal??0,max=Math.max(0,total-this.detailPage);
2474
2492
  if (ev.name === "pageup") this.detailScrollY = Math.max(0, this.detailScrollY - this.detailPage);
2475
- else if (ev.name === "pagedown") this.detailScrollY = Math.min(Math.max(0, total - this.detailPage), this.detailScrollY + this.detailPage);
2493
+ else if (ev.name === "pagedown") this.detailScrollY = Math.min(max, this.detailScrollY + this.detailPage);
2476
2494
  else if (ev.name === "home") this.detailScrollY = 0;
2477
- else this.detailScrollY = Math.max(0, total - this.detailPage);
2495
+ else this.detailScrollY = max;
2478
2496
  this.app.redraw(); return true;
2479
2497
  }
2498
+ if(!this.customEditing&&ev.ctrl&&ev.name==="up"){this.detailScrollY=Math.max(0,this.detailScrollY-this.detailPage);return true;}
2499
+ if(!this.customEditing&&ev.ctrl&&ev.name==="down"){this.detailScrollY=Math.min(Math.max(0,(this.detailTotal??0)-this.detailPage),this.detailScrollY+this.detailPage);return true;}
2480
2500
  if(this.customEditing&&ev.name==="left"){this.customCursor=Math.max(0,this.customCursor-1);return true;}
2481
2501
  if(this.customEditing&&ev.name==="right"){this.customCursor=Math.min(Array.from(draft.custom).length,this.customCursor+1);return true;}
2482
2502
  if(this.customEditing&&ev.name==="home"){this.customCursor=0;return true;}
@@ -2538,13 +2558,14 @@ export class ApprovalPopup extends Popup {
2538
2558
  const maxW = Math.max(12, app.screen.w - 2);
2539
2559
  const w = Math.min(72, maxW);
2540
2560
  const command = app.chat?.toolCommandForCall?.(frame.callId) ?? null;
2541
- const lines = [
2542
- [{ t: ` ${frame.reason ?? `工具 ${frame.toolName ?? "tool"} 请求越权执行`}`, fg: K.WARN }],
2543
- ];
2544
- if (command) lines.push([{ t: " 将执行:", fg: K.DIM, underline: true }], ...String(command).split("\n").slice(0, 6).map((line) => [{ t: " " + truncate(line, w - 4), fg: K.TXT }]));
2561
+ const wrap=(value,width)=>{const out=[];for(const raw of String(value??"").split("\n")){const gs=graphemes(raw);let line="",used=0;for(const g of gs){const gw=graphemeWidth(g);if(used+gw>width&&line){out.push(line);line="";used=0;}line+=g;used+=gw;}out.push(line);}return out;};
2562
+ const reason=frame.reason ?? `工具 ${frame.toolName ?? "tool"} 请求越权执行`;
2563
+ const lines = [[{t:" 请求原因",fg:K.DIM,underline:true}],...wrap(reason,w-6).map(line=>[{t:` ${line}`,fg:K.WARN}])];
2564
+ if (command) lines.push([{ t: " 将执行", fg: K.DIM, underline: true }], ...wrap(command,w-6).map((line) => [{ t: " " + line, fg: K.TXT }]));
2565
+ lines.push([{t:" 导航:↑/↓ 逐行 · PgUp/PgDn 翻页 · 滚轮 · ←/→ 选择",fg:K.FAINT}]);
2545
2566
  super({
2546
- x: Math.max(0, Math.floor((app.screen.w - w) / 2)), y: Math.max(0, Math.floor(app.screen.h / 2) - 4),
2547
- w, h: Math.min(app.screen.h, command ? 10 : 7), title: "⚠ 工具需要授权",
2567
+ x: Math.max(0, Math.floor((app.screen.w - w) / 2)), y: Math.max(0, Math.floor((app.screen.h - Math.min(app.screen.h - 2, Math.max(10, Math.min(24, lines.length + 4)))) / 2)),
2568
+ w, h: Math.min(app.screen.h - 2, Math.max(10, Math.min(24, lines.length + 4))), title: "⚠ 工具需要授权 · 可滚动",
2548
2569
  lines,
2549
2570
  buttons: [
2550
2571
  { label: "允许一次", action: "allowed-once" },
@@ -3837,7 +3858,7 @@ export class App {
3837
3858
  if (ev.ctrl && ev.key === "t") { this.setMode("trajectory"); return; }
3838
3859
  if (ev.ctrl && ev.key === "e") { this.quickJumpStep(); return; }
3839
3860
  if (ev.ctrl && ev.key === "j") { this.showJobs(); return; }
3840
- if (ev.ctrl && ev.key === "u") { this.showQueue(); return; }
3861
+ if (ev.ctrl && ev.key === "n") { this.showQueue(); return; }
3841
3862
  if (ev.ctrl && ev.key === "y") {
3842
3863
  const next = busyEnter() === "queue" ? "steer" : "queue";
3843
3864
  saveTuiConfig({ busyEnter: next });
@@ -4031,7 +4052,6 @@ export class App {
4031
4052
  if (rawGoal && !["complete", "completed", "cleared"].includes(rawGoal.phase)) {
4032
4053
  row0.left.push({ t: ` 🎯 ${truncate(rawGoal.objective ?? "目标", 14)} · Ctrl+G `, fg: T.SELFG, bg: T.WARN, bold: true });
4033
4054
  }
4034
- if (this.queueItems.length) row0.left.push({ t: ` ⏳队列 ${this.queueItems.length} (Ctrl+U) `, fg: T.SELFG, bg: T.WARN, bold: true });
4035
4055
  if (this.sidebarVisible) row0.left.push({ t: " " + truncate(t || "(未选择会话)", 40) + " ", fg: T.TXT, bg: T.STATUSBG });
4036
4056
  else row0.left.push({ t: " " + truncate(t || "(未选择会话)", 40) + " ", fg: T.TXT, bg: T.STATUSBG });
4037
4057
  if (cur?.running) row0.left.push({ t: " ●运行 ", fg: T.OK, bg: T.STATUSBG });
@@ -4118,6 +4138,7 @@ export class App {
4118
4138
  row2.left.push({ t: ` ${done}已完成${failed > 0 ? ` ${failed}失败` : ""} `, fg: done > 0 ? T.OK : T.FAINT, bg: T.STATUSBG });
4119
4139
  row2.left.push({ t: ` ${subStats.running > 0 ? `${subStats.running} 个子代理运行中` : "没有子代理运行"} ${subStats.completed}已完成 `, fg: subStats.running > 0 ? T.PURPLE : T.FAINT, bg: T.STATUSBG, bold: subStats.running > 0 });
4120
4140
  if (sub) row2.left.push({ t: ` 🛰 ${truncate(sub.label ?? sub.mode ?? "子代理", 20)} `, fg: T.PURPLE, bg: T.STATUSBG });
4141
+ if(this.queueItems.length)row2.left.push({t:` 有${this.queueItems.length}条命令正在排队 Ctrl+N查看详情 `,fg:T.SELFG,bg:T.WARN,bold:true});
4121
4142
  row2.right.push({ t: " Ctrl+J 任务/子代理 ", fg: T.DIM, bg: T.STATUSBG });
4122
4143
  rows.push(row2);
4123
4144
  }
package/src/widgets.js CHANGED
@@ -788,8 +788,8 @@ export class Popup extends Widget {
788
788
  }
789
789
  if (this.maxScroll() > 0) {
790
790
  // "↑N" / "↓N" overflow indicators on the border row
791
- if (this.scrollY > 0) screen.text(this.x + this.w - 6, this.y, ` ↑${this.scrollY}`, { fg: T.ACCENT, bg: T.BG2 });
792
- if (this.scrollY < this.maxScroll()) screen.text(this.x + this.w - 8, this.y + this.h - 1, ` ↓${this.maxScroll() - this.scrollY}`, { fg: T.ACCENT, bg: T.BG2 });
791
+ if (this.scrollY > 0) screen.text(this.x + this.w - 10, this.y, ` ↑ ${this.scrollY}/${this.lines.length}`, { fg: T.ACCENT, bg: T.BG2 });
792
+ if (this.scrollY < this.maxScroll()) screen.text(this.x + this.w - 12, this.y + this.h - 1, ` ↓ ${this.scrollY + this.contentRows()}/${this.lines.length}`, { fg: T.ACCENT, bg: T.BG2 });
793
793
  }
794
794
  } else {
795
795
  for (const line of this.lines) draw(line);
@@ -832,6 +832,8 @@ export class Popup extends Widget {
832
832
  onKey(ev) {
833
833
  if (ev.type !== "key") return false;
834
834
  if (this.scrollable) {
835
+ if (ev.name === "up") { this.scrollY = Math.max(0, this.scrollY - 1); return true; }
836
+ if (ev.name === "down") { this.scrollY = Math.min(this.maxScroll(), this.scrollY + 1); return true; }
835
837
  if (ev.name === "pgup") { this.scrollY = Math.max(0, this.scrollY - this.contentRows()); return true; }
836
838
  if (ev.name === "pgdn") { this.scrollY = Math.min(this.maxScroll(), this.scrollY + this.contentRows()); return true; }
837
839
  }