dsh-neotui 0.2.0 → 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,37 @@
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
+
25
+ ## 0.2.1 — 2026-08-16
26
+
27
+ ### Fixed
28
+
29
+ - AskUser 问题始终显示“输入自己的回答”和常驻输入栏。
30
+ - “跳过此问题”改为真实、可导航的列表项,不再使用重复的底部按钮。
31
+ - 自定义回答支持左右键、Home、End、Backspace、Delete 和光标位置插入。
32
+ - 鼠标只在选项实际文字区域内生效,同行空白区域不再误提交。
33
+ - 自定义输入栏固定在自定义回答项下一行,跳过项固定在输入栏下方。
34
+
3
35
  ## 0.2.0 — 2026-08-16
4
36
 
5
37
  ### Added
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-neotui",
3
- "version": "0.2.0",
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,22 +2344,36 @@ 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 ? "✎ 计划审阅" : "❓ 需要你的回答",
2358
2375
  lines: ["", ...questions.map((q) => q.question ?? q.id)],
2359
- buttons: planReview ? [] : [
2360
- { label: "回答…", action: "custom" },
2361
- { label: "跳过", action: "cancel" },
2362
- ],
2376
+ buttons: [],
2363
2377
  });
2364
2378
  this.app = app;
2365
2379
  this.frame = frame;
@@ -2368,14 +2382,13 @@ export class QuestionPopup extends Popup {
2368
2382
  this.detailScrollY = 0;
2369
2383
  this.detailPage = 1;
2370
2384
  this.optionRows = [];
2385
+ this.optionHitboxes = [];
2371
2386
  this.questionIdx = 0;
2372
2387
  this.drafts = questions.map(() => ({ selected: [], custom: "", skipped: false }));
2373
2388
  this.selIdx = 0;
2374
- this.onAction = (btn) => {
2375
- if (btn.action === "cancel") this.#skipCurrent();
2376
- else if (btn.action === "custom") this.#continueCurrent();
2377
- else if (btn.action === "__cancel__") this.#cancel();
2378
- };
2389
+ this.customEditing = false;
2390
+ this.customCursor = 0;
2391
+ this.onAction = (btn) => { if (btn.action === "__cancel__") this.#cancel(); };
2379
2392
  this.#layout();
2380
2393
  }
2381
2394
 
@@ -2391,35 +2404,45 @@ export class QuestionPopup extends Popup {
2391
2404
  const q = this.questions[this.questionIdx];
2392
2405
  if (!q) return;
2393
2406
  const draft = this.drafts[this.questionIdx];
2394
- let ly = this.y + 1;
2395
- screen.text(this.x + 2, ly++, truncate(`▎ ${q.header ?? `问题 ${this.questionIdx + 1}/${this.questions.length}`}`, this.w - 4), { fg: K.ACCENT, bold: true });
2396
- screen.text(this.x + 2, ly++, truncate(q.question ?? "", this.w - 4), { fg: K.TXT });
2397
- if (this.planReview) screen.text(this.x + 2, ly++, "Enter 执行计划 · ↓ 继续规划 · Esc 取消审阅", { fg: K.FAINT });
2398
2407
  const opts = q.options ?? [];
2399
- if (q.detail) {
2400
- const detailLines = String(q.detail).split("\n");
2401
- const optionRows = opts.reduce((n, option) => n + (option.description ? 2 : 1), 0);
2402
- const room = Math.max(1, this.y + this.h - 4 - ly - optionRows);
2403
- const maxScroll = Math.max(0, detailLines.length - room);
2404
- this.detailScrollY = Math.max(0, Math.min(this.detailScrollY, maxScroll));
2405
- this.detailPage = room;
2406
- const visible = detailLines.slice(this.detailScrollY, this.detailScrollY + room);
2407
- for (const detail of visible) screen.text(this.x + 2, ly++, truncate(detail, this.w - 4), { fg: K.DIM });
2408
- if (this.planReview && detailLines.length > room && ly < this.y + this.h - 3) {
2409
- const start = this.detailScrollY + 1, end = Math.min(detailLines.length, this.detailScrollY + room);
2410
- screen.text(this.x + 2, ly++, `计划 ${start}–${end} / ${detailLines.length} · PgUp/PgDn/Home/End/滚轮`, { fg: K.FAINT });
2411
- }
2412
- }
2413
- this.optionRows = [];
2414
- for (let i = 0; i < opts.length && ly < this.y + this.h - 3; i++) {
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;
2425
+ this.optionRows = []; this.optionHitboxes = [];
2426
+ for (let i = 0; i < opts.length; i++) {
2415
2427
  this.optionRows[i] = ly;
2416
2428
  const chosen = draft.selected.includes(opts[i].label);
2417
2429
  const cursor = this.selIdx === i;
2418
2430
  const glyph = q.multiSelect ? (chosen ? "☑" : "☐") : (chosen ? "●" : "○");
2419
- screen.text(this.x + 2, ly++, truncate(` ${cursor ? "▸" : " "} ${glyph} ${opts[i].label}`, this.w - 6), { fg: cursor ? T.SELFG : K.TXT, bg: cursor ? T.MENUSEL : -1 });
2420
- 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 });
2431
+ const optionText=truncate(` ${cursor ? "▸" : " "} ${glyph} ${opts[i].label}`, this.w - 6);
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};
2434
+ screen.text(this.x + 2, ly++, optionText, { fg: cursor ? T.SELFG : K.TXT, bg: cursor ? T.MENUSEL : -1 });
2435
+ for(const line of descLines)screen.text(this.x+7,ly++,line,{fg:K.FAINT,bg:cursor?T.MENUSEL:-1});
2436
+ }
2437
+ if (!this.planReview) {
2438
+ this.optionRows[opts.length] = ly;
2439
+ const cursor = this.selIdx === opts.length;
2440
+ const customText=truncate(` ${cursor ? "▸" : " "} ✎ 输入自己的回答`, this.w - 6);
2441
+ this.optionHitboxes[opts.length]={x1:this.x+2,x2:this.x+2+strWidth(customText)-1,y1:ly,y2:ly};
2442
+ screen.text(this.x + 2, ly++, customText, { fg: cursor ? T.SELFG : K.ACCENT, bg: cursor ? 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});}
2421
2445
  }
