dsh-neotui 0.0.10 → 0.0.11
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 +3 -2
- package/package.json +1 -1
- package/src/views.js +69 -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` 回最新 |
|
|
@@ -102,7 +103,7 @@ tui/app/ dsh-neotui-app bundle:cordis.patch.yml + tui-startup/tui-r
|
|
|
102
103
|
- 轨迹 详细/简略:每个 step 左键单击(▸/▾)或右键菜单第一项 →「展开(详细)/ 折叠(简略)」内联展开事件列表(仿对话页);跳转定位的 step 自动展开 + 高亮闪烁
|
|
103
104
|
- 轨迹窗口模型:转跳只显示 `step±20`;PgUp/PgDn 按 10 步扩展窗口(按需向后翻页加载),Home 加载到 step 1–20,End 回到最新 20 步,窗口指示行显示「窗口 step A–B · 共 N 步」
|
|
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/views.js
CHANGED
|
@@ -850,7 +850,7 @@ export class ChatView extends Widget {
|
|
|
850
850
|
const node = this.nodes[info.nodeIdx];
|
|
851
851
|
if (node?.kind === "assistant" && info.blockIdx !== null) {
|
|
852
852
|
const b = node.blocks[info.blockIdx];
|
|
853
|
-
if (b && (b.kind === "tool" || b.kind === "reasoning" || b.kind === "other")) {
|
|
853
|
+
if (b && (b.kind === "tool" || b.kind === "reasoning" || b.kind === "other" || b.kind === "text")) {
|
|
854
854
|
const key = `${info.nodeIdx}:${info.blockIdx}`;
|
|
855
855
|
if (b.kind === "reasoning") {
|
|
856
856
|
// clean two-state override: expand ⇄ collapse (never a no-op click)
|
|
@@ -891,7 +891,6 @@ export class ChatView extends Widget {
|
|
|
891
891
|
if (this.hasMore) { lines.push([{ t: "▲ 更早的记录", fg: K.FAINT }]); mark(-1); }
|
|
892
892
|
if (skipCount > 0) { lines.push([{ t: `… 更早 ${skipCount} 条记录(PgUp 加载)`, fg: K.FAINT }]); mark(-1); }
|
|
893
893
|
|
|
894
|
-
const MAX_TEXT = 4000;
|
|
895
894
|
for (let ni = 0; ni < nodes.length; ni++) {
|
|
896
895
|
const node = nodes[ni];
|
|
897
896
|
const realIdx = ni + skipCount;
|
|
@@ -999,7 +998,8 @@ export class ChatView extends Widget {
|
|
|
999
998
|
lines.push([{ t: "💭 思考" + (b.streaming ? "…" : "") + thinkMeta + (open ? " [t 折叠]" : " [t 展开]"), fg: K.FAINT }]);
|
|
1000
999
|
mark(realIdx, bi);
|
|
1001
1000
|
if (open) {
|
|
1002
|
-
|
|
1001
|
+
// Expanded = the WHOLE reasoning (no hidden remainder).
|
|
1002
|
+
for (const ln of renderMd(b.text ?? "", w - 4)) { lines.push([{ t: " " }, ...ln]); mark(realIdx, bi); }
|
|
1003
1003
|
} else {
|
|
1004
1004
|
// collapsed: three-line preview
|
|
1005
1005
|
for (const ln of renderMd(truncateText(b.text, 400), w - 4).slice(0, 3)) {
|
|
@@ -1059,18 +1059,27 @@ export class ChatView extends Widget {
|
|
|
1059
1059
|
sep();
|
|
1060
1060
|
} else {
|
|
1061
1061
|
beginCard("CARD");
|
|
1062
|
+
// Text blocks collapse per-block like tool/reasoning blocks:
|
|
1063
|
+
// collapsed shows a 3-line preview + trailer, expanded the whole
|
|
1064
|
+
// text. Clicking (or the right-click 展开/折叠) toggles it.
|
|
1065
|
+
const key = `${realIdx}:${bi}`;
|
|
1066
|
+
const open = !this.collapsedBlocks.has(key);
|
|
1062
1067
|
const text = b.text ?? "";
|
|
1063
|
-
const
|
|
1068
|
+
const glyph = open ? "▾" : "▸";
|
|
1069
|
+
const mdW = Math.max(10, w - 6 - strWidth(stepTag));
|
|
1070
|
+
const md = open
|
|
1071
|
+
? renderMd(text, mdW)
|
|
1072
|
+
: renderMd(truncateText(text, 600), mdW).slice(0, 3);
|
|
1064
1073
|
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 };
|
|
1074
|
+
lines.push([{ t: " " }, { t: glyph + " ", fg: K.FAINT }, { t: stepTag, fg: K.FAINT }, ...md[0]]);
|
|
1075
|
+
mark(realIdx, bi);
|
|
1076
|
+
for (const ln of md.slice(1)) { lines.push([{ t: " " }, ...ln]); mark(realIdx, bi); }
|
|
1072
1077
|
} else {
|
|
1073
|
-
lines.push([{ t: " " + stepTag, fg: K.FAINT }]);
|
|
1078
|
+
lines.push([{ t: " " + glyph + " " + stepTag, fg: K.FAINT }]);
|
|
1079
|
+
mark(realIdx, bi);
|
|
1080
|
+
}
|
|
1081
|
+
if (!open && text.length > 0) {
|
|
1082
|
+
lines.push([{ t: ` …共 ${text.length} 字(点击展开)`, fg: K.FAINT }]);
|
|
1074
1083
|
mark(realIdx, bi);
|
|
1075
1084
|
}
|
|
1076
1085
|
sep();
|
|
@@ -1298,13 +1307,13 @@ export class ChatView extends Widget {
|
|
|
1298
1307
|
case "pgup": if (this.view.scrollY === 0) { this.loadOlder(); return true; } return this.view.scroll(-this.view.h);
|
|
1299
1308
|
case "pgdn": return this.view.scroll(this.view.h);
|
|
1300
1309
|
}
|
|
1301
|
-
if (ev.name === "char" && ev.key === "g" && !ev.ctrl) {
|
|
1310
|
+
if (ev.name === "char" && ev.key === "g" && !ev.ctrl && !ev.shift) {
|
|
1302
1311
|
if (this.gKey) { this.gKey = false; this.view.scrollY = 0; return true; }
|
|
1303
1312
|
this.gKey = true;
|
|
1304
1313
|
this.app.toast("再按 g 回顶");
|
|
1305
1314
|
return true;
|
|
1306
1315
|
}
|
|
1307
|
-
if (ev.name === "char" && ev.key === "
|
|
1316
|
+
if (ev.name === "char" && ev.key === "g" && ev.shift) { this.view.scrollY = this.view.maxScroll(); return true; }
|
|
1308
1317
|
if (ev.name === "escape" && this.app.searchQuery) { this.app.searchQuery = null; this.queueRebuild(); return true; }
|
|
1309
1318
|
if (ev.name === "escape" && this.selStart !== null) { this.selStart = this.selEnd = null; this.app.redraw(); return true; }
|
|
1310
1319
|
if (ev.name === "char" && ev.key === "i" && !ev.ctrl) { this.app.focus(this.input); return true; }
|
|
@@ -1891,18 +1900,26 @@ export class App {
|
|
|
1891
1900
|
async loadFeedback() {
|
|
1892
1901
|
if (!this.currentSession) return;
|
|
1893
1902
|
try {
|
|
1894
|
-
const res = await this.api.rpcCall("messageFeedback/list", { sessionId: this.currentSession });
|
|
1903
|
+
const res = await this.api.rpcCall("messageFeedback/list", { request: { sessionId: this.currentSession } });
|
|
1895
1904
|
this.feedbackMap = new Map();
|
|
1896
|
-
for (const item of res
|
|
1905
|
+
for (const item of res?.value?.items ?? res?.items ?? []) this.feedbackMap.set(item.messageId, item);
|
|
1897
1906
|
} catch { this.feedbackMap = new Map(); }
|
|
1898
1907
|
}
|
|
1899
1908
|
|
|
1900
1909
|
async feedback(messageId, rating) {
|
|
1901
1910
|
const existing = this.feedbackMap?.get(messageId);
|
|
1902
1911
|
try {
|
|
1903
|
-
const
|
|
1904
|
-
|
|
1912
|
+
const res = await this.api.rpcCall("messageFeedback/put", {
|
|
1913
|
+
request: {
|
|
1914
|
+
sessionId: this.currentSession, messageId, rating,
|
|
1915
|
+
ifVersion: existing?.version ?? null,
|
|
1916
|
+
},
|
|
1905
1917
|
});
|
|
1918
|
+
if (res?.ok === false) {
|
|
1919
|
+
this.toast(`反馈失败: ${res.error?.code ?? res.error?.message ?? "unknown"}`);
|
|
1920
|
+
return;
|
|
1921
|
+
}
|
|
1922
|
+
const item = res?.value ?? res;
|
|
1906
1923
|
this.feedbackMap = this.feedbackMap ?? new Map();
|
|
1907
1924
|
this.feedbackMap.set(messageId, item);
|
|
1908
1925
|
this.toast(rating === "positive" ? "已记录 👍" : "已记录 👎");
|
|
@@ -1913,7 +1930,13 @@ export class App {
|
|
|
1913
1930
|
const existing = this.feedbackMap?.get(messageId);
|
|
1914
1931
|
if (!existing) return;
|
|
1915
1932
|
try {
|
|
1916
|
-
await this.api.rpcCall("messageFeedback/delete", {
|
|
1933
|
+
const res = await this.api.rpcCall("messageFeedback/delete", {
|
|
1934
|
+
request: { sessionId: this.currentSession, messageId, ifVersion: existing.version },
|
|
1935
|
+
});
|
|
1936
|
+
if (res?.ok === false) {
|
|
1937
|
+
this.toast(`删除反馈失败: ${res.error?.code ?? res.error?.message ?? "unknown"}`);
|
|
1938
|
+
return;
|
|
1939
|
+
}
|
|
1917
1940
|
this.feedbackMap.delete(messageId);
|
|
1918
1941
|
this.toast("已删除反馈");
|
|
1919
1942
|
} catch (e) { this.toast(`删除反馈失败: ${e.message}`); }
|
|
@@ -2290,16 +2313,29 @@ export class App {
|
|
|
2290
2313
|
}
|
|
2291
2314
|
if (this.menu) {
|
|
2292
2315
|
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
|
-
|
|
2316
|
+
if (ev.type === "key") { this.menu.onKey(ev); this.redraw(); return; }
|
|
2317
|
+
if (ev.type === "mouse") {
|
|
2318
|
+
if (ev.kind === "press" && ev.button === 0 && !this.menu.inside(ev.x, ev.y)) {
|
|
2319
|
+
this.menu = null; this.swallowRelease = true; this.redraw(); return;
|
|
2320
|
+
}
|
|
2321
|
+
if (ev.kind === "press" && ev.button === 2) {
|
|
2322
|
+
if (this.menu.inside(ev.x, ev.y)) { this.redraw(); return; } // right-click on the menu itself: keep it open
|
|
2323
|
+
// OS-style: a right-click anywhere else closes the current menu and
|
|
2324
|
+
// the SAME press falls through to open a fresh menu at the new spot.
|
|
2325
|
+
this.menu = null;
|
|
2326
|
+
this.swallowRelease = true;
|
|
2327
|
+
this.redraw();
|
|
2328
|
+
// fall through to the normal mouse routing below
|
|
2329
|
+
} else {
|
|
2297
2330
|
this.menu.onMouse(ev);
|
|
2298
2331
|
if (ev.kind === "press" && this.menu !== before) this.swallowRelease = true;
|
|
2332
|
+
this.redraw();
|
|
2333
|
+
return;
|
|
2299
2334
|
}
|
|
2335
|
+
} else {
|
|
2336
|
+
this.redraw();
|
|
2337
|
+
return;
|
|
2300
2338
|
}
|
|
2301
|
-
this.redraw();
|
|
2302
|
-
return;
|
|
2303
2339
|
}
|
|
2304
2340
|
if (this.overlay) {
|
|
2305
2341
|
const before = this.overlay;
|
|
@@ -2443,7 +2479,14 @@ export class App {
|
|
|
2443
2479
|
return;
|
|
2444
2480
|
}
|
|
2445
2481
|
if (ev.text.length === 1) {
|
|
2446
|
-
|
|
2482
|
+
// Legacy terminals deliver Shift+letter as the UPPERCASE char with no
|
|
2483
|
+
// modifier info — recover the shift flag from the case so Shift+T
|
|
2484
|
+
// (todo fold) and G (scroll bottom) work everywhere.
|
|
2485
|
+
const asKey = {
|
|
2486
|
+
type: "key", name: "char",
|
|
2487
|
+
key: ev.text.toLowerCase(), text: ev.text,
|
|
2488
|
+
ctrl: false, alt: false, shift: ev.text !== ev.text.toLowerCase(),
|
|
2489
|
+
};
|
|
2447
2490
|
if (this.chat.onKey(asKey)) { this.redraw(); return; }
|
|
2448
2491
|
if (this.focused && this.focused !== this.chat.input && this.focused.onKey(asKey)) { this.redraw(); return; }
|
|
2449
2492
|
this.toast("按 i 进入输入");
|