dsh-neotui 0.1.4 → 0.1.5

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-neotui",
3
- "version": "0.1.4",
3
+ "version": "0.1.5",
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
@@ -1386,6 +1386,7 @@ export class JobsPanel extends Popup {
1386
1386
  title: "后台任务(Ctrl+J 查看详情)", lines: [],
1387
1387
  buttons: [{ label: "关闭(q)", action: "close" }],
1388
1388
  onAction: () => app.closeOverlay(),
1389
+ scrollable: true, // expanded details scroll instead of being clipped
1389
1390
  });
1390
1391
  this.app = app;
1391
1392
  this.jobs = jobs;
@@ -1394,19 +1395,40 @@ export class JobsPanel extends Popup {
1394
1395
  this.rowOf = []; // rendered line → job index (-1 = chrome/detail)
1395
1396
  this.rebuild();
1396
1397
  }
1398
+ /** Width-aware character cut (no ellipsis — continuation chunks follow). */
1399
+ static #cutWidth(s, w) {
1400
+ let out = "", cw = 0;
1401
+ for (const ch of s) {
1402
+ const c = strWidth(ch);
1403
+ if (cw + c > w) break;
1404
+ out += ch; cw += c;
1405
+ }
1406
+ return out;
1407
+ }
1397
1408
  #detailLines(j) {
1409
+ // Expanded = EVERYTHING: every field, full values — long commands wrap
1410
+ // across lines instead of being truncated (the web clips them; we don't).
1398
1411
  const lines = [];
1412
+ const budget = Math.max(20, this.w - 10);
1399
1413
  for (const [k, v] of Object.entries(j)) {
1400
1414
  if (["status", "kind", "label"].includes(k)) continue;
1401
1415
  const s = v !== null && typeof v === "object" ? JSON.stringify(v) : String(v ?? "");
1402
1416
  if (s === "") continue;
1403
- lines.push([{ t: ` ${k}: ${truncate(s, this.w - 14)}`, fg: K.DIM }]);
1404
- if (lines.length > 8) break;
1417
+ let rest = s;
1418
+ let first = true;
1419
+ while (rest.length > 0 || first) {
1420
+ const head = first ? `${k}: ` : " ";
1421
+ const take = JobsPanel.#cutWidth(rest, budget - strWidth(head));
1422
+ lines.push([{ t: ` ${head}${take}`, fg: K.DIM }]);
1423
+ rest = rest.slice(take.length);
1424
+ first = false;
1425
+ if (lines.length > 200) break; // pathological safety, never reached in practice
1426
+ }
1405
1427
  }
1406
1428
  return lines;
1407
1429
  }
