dsh-neotui 0.2.1 → 0.2.3
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/CHANGELOG.md +22 -0
- package/README.md +5 -4
- package/package.json +1 -1
- package/src/config.js +2 -1
- package/src/panels.js +12 -25
- package/src/views.js +188 -65
- package/src/widgets.js +4 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,27 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.2.2 — 2026-08-16
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- 底栏在子代理状态后显示“有 x 条命令正在排队 Ctrl+N 查看详情”。
|
|
8
|
+
- Ctrl+N 打开精简排队命令面板;每条命令一行,支持 `dd` 删除。
|
|
9
|
+
|
|
10
|
+
### Changed
|
|
11
|
+
|
|
12
|
+
- AskUser 窗口按实际内容自适应高度,长内容才启用滚动。
|
|
13
|
+
- 选项标签与描述只显示一次,完整描述按终端宽度换行。
|
|
14
|
+
- 自定义答案常驻在选项下一行并支持最多六行自动换行。
|
|
15
|
+
- 长正文使用 Ctrl+↑/Ctrl+↓、PgUp/PgDn、Home/End 和滚轮翻页。
|
|
16
|
+
- 权限审批窗口保留完整原因和命令,明确显示滚动提示与位置。
|
|
17
|
+
|
|
18
|
+
### Fixed
|
|
19
|
+
|
|
20
|
+
- AskUser 自定义回答、真实跳过项、精确鼠标 hitbox 和文本光标编辑。
|
|
21
|
+
- 权限申请窗口支持 ↑/↓ 逐行滚动,不改变安全默认“拒绝”。
|
|
22
|
+
- 超长审批命令不再只保留前六行。
|
|
23
|
+
- Queue 面板移除未要求的编辑与 steering 操作,`dd` 同时兼容 text/key 事件。
|
|
24
|
+
|
|
3
25
|
## 0.2.1 — 2026-08-16
|
|
4
26
|
|
|
5
27
|
### Fixed
|
package/README.md
CHANGED
|
@@ -131,7 +131,7 @@ Kitty graphics 可用时,图片在文件选择器和附件预览中等比例
|
|
|
131
131
|
- `queue`:加入下一回合队列;
|
|
132
132
|
- `steer`:追加到当前回合。
|
|
133
133
|
|
|
134
|
-
`Ctrl+Y` 切换策略,`Ctrl+
|
|
134
|
+
`Ctrl+Y` 切换策略,`Ctrl+N` 打开排队命令详情。队列面板每条命令一行,使用 `↑/↓` 选择、`dd` 删除、`Esc` 关闭。
|
|
135
135
|
|
|
136
136
|
## 快捷键
|
|
137
137
|
|
|
@@ -144,14 +144,15 @@ Kitty graphics 可用时,图片在文件选择器和附件预览中等比例
|
|
|
144
144
|
| NORMAL | `Ctrl+F` | 跨会话模糊定位 |
|
|
145
145
|
| NORMAL | `Ctrl+B` | 显示/隐藏侧栏 |
|
|
146
146
|
| NORMAL | `Ctrl+M` | 模型与思考强度 |
|
|
147
|
-
| NORMAL | `
|
|
147
|
+
| NORMAL | `Shift+Tab` / `F8` | 权限模式轮换 |
|
|
148
|
+
| NORMAL | `F9` | Agent 模式 |
|
|
148
149
|
| NORMAL | `Ctrl+W` | 工作区 |
|
|
149
150
|
| NORMAL | `Ctrl+Shift+W` | 添加工作区 |
|
|
150
151
|
| NORMAL | `Ctrl+T` | 轨迹视图 |
|
|
151
|
-
| NORMAL | `
|
|
152
|
+
| NORMAL | `Tab` | 对话/轨迹切换 |
|
|
152
153
|
| NORMAL | `Ctrl+E` | 按 step 快速跳转 |
|
|
153
154
|
| NORMAL | `Ctrl+J` | 后台任务与 Subagent |
|
|
154
|
-
| NORMAL | `Ctrl+
|
|
155
|
+
| NORMAL | `Ctrl+N` | 排队命令详情 |
|
|
155
156
|
| NORMAL | `Ctrl+G` | Goal / TODO |
|
|
156
157
|
| NORMAL | `Ctrl+S` | Settings |
|
|
157
158
|
| NORMAL | `Ctrl+A` | Subagent |
|
package/package.json
CHANGED
package/src/config.js
CHANGED
|
@@ -74,7 +74,8 @@ export const DEFAULT_KEYBINDINGS = {
|
|
|
74
74
|
leaveInsert: { mode: "insert", key: "Esc" }, sessionFilter: { mode: "normal", key: "/" }, newSession: { mode: "normal", key: "n" },
|
|
75
75
|
top: { mode: "normal", key: "g g" }, bottom: { mode: "normal", key: "G" }, prevQuestion: { mode: "normal", key: "[" }, nextQuestion: { mode: "normal", key: "]" },
|
|
76
76
|
expandInput: { mode: "insert", key: "Ctrl+L" }, copyInput: { mode: "insert", key: "Ctrl+Shift+C" }, panel: { mode: "all", key: "Ctrl+Space" },
|
|
77
|
-
model: { mode: "normal", key: "Ctrl+M" }, trajectory: { mode: "normal", key: "Ctrl+T" }, homeSwitch: { mode: "normal", key: "
|
|
77
|
+
model: { mode: "normal", key: "Ctrl+M" }, trajectory: { mode: "normal", key: "Ctrl+T" }, homeSwitch: { mode: "normal", key: "Tab" },
|
|
78
|
+
permissionRotate: { mode: "normal", key: "Shift+Tab" },
|
|
78
79
|
workspace: { mode: "normal", key: "Ctrl+W" }, settings: { mode: "normal", key: "Ctrl+S" }, subagent: { mode: "normal", key: "Ctrl+A" },
|
|
79
80
|
skills: { mode: "normal", key: "Ctrl+K" }, goal: { mode: "normal", key: "Ctrl+G" }, jobs: { mode: "normal", key: "Ctrl+J" },
|
|
80
81
|
stepJump: { mode: "normal", key: "Ctrl+E" }, sidebar: { mode: "normal", key: "Ctrl+B" }, quit: { mode: "all", key: "Ctrl+Q" },
|
package/src/panels.js
CHANGED
|
@@ -1453,7 +1453,7 @@ export class ControlPanel extends Widget {
|
|
|
1453
1453
|
const b=keyBindings();
|
|
1454
1454
|
const row=(id,desc)=>[`${(b[id]?.mode??"all").toUpperCase()}\t${b[id]?.key??""}`,desc,null,id];
|
|
1455
1455
|
return [
|
|
1456
|
-
row("think","思考块 展开/折叠"),row("tools","工具块 展开/折叠"),row("insert","进入输入"),row("leaveInsert","退出输入"),row("sessionFilter","筛选会话"),row("newSession","新建会话"),row("top","滚动到顶"),row("bottom","滚动到底"),row("prevQuestion","上一提问的终点"),row("nextQuestion","下一提问的终点"),row("expandInput","输入栏 展开/折叠"),row("copyInput","复制输入栏选区"),row("panel","控制面板"),row("model","切换模型"),row("trajectory","轨迹视图"),row("homeSwitch","对话/轨迹切换"),row("workspace","工作区"),row("settings","设置"),row("subagent","子代理"),row("skills","技能"),row("goal","目标"),row("jobs","后台任务"),row("stepJump","步骤转跳"),row("sidebar","侧栏显示/隐藏"),row("quit","退出"),
|
|
1456
|
+
row("think","思考块 展开/折叠"),row("tools","工具块 展开/折叠"),row("insert","进入输入"),row("leaveInsert","退出输入"),row("sessionFilter","筛选会话"),row("newSession","新建会话"),row("top","滚动到顶"),row("bottom","滚动到底"),row("prevQuestion","上一提问的终点"),row("nextQuestion","下一提问的终点"),row("expandInput","输入栏 展开/折叠"),row("copyInput","复制输入栏选区"),row("panel","控制面板"),row("model","切换模型"),row("trajectory","轨迹视图"),row("homeSwitch","对话/轨迹切换"),row("permissionRotate","权限模式轮换"),row("workspace","工作区"),row("settings","设置"),row("subagent","子代理"),row("skills","技能"),row("goal","目标"),row("jobs","后台任务"),row("stepJump","步骤转跳"),row("sidebar","侧栏显示/隐藏"),row("quit","退出"),
|
|
1457
1457
|
];
|
|
1458
1458
|
}
|
|
1459
1459
|
items() {
|
|
@@ -1669,7 +1669,8 @@ export class JobsPanel extends Popup {
|
|
|
1669
1669
|
for (let i = 0; i < this.subagents.length; i++) {
|
|
1670
1670
|
const child = this.subagents[i], bg = i === this.sel ? T.MENUSEL : T.BG2;
|
|
1671
1671
|
const status = child.activity ?? child.status ?? child.mode ?? "idle";
|
|
1672
|
-
|
|
1672
|
+
const open = this.expanded.has(i);
|
|
1673
|
+
lines.push([{ t: ` ${open ? "▾" : "▸"} 🛰 ${truncate(child.label ?? child.sessionId ?? child.id ?? "子代理", 42)} `, fg: K.TXT, bg, bold: i === this.sel }, { t: status, fg: status === "running" ? K.WARN : K.DIM, bg }]); rowOf.push(i);
|
|
1673
1674
|
if (this.expanded.has(i)) for (const [key, value] of Object.entries(child)) { lines.push([{ t: ` ${key}: ${truncate(typeof value === "object" ? JSON.stringify(value) : value, this.w - 16)}`, fg: K.DIM }]); rowOf.push(-1); }
|
|
1674
1675
|
}
|
|
1675
1676
|
this.lines = lines; this.rowOf = rowOf; this.#ensureVisible(); return;
|
|
@@ -1751,18 +1752,17 @@ export class QueuePanel extends Popup {
|
|
|
1751
1752
|
const items = app.queueItems ?? [];
|
|
1752
1753
|
const w = Math.max(24, Math.min(84, app.screen.w - 4));
|
|
1753
1754
|
const h = Math.max(7, Math.min(24, app.screen.h - 4));
|
|
1754
|
-
super({ x: Math.max(0, Math.floor((app.screen.w - w) / 2)), y: Math.max(0, Math.floor((app.screen.h - h) / 2)), w, h, title: "
|
|
1755
|
-
this.app = app; this.items = items; this.sel = 0; this.pending = false; this.
|
|
1755
|
+
super({ x: Math.max(0, Math.floor((app.screen.w - w) / 2)), y: Math.max(0, Math.floor((app.screen.h - h) / 2)), w, h, title: "排队命令 · ↑↓ 选择 · dd 删除 · Esc 关闭", lines: [], buttons: [], scrollable: true });
|
|
1756
|
+
this.app = app; this.items = items; this.sel = 0; this.pending = false; this.dArmed=false; this.rebuild();
|
|
1756
1757
|
}
|
|
1757
1758
|
rebuild() {
|
|
1758
|
-
const lines = [
|
|
1759
|
+
const lines = [];
|
|
1759
1760
|
for (let i = 0; i < this.items.length; i++) {
|
|
1760
1761
|
const item = this.items[i];
|
|
1761
1762
|
const text = partsText(item.message?.content).replace(/\s+/g, " ");
|
|
1762
1763
|
lines.push([{ t: `${i === this.sel ? "▸" : " "} ${item.placement === "queued" ? "⏳" : item.placement === "steering" ? "↪" : "ℹ"} ${truncate(text || item.id, this.w - 8)}`, fg: i === this.sel ? T.SELFG : K.TXT, bg: i === this.sel ? T.MENUSEL : -1 }]);
|
|
1763
1764
|
}
|
|
1764
1765
|
if (!this.items.length) lines.push([{ t: " (队列为空)", fg: K.FAINT }]);
|
|
1765
|
-
if (this.editInput) lines.push([{ t: ` 编辑: ${truncate(this.editInput.value, this.w - 8)}`, fg: K.ACCENT }]);
|
|
1766
1766
|
this.lines = lines;
|
|
1767
1767
|
}
|
|
1768
1768
|
syncItems(items) {
|
|
@@ -1782,7 +1782,6 @@ export class QueuePanel extends Popup {
|
|
|
1782
1782
|
const action = kind === "edit" ? { kind, content: [{ type: "text", text: content }] } : { kind };
|
|
1783
1783
|
await this.app.api.call("session.updateQueue", { sessionId: this.app.currentSession, itemId: item.id, action });
|
|
1784
1784
|
if (kind === "remove") this.syncItems(this.items.filter((row) => row.id !== item.id));
|
|
1785
|
-
this.editInput = null;
|
|
1786
1785
|
} catch (e) {
|
|
1787
1786
|
const code = this.#errorCode(e);
|
|
1788
1787
|
if (code === "queue-item-not-found" || /queue-item-not-found/.test(e.message ?? "")) {
|
|
@@ -1793,31 +1792,19 @@ export class QueuePanel extends Popup {
|
|
|
1793
1792
|
} else this.app.toast(`队列操作失败: ${e.message}`);
|
|
1794
1793
|
} finally { this.pending = false; this.rebuild(); this.app.redraw(); }
|
|
1795
1794
|
}
|
|
1796
|
-
#beginEdit() {
|
|
1797
|
-
const item = this.items[this.sel];
|
|
1798
|
-
if (!item || item.placement !== "queued") { this.app.toast("只有排队消息可以编辑"); return; }
|
|
1799
|
-
this.editInput = new Input({ x: this.x + 2, y: this.y + this.h - 3, w: this.w - 4, h: 1, prompt: "编辑: ", allowEmptyEnter: false, onEnter: (value) => this.#mutate("edit", value) });
|
|
1800
|
-
this.editInput.setValue(partsText(item.message?.content));
|
|
1801
|
-
this.rebuild();
|
|
1802
|
-
}
|
|
1803
1795
|
onKey(ev) {
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
if (this.editInput.onKey(ev)) { this.rebuild(); this.app.redraw(); return true; }
|
|
1807
|
-
}
|
|
1796
|
+
const ch=ev.type==="text"?ev.text:ev.type==="key"&&ev.name==="char"?ev.key:null;
|
|
1797
|
+
if(ch==="d"){if(this.dArmed){this.dArmed=false;this.#mutate("remove");}else{this.dArmed=true;this.app.toast("再按 d 删除这条排队命令");}return true;}
|
|
1808
1798
|
if (ev.type === "key") {
|
|
1809
1799
|
if (ev.name === "escape") { this.app.closeOverlay(); return true; }
|
|
1810
|
-
if (ev.name === "up") { this.sel = Math.max(0, this.sel - 1); this.rebuild(); return true; }
|
|
1811
|
-
if (ev.name === "down") { this.sel = Math.min(this.items.length - 1, this.sel + 1); this.rebuild(); return true; }
|
|
1812
|
-
if (ev.name === "char" && ev.key === "e" && !ev.ctrl) { this.#beginEdit(); return true; }
|
|
1813
|
-
if (ev.name === "char" && ev.key === "s" && !ev.ctrl) { this.#mutate("steer"); return true; }
|
|
1814
|
-
if (ev.name === "char" && ev.key === "d" && !ev.ctrl) { this.#mutate("remove"); return true; }
|
|
1800
|
+
if (ev.name === "up") { this.dArmed=false;this.sel = Math.max(0, this.sel - 1); this.rebuild(); return true; }
|
|
1801
|
+
if (ev.name === "down") { this.dArmed=false;this.sel = Math.min(this.items.length - 1, this.sel + 1); this.rebuild(); return true; }
|
|
1815
1802
|
}
|
|
1816
|
-
return
|
|
1803
|
+
this.dArmed=false;return false;
|
|
1817
1804
|
}
|
|
1818
1805
|
onMouse(ev) {
|
|
1819
1806
|
if (ev.kind === "press" && ev.button === 0) {
|
|
1820
|
-
const idx = ev.y - this.y -
|
|
1807
|
+
const idx = ev.y - this.y - 1;
|
|
1821
1808
|
if (idx >= 0 && idx < this.items.length) { this.sel = idx; this.rebuild(); this.app.redraw(); return true; }
|
|
1822
1809
|
}
|
|
1823
1810
|
return super.onMouse(ev);
|
package/src/views.js
CHANGED
|
@@ -5,6 +5,7 @@ import { truncate, strWidth, bars, fmtDuration, fmtClock, fmtDateTime, graphemes
|
|
|
5
5
|
import { readFileSync, appendFileSync, mkdirSync } from "node:fs";
|
|
6
6
|
import { join } from "node:path";
|
|
7
7
|
import { spawnSync } from "node:child_process";
|
|
8
|
+
import { createRequire } from "node:module";
|
|
8
9
|
import { Widget, List, ScrollView, Input, Popup, Menu, StatusBar } from "./widgets.js";
|
|
9
10
|
import { UploadPicker } from "./file-picker.js";
|
|
10
11
|
import { userPrefix, saveTuiConfig, loadTuiConfig, userName, busyEnter, foldDefaults, keyBindings, DEFAULT_KEYBINDINGS } from "./config.js";
|
|
@@ -20,6 +21,67 @@ import { T, themeName, cycleTheme } from "./theme.js";
|
|
|
20
21
|
// Live theme accessor: K.K.DIM etc. resolve against the active palette at render time.
|
|
21
22
|
const K = new Proxy({}, { get(_k, key) { return T[key]; } });
|
|
22
23
|
|
|
24
|
+
const require = createRequire(import.meta.url);
|
|
25
|
+
export const TUI_VERSION = (() => {
|
|
26
|
+
try { return JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8")).version; }
|
|
27
|
+
catch { return "unknown"; }
|
|
28
|
+
})();
|
|
29
|
+
let dshVersionCache = null;
|
|
30
|
+
export function installedDshVersion(run = spawnSync) {
|
|
31
|
+
if (dshVersionCache) return dshVersionCache;
|
|
32
|
+
if (process.env.DSH_VERSION) return (dshVersionCache = process.env.DSH_VERSION.replace(/^v/, ""));
|
|
33
|
+
try {
|
|
34
|
+
const file = require.resolve("@deepseek-ai/dsh/package.json");
|
|
35
|
+
const version = JSON.parse(readFileSync(file, "utf8")).version;
|
|
36
|
+
if (version) return (dshVersionCache = version);
|
|
37
|
+
} catch {}
|
|
38
|
+
try {
|
|
39
|
+
const result = run("dsh", ["--version"], { encoding: "utf8", timeout: 2000 });
|
|
40
|
+
const version = String(result.stdout ?? "").trim().replace(/^v/, "");
|
|
41
|
+
if (result.status === 0 && version) return (dshVersionCache = version);
|
|
42
|
+
} catch {}
|
|
43
|
+
return (dshVersionCache = "unknown");
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async function latestNpmVersion(name) {
|
|
47
|
+
const controller = new AbortController();
|
|
48
|
+
const timer = setTimeout(() => controller.abort(), 5000);
|
|
49
|
+
try {
|
|
50
|
+
const response = await fetch(`https://registry.npmjs.org/${encodeURIComponent(name)}/latest`, {
|
|
51
|
+
headers: { accept: "application/json" }, signal: controller.signal,
|
|
52
|
+
});
|
|
53
|
+
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
|
54
|
+
const body = await response.json();
|
|
55
|
+
if (typeof body?.version !== "string" || !body.version) throw new Error("缺少版本号");
|
|
56
|
+
return body.version;
|
|
57
|
+
} finally { clearTimeout(timer); }
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Semver comparison for update checks. Returns null for an unparseable value,
|
|
61
|
+
* otherwise -1/0/1 for left older/equal/newer than right. In particular, a
|
|
62
|
+
* locally newer build must never be advertised as "可更新" to an older npm tag. */
|
|
63
|
+
function compareSemver(left, right) {
|
|
64
|
+
const parse = (value) => {
|
|
65
|
+
const match = /^v?(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/.exec(String(value ?? ""));
|
|
66
|
+
if (!match) return null;
|
|
67
|
+
return { core: match.slice(1, 4).map(Number), pre: match[4]?.split(".") ?? [] };
|
|
68
|
+
};
|
|
69
|
+
const a = parse(left), b = parse(right);
|
|
70
|
+
if (!a || !b) return null;
|
|
71
|
+
for (let i = 0; i < 3; i++) if (a.core[i] !== b.core[i]) return a.core[i] > b.core[i] ? 1 : -1;
|
|
72
|
+
if (a.pre.length === 0 || b.pre.length === 0) return a.pre.length === b.pre.length ? 0 : a.pre.length === 0 ? 1 : -1;
|
|
73
|
+
const n = Math.max(a.pre.length, b.pre.length);
|
|
74
|
+
for (let i = 0; i < n; i++) {
|
|
75
|
+
if (a.pre[i] === undefined || b.pre[i] === undefined) return a.pre[i] === b.pre[i] ? 0 : a.pre[i] === undefined ? -1 : 1;
|
|
76
|
+
if (a.pre[i] === b.pre[i]) continue;
|
|
77
|
+
const ai = /^\d+$/.test(a.pre[i]), bi = /^\d+$/.test(b.pre[i]);
|
|
78
|
+
if (ai && bi) return Number(a.pre[i]) > Number(b.pre[i]) ? 1 : -1;
|
|
79
|
+
if (ai !== bi) return ai ? -1 : 1;
|
|
80
|
+
return a.pre[i] > b.pre[i] ? 1 : -1;
|
|
81
|
+
}
|
|
82
|
+
return 0;
|
|
83
|
+
}
|
|
84
|
+
|
|
23
85
|
// ---- Tool card renderers (host-computed view models) ----
|
|
24
86
|
|
|
25
87
|
function renderToolCard(view, width, expanded) {
|
|
@@ -913,7 +975,6 @@ export class ChatView extends Widget {
|
|
|
913
975
|
this.thinkMode = fd.think ? "expanded" : "collapsed"; // t toggles
|
|
914
976
|
this.bashMode = fd.bash ? "expanded" : "collapsed"; // b toggles
|
|
915
977
|
this.todosVisible = fd.todos; // Shift+T toggles
|
|
916
|
-
this.todoSeen = false; // once seen, the todo box keeps its height
|
|
917
978
|
this.running = false;
|
|
918
979
|
this.hasMore = false;
|
|
919
980
|
this.loadingOlder = false;
|
|
@@ -1100,20 +1161,15 @@ export class ChatView extends Widget {
|
|
|
1100
1161
|
}
|
|
1101
1162
|
}
|
|
1102
1163
|
|
|
1103
|
-
/** Height of the collapsible todo block
|
|
1104
|
-
*
|
|
1105
|
-
* list in the background while the user reads, and a height that tracks
|
|
1106
|
-
* the count reflows the whole layout every time (the idle 2-line shifts). */
|
|
1164
|
+
/** Height of the collapsible todo block: one framed row per visible task,
|
|
1165
|
+
* capped at six items. Short lists must not reserve blank body rows. */
|
|
1107
1166
|
todoHeight() {
|
|
1108
1167
|
const todos = this.app.todos;
|
|
1109
1168
|
const subagent = this.app.projections.subagent;
|
|
1110
|
-
|
|
1111
|
-
if (
|
|
1112
|
-
|
|
1113
|
-
this.
|
|
1114
|
-
// The task dock owns a framed header and footer. Folded keeps the framed
|
|
1115
|
-
// two-row strip visible rather than blending one text row into transcript.
|
|
1116
|
-
return h + (this.todosVisible ? 8 : 2);
|
|
1169
|
+
const subagentRows = subagent ? 1 : 0;
|
|
1170
|
+
if (!todos || todos.length === 0) return subagentRows;
|
|
1171
|
+
// Header + actual body rows + footer. Folded retains only the frame.
|
|
1172
|
+
return subagentRows + (this.todosVisible ? Math.min(todos.length, 6) + 2 : 2);
|
|
1117
1173
|
}
|
|
1118
1174
|
|
|
1119
1175
|
divingNode() { return [...this.nodes].reverse().find((node) => node.kind === "turn-progress") ?? null; }
|
|
@@ -1980,8 +2036,20 @@ export class ChatView extends Widget {
|
|
|
1980
2036
|
let y = this.view.y + 1;
|
|
1981
2037
|
const put = (t, fg, bold) => { if (y < this.view.y + this.view.h) { screen.text(cx, y, t, { fg, attrs: bold ? 1 : 0 }); } y++; };
|
|
1982
2038
|
put("", 0, false);
|
|
1983
|
-
|
|
1984
|
-
|
|
2039
|
+
this.welcomeVersionRows = [];
|
|
2040
|
+
const versionLine = (name, version, key, fg, bold) => {
|
|
2041
|
+
const check = this.app.versionChecks?.[key];
|
|
2042
|
+
const status = check?.state === "checking" ? "← 检查更新…"
|
|
2043
|
+
: check?.state === "current" ? "← 已是最新"
|
|
2044
|
+
: check?.state === "update" ? `← 可更新 ${check.latest}`
|
|
2045
|
+
: check?.state === "error" ? "← 检查失败(点击重试)"
|
|
2046
|
+
: "← 检查更新";
|
|
2047
|
+
const text = ` ${name} ${version === "unknown" ? "版本未知" : `v${version}`} ${status}`;
|
|
2048
|
+
put(text, fg, bold);
|
|
2049
|
+
this.welcomeVersionRows[y - 1] = { key, x1: cx, x2: cx + strWidth(text) - 1 };
|
|
2050
|
+
};
|
|
2051
|
+
versionLine("DeepSeek Harness", this.app.dshVersion ?? "unknown", "dsh", T.HEADING, true);
|
|
2052
|
+
versionLine("dsh-neotui", TUI_VERSION, "tui", T.FAINT, false);
|
|
1985
2053
|
put("", 0, false);
|
|
1986
2054
|
if (this.app.currentSession == null) {
|
|
1987
2055
|
put(" 打开一个会话开始,或 Ctrl+N 新建", T.DIM, false);
|
|
@@ -1990,6 +2058,7 @@ export class ChatView extends Widget {
|
|
|
1990
2058
|
put(" 请选择模式(F9 或点击下方,选择后立即生效):", T.WARN, true);
|
|
1991
2059
|
put("", 0, false);
|
|
1992
2060
|
this.welcomeModes = [];
|
|
2061
|
+
const currentPreset = this.app.sessions.find((s) => s.sessionId === this.app.currentSession)?.agentPreset;
|
|
1993
2062
|
const presets = [
|
|
1994
2063
|
["standard", "标准模式", "完整编码 Agent(文件/Shell/检索/Skills/目标/子代理)"],
|
|
1995
2064
|
["code", "PTC 模式", "标准模式能力 + Code Mode SDK 单程序多步操作"],
|
|
@@ -1998,8 +2067,10 @@ export class ChatView extends Widget {
|
|
|
1998
2067
|
];
|
|
1999
2068
|
for (const [id, name, desc] of presets) {
|
|
2000
2069
|
if (y < this.view.y + this.view.h) {
|
|
2001
|
-
|
|
2002
|
-
|
|
2070
|
+
const selected = id === currentPreset;
|
|
2071
|
+
const label = `${selected ? "●" : "○"} ${name}${selected ? " [当前]" : ""}`;
|
|
2072
|
+
screen.text(cx, y, ` ${label}`, { fg: selected ? T.OK : T.ACCENT, bg: selected ? T.MENUSEL : -1, attrs: 1 });
|
|
2073
|
+
screen.text(cx + 2 + strWidth(label) + 1, y, truncate(desc, Math.max(1, this.view.w - strWidth(label) - 8)), { fg: selected ? T.TXT : T.DIM, bg: selected ? T.MENUSEL : -1 });
|
|
2003
2074
|
this.welcomeModes[y] = id;
|
|
2004
2075
|
}
|
|
2005
2076
|
y++;
|
|
@@ -2148,6 +2219,8 @@ export class ChatView extends Widget {
|
|
|
2148
2219
|
if (this.app.focused !== this.app.chat?.input) this.app.focus(this);
|
|
2149
2220
|
// Welcome-screen mode click: select the preset under the cursor.
|
|
2150
2221
|
if (this.nodes.length === 0 && ev.kind === "press" && ev.button === 0) {
|
|
2222
|
+
const versionHit = this.welcomeVersionRows?.[ev.y];
|
|
2223
|
+
if (versionHit && ev.x >= versionHit.x1 && ev.x <= versionHit.x2) { this.app.checkUpdates(versionHit.key, true); return true; }
|
|
2151
2224
|
const id = this.welcomeModes[ev.y];
|
|
2152
2225
|
if (id) { this.app.selectPreset(id); return true; }
|
|
2153
2226
|
}
|
|
@@ -2344,14 +2417,31 @@ function truncateText(s, n) {
|
|
|
2344
2417
|
return s.length > n ? s.slice(0, n) + "\n…(截断)" : s;
|
|
2345
2418
|
}
|
|
2346
2419
|
|
|
2420
|
+
function wrapDisplayText(value, width) {
|
|
2421
|
+
const out = [];
|
|
2422
|
+
for (const raw of String(value ?? "").split("\n")) {
|
|
2423
|
+
if (!raw) { out.push(""); continue; }
|
|
2424
|
+
let line = "", used = 0;
|
|
2425
|
+
for (const g of graphemes(raw)) {
|
|
2426
|
+
const gw = graphemeWidth(g);
|
|
2427
|
+
if (line && used + gw > width) { out.push(line); line = ""; used = 0; }
|
|
2428
|
+
line += g; used += gw;
|
|
2429
|
+
}
|
|
2430
|
+
out.push(line);
|
|
2431
|
+
}
|
|
2432
|
+
return out;
|
|
2433
|
+
}
|
|
2434
|
+
|
|
2347
2435
|
// ---- Question popup ----
|
|
2348
2436
|
|
|
2349
2437
|
export class QuestionPopup extends Popup {
|
|
2350
2438
|
constructor({ app, frame }) {
|
|
2351
2439
|
const questions = frame.questions ?? [];
|
|
2352
2440
|
const planReview = questions.length === 1 && questions[0]?.intent?.kind === "plan-review";
|
|
2353
|
-
const w = Math.max(12, Math.min(planReview ?
|
|
2354
|
-
const
|
|
2441
|
+
const w = Math.max(12, Math.min(planReview ? 96 : 92, app.screen.w - 2));
|
|
2442
|
+
const wrapCount=(value,width)=>wrapDisplayText(value,width).length;
|
|
2443
|
+
const estimated=questions.reduce((n,q)=>n+1+wrapCount(q.question??"",w-4)+(q.detail?wrapCount(q.detail,w-4):0)+(q.options??[]).reduce((m,o)=>m+1+(o.description?wrapCount(o.description,w-10):0),0)+(q.intent?.kind==="plan-review"?0:4),0)+2;
|
|
2444
|
+
const h=Math.max(10,Math.min(app.screen.h-2,estimated));
|
|
2355
2445
|
super({
|
|
2356
2446
|
x: Math.max(0, Math.floor((app.screen.w - w) / 2)), y: Math.max(0, Math.floor((app.screen.h - h) / 2)),
|
|
2357
2447
|
w, h, title: planReview ? "✎ 计划审阅" : "❓ 需要你的回答",
|
|
@@ -2387,44 +2477,44 @@ export class QuestionPopup extends Popup {
|
|
|
2387
2477
|
const q = this.questions[this.questionIdx];
|
|
2388
2478
|
if (!q) return;
|
|
2389
2479
|
const draft = this.drafts[this.questionIdx];
|
|
2390
|
-
let ly = this.y + 1;
|
|
2391
|
-
screen.text(this.x + 2, ly++, truncate(`▎ ${q.header ?? `问题 ${this.questionIdx + 1}/${this.questions.length}`}`, this.w - 4), { fg: K.ACCENT, bold: true });
|
|
2392
|
-
screen.text(this.x + 2, ly++, truncate(q.question ?? "", this.w - 4), { fg: K.TXT });
|
|
2393
|
-
if (this.planReview) screen.text(this.x + 2, ly++, "Enter 执行计划 · ↓ 继续规划 · Esc 取消审阅", { fg: K.FAINT });
|
|
2394
2480
|
const opts = q.options ?? [];
|
|
2395
|
-
|
|
2396
|
-
|
|
2397
|
-
|
|
2398
|
-
|
|
2399
|
-
|
|
2400
|
-
this.
|
|
2401
|
-
this.
|
|
2402
|
-
|
|
2403
|
-
|
|
2404
|
-
|
|
2405
|
-
|
|
2406
|
-
|
|
2407
|
-
|
|
2408
|
-
}
|
|
2481
|
+
const optionDescriptionRows=opts.reduce((n,o)=>n+(o.description?wrapDisplayText(o.description,this.w-10).length:0),0);
|
|
2482
|
+
const actionRows = opts.length + optionDescriptionRows + (this.planReview ? 0 : Math.min(6,Math.max(3,wrapDisplayText(draft.custom||"在此输入…",this.w-10).length))+2);
|
|
2483
|
+
const actionTop = Math.max(this.y + 5, this.y + this.h - 1 - Math.min(actionRows,this.h-6));
|
|
2484
|
+
const doc = [
|
|
2485
|
+
{ text: `▎ ${q.header ?? `问题 ${this.questionIdx + 1}/${this.questions.length}`}`, fg: K.ACCENT, bold: true },
|
|
2486
|
+
...wrapDisplayText(q.question ?? "", this.w - 4).map((text) => ({ text, fg: K.TXT })),
|
|
2487
|
+
...(q.detail ? wrapDisplayText(q.detail, this.w - 4).map((text) => ({ text, fg: K.DIM })) : []),
|
|
2488
|
+
];
|
|
2489
|
+
|
|
2490
|
+
const room = Math.max(1, actionTop - (this.y + 1));
|
|
2491
|
+
const maxScroll = Math.max(0, doc.length - room);
|
|
2492
|
+
this.detailScrollY = Math.max(0, Math.min(this.detailScrollY, maxScroll)); this.detailPage = room; this.detailTotal = doc.length;
|
|
2493
|
+
let ly = this.y + 1;
|
|
2494
|
+
for (const line of doc.slice(this.detailScrollY, this.detailScrollY + room)) screen.text(this.x + 2, ly++, truncate(line.text, this.w - 4), { fg: line.fg, attrs: line.bold ? 1 : 0 });
|
|
2495
|
+
if(maxScroll>0){screen.text(this.x+this.w-26,this.y,`正文 Ctrl+↑↓ 翻页 ${this.detailScrollY+1}-${Math.min(doc.length,this.detailScrollY+room)}/${doc.length}`,{fg:K.ACCENT,bg:T.BG2});}
|
|
2496
|
+
screen.hline(this.x+1,this.x+this.w-2,actionTop-1,"─",{fg:T.BORDER2,bg:T.BG2});screen.text(this.x+3,actionTop-1," 回答(↑/↓ 选择) ",{fg:K.ACCENT,bg:T.BG2});
|
|
2497
|
+
ly=actionTop;
|
|
2409
2498
|
this.optionRows = []; this.optionHitboxes = [];
|
|
2410
|
-
for (let i = 0; i < opts.length
|
|
2499
|
+
for (let i = 0; i < opts.length; i++) {
|
|
2411
2500
|
this.optionRows[i] = ly;
|
|
2412
2501
|
const chosen = draft.selected.includes(opts[i].label);
|
|
2413
2502
|
const cursor = this.selIdx === i;
|
|
2414
2503
|
const glyph = q.multiSelect ? (chosen ? "☑" : "☐") : (chosen ? "●" : "○");
|
|
2415
2504
|
const optionText=truncate(` ${cursor ? "▸" : " "} ${glyph} ${opts[i].label}`, this.w - 6);
|
|
2416
|
-
|
|
2505
|
+
const descLines=opts[i].description?wrapDisplayText(opts[i].description,this.w-10):[];
|
|
2506
|
+
this.optionHitboxes[i]={x1:this.x+2,x2:this.x+2+strWidth(optionText)-1,y1:ly,y2:ly+descLines.length};
|
|
2417
2507
|
screen.text(this.x + 2, ly++, optionText, { fg: cursor ? T.SELFG : K.TXT, bg: cursor ? T.MENUSEL : -1 });
|
|
2418
|
-
|
|
2508
|
+
for(const line of descLines)screen.text(this.x+7,ly++,line,{fg:K.FAINT,bg:cursor?T.MENUSEL:-1});
|
|
2419
2509
|
}
|
|
2420
|
-
if (!this.planReview
|
|
2510
|
+
if (!this.planReview) {
|
|
2421
2511
|
this.optionRows[opts.length] = ly;
|
|
2422
2512
|
const cursor = this.selIdx === opts.length;
|
|
2423
2513
|
const customText=truncate(` ${cursor ? "▸" : " "} ✎ 输入自己的回答`, this.w - 6);
|
|
2424
2514
|
this.optionHitboxes[opts.length]={x1:this.x+2,x2:this.x+2+strWidth(customText)-1,y1:ly,y2:ly};
|
|
2425
2515
|
screen.text(this.x + 2, ly++, customText, { fg: cursor ? T.SELFG : K.ACCENT, bg: cursor ? T.MENUSEL : -1 });
|
|
2426
|
-
|
|
2427
|
-
|
|
2516
|
+
{const chars=Array.from(draft.custom),shown=this.customEditing?[...chars.slice(0,this.customCursor),"▏",...chars.slice(this.customCursor)].join(""):draft.custom;const inputLines=wrapDisplayText(shown||"在此输入…",this.w-10).slice(-6);for(let i=0;i<inputLines.length;i++)screen.text(this.x+4,ly++,`${i===0?"> ":" "}${inputLines[i]}`,{fg:this.customEditing?K.TXT:K.FAINT,bg:this.customEditing?T.BG2:-1});}
|
|
2517
|
+
{this.optionRows[opts.length+1]=ly;const skip=this.selIdx===opts.length+1,skipText=` ${skip?"▸":" "} ↷ 跳过此问题`;this.optionHitboxes[opts.length+1]={x1:this.x+2,x2:this.x+2+strWidth(skipText)-1,y1:ly,y2:ly};screen.text(this.x+2,ly++,skipText,{fg:skip?T.SELFG:K.FAINT,bg:skip?T.MENUSEL:-1});}
|
|
2428
2518
|
}
|
|
2429
2519
|
}
|
|
2430
2520
|
|
|
@@ -2441,8 +2531,9 @@ export class QuestionPopup extends Popup {
|
|
|
2441
2531
|
}
|
|
2442
2532
|
|
|
2443
2533
|
onMouse(ev) {
|
|
2444
|
-
if (
|
|
2445
|
-
|
|
2534
|
+
if (ev.kind === "wheel-up" || ev.kind === "wheel-down") {
|
|
2535
|
+
const max=Math.max(0,(this.detailTotal??0)-(this.detailPage??1));
|
|
2536
|
+
this.detailScrollY = Math.max(0,Math.min(max,this.detailScrollY + (ev.kind === "wheel-up" ? -3 : 3)));
|
|
2446
2537
|
this.app.redraw(); return true;
|
|
2447
2538
|
}
|
|
2448
2539
|
if (ev.kind === "press" && ev.button === 0) {
|
|
@@ -2469,14 +2560,16 @@ export class QuestionPopup extends Popup {
|
|
|
2469
2560
|
const choices = count + (this.planReview ? 0 : 2);
|
|
2470
2561
|
if (ev.type === "text") { if(this.customEditing||this.selIdx===count){this.customEditing=true;draft.selected=[];const chars=Array.from(draft.custom);chars.splice(this.customCursor,0,...Array.from(ev.text));draft.custom=chars.join("");this.customCursor+=Array.from(ev.text).length;draft.skipped=false;return true;} return false; }
|
|
2471
2562
|
if (ev.type !== "key") return false;
|
|
2472
|
-
if (
|
|
2473
|
-
const total
|
|
2563
|
+
if (["pageup", "pagedown", "home", "end"].includes(ev.name)) {
|
|
2564
|
+
const total=this.detailTotal??0,max=Math.max(0,total-this.detailPage);
|
|
2474
2565
|
if (ev.name === "pageup") this.detailScrollY = Math.max(0, this.detailScrollY - this.detailPage);
|
|
2475
|
-
else if (ev.name === "pagedown") this.detailScrollY = Math.min(
|
|
2566
|
+
else if (ev.name === "pagedown") this.detailScrollY = Math.min(max, this.detailScrollY + this.detailPage);
|
|
2476
2567
|
else if (ev.name === "home") this.detailScrollY = 0;
|
|
2477
|
-
else this.detailScrollY =
|
|
2568
|
+
else this.detailScrollY = max;
|
|
2478
2569
|
this.app.redraw(); return true;
|
|
2479
2570
|
}
|
|
2571
|
+
if(!this.customEditing&&ev.ctrl&&ev.name==="up"){this.detailScrollY=Math.max(0,this.detailScrollY-this.detailPage);return true;}
|
|
2572
|
+
if(!this.customEditing&&ev.ctrl&&ev.name==="down"){this.detailScrollY=Math.min(Math.max(0,(this.detailTotal??0)-this.detailPage),this.detailScrollY+this.detailPage);return true;}
|
|
2480
2573
|
if(this.customEditing&&ev.name==="left"){this.customCursor=Math.max(0,this.customCursor-1);return true;}
|
|
2481
2574
|
if(this.customEditing&&ev.name==="right"){this.customCursor=Math.min(Array.from(draft.custom).length,this.customCursor+1);return true;}
|
|
2482
2575
|
if(this.customEditing&&ev.name==="home"){this.customCursor=0;return true;}
|
|
@@ -2538,13 +2631,14 @@ export class ApprovalPopup extends Popup {
|
|
|
2538
2631
|
const maxW = Math.max(12, app.screen.w - 2);
|
|
2539
2632
|
const w = Math.min(72, maxW);
|
|
2540
2633
|
const command = app.chat?.toolCommandForCall?.(frame.callId) ?? null;
|
|
2541
|
-
const
|
|
2542
|
-
|
|
2543
|
-
];
|
|
2544
|
-
if (command) lines.push([{ t: "
|
|
2634
|
+
const wrap=(value,width)=>{const out=[];for(const raw of String(value??"").split("\n")){const gs=graphemes(raw);let line="",used=0;for(const g of gs){const gw=graphemeWidth(g);if(used+gw>width&&line){out.push(line);line="";used=0;}line+=g;used+=gw;}out.push(line);}return out;};
|
|
2635
|
+
const reason=frame.reason ?? `工具 ${frame.toolName ?? "tool"} 请求越权执行`;
|
|
2636
|
+
const lines = [[{t:" 请求原因",fg:K.DIM,underline:true}],...wrap(reason,w-6).map(line=>[{t:` ${line}`,fg:K.WARN}])];
|
|
2637
|
+
if (command) lines.push([{ t: " 将执行", fg: K.DIM, underline: true }], ...wrap(command,w-6).map((line) => [{ t: " " + line, fg: K.TXT }]));
|
|
2638
|
+
lines.push([{t:" 导航:↑/↓ 逐行 · PgUp/PgDn 翻页 · 滚轮 · ←/→ 选择",fg:K.FAINT}]);
|
|
2545
2639
|
super({
|
|
2546
|
-
x: Math.max(0, Math.floor((app.screen.w - w) / 2)), y: Math.max(0, Math.floor(app.screen.h
|
|
2547
|
-
w, h: Math.min(app.screen.h
|
|
2640
|
+
x: Math.max(0, Math.floor((app.screen.w - w) / 2)), y: Math.max(0, Math.floor((app.screen.h - Math.min(app.screen.h - 2, Math.max(10, Math.min(24, lines.length + 4)))) / 2)),
|
|
2641
|
+
w, h: Math.min(app.screen.h - 2, Math.max(10, Math.min(24, lines.length + 4))), title: "⚠ 工具需要授权 · 可滚动",
|
|
2548
2642
|
lines,
|
|
2549
2643
|
buttons: [
|
|
2550
2644
|
{ label: "允许一次", action: "allowed-once" },
|
|
@@ -2575,11 +2669,12 @@ export class ApprovalPopup extends Popup {
|
|
|
2575
2669
|
// ---- App ----
|
|
2576
2670
|
|
|
2577
2671
|
export class App {
|
|
2578
|
-
constructor({ screen, term, api, base, log }) {
|
|
2672
|
+
constructor({ screen, term, api, base, log, versionFetcher = latestNpmVersion }) {
|
|
2579
2673
|
this.screen = screen;
|
|
2580
2674
|
this.term = term;
|
|
2581
2675
|
this.api = api;
|
|
2582
2676
|
this.log = log ?? (() => {});
|
|
2677
|
+
this.versionFetcher = versionFetcher;
|
|
2583
2678
|
this.popup = null;
|
|
2584
2679
|
this.activePrompt = null;
|
|
2585
2680
|
this.promptQueue = [];
|
|
@@ -2605,6 +2700,11 @@ export class App {
|
|
|
2605
2700
|
this.tokenUsage = null;
|
|
2606
2701
|
this.sessions = [];
|
|
2607
2702
|
this.currentSession = null;
|
|
2703
|
+
this.dshVersion = installedDshVersion();
|
|
2704
|
+
this.versionChecks = {
|
|
2705
|
+
dsh: { state: "idle", latest: null },
|
|
2706
|
+
tui: { state: "idle", latest: null },
|
|
2707
|
+
};
|
|
2608
2708
|
this.searchActive = false;
|
|
2609
2709
|
this.overlay = null; // Picker / Popup / ImagePopup modal
|
|
2610
2710
|
this.mode = "chat"; // chat | workspace | trajectory
|
|
@@ -2672,6 +2772,31 @@ export class App {
|
|
|
2672
2772
|
this.setMode(this.mode === "trajectory" ? "chat" : "trajectory");
|
|
2673
2773
|
}
|
|
2674
2774
|
|
|
2775
|
+
async checkUpdates(target = null, notify = false) {
|
|
2776
|
+
const specs = {
|
|
2777
|
+
dsh: { package: "@deepseek-ai/dsh", current: this.dshVersion, label: "DeepSeek Harness" },
|
|
2778
|
+
tui: { package: "dsh-neotui", current: TUI_VERSION, label: "dsh-neotui" },
|
|
2779
|
+
};
|
|
2780
|
+
const keys = target ? [target] : Object.keys(specs);
|
|
2781
|
+
await Promise.all(keys.map(async (key) => {
|
|
2782
|
+
const spec = specs[key];
|
|
2783
|
+
if (!spec) return;
|
|
2784
|
+
this.versionChecks[key] = { state: "checking", latest: null };
|
|
2785
|
+
this.redraw();
|
|
2786
|
+
try {
|
|
2787
|
+
const latest = await this.versionFetcher(spec.package);
|
|
2788
|
+
const comparison = compareSemver(spec.current, latest);
|
|
2789
|
+
const state = comparison === null ? (latest === spec.current ? "current" : "update") : comparison < 0 ? "update" : "current";
|
|
2790
|
+
this.versionChecks[key] = { state, latest };
|
|
2791
|
+
if (notify) this.toast(state === "current" ? `${spec.label} 已是最新版本 ${spec.current}` : `${spec.label} 可更新: ${spec.current} → ${latest}`);
|
|
2792
|
+
} catch (error) {
|
|
2793
|
+
this.versionChecks[key] = { state: "error", latest: null };
|
|
2794
|
+
if (notify) this.toast(`${spec.label} 更新检查失败: ${error.message}`);
|
|
2795
|
+
}
|
|
2796
|
+
this.redraw();
|
|
2797
|
+
}));
|
|
2798
|
+
}
|
|
2799
|
+
|
|
2675
2800
|
/** Chat → trajectory: open the trajectory panel at the step containing a
|
|
2676
2801
|
* chat node (right-click menu), loading older steps on demand. */
|
|
2677
2802
|
async jumpToTrajectoryNode(nodeIdx) {
|
|
@@ -2804,6 +2929,7 @@ export class App {
|
|
|
2804
2929
|
this.provider = host.provider ?? "";
|
|
2805
2930
|
this.model = host.model ?? "";
|
|
2806
2931
|
} catch (e) { this.log(`[app] host.describe: ${e.message}`); }
|
|
2932
|
+
void this.checkUpdates();
|
|
2807
2933
|
await this.refreshSessions();
|
|
2808
2934
|
// A /restart handoff carries the session to reopen: resume it instead of
|
|
2809
2935
|
// minting a fresh blank session (which was leaking a stray "new session"
|
|
@@ -3477,7 +3603,9 @@ export class App {
|
|
|
3477
3603
|
}
|
|
3478
3604
|
try {
|
|
3479
3605
|
await this.api.call("agentPreset.select", { sessionId: this.currentSession, agentPreset: id });
|
|
3606
|
+
if (sess) sess.agentPreset = id;
|
|
3480
3607
|
this.toast(`模式已切换: ${modeName(id)}`);
|
|
3608
|
+
this.redraw();
|
|
3481
3609
|
this.refreshSessions();
|
|
3482
3610
|
} catch (e) {
|
|
3483
3611
|
if (e.code === "agent-preset-locked") { this.toast("会话已开始,模式固定;已设为新会话默认"); this.setDefaultPreset(id); }
|
|
@@ -3526,7 +3654,7 @@ export class App {
|
|
|
3526
3654
|
} catch (e) { this.toast(`权限切换失败: ${e.message}`); }
|
|
3527
3655
|
}
|
|
3528
3656
|
|
|
3529
|
-
/** F8: cycle read-only → workspace-write → danger-full-access
|
|
3657
|
+
/** Shift+Tab/F8: cycle read-only → workspace-write → danger-full-access. */
|
|
3530
3658
|
rotatePermission() {
|
|
3531
3659
|
const order = ["read-only", "workspace-write", "danger-full-access"];
|
|
3532
3660
|
const cur = this.projections.permissions?.currentValue;
|
|
@@ -3805,12 +3933,8 @@ export class App {
|
|
|
3805
3933
|
this.redraw();
|
|
3806
3934
|
return;
|
|
3807
3935
|
}
|
|
3808
|
-
if (ev.name === "
|
|
3809
|
-
|
|
3810
|
-
else if (this.mode === "chat") this.setMode("trajectory");
|
|
3811
|
-
this.redraw();
|
|
3812
|
-
return;
|
|
3813
|
-
}
|
|
3936
|
+
if (ev.name === "tab" && !ev.ctrl) { this.toggleChatTrajectory(); return; }
|
|
3937
|
+
if (ev.name === "backtab" && !ev.ctrl) { this.rotatePermission(); return; }
|
|
3814
3938
|
if (ev.ctrl && ev.key === "q") { this.stop(); return; }
|
|
3815
3939
|
if (ev.ctrl && ev.key === "c") {
|
|
3816
3940
|
// NORMAL-mode Ctrl+C: two presses within the toast window exit the
|
|
@@ -3823,7 +3947,6 @@ export class App {
|
|
|
3823
3947
|
this.toast("再按一次 Ctrl+C 退出 TUI");
|
|
3824
3948
|
return;
|
|
3825
3949
|
}
|
|
3826
|
-
if (ev.ctrl && ev.key === "n") { this.focus(this.chat); this.redraw(); return; }
|
|
3827
3950
|
if (ev.ctrl && ev.key === "o") { this.overlay = new AttachmentPanel(this); this.focus(this.overlay); this.redraw(); return; }
|
|
3828
3951
|
if (ev.ctrl && ev.key === "b") { this.toggleSidebar(); return; }
|
|
3829
3952
|
if (ev.ctrl && ev.key === "p") { this.overlay = new ControlPanel(this, { startPage: 1 }); this.redraw(); return; }
|
|
@@ -3837,7 +3960,7 @@ export class App {
|
|
|
3837
3960
|
if (ev.ctrl && ev.key === "t") { this.setMode("trajectory"); return; }
|
|
3838
3961
|
if (ev.ctrl && ev.key === "e") { this.quickJumpStep(); return; }
|
|
3839
3962
|
if (ev.ctrl && ev.key === "j") { this.showJobs(); return; }
|
|
3840
|
-
if (ev.ctrl && ev.key === "
|
|
3963
|
+
if (ev.ctrl && ev.key === "n") { this.showQueue(); return; }
|
|
3841
3964
|
if (ev.ctrl && ev.key === "y") {
|
|
3842
3965
|
const next = busyEnter() === "queue" ? "steer" : "queue";
|
|
3843
3966
|
saveTuiConfig({ busyEnter: next });
|
|
@@ -4031,7 +4154,6 @@ export class App {
|
|
|
4031
4154
|
if (rawGoal && !["complete", "completed", "cleared"].includes(rawGoal.phase)) {
|
|
4032
4155
|
row0.left.push({ t: ` 🎯 ${truncate(rawGoal.objective ?? "目标", 14)} · Ctrl+G `, fg: T.SELFG, bg: T.WARN, bold: true });
|
|
4033
4156
|
}
|
|
4034
|
-
if (this.queueItems.length) row0.left.push({ t: ` ⏳队列 ${this.queueItems.length} (Ctrl+U) `, fg: T.SELFG, bg: T.WARN, bold: true });
|
|
4035
4157
|
if (this.sidebarVisible) row0.left.push({ t: " " + truncate(t || "(未选择会话)", 40) + " ", fg: T.TXT, bg: T.STATUSBG });
|
|
4036
4158
|
else row0.left.push({ t: " " + truncate(t || "(未选择会话)", 40) + " ", fg: T.TXT, bg: T.STATUSBG });
|
|
4037
4159
|
if (cur?.running) row0.left.push({ t: " ●运行 ", fg: T.OK, bg: T.STATUSBG });
|
|
@@ -4118,6 +4240,7 @@ export class App {
|
|
|
4118
4240
|
row2.left.push({ t: ` ${done}已完成${failed > 0 ? ` ${failed}失败` : ""} `, fg: done > 0 ? T.OK : T.FAINT, bg: T.STATUSBG });
|
|
4119
4241
|
row2.left.push({ t: ` ${subStats.running > 0 ? `${subStats.running} 个子代理运行中` : "没有子代理运行"} ${subStats.completed}已完成 `, fg: subStats.running > 0 ? T.PURPLE : T.FAINT, bg: T.STATUSBG, bold: subStats.running > 0 });
|
|
4120
4242
|
if (sub) row2.left.push({ t: ` 🛰 ${truncate(sub.label ?? sub.mode ?? "子代理", 20)} `, fg: T.PURPLE, bg: T.STATUSBG });
|
|
4243
|
+
if(this.queueItems.length)row2.left.push({t:` 有${this.queueItems.length}条命令正在排队 Ctrl+N查看详情 `,fg:T.SELFG,bg:T.WARN,bold:true});
|
|
4121
4244
|
row2.right.push({ t: " Ctrl+J 任务/子代理 ", fg: T.DIM, bg: T.STATUSBG });
|
|
4122
4245
|
rows.push(row2);
|
|
4123
4246
|
}
|
package/src/widgets.js
CHANGED
|
@@ -788,8 +788,8 @@ export class Popup extends Widget {
|
|
|
788
788
|
}
|
|
789
789
|
if (this.maxScroll() > 0) {
|
|
790
790
|
// "↑N" / "↓N" overflow indicators on the border row
|
|
791
|
-
if (this.scrollY > 0) screen.text(this.x + this.w -
|
|
792
|
-
if (this.scrollY < this.maxScroll()) screen.text(this.x + this.w -
|
|
791
|
+
if (this.scrollY > 0) screen.text(this.x + this.w - 10, this.y, ` ↑ ${this.scrollY}/${this.lines.length}`, { fg: T.ACCENT, bg: T.BG2 });
|
|
792
|
+
if (this.scrollY < this.maxScroll()) screen.text(this.x + this.w - 12, this.y + this.h - 1, ` ↓ ${this.scrollY + this.contentRows()}/${this.lines.length}`, { fg: T.ACCENT, bg: T.BG2 });
|
|
793
793
|
}
|
|
794
794
|
} else {
|
|
795
795
|
for (const line of this.lines) draw(line);
|
|
@@ -832,6 +832,8 @@ export class Popup extends Widget {
|
|
|
832
832
|
onKey(ev) {
|
|
833
833
|
if (ev.type !== "key") return false;
|
|
834
834
|
if (this.scrollable) {
|
|
835
|
+
if (ev.name === "up") { this.scrollY = Math.max(0, this.scrollY - 1); return true; }
|
|
836
|
+
if (ev.name === "down") { this.scrollY = Math.min(this.maxScroll(), this.scrollY + 1); return true; }
|
|
835
837
|
if (ev.name === "pgup") { this.scrollY = Math.max(0, this.scrollY - this.contentRows()); return true; }
|
|
836
838
|
if (ev.name === "pgdn") { this.scrollY = Math.min(this.maxScroll(), this.scrollY + this.contentRows()); return true; }
|
|
837
839
|
}
|