dsh-neotui 0.0.11 → 0.0.12
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/README.md +1 -1
- package/package.json +1 -1
- package/src/panels.js +98 -57
- package/src/views.js +21 -0
package/README.md
CHANGED
|
@@ -101,7 +101,7 @@ tui/app/ dsh-neotui-app bundle:cordis.patch.yml + tui-startup/tui-r
|
|
|
101
101
|
- 会话内搜索:**Ctrl+F** 模糊搜本会话消息/工具名,回车跳转
|
|
102
102
|
- 轨迹详情:点击回合行弹窗(工具调用 + 耗时),`/` 过滤回合;**增量加载**(每页 20 回合逐页渲染,最近回合秒开,历史步骤按需翻页,不再阻塞等待全量历史)
|
|
103
103
|
- 轨迹 详细/简略:每个 step 左键单击(▸/▾)或右键菜单第一项 →「展开(详细)/ 折叠(简略)」内联展开事件列表(仿对话页);跳转定位的 step 自动展开 + 高亮闪烁
|
|
104
|
-
-
|
|
104
|
+
- 轨迹窗口模型:转跳只显示目标 step **±20 个相邻步骤**;PgUp/PgDn 按 10 步扩展窗口(按需向后翻页加载),Home 加载到最早 20 步,End 回到最新 20 步,窗口指示行显示「窗口 #N–#M(已加载 X) · step a–b」;窗口边界基于事件序号(seq)而非 step 编号——会话压缩后 step 编号会重新计数,seq 始终唯一
|
|
105
105
|
- 工作区文件搜索:工作区面板内直接打字(`/`)模糊搜文件名,回车预览
|
|
106
106
|
- 消息反馈:助手消息右键 👍/👎(`messageFeedback/put` Typert RPC,`{request:…}` 载荷),已评状态回显、可删除
|
|
107
107
|
- 图片画廊:多图时 ←/→ 切换,标题显示 (N/M) + 尺寸
|
package/package.json
CHANGED
package/src/panels.js
CHANGED
|
@@ -556,12 +556,12 @@ export class TrajectoryPanel extends Widget {
|
|
|
556
556
|
this.allEvents = [];
|
|
557
557
|
this.sessionId = null;
|
|
558
558
|
this.expandedSteps = new Set(); // step identity keys rendered 详细 (expanded)
|
|
559
|
-
this.
|
|
559
|
+
this.flashKey = null; // step key just jumped to (brief highlight)
|
|
560
560
|
this.flashUntil = 0;
|
|
561
561
|
this.loadPromise = null; // dedupes concurrent load(currentSession)
|
|
562
562
|
this.loadTarget = null;
|
|
563
|
-
this.
|
|
564
|
-
this.
|
|
563
|
+
this.winSeqLo = null; // visible window = first-event SEQ range; null = follow the tail
|
|
564
|
+
this.winSeqHi = null;
|
|
565
565
|
// LEFT click toggles a step's 详细/简略 expansion (the ▸/▾ triangle).
|
|
566
566
|
this.view = new ScrollView({ x: this.x, y: this.y, w: this.w, h: this.h, showScrollbar: true, onClick: (y) => this.#clickLine(y) });
|
|
567
567
|
this.stepLines = [];
|
|
@@ -665,98 +665,127 @@ export class TrajectoryPanel extends Widget {
|
|
|
665
665
|
return -1;
|
|
666
666
|
}
|
|
667
667
|
|
|
668
|
-
/** Load older pages until
|
|
669
|
-
* step is reached). Used by jumps and Home
|
|
670
|
-
async
|
|
668
|
+
/** Load older pages until at least `minCount` steps are loaded (or the
|
|
669
|
+
* session's first step is reached). Used by jumps and Home. */
|
|
670
|
+
async ensureCount(minCount, maxPages = 80) {
|
|
671
671
|
for (let i = 0; i < maxPages; i++) {
|
|
672
|
-
if (!this.hasMore) break;
|
|
673
|
-
|
|
674
|
-
this.app.setStatus(`加载更早轨迹…(当前最早 step ${this.steps[0]?.step ?? "?"})`);
|
|
672
|
+
if (!this.hasMore || this.steps.length >= minCount) break;
|
|
673
|
+
this.app.setStatus(`加载更早轨迹…(已加载 ${this.steps.length} 步)`);
|
|
675
674
|
await this.loadOlder();
|
|
676
675
|
}
|
|
677
676
|
this.app.setStatus("");
|
|
678
677
|
}
|
|
679
678
|
|
|
680
|
-
/**
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
679
|
+
/** The visible window is a SEQ RANGE (first-event seqs are globally unique
|
|
680
|
+
* and monotonic; the server's step numbers restart after compactions and
|
|
681
|
+
* cannot be used as boundaries). null = follow the tail (newest 20). */
|
|
682
|
+
setWindow(loSeq, hiSeq) {
|
|
683
|
+
this.winSeqLo = loSeq;
|
|
684
|
+
this.winSeqHi = hiSeq;
|
|
684
685
|
this.buildLines();
|
|
685
686
|
}
|
|
686
687
|
|
|
687
|
-
/**
|
|
688
|
-
#
|
|
688
|
+
/** Tail-follow window: the newest 20 loaded steps. */
|
|
689
|
+
#tailWindow() {
|
|
690
|
+
const n = this.steps.length;
|
|
691
|
+
if (n === 0) return;
|
|
692
|
+
const lo = Math.max(0, n - 20);
|
|
693
|
+
this.winSeqLo = this.stepKey(this.steps[lo]);
|
|
694
|
+
this.winSeqHi = this.stepKey(this.steps[n - 1]);
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
/** Seq of the step at the top of the viewport (for anchoring after growth). */
|
|
698
|
+
#topVisibleSeq() {
|
|
689
699
|
const si = this.stepLines[this.view.scrollY];
|
|
690
|
-
return si !== undefined ? this.steps[si]
|
|
700
|
+
return si !== undefined ? this.stepKey(this.steps[si]) : null;
|
|
691
701
|
}
|
|
692
702
|
|
|
693
|
-
/** Scroll so the given step
|
|
694
|
-
#anchorScroll(
|
|
695
|
-
const li = this.stepLines.findIndex((si) => this.steps[si]
|
|
696
|
-
if (li >= 0) this.view.scrollY = Math.max(0, li);
|
|
703
|
+
/** Scroll so the given step seq sits at the top of the viewport. */
|
|
704
|
+
#anchorScroll(seq) {
|
|
705
|
+
const li = this.stepLines.findIndex((si) => this.stepKey(this.steps[si]) === seq);
|
|
706
|
+
if (li >= 0) this.view.scrollY = Math.max(0, Math.min(li, this.view.maxScroll()));
|
|
697
707
|
}
|
|
698
708
|
|
|
699
709
|
/** Scroll to a step: open a ±20 window around it (loading older pages on
|
|
700
710
|
* demand), auto-expand and highlight the step. */
|
|
701
711
|
async jumpToStep(si) {
|
|
702
712
|
if (si < 0 || si >= this.steps.length) return;
|
|
703
|
-
const
|
|
704
|
-
this.expandedSteps.add(
|
|
705
|
-
this.
|
|
713
|
+
const key = this.stepKey(this.steps[si]);
|
|
714
|
+
this.expandedSteps.add(key);
|
|
715
|
+
this.flashKey = key;
|
|
706
716
|
this.flashUntil = Date.now() + 3000;
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
this.
|
|
717
|
+
// load older pages until at least 20 steps sit above the target
|
|
718
|
+
for (let i = 0; i < 80 && this.hasMore; i++) {
|
|
719
|
+
if (this.steps.findIndex((s) => this.stepKey(s) === key) >= 20) break;
|
|
720
|
+
await this.loadOlder();
|
|
721
|
+
}
|
|
722
|
+
const idx = this.steps.findIndex((s) => this.stepKey(s) === key);
|
|
723
|
+
if (idx < 0) return;
|
|
724
|
+
const lo = Math.max(0, idx - 20), hi = Math.min(this.steps.length - 1, idx + 20);
|
|
725
|
+
this.setWindow(this.stepKey(this.steps[lo]), this.stepKey(this.steps[hi]));
|
|
726
|
+
const li = this.stepLines.indexOf(idx);
|
|
727
|
+
this.view.scrollY = li >= 0 ? Math.max(0, Math.min(li - 2, this.view.maxScroll())) : 0;
|
|
713
728
|
this.app.redraw();
|
|
714
729
|
}
|
|
715
730
|
|
|
716
731
|
/** PgUp: extend the window 10 steps upward (loading older if needed),
|
|
717
732
|
* keeping the view anchored on the step that was at the top. */
|
|
718
733
|
async extendUp() {
|
|
719
|
-
|
|
720
|
-
if (this.
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
734
|
+
if (this.winSeqLo == null) this.#tailWindow();
|
|
735
|
+
if (this.steps.length === 0) return;
|
|
736
|
+
let topIdx = this.steps.findIndex((s) => this.stepKey(s) === this.winSeqLo);
|
|
737
|
+
if (topIdx < 0) topIdx = 0;
|
|
738
|
+
if (topIdx === 0 && !this.hasMore) { this.app.toast("已到最早步骤"); return; }
|
|
739
|
+
// ensure at least 10 steps above the window top are loaded
|
|
740
|
+
for (let i = 0; i < 80 && this.hasMore && topIdx < 10; i++) {
|
|
741
|
+
await this.loadOlder();
|
|
742
|
+
topIdx = this.steps.findIndex((s) => this.stepKey(s) === this.winSeqLo);
|
|
724
743
|
}
|
|
725
|
-
const
|
|
726
|
-
this.
|
|
727
|
-
if (!this.steps.length || this.steps[0].step > this.winLo) await this.ensureLoaded(this.winLo);
|
|
744
|
+
const anchorSeq = this.#topVisibleSeq();
|
|
745
|
+
this.winSeqLo = this.stepKey(this.steps[Math.max(0, topIdx - 10)]);
|
|
728
746
|
this.buildLines();
|
|
729
|
-
this.#anchorScroll(
|
|
747
|
+
if (anchorSeq != null) this.#anchorScroll(anchorSeq);
|
|
730
748
|
this.app.redraw();
|
|
731
749
|
}
|
|
732
750
|
|
|
733
751
|
/** PgDn: extend the window 10 steps downward (the newer steps are already
|
|
734
752
|
* loaded — the tail is always kept). */
|
|
735
753
|
extendDown() {
|
|
736
|
-
|
|
737
|
-
if (this.
|
|
738
|
-
|
|
739
|
-
|
|
754
|
+
if (this.winSeqLo == null) this.#tailWindow();
|
|
755
|
+
if (this.steps.length === 0) return;
|
|
756
|
+
let bottomIdx = this.steps.length - 1;
|
|
757
|
+
for (let i = this.steps.length - 1; i >= 0; i--) {
|
|
758
|
+
if (this.stepKey(this.steps[i]) <= this.winSeqHi) { bottomIdx = i; break; }
|
|
759
|
+
}
|
|
760
|
+
const target = Math.min(this.steps.length - 1, bottomIdx + 10);
|
|
761
|
+
if (target === bottomIdx) { this.app.toast("已到最新步骤"); return; }
|
|
762
|
+
this.winSeqHi = this.stepKey(this.steps[target]);
|
|
740
763
|
this.buildLines();
|
|
741
764
|
this.app.redraw();
|
|
742
765
|
}
|
|
743
766
|
|
|
744
767
|
/** Home: jump to the very first steps (loading all the way back). */
|
|
745
768
|
async gotoHome() {
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
this.app.
|
|
769
|
+
for (let i = 0; i < 80 && this.hasMore; i++) {
|
|
770
|
+
this.app.setStatus(`加载全部步骤…(已加载 ${this.steps.length} 步)`);
|
|
771
|
+
await this.loadOlder();
|
|
772
|
+
}
|
|
773
|
+
this.app.setStatus("");
|
|
774
|
+
if (this.steps.length === 0) return;
|
|
775
|
+
const hi = Math.min(19, this.steps.length - 1);
|
|
776
|
+
this.setWindow(this.stepKey(this.steps[0]), this.stepKey(this.steps[hi]));
|
|
777
|
+
this.view.scrollY = 0;
|
|
778
|
+
this.app.toast("已跳到最早步骤");
|
|
751
779
|
this.app.redraw();
|
|
752
780
|
}
|
|
753
781
|
|
|
754
782
|
/** End: jump to the newest steps. */
|
|
755
783
|
gotoEnd() {
|
|
756
|
-
|
|
757
|
-
|
|
784
|
+
if (this.steps.length === 0) return;
|
|
785
|
+
const lo = Math.max(0, this.steps.length - 20);
|
|
786
|
+
this.setWindow(this.stepKey(this.steps[lo]), this.stepKey(this.steps[this.steps.length - 1]));
|
|
758
787
|
this.view.scrollY = this.view.maxScroll();
|
|
759
|
-
this.app.toast(
|
|
788
|
+
this.app.toast("已跳到最新步骤");
|
|
760
789
|
this.app.redraw();
|
|
761
790
|
}
|
|
762
791
|
|
|
@@ -773,7 +802,7 @@ export class TrajectoryPanel extends Widget {
|
|
|
773
802
|
}
|
|
774
803
|
if (si < 0 && messageId) {
|
|
775
804
|
// still not found — the message is far back; scan everything (bounded)
|
|
776
|
-
await this.
|
|
805
|
+
await this.ensureCount(Infinity, 60);
|
|
777
806
|
si = this.indexOfMessage(messageId);
|
|
778
807
|
}
|
|
779
808
|
if (si >= 0) {
|
|
@@ -871,8 +900,17 @@ export class TrajectoryPanel extends Widget {
|
|
|
871
900
|
buildLines() {
|
|
872
901
|
const w = Math.max(40, this.w - 2);
|
|
873
902
|
const N = this.totalSteps();
|
|
874
|
-
|
|
875
|
-
|
|
903
|
+
// Window boundaries are SEQ-based (step numbers restart after compaction).
|
|
904
|
+
let loSeq = this.winSeqLo, hiSeq = this.winSeqHi;
|
|
905
|
+
if (loSeq == null && this.steps.length) {
|
|
906
|
+
const lo = Math.max(0, this.steps.length - 20);
|
|
907
|
+
loSeq = this.stepKey(this.steps[lo]);
|
|
908
|
+
hiSeq = this.stepKey(this.steps[this.steps.length - 1]);
|
|
909
|
+
}
|
|
910
|
+
const winIdxLo = this.steps.findIndex((s) => this.stepKey(s) === loSeq);
|
|
911
|
+
const winIdxHi = this.steps.findIndex((s) => this.stepKey(s) === hiSeq);
|
|
912
|
+
const loStepNum = this.steps[winIdxLo]?.step ?? "?";
|
|
913
|
+
const hiStepNum = this.steps[winIdxHi]?.step ?? "?";
|
|
876
914
|
const lines = [];
|
|
877
915
|
lines.push([{ t: "轨迹 — 步骤时间轴(左键展开/折叠 · PgUp/PgDn 上下加载 · Home/End 首尾 · Ctrl+E 转跳 · r 刷新)", fg: K.ACCENT, bold: true }]);
|
|
878
916
|
if (this.hasMore) lines.push([{ t: "▲ 更早步骤(点击 / PgUp 向上加载 10 步)", fg: K.FAINT }]);
|
|
@@ -881,7 +919,7 @@ export class TrajectoryPanel extends Widget {
|
|
|
881
919
|
if (st) {
|
|
882
920
|
lines.push([{ t: `回合 ${st.turns} · 步骤 ${st.steps} · LLM ${fmtMs(st.llmMs)} · 工具 ${fmtMs(st.toolMs)}`, fg: K.DIM }]);
|
|
883
921
|
}
|
|
884
|
-
lines.push([{ t: `窗口
|
|
922
|
+
lines.push([{ t: `窗口 #${winIdxLo + 1}–#${winIdxHi + 1}(已加载 ${this.steps.length}${this.hasMore ? "+" : ""})· step ${loStepNum}–${hiStepNum}${this.winSeqLo == null ? "(跟随最新)" : ""}:`, fg: K.DIM, underline: true }]);
|
|
885
923
|
this.stepLines = [];
|
|
886
924
|
const list = this.query
|
|
887
925
|
? this.steps.filter((t) => t.events.some((e) => {
|
|
@@ -889,7 +927,10 @@ export class TrajectoryPanel extends Widget {
|
|
|
889
927
|
const hay = `${e.type} ${d.name ?? ""} ${typeof d.content === "string" ? d.content : ""}`.toLowerCase();
|
|
890
928
|
return hay.includes(this.query.toLowerCase());
|
|
891
929
|
}))
|
|
892
|
-
: this.steps.filter((s) =>
|
|
930
|
+
: this.steps.filter((s) => {
|
|
931
|
+
const k = this.stepKey(s);
|
|
932
|
+
return k >= loSeq && k <= hiSeq;
|
|
933
|
+
});
|
|
893
934
|
for (const step of list.reverse()) {
|
|
894
935
|
const si = this.steps.indexOf(step);
|
|
895
936
|
const tools = [...new Set(step.events.filter((e) => e.type === "tool/call").map((e) => e.data?.name))];
|
|
@@ -900,7 +941,7 @@ export class TrajectoryPanel extends Widget {
|
|
|
900
941
|
const bg = tools.length ? (hasResult ? T.TOOLOK : T.TOOLBG) : hasReasoning ? T.THINKBG : T.CARD;
|
|
901
942
|
const summary = tools.slice(0, 3).join(",") || (hasReasoning ? "模型推理" : "纯文本");
|
|
902
943
|
const open = this.expandedSteps.has(this.stepKey(step)); // 详细
|
|
903
|
-
const flash = this.
|
|
944
|
+
const flash = this.flashKey === this.stepKey(step) && Date.now() < this.flashUntil;
|
|
904
945
|
const rowBg = flash ? T.ACCENT : bg;
|
|
905
946
|
const label = `${open ? "▾" : "▸"} step ${String(step.step).padStart(3)} ${pad(dur, 8)} ${summary} ${open ? "[折叠]" : "[展开]"}`;
|
|
906
947
|
const segs = [{ t: label, fg: flash ? T.SELFG : K.TXT, bg: rowBg, bold: true }];
|
|
@@ -971,7 +1012,7 @@ export class TrajectoryPanel extends Widget {
|
|
|
971
1012
|
}
|
|
972
1013
|
if (ev.name === "backspace") { this.query = this.query.slice(0, -1); this.buildLines(); this.app.redraw(); return true; }
|
|
973
1014
|
if (ev.name === "char" && ev.key === "r" && !ev.ctrl) {
|
|
974
|
-
this.
|
|
1015
|
+
this.winSeqLo = this.winSeqHi = null;
|
|
975
1016
|
this.steps = [];
|
|
976
1017
|
this.load(this.sessionId);
|
|
977
1018
|
return true;
|
package/src/views.js
CHANGED
|
@@ -848,6 +848,25 @@ export class ChatView extends Widget {
|
|
|
848
848
|
if (ref) { this.app.openImage(ref, { all: node.images, index: info.imgIdx }); return true; }
|
|
849
849
|
}
|
|
850
850
|
const node = this.nodes[info.nodeIdx];
|
|
851
|
+
// Expand/collapse changes the line count ABOVE the viewport, which would
|
|
852
|
+
// leave scrollY pointing at unrelated content (the "chaotic" jump).
|
|
853
|
+
// Anchor: after the rebuild, scroll so the clicked block's first line
|
|
854
|
+
// sits at the top of the viewport. At the bottom the tail-follow snap
|
|
855
|
+
// in setLines is the right behavior instead.
|
|
856
|
+
const wasAtBottom = this.view.scrollY + this.view.h >= this.view.lines.length - 1;
|
|
857
|
+
const reanchor = (match) => {
|
|
858
|
+
if (wasAtBottom) return;
|
|
859
|
+
let fallback = -1;
|
|
860
|
+
for (let i = 0; i < this.lineMap.length; i++) {
|
|
861
|
+
if (!match(this.lineMap[i])) continue;
|
|
862
|
+
if ((this.lines[i] ?? []).some((g) => g.t.trim() !== "")) {
|
|
863
|
+
this.view.scrollY = Math.max(0, Math.min(i, this.view.maxScroll()));
|
|
864
|
+
return;
|
|
865
|
+
}
|
|
866
|
+
if (fallback < 0) fallback = i;
|
|
867
|
+
}
|
|
868
|
+
if (fallback >= 0) this.view.scrollY = Math.max(0, Math.min(fallback, this.view.maxScroll()));
|
|
869
|
+
};
|
|
851
870
|
if (node?.kind === "assistant" && info.blockIdx !== null) {
|
|
852
871
|
const b = node.blocks[info.blockIdx];
|
|
853
872
|
if (b && (b.kind === "tool" || b.kind === "reasoning" || b.kind === "other" || b.kind === "text")) {
|
|
@@ -862,6 +881,7 @@ export class ChatView extends Widget {
|
|
|
862
881
|
else this.collapsedBlocks.add(key);
|
|
863
882
|
}
|
|
864
883
|
this.#rebuild();
|
|
884
|
+
reanchor((m) => m?.nodeIdx === info.nodeIdx && m?.blockIdx === info.blockIdx);
|
|
865
885
|
return true;
|
|
866
886
|
}
|
|
867
887
|
}
|
|
@@ -869,6 +889,7 @@ export class ChatView extends Widget {
|
|
|
869
889
|
if (this.expanded.has(info.nodeIdx)) this.expanded.delete(info.nodeIdx);
|
|
870
890
|
else this.expanded.add(info.nodeIdx);
|
|
871
891
|
this.#rebuild();
|
|
892
|
+
reanchor((m) => m?.nodeIdx === info.nodeIdx);
|
|
872
893
|
return true;
|
|
873
894
|
}
|
|
874
895
|
}
|