1408
1430
  rebuild() {
1409
- let lines = [[{ t: " 这些任务在后台运行,不阻塞会话 — Enter/→/l 展开,←/h 折叠,q 关闭", fg: K.DIM }]];
1431
+ let lines = [[{ t: " 这些任务在后台运行,不阻塞会话 — Enter/→/l 展开,←/h 折叠,PgUp/PgDn 滚动,q 关闭", fg: K.DIM }]];
1410
1432
  const rowOf = [-1];
1411
1433
  const jobs = this.jobs;
1412
1434
  if (jobs.length === 0) {
@@ -1427,11 +1449,19 @@ export class JobsPanel extends Popup {
1427
1449
  rowOf.push(i);
1428
1450
  if (open) for (const fl of this.#detailLines(j)) { lines.push(fl); rowOf.push(-1); }
1429
1451
  }
1430
- if (lines.length > this.h - 3) {
1431
- lines = [...lines.slice(0, this.h - 4), [{ t: ` …共 ${jobs.length} 个任务`, fg: K.FAINT }]];
1432
- }
1452
+ // keep the full content — the panel scrolls now (no more hard clip)
1433
1453
  this.lines = lines;
1434
1454
  this.rowOf = rowOf;
1455
+ this.#ensureVisible();
1456
+ }
1457
+ /** Keep the selected job row inside the scrollable viewport. */
1458
+ #ensureVisible() {
1459
+ const avail = this.contentRows();
1460
+ const row = this.rowOf.findIndex((r) => r === this.sel);
1461
+ if (row < 0) return;
1462
+ if (row < this.scrollY) this.scrollY = row;
1463
+ else if (row >= this.scrollY + avail) this.scrollY = row - avail + 1;
1464
+ this.scrollY = Math.max(0, Math.min(this.scrollY, this.maxScroll()));
1435
1465
  }
1436
1466
  #toggle(i) {
1437
1467
  if (this.expanded.has(i)) this.expanded.delete(i);
@@ -1459,7 +1489,7 @@ export class JobsPanel extends Popup {
1459
1489
  return super.onKey(ev);
1460
1490
  }
1461
1491
  onMouse(ev) {
1462
- if (super.onMouse(ev)) return true; // buttons
1492
+ if (super.onMouse(ev)) return true; // buttons + wheel scrolling
1463
1493
  if (ev.kind === "press" && ev.button === 0) {
1464
1494
  const i = ev.y - this.y - 1;
1465
1495
  const jIdx = this.rowOf[i];
package/src/widgets.js CHANGED
@@ -510,12 +510,23 @@ export class Popup extends Widget {
510
510
  this.btnIdx = 0;
511
511
  this.onAction = opts.onAction ?? null;
512
512
  this.fg = opts.fg;
513
+ // opt-in vertical scrolling: content longer than the box scrolls with
514
+ // PgUp/PgDn/up/down/wheel instead of being clipped away
515
+ this.scrollable = opts.scrollable ?? false;
516
+ this.scrollY = 0;
517
+ }
518
+ contentRows() {
519
+ // rows between the border and the buttons row
520
+ return this.h - 2 - (this.buttons.length ? 1 : 0);
521
+ }
522
+ maxScroll() {
523
+ return Math.max(0, this.lines.length - this.contentRows());
513
524
  }
514
525
  render(screen) {
515
526
  screen.fillRect(this.x, this.y, this.x + this.w - 1, this.y + this.h - 1, " ", { bg: T.BG2 });
516
527
  screen.box(this.x, this.y, this.x + this.w - 1, this.y + this.h - 1, { fg: this.fg ?? 0x67b7ff, bg: T.BG2 }, this.title);
517
528
  let ly = this.y + 1;
518
- for (const line of this.lines) {
529
+ const draw = (line) => {
519
530
  if (Array.isArray(line)) {
520
531
  // styled line: array of segments
521
532
  let px = this.x + 2;
@@ -532,6 +543,23 @@ export class Popup extends Widget {
532
543
  screen.text(this.x + 2, ly, truncate(String(line), this.w - 4), { fg: T.TXT, bg: T.BG2 });
533
544
  }
534
545
  ly++;
546
+ };
547
+ if (this.scrollable) {
548
+ const avail = this.contentRows();
549
+ this.scrollY = Math.max(0, Math.min(this.scrollY, this.maxScroll()));
550
+ const start = this.scrollY;
551
+ for (let i = 0; i < avail; i++) {
552
+ const line = this.lines[start + i];
553
+ if (line === undefined) break;
554
+ draw(line);
555
+ }
556
+ if (this.maxScroll() > 0) {
557
+ // "↑N" / "↓N" overflow indicators on the border row
558
+ if (this.scrollY > 0) screen.text(this.x + this.w - 6, this.y, ` ↑${this.scrollY}`, { fg: T.ACCENT, bg: T.BG2 });
559
+ 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 });
560
+ }
561
+ } else {
562
+ for (const line of this.lines) draw(line);
535
563
  }
536
564
  // buttons on last row
537
565
  if (this.buttons.length) {
@@ -550,6 +578,10 @@ export class Popup extends Widget {
550
578
  }
551
579
  }
552
580
  onMouse(ev) {
581
+ if (this.scrollable) {
582
+ if (ev.kind === "wheel-up") { this.scrollY = Math.max(0, this.scrollY - 3); return true; }
583
+ if (ev.kind === "wheel-down") { this.scrollY = Math.min(this.maxScroll(), this.scrollY + 3); return true; }
584
+ }
553
585
  if (ev.kind === "press" && ev.button === 0 && this.buttons.length) {
554
586
  let bx = this.x + 2;
555
587
  const by = this.y + this.h - 2;
@@ -566,6 +598,10 @@ export class Popup extends Widget {
566
598
  }
567
599
  onKey(ev) {
568
600
  if (ev.type !== "key") return false;
601
+ if (this.scrollable) {
602
+ if (ev.name === "pgup") { this.scrollY = Math.max(0, this.scrollY - this.contentRows()); return true; }
603
+ if (ev.name === "pgdn") { this.scrollY = Math.min(this.maxScroll(), this.scrollY + this.contentRows()); return true; }
604
+ }
569
605
  if (ev.name === "escape") { this.onAction?.({ label: "__cancel__", action: "__cancel__" }, -1); return true; }
570
606
  if (ev.name === "tab") { this.btnIdx = (this.btnIdx + 1) % Math.max(1, this.buttons.length); return true; }
571
607
  if (ev.name === "left") { this.btnIdx = Math.max(0, this.btnIdx - 1); return true; }