dsh-neotui 0.2.2 → 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/README.md +5 -4
- package/package.json +1 -1
- package/src/config.js +2 -1
- package/src/panels.js +3 -2
- package/src/views.js +127 -25
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;
|
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
|
}
|
|
@@ -2596,11 +2669,12 @@ export class ApprovalPopup extends Popup {
|
|
|
2596
2669
|
// ---- App ----
|
|
2597
2670
|
|
|
2598
2671
|
export class App {
|
|
2599
|
-
constructor({ screen, term, api, base, log }) {
|
|
2672
|
+
constructor({ screen, term, api, base, log, versionFetcher = latestNpmVersion }) {
|
|
2600
2673
|
this.screen = screen;
|
|
2601
2674
|
this.term = term;
|
|
2602
2675
|
this.api = api;
|
|
2603
2676
|
this.log = log ?? (() => {});
|
|
2677
|
+
this.versionFetcher = versionFetcher;
|
|
2604
2678
|
this.popup = null;
|
|
2605
2679
|
this.activePrompt = null;
|
|
2606
2680
|
this.promptQueue = [];
|
|
@@ -2626,6 +2700,11 @@ export class App {
|
|
|
2626
2700
|
this.tokenUsage = null;
|
|
2627
2701
|
this.sessions = [];
|
|
2628
2702
|
this.currentSession = null;
|
|
2703
|
+
this.dshVersion = installedDshVersion();
|
|
2704
|
+
this.versionChecks = {
|
|
2705
|
+
dsh: { state: "idle", latest: null },
|
|
2706
|
+
tui: { state: "idle", latest: null },
|
|
2707
|
+
};
|
|
2629
2708
|
this.searchActive = false;
|
|
2630
2709
|
this.overlay = null; // Picker / Popup / ImagePopup modal
|
|
2631
2710
|
this.mode = "chat"; // chat | workspace | trajectory
|
|
@@ -2693,6 +2772,31 @@ export class App {
|
|
|
2693
2772
|
this.setMode(this.mode === "trajectory" ? "chat" : "trajectory");
|
|
2694
2773
|
}
|
|
2695
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
|
+
|
|
2696
2800
|
/** Chat → trajectory: open the trajectory panel at the step containing a
|
|
2697
2801
|
* chat node (right-click menu), loading older steps on demand. */
|
|
2698
2802
|
async jumpToTrajectoryNode(nodeIdx) {
|
|
@@ -2825,6 +2929,7 @@ export class App {
|
|
|
2825
2929
|
this.provider = host.provider ?? "";
|
|
2826
2930
|
this.model = host.model ?? "";
|
|
2827
2931
|
} catch (e) { this.log(`[app] host.describe: ${e.message}`); }
|
|
2932
|
+
void this.checkUpdates();
|
|
2828
2933
|
await this.refreshSessions();
|
|
2829
2934
|
// A /restart handoff carries the session to reopen: resume it instead of
|
|
2830
2935
|
// minting a fresh blank session (which was leaking a stray "new session"
|
|
@@ -3498,7 +3603,9 @@ export class App {
|
|
|
3498
3603
|
}
|
|
3499
3604
|
try {
|
|
3500
3605
|
await this.api.call("agentPreset.select", { sessionId: this.currentSession, agentPreset: id });
|
|
3606
|
+
if (sess) sess.agentPreset = id;
|
|
3501
3607
|
this.toast(`模式已切换: ${modeName(id)}`);
|
|
3608
|
+
this.redraw();
|
|
3502
3609
|
this.refreshSessions();
|
|
3503
3610
|
} catch (e) {
|
|
3504
3611
|
if (e.code === "agent-preset-locked") { this.toast("会话已开始,模式固定;已设为新会话默认"); this.setDefaultPreset(id); }
|
|
@@ -3547,7 +3654,7 @@ export class App {
|
|
|
3547
3654
|
} catch (e) { this.toast(`权限切换失败: ${e.message}`); }
|
|
3548
3655
|
}
|
|
3549
3656
|
|
|
3550
|
-
/** F8: cycle read-only → workspace-write → danger-full-access
|
|
3657
|
+
/** Shift+Tab/F8: cycle read-only → workspace-write → danger-full-access. */
|
|
3551
3658
|
rotatePermission() {
|
|
3552
3659
|
const order = ["read-only", "workspace-write", "danger-full-access"];
|
|
3553
3660
|
const cur = this.projections.permissions?.currentValue;
|
|
@@ -3826,12 +3933,8 @@ export class App {
|
|
|
3826
3933
|
this.redraw();
|
|
3827
3934
|
return;
|
|
3828
3935
|
}
|
|
3829
|
-
if (ev.name === "
|
|
3830
|
-
|
|
3831
|
-
else if (this.mode === "chat") this.setMode("trajectory");
|
|
3832
|
-
this.redraw();
|
|
3833
|
-
return;
|
|
3834
|
-
}
|
|
3936
|
+
if (ev.name === "tab" && !ev.ctrl) { this.toggleChatTrajectory(); return; }
|
|
3937
|
+
if (ev.name === "backtab" && !ev.ctrl) { this.rotatePermission(); return; }
|
|
3835
3938
|
if (ev.ctrl && ev.key === "q") { this.stop(); return; }
|
|
3836
3939
|
if (ev.ctrl && ev.key === "c") {
|
|
3837
3940
|
// NORMAL-mode Ctrl+C: two presses within the toast window exit the
|
|
@@ -3844,7 +3947,6 @@ export class App {
|
|
|
3844
3947
|
this.toast("再按一次 Ctrl+C 退出 TUI");
|
|
3845
3948
|
return;
|
|
3846
3949
|
}
|
|
3847
|
-
if (ev.ctrl && ev.key === "n") { this.focus(this.chat); this.redraw(); return; }
|
|
3848
3950
|
if (ev.ctrl && ev.key === "o") { this.overlay = new AttachmentPanel(this); this.focus(this.overlay); this.redraw(); return; }
|
|
3849
3951
|
if (ev.ctrl && ev.key === "b") { this.toggleSidebar(); return; }
|
|
3850
3952
|
if (ev.ctrl && ev.key === "p") { this.overlay = new ControlPanel(this, { startPage: 1 }); this.redraw(); return; }
|