dsh-neotui 0.1.0 → 0.1.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/package.json +1 -1
- package/src/panels.js +2 -0
- package/src/views.js +80 -10
package/package.json
CHANGED
package/src/panels.js
CHANGED
|
@@ -1216,6 +1216,8 @@ export class ControlPanel extends Widget {
|
|
|
1216
1216
|
["n", "新建会话", () => this.app.newSession()],
|
|
1217
1217
|
["g g", "滚动到顶", () => { this.app.closeOverlay(); this.app.chat.view.scrollY = 0; }],
|
|
1218
1218
|
["G", "滚动到底", () => { this.app.closeOverlay(); this.app.chat.view.scrollY = this.app.chat.view.maxScroll(); }],
|
|
1219
|
+
["[", "上一提问的终点", () => { this.app.closeOverlay(); this.app.focus(this.app.chat); this.app.chat.onKey({ type: "key", name: "char", key: "[", text: "[", ctrl: false, alt: false, shift: false }); }],
|
|
1220
|
+
["]", "下一提问的终点", () => { this.app.closeOverlay(); this.app.focus(this.app.chat); this.app.chat.onKey({ type: "key", name: "char", key: "]", text: "]", ctrl: false, alt: false, shift: false }); }],
|
|
1219
1221
|
["Ctrl+P", "控制面板", () => { this.page = 1; this.sel = 0; this.app.redraw(); }],
|
|
1220
1222
|
["Ctrl+M", "切换模型", () => { this.app.overlay = buildModelPicker(this.app); }],
|
|
1221
1223
|
["Ctrl+T", "轨迹视图", () => { this.app.closeOverlay(); this.app.setMode("trajectory"); }],
|
package/src/views.js
CHANGED
|
@@ -784,8 +784,8 @@ export class ChatView extends Widget {
|
|
|
784
784
|
this.view.follow = true;
|
|
785
785
|
}
|
|
786
786
|
|
|
787
|
-
async loadOlder() {
|
|
788
|
-
if (!this.hasMore || this.loadingOlder || this.minSeq == null) return;
|
|
787
|
+
async loadOlder(onDone = null) {
|
|
788
|
+
if (!this.hasMore || this.loadingOlder || this.minSeq == null) { onDone?.(); return; }
|
|
789
789
|
this.loadingOlder = true;
|
|
790
790
|
this.app.setStatus("加载更早记录…");
|
|
791
791
|
try {
|
|
@@ -803,6 +803,51 @@ export class ChatView extends Widget {
|
|
|
803
803
|
}
|
|
804
804
|
this.loadingOlder = false;
|
|
805
805
|
this.#rebuild();
|
|
806
|
+
onDone?.();
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
/** [ / ] — jump the viewport to the END of the previous (dir -1) or next
|
|
810
|
+
* (dir +1) user question. `previous` walks to the question entirely above
|
|
811
|
+
* the viewport top (loading older history first when needed); `next` walks
|
|
812
|
+
* to the first question that starts below the viewport top. */
|
|
813
|
+
#jumpQuestion(dir) {
|
|
814
|
+
this.flushRebuild(); // lineMap must reflect the current nodes array
|
|
815
|
+
const top = this.view.scrollY;
|
|
816
|
+
const qs = [];
|
|
817
|
+
for (let i = 0; i < this.nodes.length; i++) {
|
|
818
|
+
if (this.nodes[i]?.kind !== "user") continue;
|
|
819
|
+
let first = -1, last = -1;
|
|
820
|
+
for (let li = 0; li < this.lineMap.length; li++) {
|
|
821
|
+
if (this.lineMap[li]?.nodeIdx === i) { if (first < 0) first = li; last = li; }
|
|
822
|
+
}
|
|
823
|
+
if (first < 0) continue; // node outside the rendered window
|
|
824
|
+
qs.push({ first, last });
|
|
825
|
+
}
|
|
826
|
+
let target = null;
|
|
827
|
+
if (dir < 0) {
|
|
828
|
+
for (let k = qs.length - 1; k >= 0; k--) {
|
|
829
|
+
if (qs[k].last < top) { target = qs[k]; break; }
|
|
830
|
+
}
|
|
831
|
+
if (!target && this.hasMore && this.minSeq != null) {
|
|
832
|
+
this.loadOlder(() => this.#jumpQuestion(-1)); // after the prepend, retry against the fuller window
|
|
833
|
+
return true;
|
|
834
|
+
}
|
|
835
|
+
} else {
|
|
836
|
+
target = qs.find((q) => q.first > top) ?? null;
|
|
837
|
+
}
|
|
838
|
+
if (!target) {
|
|
839
|
+
this.app.toast(dir < 0 ? "已到最早的问题" : "已到最后的问题");
|
|
840
|
+
return false;
|
|
841
|
+
}
|
|
842
|
+
// the question's last CONTENT line (skip trailing blank separator rows);
|
|
843
|
+
// put it ~3 rows into the viewport, never above the question's own start
|
|
844
|
+
let end = target.last;
|
|
845
|
+
while (end > target.first && !(this.lines[end] ?? []).some((g) => g.t.trim() !== "")) end--;
|
|
846
|
+
this.view.anchorLock = null;
|
|
847
|
+
this.view.follow = false;
|
|
848
|
+
this.view.scrollY = Math.max(0, Math.max(target.first, end - 3));
|
|
849
|
+
this.app.redraw();
|
|
850
|
+
return true;
|
|
806
851
|
}
|
|
807
852
|
|
|
808
853
|
onFrame(frame) {
|
|
@@ -1540,6 +1585,11 @@ export class ChatView extends Widget {
|
|
|
1540
1585
|
return true;
|
|
1541
1586
|
}
|
|
1542
1587
|
if (ev.name === "char" && ev.key === "g" && ev.shift) { this.view.anchorLock = null; this.view.follow = true; this.view.scrollY = this.view.maxScroll(); return true; }
|
|
1588
|
+
// [ / ] — jump to the end of the previous / next user question. Guarded by
|
|
1589
|
+
// focus so the sidebar's session-move [ ] keys keep working there.
|
|
1590
|
+
if (ev.name === "char" && (ev.key === "[" || ev.key === "]") && !ev.ctrl && !ev.shift && this.app.focused === this) {
|
|
1591
|
+
return this.#jumpQuestion(ev.key === "[" ? -1 : 1);
|
|
1592
|
+
}
|
|
1543
1593
|
if (ev.name === "escape" && this.app.searchQuery) { this.app.searchQuery = null; this.queueRebuild(); return true; }
|
|
1544
1594
|
if (ev.name === "escape" && this.selStart !== null) { this.selStart = this.selEnd = null; this.app.redraw(); return true; }
|
|
1545
1595
|
if (ev.name === "char" && ev.key === "i" && !ev.ctrl) { this.app.focus(this.input); return true; }
|
|
@@ -1768,6 +1818,8 @@ export class App {
|
|
|
1768
1818
|
this.overlay = null; // Picker / Popup / ImagePopup modal
|
|
1769
1819
|
this.mode = "chat"; // chat | workspace | trajectory
|
|
1770
1820
|
this.sidebarVisible = true; // Ctrl+B hides the whole session pane (nvim-style)
|
|
1821
|
+
this.sidebarWidth = 30; // draggable divider: the session pane's column width
|
|
1822
|
+
this.draggingDivider = false;
|
|
1771
1823
|
this.feedbackMap = new Map(); // messageId → {rating, version}
|
|
1772
1824
|
this.searchQuery = null; // active find-in-conversation term (highlight)
|
|
1773
1825
|
this.findQuery = null; // term being typed in the find picker
|
|
@@ -1779,10 +1831,10 @@ export class App {
|
|
|
1779
1831
|
this.skillsPanel = null;
|
|
1780
1832
|
|
|
1781
1833
|
this.sidebar = new SidebarTree(this);
|
|
1782
|
-
this.sidebar.w =
|
|
1834
|
+
this.sidebar.w = this.sidebarWidth;
|
|
1783
1835
|
this.sidebar.h = screen.h - 1;
|
|
1784
|
-
this.searchInput = new Input({ x: 0, y: 0, w:
|
|
1785
|
-
this.chat = new ChatView({ x:
|
|
1836
|
+
this.searchInput = new Input({ x: 0, y: 0, w: this.sidebarWidth, h: 1, prompt: "/ ", placeholder: "搜索会话…" });
|
|
1837
|
+
this.chat = new ChatView({ x: this.sidebarWidth, y: 0, w: screen.w - this.sidebarWidth, h: screen.h - 1, app: this });
|
|
1786
1838
|
this.status = new StatusBar({ x: 0, y: screen.h - 1, w: screen.w, h: 1 });
|
|
1787
1839
|
this.focus(this.chat);
|
|
1788
1840
|
this.layout();
|
|
@@ -1795,11 +1847,12 @@ export class App {
|
|
|
1795
1847
|
}
|
|
1796
1848
|
|
|
1797
1849
|
layout() {
|
|
1798
|
-
const x = this.sidebarVisible ?
|
|
1850
|
+
const x = this.sidebarVisible ? this.sidebarWidth : 0;
|
|
1799
1851
|
const w = this.screen.w - x;
|
|
1800
1852
|
const footerH = this.footerHeight();
|
|
1801
1853
|
const mainH = this.screen.h - 1 - footerH;
|
|
1802
|
-
this.sidebar.x = 0; this.sidebar.y = 0; this.sidebar.w =
|
|
1854
|
+
this.sidebar.x = 0; this.sidebar.y = 0; this.sidebar.w = this.sidebarWidth; this.sidebar.h = this.screen.h - 1;
|
|
1855
|
+
this.searchInput.w = this.sidebarWidth;
|
|
1803
1856
|
this.chat.resize(x, 1, w, mainH);
|
|
1804
1857
|
for (const p of [this.workspacePanel, this.trajectoryPanel, this.settingsPanel, this.subagentPanel, this.skillsPanel]) {
|
|
1805
1858
|
if (p?.relayout) p.relayout(x, 1, w, mainH);
|
|
@@ -2554,7 +2607,7 @@ export class App {
|
|
|
2554
2607
|
}
|
|
2555
2608
|
|
|
2556
2609
|
#renderTabBar(s) {
|
|
2557
|
-
const x = this.sidebarVisible ?
|
|
2610
|
+
const x = this.sidebarVisible ? this.sidebarWidth : 0;
|
|
2558
2611
|
const w = this.screen.w - x;
|
|
2559
2612
|
s.fillRect(x, 0, x + w - 1, 0, " ", { bg: T.PANEL });
|
|
2560
2613
|
const tabs = [...this.#modeTabs()];
|
|
@@ -2571,7 +2624,7 @@ export class App {
|
|
|
2571
2624
|
}
|
|
2572
2625
|
|
|
2573
2626
|
#clickTab(px) {
|
|
2574
|
-
const x = this.sidebarVisible ?
|
|
2627
|
+
const x = this.sidebarVisible ? this.sidebarWidth : 0;
|
|
2575
2628
|
const tabs = [...this.#modeTabs()];
|
|
2576
2629
|
const panelLabel = this.#panelLabel(this.mode);
|
|
2577
2630
|
if (panelLabel) tabs.push([this.mode, panelLabel]);
|
|
@@ -2661,7 +2714,7 @@ export class App {
|
|
|
2661
2714
|
}
|
|
2662
2715
|
|
|
2663
2716
|
// tab bar clicks (row 0 of the main area)
|
|
2664
|
-
if (ev.type === "mouse" && ev.kind === "press" && ev.button === 0 && ev.y === 0 && ev.x >= (this.sidebarVisible ?
|
|
2717
|
+
if (ev.type === "mouse" && ev.kind === "press" && ev.button === 0 && ev.y === 0 && ev.x >= (this.sidebarVisible ? this.sidebarWidth : 0)) {
|
|
2665
2718
|
if (this.#clickTab(ev.x)) { this.redraw(); return; }
|
|
2666
2719
|
}
|
|
2667
2720
|
// mouse routes by position (click = focus + dispatch)
|
|
@@ -2679,6 +2732,23 @@ export class App {
|
|
|
2679
2732
|
// unhandled keys fall through to global shortcuts
|
|
2680
2733
|
}
|
|
2681
2734
|
if (ev.type === "mouse") {
|
|
2735
|
+
// Draggable sidebar divider: press on the boundary column (±1) starts a
|
|
2736
|
+
// resize; drag events (motion flag) update the width live.
|
|
2737
|
+
const divX = this.sidebarVisible ? this.sidebarWidth : -1;
|
|
2738
|
+
const onDivider = divX >= 0 && ev.y >= 1 && ev.x >= divX - 1 && ev.x <= divX + 1;
|
|
2739
|
+
if (ev.kind === "press" && ev.button === 0 && onDivider) {
|
|
2740
|
+
this.draggingDivider = true;
|
|
2741
|
+
this.redraw();
|
|
2742
|
+
return;
|
|
2743
|
+
}
|
|
2744
|
+
if (this.draggingDivider) {
|
|
2745
|
+
if (ev.kind === "drag" && ev.button === 0) {
|
|
2746
|
+
const nw = Math.max(14, Math.min(ev.x, Math.floor(this.screen.w * 0.6)));
|
|
2747
|
+
if (nw !== this.sidebarWidth) { this.sidebarWidth = nw; this.layout(); this.redraw(); }
|
|
2748
|
+
return;
|
|
2749
|
+
}
|
|
2750
|
+
if (ev.kind === "release" && ev.button === 0) { this.draggingDivider = false; this.redraw(); return; }
|
|
2751
|
+
}
|
|
2682
2752
|
// Pure mouse motion must never change focus/mode (vim-style: INSERT is
|
|
2683
2753
|
// keyboard-only). It reaches just the already-focused widget (drag select).
|
|
2684
2754
|
if (ev.motion) {
|