2422
- if (draft.custom) screen.text(this.x + 2, Math.min(ly, this.y + this.h - 3), truncate(`自定义: ${draft.custom}`, this.w - 4), { fg: K.ACCENT });
2423
2446
  }
2424
2447
 
2425
2448
  #choose(i) {
@@ -2435,20 +2458,24 @@ export class QuestionPopup extends Popup {
2435
2458
  }
2436
2459
 
2437
2460
  onMouse(ev) {
2438
- if (this.planReview && (ev.kind === "wheel-up" || ev.kind === "wheel-down")) {
2439
- 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)));
2440
2464
  this.app.redraw(); return true;
2441
2465
  }
2442
2466
  if (ev.kind === "press" && ev.button === 0) {
2443
2467
  const q = this.questions[this.questionIdx];
2444
2468
  for (let i = 0; i < (q?.options?.length ?? 0); i++) {
2445
- const ly = this.optionRows[i];
2446
- if (ly != null && (ev.y === ly || (q.options[i].description && ev.y === ly + 1))) {
2447
- this.selIdx = i; this.#choose(i);
2469
+ const box=this.optionHitboxes[i];
2470
+ if (box && ev.x>=box.x1 && ev.x<=box.x2 && ev.y>=box.y1 && ev.y<=box.y2) {
2471
+ this.selIdx = i; this.customEditing = false; this.#choose(i);
2448
2472
  if (!q.multiSelect) this.#continueCurrent();
2449
2473
  return true;
2450
2474
  }
2451
2475
  }
2476
+ const count=q?.options?.length??0,customBox=this.optionHitboxes[count];
2477
+ if(customBox&&ev.x>=customBox.x1&&ev.x<=customBox.x2&&ev.y===customBox.y1){this.selIdx=count;this.customEditing=true;this.customCursor=Array.from(this.drafts[this.questionIdx].custom).length;this.drafts[this.questionIdx].selected=[];this.app.redraw();return true;}
2478
+ const skipBox=this.optionHitboxes[count+1];if(skipBox&&ev.x>=skipBox.x1&&ev.x<=skipBox.x2&&ev.y===skipBox.y1){this.selIdx=count+1;this.#skipCurrent();return true;}
2452
2479
  }
2453
2480
  return super.onMouse(ev);
2454
2481
  }
@@ -2456,22 +2483,33 @@ export class QuestionPopup extends Popup {
2456
2483
  onKey(ev) {
2457
2484
  const q = this.questions[this.questionIdx];
2458
2485
  const draft = this.drafts[this.questionIdx];
2459
- if (ev.type === "text") { draft.custom += ev.text; draft.skipped = false; return true; }
2460
- if (ev.type !== "key") return false;
2461
2486
  const count = q?.options?.length ?? 0;
2462
- if (this.planReview && ["pageup", "pagedown", "home", "end"].includes(ev.name)) {
2463
- const total = String(q?.detail ?? "").split("\n").length;
2487
+ const choices = count + (this.planReview ? 0 : 2);
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; }
2489
+ if (ev.type !== "key") return false;
2490
+ if (["pageup", "pagedown", "home", "end"].includes(ev.name)) {
2491
+ const total=this.detailTotal??0,max=Math.max(0,total-this.detailPage);
2464
2492
  if (ev.name === "pageup") this.detailScrollY = Math.max(0, this.detailScrollY - this.detailPage);
2465
- 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);
2466
2494
  else if (ev.name === "home") this.detailScrollY = 0;
2467
- else this.detailScrollY = Math.max(0, total - this.detailPage);
2495
+ else this.detailScrollY = max;
2468
2496
  this.app.redraw(); return true;
2469
2497
  }
2470
- if (ev.name === "up") { this.selIdx = Math.max(0, this.selIdx - 1); return true; }
2471
- if (ev.name === "down") { this.selIdx = Math.min(Math.max(0, count - 1), this.selIdx + 1); return true; }
2472
- if (ev.name === "char" && ev.key === " " && count) { this.#choose(this.selIdx); return true; }
2473
- if (ev.name === "backspace" && draft.custom) { draft.custom = Array.from(draft.custom).slice(0, -1).join(""); return true; }
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;}
2500
+ if(this.customEditing&&ev.name==="left"){this.customCursor=Math.max(0,this.customCursor-1);return true;}
2501
+ if(this.customEditing&&ev.name==="right"){this.customCursor=Math.min(Array.from(draft.custom).length,this.customCursor+1);return true;}
2502
+ if(this.customEditing&&ev.name==="home"){this.customCursor=0;return true;}
2503
+ if(this.customEditing&&ev.name==="end"){this.customCursor=Array.from(draft.custom).length;return true;}
2504
+ if (ev.name === "up") { this.customEditing=false;this.selIdx = Math.max(0, this.selIdx - 1); return true; }
2505
+ if (ev.name === "down") { this.customEditing=false;this.selIdx = Math.min(Math.max(0, choices - 1), this.selIdx + 1); return true; }
2506
+ if (ev.name === "char" && ev.key === " " && count && this.selIdx<count) { this.#choose(this.selIdx); return true; }
2507
+ if (ev.name === "backspace" && this.customEditing && this.customCursor>0) {const chars=Array.from(draft.custom);chars.splice(this.customCursor-1,1);draft.custom=chars.join("");this.customCursor--;return true;}
2508
+ if(ev.name==="delete"&&this.customEditing){const chars=Array.from(draft.custom);if(this.customCursor<chars.length){chars.splice(this.customCursor,1);draft.custom=chars.join("");}return true;}
2474
2509
  if (ev.name === "enter") {
2510
+ if(!this.planReview&&this.selIdx===count+1){this.#skipCurrent();return true;}
2511
+ if(!this.planReview&&this.selIdx===count&&!this.customEditing){this.customEditing=true;this.customCursor=Array.from(draft.custom).length;draft.selected=[];return true;}
2512
+ if(this.customEditing){if(!draft.custom.trim()){this.app.toast("请输入自己的回答");return true;}this.#continueCurrent();return true;}
2475
2513
  if (count && draft.selected.length === 0 && !draft.custom) this.#choose(this.selIdx);
2476
2514
  this.#continueCurrent(); return true;
2477
2515
  }
@@ -2491,7 +2529,7 @@ export class QuestionPopup extends Popup {
2491
2529
  }
2492
2530
 
2493
2531
  #advanceOrSubmit() {
2494
- if (this.questionIdx < this.questions.length - 1) { this.questionIdx++; this.selIdx = 0; this.detailScrollY = 0; this.#layout(); this.app.redraw(); return; }
2532
+ if (this.questionIdx < this.questions.length - 1) { this.questionIdx++; this.selIdx = 0; this.customEditing=false;this.customCursor=0;this.detailScrollY = 0; this.#layout(); this.app.redraw(); return; }
2495
2533
  this.#submit();
2496
2534
  }
2497
2535
 
@@ -2520,13 +2558,14 @@ export class ApprovalPopup extends Popup {
2520
2558
  const maxW = Math.max(12, app.screen.w - 2);
2521
2559
  const w = Math.min(72, maxW);
2522
2560
  const command = app.chat?.toolCommandForCall?.(frame.callId) ?? null;
2523
- const lines = [
2524
- [{ t: ` ${frame.reason ?? `工具 ${frame.toolName ?? "tool"} 请求越权执行`}`, fg: K.WARN }],
2525
- ];
2526
- 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}]);
2527
2566
  super({
2528
- x: Math.max(0, Math.floor((app.screen.w - w) / 2)), y: Math.max(0, Math.floor(app.screen.h / 2) - 4),
2529
- 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: "⚠ 工具需要授权 · 可滚动",
2530
2569
  lines,
2531
2570
  buttons: [
2532
2571
  { label: "允许一次", action: "allowed-once" },
@@ -3819,7 +3858,7 @@ export class App {
3819
3858
  if (ev.ctrl && ev.key === "t") { this.setMode("trajectory"); return; }
3820
3859
  if (ev.ctrl && ev.key === "e") { this.quickJumpStep(); return; }
3821
3860
  if (ev.ctrl && ev.key === "j") { this.showJobs(); return; }
3822
- if (ev.ctrl && ev.key === "u") { this.showQueue(); return; }
3861
+ if (ev.ctrl && ev.key === "n") { this.showQueue(); return; }
3823
3862
  if (ev.ctrl && ev.key === "y") {
3824
3863
  const next = busyEnter() === "queue" ? "steer" : "queue";
3825
3864
  saveTuiConfig({ busyEnter: next });
@@ -4013,7 +4052,6 @@ export class App {
4013
4052
  if (rawGoal && !["complete", "completed", "cleared"].includes(rawGoal.phase)) {
4014
4053
  row0.left.push({ t: ` 🎯 ${truncate(rawGoal.objective ?? "目标", 14)} · Ctrl+G `, fg: T.SELFG, bg: T.WARN, bold: true });
4015
4054
  }
4016
- if (this.queueItems.length) row0.left.push({ t: ` ⏳队列 ${this.queueItems.length} (Ctrl+U) `, fg: T.SELFG, bg: T.WARN, bold: true });
4017
4055
  if (this.sidebarVisible) row0.left.push({ t: " " + truncate(t || "(未选择会话)", 40) + " ", fg: T.TXT, bg: T.STATUSBG });
4018
4056
  else row0.left.push({ t: " " + truncate(t || "(未选择会话)", 40) + " ", fg: T.TXT, bg: T.STATUSBG });
4019
4057
  if (cur?.running) row0.left.push({ t: " ●运行 ", fg: T.OK, bg: T.STATUSBG });
@@ -4100,6 +4138,7 @@ export class App {
4100
4138
  row2.left.push({ t: ` ${done}已完成${failed > 0 ? ` ${failed}失败` : ""} `, fg: done > 0 ? T.OK : T.FAINT, bg: T.STATUSBG });
4101
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 });
4102
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});
4103
4142
  row2.right.push({ t: " Ctrl+J 任务/子代理 ", fg: T.DIM, bg: T.STATUSBG });
4104
4143
  rows.push(row2);
4105
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
  }