dsh-neotui 0.0.10 → 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 +4 -3
- package/package.json +1 -1
- package/src/panels.js +98 -57
- package/src/views.js +90 -26
package/README.md
CHANGED
|
@@ -59,7 +59,8 @@ tui/app/ dsh-neotui-app bundle:cordis.patch.yml + tui-startup/tui-r
|
|
|
59
59
|
| 设置次级页 | 在「设置」页内 **Shift+Tab** 翻次级页(常规 / 插件) |
|
|
60
60
|
| 主页切换 | **Shift+Tab** 在对话 ↔ 轨迹之间切换;顶部标签页(对话/轨迹/工作区/设置/技能/子代理)可点击直达 |
|
|
61
61
|
| 输入模式 | `i` 或点击进入,**Esc 退出**(nvim normal/insert);未聚焦时字母=快捷键,多字粘贴直接输入 |
|
|
62
|
-
| 思考/工具折叠 | `t` 思考块展开折叠,`b` 工具块(bash 等)展开折叠;右键消息 →「展开 /
|
|
62
|
+
| 思考/工具折叠 | `t` 思考块展开折叠,`b` 工具块(bash 等)展开折叠;右键消息 →「展开 / 折叠」折叠单个输出块;正文文本块同样支持点击/右键折叠(▸/▾ + 「…共 N 字」尾注);展开的思考块渲染**全文**,不再截断 |
|
|
63
|
+
| 右键菜单 | 菜单打开后**再次右键**任意位置:立即关闭当前菜单并在新位置打开新菜单(OS 式);左键点击菜单外仍是关闭 |
|
|
63
64
|
| 对话 ↔ 轨迹转跳 | 对话里右键消息 →「转跳轨迹」定位到对应 step(自动展开+高亮);轨迹里右键 step →「转跳对话」回到该消息 |
|
|
64
65
|
| 步骤快速转跳 | **Ctrl+E**:fzf 式过滤 step 列表(编号/耗时/工具/消息预览),回车定位到该 step 的轨迹窗口 |
|
|
65
66
|
| 轨迹窗口导航 | 转跳后只显示 `step±20` 窗口;**PgUp/PgDn** 上下各加载 10 步,**Home/End** 跳到最早/最新 20 步,`r` 回最新 |
|
|
@@ -100,9 +101,9 @@ tui/app/ dsh-neotui-app bundle:cordis.patch.yml + tui-startup/tui-r
|
|
|
100
101
|
- 会话内搜索:**Ctrl+F** 模糊搜本会话消息/工具名,回车跳转
|
|
101
102
|
- 轨迹详情:点击回合行弹窗(工具调用 + 耗时),`/` 过滤回合;**增量加载**(每页 20 回合逐页渲染,最近回合秒开,历史步骤按需翻页,不再阻塞等待全量历史)
|
|
102
103
|
- 轨迹 详细/简略:每个 step 左键单击(▸/▾)或右键菜单第一项 →「展开(详细)/ 折叠(简略)」内联展开事件列表(仿对话页);跳转定位的 step 自动展开 + 高亮闪烁
|
|
103
|
-
-
|
|
104
|
+
- 轨迹窗口模型:转跳只显示目标 step **±20 个相邻步骤**;PgUp/PgDn 按 10 步扩展窗口(按需向后翻页加载),Home 加载到最早 20 步,End 回到最新 20 步,窗口指示行显示「窗口 #N–#M(已加载 X) · step a–b」;窗口边界基于事件序号(seq)而非 step 编号——会话压缩后 step 编号会重新计数,seq 始终唯一
|
|
104
105
|
- 工作区文件搜索:工作区面板内直接打字(`/`)模糊搜文件名,回车预览
|
|
105
|
-
- 消息反馈:助手消息右键 👍/👎(`messageFeedback/put` Typert RPC
|
|
106
|
+
- 消息反馈:助手消息右键 👍/👎(`messageFeedback/put` Typert RPC,`{request:…}` 载荷),已评状态回显、可删除
|
|
106
107
|
- 图片画廊:多图时 ←/→ 切换,标题显示 (N/M) + 尺寸
|
|
107
108
|
- 会话内搜索高亮:Ctrl+F 跳转后匹配词高亮(黄底),Esc 清除
|
|
108
109
|
- 轨迹逐事件详情:点击回合 → 事件列表 → 选事件看全文
|
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,9 +848,28 @@ 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
|
-
if (b && (b.kind === "tool" || b.kind === "reasoning" || b.kind === "other")) {
|
|
872
|
+
if (b && (b.kind === "tool" || b.kind === "reasoning" || b.kind === "other" || b.kind === "text")) {
|
|
854
873
|
const key = `${info.nodeIdx}:${info.blockIdx}`;
|
|
855
874
|
if (b.kind === "reasoning") {
|
|
856
875
|
// clean two-state override: expand ⇄ collapse (never a no-op click)
|
|
@@ -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
|
}
|
|
@@ -891,7 +912,6 @@ export class ChatView extends Widget {
|
|
|
891
912
|
if (this.hasMore) { lines.push([{ t: "▲ 更早的记录", fg: K.FAINT }]); mark(-1); }
|
|
892
913
|
if (skipCount > 0) { lines.push([{ t: `… 更早 ${skipCount} 条记录(PgUp 加载)`, fg: K.FAINT }]); mark(-1); }
|
|
893
914
|
|
|
894
|
-
const MAX_TEXT = 4000;
|
|
895
915
|
for (let ni = 0; ni < nodes.length; ni++) {
|
|
896
916
|
const node = nodes[ni];
|
|
897
917
|
const realIdx = ni + skipCount;
|
|
@@ -999,7 +1019,8 @@ export class ChatView extends Widget {
|
|
|
999
1019
|
lines.push([{ t: "💭 思考" + (b.streaming ? "…" : "") + thinkMeta + (open ? " [t 折叠]" : " [t 展开]"), fg: K.FAINT }]);
|
|
1000
1020
|
mark(realIdx, bi);
|
|
1001
1021
|
if (open) {
|
|
1002
|
-
|
|
1022
|
+
// Expanded = the WHOLE reasoning (no hidden remainder).
|
|
1023
|
+
for (const ln of renderMd(b.text ?? "", w - 4)) { lines.push([{ t: " " }, ...ln]); mark(realIdx, bi); }
|
|
1003
1024
|
} else {
|
|
1004
1025
|
// collapsed: three-line preview
|
|
1005
1026
|
for (const ln of renderMd(truncateText(b.text, 400), w - 4).slice(0, 3)) {
|
|
@@ -1059,18 +1080,27 @@ export class ChatView extends Widget {
|
|
|
1059
1080
|
sep();
|
|
1060
1081
|
} else {
|
|
1061
1082
|
beginCard("CARD");
|
|
1083
|
+
// Text blocks collapse per-block like tool/reasoning blocks:
|
|
1084
|
+
// collapsed shows a 3-line preview + trailer, expanded the whole
|
|
1085
|
+
// text. Clicking (or the right-click 展开/折叠) toggles it.
|
|
1086
|
+
const key = `${realIdx}:${bi}`;
|
|
1087
|
+
const open = !this.collapsedBlocks.has(key);
|
|
1062
1088
|
const text = b.text ?? "";
|
|
1063
|
-
const
|
|
1089
|
+
const glyph = open ? "▾" : "▸";
|
|
1090
|
+
const mdW = Math.max(10, w - 6 - strWidth(stepTag));
|
|
1091
|
+
const md = open
|
|
1092
|
+
? renderMd(text, mdW)
|
|
1093
|
+
: renderMd(truncateText(text, 600), mdW).slice(0, 3);
|
|
1064
1094
|
if (md.length > 0) {
|
|
1065
|
-
lines.push([{ t: " " }, { t: stepTag, fg: K.FAINT }, ...md[0]]);
|
|
1066
|
-
mark(realIdx);
|
|
1067
|
-
for (const ln of md.slice(1)) { lines.push([{ t: " " }, ...ln]); mark(realIdx); }
|
|
1068
|
-
// Label the block's LAST line with its blockIdx IN PLACE. A
|
|
1069
|
-
// pushed mark without a line would desync lineMap from lines
|
|
1070
|
-
// and shift every later click mapping (the off-by-one bug).
|
|
1071
|
-
lineMap[lineMap.length - 1] = { nodeIdx: realIdx, blockIdx: bi };
|
|
1095
|
+
lines.push([{ t: " " }, { t: glyph + " ", fg: K.FAINT }, { t: stepTag, fg: K.FAINT }, ...md[0]]);
|
|
1096
|
+
mark(realIdx, bi);
|
|
1097
|
+
for (const ln of md.slice(1)) { lines.push([{ t: " " }, ...ln]); mark(realIdx, bi); }
|
|
1072
1098
|
} else {
|
|
1073
|
-
lines.push([{ t: " " + stepTag, fg: K.FAINT }]);
|
|
1099
|
+
lines.push([{ t: " " + glyph + " " + stepTag, fg: K.FAINT }]);
|
|
1100
|
+
mark(realIdx, bi);
|
|
1101
|
+
}
|
|
1102
|
+
if (!open && text.length > 0) {
|
|
1103
|
+
lines.push([{ t: ` …共 ${text.length} 字(点击展开)`, fg: K.FAINT }]);
|
|
1074
1104
|
mark(realIdx, bi);
|
|
1075
1105
|
}
|
|
1076
1106
|
sep();
|
|
@@ -1298,13 +1328,13 @@ export class ChatView extends Widget {
|
|
|
1298
1328
|
case "pgup": if (this.view.scrollY === 0) { this.loadOlder(); return true; } return this.view.scroll(-this.view.h);
|
|
1299
1329
|
case "pgdn": return this.view.scroll(this.view.h);
|
|
1300
1330
|
}
|
|
1301
|
-
if (ev.name === "char" && ev.key === "g" && !ev.ctrl) {
|
|
1331
|
+
if (ev.name === "char" && ev.key === "g" && !ev.ctrl && !ev.shift) {
|
|
1302
1332
|
if (this.gKey) { this.gKey = false; this.view.scrollY = 0; return true; }
|
|
1303
1333
|
this.gKey = true;
|
|
1304
1334
|
this.app.toast("再按 g 回顶");
|
|
1305
1335
|
return true;
|
|
1306
1336
|
}
|
|
1307
|
-
if (ev.name === "char" && ev.key === "
|
|
1337
|
+
if (ev.name === "char" && ev.key === "g" && ev.shift) { this.view.scrollY = this.view.maxScroll(); return true; }
|
|
1308
1338
|
if (ev.name === "escape" && this.app.searchQuery) { this.app.searchQuery = null; this.queueRebuild(); return true; }
|
|
1309
1339
|
if (ev.name === "escape" && this.selStart !== null) { this.selStart = this.selEnd = null; this.app.redraw(); return true; }
|
|
1310
1340
|
if (ev.name === "char" && ev.key === "i" && !ev.ctrl) { this.app.focus(this.input); return true; }
|
|
@@ -1891,18 +1921,26 @@ export class App {
|
|
|
1891
1921
|
async loadFeedback() {
|
|
1892
1922
|
if (!this.currentSession) return;
|
|
1893
1923
|
try {
|
|
1894
|
-
const res = await this.api.rpcCall("messageFeedback/list", { sessionId: this.currentSession });
|
|
1924
|
+
const res = await this.api.rpcCall("messageFeedback/list", { request: { sessionId: this.currentSession } });
|
|
1895
1925
|
this.feedbackMap = new Map();
|
|
1896
|
-
for (const item of res
|
|
1926
|
+
for (const item of res?.value?.items ?? res?.items ?? []) this.feedbackMap.set(item.messageId, item);
|
|
1897
1927
|
} catch { this.feedbackMap = new Map(); }
|
|
1898
1928
|
}
|
|
1899
1929
|
|
|
1900
1930
|
async feedback(messageId, rating) {
|
|
1901
1931
|
const existing = this.feedbackMap?.get(messageId);
|
|
1902
1932
|
try {
|
|
1903
|
-
const
|
|
1904
|
-
|
|
1933
|
+
const res = await this.api.rpcCall("messageFeedback/put", {
|
|
1934
|
+
request: {
|
|
1935
|
+
sessionId: this.currentSession, messageId, rating,
|
|
1936
|
+
ifVersion: existing?.version ?? null,
|
|
1937
|
+
},
|
|
1905
1938
|
});
|
|
1939
|
+
if (res?.ok === false) {
|
|
1940
|
+
this.toast(`反馈失败: ${res.error?.code ?? res.error?.message ?? "unknown"}`);
|
|
1941
|
+
return;
|
|
1942
|
+
}
|
|
1943
|
+
const item = res?.value ?? res;
|
|
1906
1944
|
this.feedbackMap = this.feedbackMap ?? new Map();
|
|
1907
1945
|
this.feedbackMap.set(messageId, item);
|
|
1908
1946
|
this.toast(rating === "positive" ? "已记录 👍" : "已记录 👎");
|
|
@@ -1913,7 +1951,13 @@ export class App {
|
|
|
1913
1951
|
const existing = this.feedbackMap?.get(messageId);
|
|
1914
1952
|
if (!existing) return;
|
|
1915
1953
|
try {
|
|
1916
|
-
await this.api.rpcCall("messageFeedback/delete", {
|
|
1954
|
+
const res = await this.api.rpcCall("messageFeedback/delete", {
|
|
1955
|
+
request: { sessionId: this.currentSession, messageId, ifVersion: existing.version },
|
|
1956
|
+
});
|
|
1957
|
+
if (res?.ok === false) {
|
|
1958
|
+
this.toast(`删除反馈失败: ${res.error?.code ?? res.error?.message ?? "unknown"}`);
|
|
1959
|
+
return;
|
|
1960
|
+
}
|
|
1917
1961
|
this.feedbackMap.delete(messageId);
|
|
1918
1962
|
this.toast("已删除反馈");
|
|
1919
1963
|
} catch (e) { this.toast(`删除反馈失败: ${e.message}`); }
|
|
@@ -2290,16 +2334,29 @@ export class App {
|
|
|
2290
2334
|
}
|
|
2291
2335
|
if (this.menu) {
|
|
2292
2336
|
const before = this.menu;
|
|
2293
|
-
if (ev.type === "key") this.menu.onKey(ev);
|
|
2294
|
-
|
|
2295
|
-
if (ev.kind === "press" && ev.button === 0 && !this.menu.inside(ev.x, ev.y)) {
|
|
2296
|
-
|
|
2337
|
+
if (ev.type === "key") { this.menu.onKey(ev); this.redraw(); return; }
|
|
2338
|
+
if (ev.type === "mouse") {
|
|
2339
|
+
if (ev.kind === "press" && ev.button === 0 && !this.menu.inside(ev.x, ev.y)) {
|
|
2340
|
+
this.menu = null; this.swallowRelease = true; this.redraw(); return;
|
|
2341
|
+
}
|
|
2342
|
+
if (ev.kind === "press" && ev.button === 2) {
|
|
2343
|
+
if (this.menu.inside(ev.x, ev.y)) { this.redraw(); return; } // right-click on the menu itself: keep it open
|
|
2344
|
+
// OS-style: a right-click anywhere else closes the current menu and
|
|
2345
|
+
// the SAME press falls through to open a fresh menu at the new spot.
|
|
2346
|
+
this.menu = null;
|
|
2347
|
+
this.swallowRelease = true;
|
|
2348
|
+
this.redraw();
|
|
2349
|
+
// fall through to the normal mouse routing below
|
|
2350
|
+
} else {
|
|
2297
2351
|
this.menu.onMouse(ev);
|
|
2298
2352
|
if (ev.kind === "press" && this.menu !== before) this.swallowRelease = true;
|
|
2353
|
+
this.redraw();
|
|
2354
|
+
return;
|
|
2299
2355
|
}
|
|
2356
|
+
} else {
|
|
2357
|
+
this.redraw();
|
|
2358
|
+
return;
|
|
2300
2359
|
}
|
|
2301
|
-
this.redraw();
|
|
2302
|
-
return;
|
|
2303
2360
|
}
|
|
2304
2361
|
if (this.overlay) {
|
|
2305
2362
|
const before = this.overlay;
|
|
@@ -2443,7 +2500,14 @@ export class App {
|
|
|
2443
2500
|
return;
|
|
2444
2501
|
}
|
|
2445
2502
|
if (ev.text.length === 1) {
|
|
2446
|
-
|
|
2503
|
+
// Legacy terminals deliver Shift+letter as the UPPERCASE char with no
|
|
2504
|
+
// modifier info — recover the shift flag from the case so Shift+T
|
|
2505
|
+
// (todo fold) and G (scroll bottom) work everywhere.
|
|
2506
|
+
const asKey = {
|
|
2507
|
+
type: "key", name: "char",
|
|
2508
|
+
key: ev.text.toLowerCase(), text: ev.text,
|
|
2509
|
+
ctrl: false, alt: false, shift: ev.text !== ev.text.toLowerCase(),
|
|
2510
|
+
};
|
|
2447
2511
|
if (this.chat.onKey(asKey)) { this.redraw(); return; }
|
|
2448
2512
|
if (this.focused && this.focused !== this.chat.input && this.focused.onKey(asKey)) { this.redraw(); return; }
|
|
2449
2513
|
this.toast("按 i 进入输入");
|