dsh-neotui 0.0.15 → 0.0.17
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/views.js +110 -65
package/package.json
CHANGED
package/src/views.js
CHANGED
|
@@ -817,6 +817,7 @@ export class ChatView extends Widget {
|
|
|
817
817
|
send(text) {
|
|
818
818
|
if (!this.sessionId) return;
|
|
819
819
|
const trimmed = text.trim();
|
|
820
|
+
if (trimmed === "/reload") { this.app.reloadApp(); return; }
|
|
820
821
|
if (!trimmed) return;
|
|
821
822
|
const { parts, images, errors } = buildPromptParts(trimmed, {
|
|
822
823
|
readFile: (p) => {
|
|
@@ -836,77 +837,102 @@ export class ChatView extends Widget {
|
|
|
836
837
|
}
|
|
837
838
|
|
|
838
839
|
#clickLine(y, ev) {
|
|
839
|
-
//
|
|
840
|
-
//
|
|
841
|
-
|
|
842
|
-
//
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
840
|
+
// NO flush here: the click maps against the exact frame the user saw.
|
|
841
|
+
// Flushing would re-derive the streaming tail (the chunk-stream and the
|
|
842
|
+
// poll snapshot produce DIFFERENT block structures) and make y point at a
|
|
843
|
+
// different block — the source of the 5–6 line offsets.
|
|
844
|
+
return this.#toggleAt(this.lineMap?.[y]);
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
/** Toggle the block/node under a line-mark, then re-anchor the viewport.
|
|
848
|
+
* `info` must be the mark of the frame the user clicked on. */
|
|
849
|
+
#toggleAt(info) {
|
|
850
|
+
if (!info) return false;
|
|
851
|
+
if (info.imgIdx !== undefined) {
|
|
850
852
|
const node = this.nodes[info.nodeIdx];
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
853
|
+
const ref = node?.images?.[info.imgIdx];
|
|
854
|
+
if (ref) { this.app.openImage(ref, { all: node.images, index: info.imgIdx }); return true; }
|
|
855
|
+
}
|
|
856
|
+
const node = this.nodes[info.nodeIdx];
|
|
857
|
+
if (!node) return false;
|
|
858
|
+
// anchor context captured from the frame the user saw:
|
|
859
|
+
// - the clicked block's header line + its viewport row (primary anchor)
|
|
860
|
+
// - the viewport-top line's identity + offset (fallback for deep clicks)
|
|
861
|
+
const lineKey = (m) => (m ? `${m.nodeIdx}:${m.blockIdx ?? "n"}` : null);
|
|
862
|
+
const topKey = lineKey(this.lineMap[this.view.scrollY]) ?? null;
|
|
863
|
+
let topFirst = -1;
|
|
864
|
+
if (topKey !== null) {
|
|
865
|
+
for (let i = 0; i < this.lineMap.length; i++) {
|
|
866
|
+
if (lineKey(this.lineMap[i]) === topKey) { topFirst = i; break; }
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
const topOffset = topFirst >= 0 ? this.view.scrollY - topFirst : 0;
|
|
870
|
+
const match = info.blockIdx !== null
|
|
871
|
+
? (m) => m?.nodeIdx === info.nodeIdx && m?.blockIdx === info.blockIdx
|
|
872
|
+
: (m) => m?.nodeIdx === info.nodeIdx;
|
|
873
|
+
const firstNonEmpty = (m) => {
|
|
874
|
+
for (let i = 0; i < this.lineMap.length; i++) {
|
|
875
|
+
if (m(this.lineMap[i]) && (this.lines[i] ?? []).some((g) => g.t.trim() !== "")) return i;
|
|
876
|
+
}
|
|
877
|
+
return -1;
|
|
878
|
+
};
|
|
879
|
+
const preHeaderIdx = firstNonEmpty(match);
|
|
880
|
+
const preHeaderRow = preHeaderIdx >= 0 ? preHeaderIdx - this.view.scrollY : null;
|
|
881
|
+
const reanchor = () => {
|
|
882
|
+
// primary anchor: the clicked block's header stays at its pre-click
|
|
883
|
+
// viewport row (zero shift) — only when the header was ON-SCREEN
|
|
884
|
+
if (preHeaderRow !== null && preHeaderRow >= 0 && preHeaderIdx >= 0) {
|
|
885
|
+
const h2 = firstNonEmpty(match);
|
|
886
|
+
if (h2 >= 0) {
|
|
887
|
+
this.view.scrollY = Math.max(0, Math.min(h2 - preHeaderRow, this.view.maxScroll()));
|
|
888
|
+
return;
|
|
863
889
|
}
|
|
864
890
|
}
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
this.#rebuild();
|
|
898
|
-
reanchor();
|
|
899
|
-
return true;
|
|
891
|
+
// fallback (deep content click): restore the viewport-top position
|
|
892
|
+
if (topKey === null || topFirst < 0) return;
|
|
893
|
+
let first = -1, last = -1;
|
|
894
|
+
for (let i = 0; i < this.lineMap.length; i++) {
|
|
895
|
+
if (lineKey(this.lineMap[i]) !== topKey) continue;
|
|
896
|
+
if (first < 0) first = i;
|
|
897
|
+
last = i;
|
|
898
|
+
}
|
|
899
|
+
if (first < 0) return;
|
|
900
|
+
const target = Math.min(first + topOffset, last);
|
|
901
|
+
this.view.scrollY = Math.max(0, Math.min(target, this.view.maxScroll()));
|
|
902
|
+
};
|
|
903
|
+
// never park the viewport on a blank separator row — it reads as a
|
|
904
|
+
// spurious offset right after the click
|
|
905
|
+
const nudge = () => {
|
|
906
|
+
while (
|
|
907
|
+
this.view.scrollY < this.lineMap.length - 1 &&
|
|
908
|
+
!(this.lines[this.view.scrollY] ?? []).some((g) => g.t.trim() !== "")
|
|
909
|
+
) this.view.scrollY++;
|
|
910
|
+
};
|
|
911
|
+
if (node.kind === "assistant" && info.blockIdx !== null) {
|
|
912
|
+
const b = node.blocks[info.blockIdx];
|
|
913
|
+
if (b && (b.kind === "tool" || b.kind === "reasoning" || b.kind === "other" || b.kind === "text")) {
|
|
914
|
+
const key = `${info.nodeIdx}:${info.blockIdx}`;
|
|
915
|
+
if (b.kind === "reasoning") {
|
|
916
|
+
// clean two-state override: expand ⇄ collapse (never a no-op click)
|
|
917
|
+
const open = this.expanded.has(key) || (!this.collapsedBlocks.has(key) && this.thinkMode === "expanded");
|
|
918
|
+
if (open) { this.expanded.delete(key); this.collapsedBlocks.add(key); }
|
|
919
|
+
else { this.collapsedBlocks.delete(key); this.expanded.add(key); }
|
|
920
|
+
} else {
|
|
921
|
+
if (this.collapsedBlocks.has(key)) this.collapsedBlocks.delete(key);
|
|
922
|
+
else this.collapsedBlocks.add(key);
|
|
900
923
|
}
|
|
901
|
-
}
|
|
902
|
-
if (node && (node.kind === "assistant" || node.kind === "user")) {
|
|
903
|
-
if (this.expanded.has(info.nodeIdx)) this.expanded.delete(info.nodeIdx);
|
|
904
|
-
else this.expanded.add(info.nodeIdx);
|
|
905
924
|
this.#rebuild();
|
|
906
|
-
reanchor();
|
|
925
|
+
reanchor(); nudge();
|
|
907
926
|
return true;
|
|
908
927
|
}
|
|
909
928
|
}
|
|
929
|
+
if (node.kind === "assistant" || node.kind === "user") {
|
|
930
|
+
if (this.expanded.has(info.nodeIdx)) this.expanded.delete(info.nodeIdx);
|
|
931
|
+
else this.expanded.add(info.nodeIdx);
|
|
932
|
+
this.#rebuild();
|
|
933
|
+
reanchor(); nudge();
|
|
934
|
+
return true;
|
|
935
|
+
}
|
|
910
936
|
return false;
|
|
911
937
|
}
|
|
912
938
|
|
|
@@ -1309,14 +1335,14 @@ export class ChatView extends Widget {
|
|
|
1309
1335
|
return true;
|
|
1310
1336
|
}
|
|
1311
1337
|
if (ev.kind === "press" && ev.button === 2) {
|
|
1312
|
-
|
|
1338
|
+
// map against the frame the user saw — no flush (see #clickLine)
|
|
1313
1339
|
const y = ev.y - this.view.y + this.view.scrollY;
|
|
1314
1340
|
const info = this.lineMap?.[y];
|
|
1315
1341
|
if (info) {
|
|
1316
1342
|
const node = this.nodes[info.nodeIdx];
|
|
1317
1343
|
const items = [
|
|
1318
1344
|
{ label: "复制消息", action: () => this.app.copyNode(info.nodeIdx) },
|
|
1319
|
-
{ label: "展开 / 折叠", action: () => this.#
|
|
1345
|
+
{ label: "展开 / 折叠", action: () => this.#toggleAt(info) },
|
|
1320
1346
|
];
|
|
1321
1347
|
if (node?.id) {
|
|
1322
1348
|
items.push({ label: "转跳轨迹", action: () => this.app.jumpToTrajectoryNode(info.nodeIdx) });
|
|
@@ -2162,6 +2188,25 @@ export class App {
|
|
|
2162
2188
|
showModePicker() { this.overlay = buildModePicker(this); this.redraw(); }
|
|
2163
2189
|
showPermissionPicker() { this.overlay = buildPermissionPicker(this); this.redraw(); }
|
|
2164
2190
|
|
|
2191
|
+
/** /reload: restart the TUI process in the same terminal so a freshly
|
|
2192
|
+
* published build (the profile symlinks the repo) takes effect without
|
|
2193
|
+
* quitting and re-running `dsh` by hand. */
|
|
2194
|
+
async reloadApp() {
|
|
2195
|
+
this.toast("正在重启 TUI…");
|
|
2196
|
+
this.redraw();
|
|
2197
|
+
await new Promise((r) => setTimeout(r, 250));
|
|
2198
|
+
try { this.term?.stop?.(); } catch {}
|
|
2199
|
+
try {
|
|
2200
|
+
const { spawn } = await import("node:child_process");
|
|
2201
|
+
const child = spawn(process.argv[0], process.argv.slice(1), { detached: true, stdio: "inherit" });
|
|
2202
|
+
child.unref();
|
|
2203
|
+
} catch (e) {
|
|
2204
|
+
this.toast(`重启失败: ${e.message}(请手动重启)`);
|
|
2205
|
+
return;
|
|
2206
|
+
}
|
|
2207
|
+
process.exit(0);
|
|
2208
|
+
}
|
|
2209
|
+
|
|
2165
2210
|
/** Ctrl+E: fzf-style quick jump to a step — type to fuzzy-filter the step
|
|
2166
2211
|
* list, Enter opens the trajectory window around the picked step. */
|
|
2167
2212
|
async quickJumpStep() {
|