dsh-neotui 0.4.3 → 0.4.4
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 +32 -0
- package/package.json +1 -1
- package/src/config.js +31 -0
- package/src/file-picker.js +5 -3
- package/src/keybindings.js +13 -10
- package/src/panels.js +419 -11
- package/src/side-question.mjs +75 -0
- package/src/term.js +21 -0
- package/src/views.js +1083 -87
- package/src/widgets.js +109 -1
package/src/panels.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// timeline, jobs/goal panels, and the terminal image viewer (kitty graphics
|
|
3
3
|
// protocol with external-viewer / chafa fallbacks).
|
|
4
4
|
import { Widget, ScrollView, Input, Popup, wrapIndex } from "./widgets.js";
|
|
5
|
-
import { strWidth, truncate, pad, graphemes, graphemeWidth, bytesLabel, prettyJson } from "./text.js";
|
|
5
|
+
import { strWidth, truncate, pad, graphemes, graphemeWidth, bytesLabel, prettyJson, fmtDuration } from "./text.js";
|
|
6
6
|
import { renderMd, C } from "./md.js";
|
|
7
7
|
import { readdirSync, statSync, readFileSync, writeFileSync, existsSync } from "node:fs";
|
|
8
8
|
import { tmpdir } from "node:os";
|
|
@@ -1361,7 +1361,12 @@ export class TrajectoryPanel extends Widget {
|
|
|
1361
1361
|
const flash = this.flashKey === this.stepKey(step) && Date.now() < this.flashUntil;
|
|
1362
1362
|
const rowBg = flash ? T.ACCENT : bg;
|
|
1363
1363
|
const selected = this.stepKey(step) === this.selectedStepKey;
|
|
1364
|
-
const
|
|
1364
|
+
const srcKind = (step.events ?? []).find((e) => e.type === "user/message")?.data?.source?.kind;
|
|
1365
|
+
let summary2 = summary;
|
|
1366
|
+
let srcPrefix = "";
|
|
1367
|
+
const srcLabel = srcKind ? sourceBadge(srcKind) : "";
|
|
1368
|
+
if (srcKind === "plugin" || srcKind === "goal") { srcPrefix = `${sourceBadge(srcKind)} `; summary2 = `${srcKind === "goal" ? `目标轮 ${step.events.find((e) => e.type === "user/message")?.data?.source?.round ?? "?"}` : "注入上下文"} · ${summary}`; }
|
|
1369
|
+
const label = `${selected ? "=>" : " "} ${open ? "▾" : "▸"} ${srcPrefix}step ${String(step.step).padStart(3)} ${pad(dur, 8)} ${summary2} ${open ? "[折叠]" : "[展开]"}`;
|
|
1365
1370
|
const segs = [{ t: label, fg: flash ? T.SELFG : K.TXT, bg: rowBg, bold: true }];
|
|
1366
1371
|
const fill = w - strWidth(label);
|
|
1367
1372
|
if (fill > 0) segs.push({ t: " ".repeat(fill), bg: rowBg });
|
|
@@ -1716,7 +1721,10 @@ export class ControlPanel extends Widget {
|
|
|
1716
1721
|
const h = Math.min(24, app.screen.h - 4);
|
|
1717
1722
|
super({ x: Math.floor((app.screen.w - w) / 2), y: Math.floor((app.screen.h - h) / 2), w, h });
|
|
1718
1723
|
this.app = app;
|
|
1719
|
-
|
|
1724
|
+
// Prefix-key page (leader, nvim-style EDITING logic only): a one-key
|
|
1725
|
+
// command page as the START page; the keys fire only while this page is
|
|
1726
|
+
// shown, so it never leaks into normal mode.
|
|
1727
|
+
this.pages = ["前缀", "快捷键", "命令", "设置", "插件"];
|
|
1720
1728
|
this.page = startPage;
|
|
1721
1729
|
this.pluginQuery = ""; this.pluginFilter = false;
|
|
1722
1730
|
this.sel = 0;
|
|
@@ -1752,9 +1760,14 @@ export class ControlPanel extends Widget {
|
|
|
1752
1760
|
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("undoInput","输入撤销"),row("redoInput","输入重做"),row("insertFilePicker","输入中打开文件选择器"),row("pasteImage","输入中粘贴剪贴板图片"),row("copySelection","复制正文选区"),row("addWorkspace","添加工作区"),row("commandPalette","命令面板"),row("modePicker","模式选择器"),row("themePicker","配色主题选择器"),row("logoPicker","欢迎页 logo(预设/自定义/关闭)"),row("quitDouble","双击 Ctrl+C 退出"),row("panel","控制面板"),row("model","切换模型"),row("trajectory","轨迹视图"),row("panePrev","pane 焦点 ← 上一窗格"),row("paneNext","pane 焦点 → 下一窗格"),row("permissionRotate","权限模式轮换"),row("workspace","工作区"),row("settings","设置"),row("subagent","子代理"),row("skills","技能"),row("goal","目标"),row("jobs","后台任务"),row("queue","后台队列"),row("busyEnter","运行中 Enter 策略"),row("attachments","附件管理"),row("stepJump","步骤转跳"),row("sidebar","侧栏显示/隐藏"),row("editConfig","编辑配置文件(默认编辑器)"),row("quit","退出"),
|
|
1753
1761
|
];
|
|
1754
1762
|
}
|
|
1763
|
+
prefixRows() {
|
|
1764
|
+
// 前缀页 = 引擎同一张表的可视化(which-key);键路由走同一引擎。
|
|
1765
|
+
return this.app.prefixRowsData();
|
|
1766
|
+
}
|
|
1755
1767
|
items() {
|
|
1756
|
-
if (this.page === 0) return this.
|
|
1757
|
-
if (this.page === 1)
|
|
1768
|
+
if (this.page === 0) return this.prefixRows().map(([k, d, hint, action]) => [`${k} ${d}`, hint, action]);
|
|
1769
|
+
if (this.page === 1) return this.shortcutItems();
|
|
1770
|
+
if (this.page === 2) {
|
|
1758
1771
|
return this.commands.map((c) => [
|
|
1759
1772
|
`/${c.name}${c.input?.hint ? " " + c.input.hint : ""}`,
|
|
1760
1773
|
c.description,
|
|
@@ -1766,7 +1779,7 @@ export class ControlPanel extends Widget {
|
|
|
1766
1779
|
},
|
|
1767
1780
|
]);
|
|
1768
1781
|
}
|
|
1769
|
-
if (this.page ===
|
|
1782
|
+
if (this.page === 3) {
|
|
1770
1783
|
return [
|
|
1771
1784
|
["模型管理(含思考强度)", "切换模型并选择思考强度", () => { this.app.overlay = buildModelPicker(this.app); }],
|
|
1772
1785
|
["模式(Agent 预设)", "标准 / PTC / 极简 / 创造", () => { this.app.overlay = buildModePicker(this.app); this.app.redraw(); }],
|
|
@@ -1778,7 +1791,7 @@ export class ControlPanel extends Widget {
|
|
|
1778
1791
|
["复制会话 ID", "", () => this.app.copyText(this.app.currentSession ?? "")],
|
|
1779
1792
|
];
|
|
1780
1793
|
}
|
|
1781
|
-
if (this.plugins) {
|
|
1794
|
+
if (this.page === 4 && this.plugins) {
|
|
1782
1795
|
const q=this.pluginQuery.toLowerCase();
|
|
1783
1796
|
return this.plugins.filter((pl)=>!q||`${pl.moduleName} ${pl.fiberPhase??""}`.toLowerCase().includes(q)).map((pl) => [`${pl.enabled ? "●" : "○"} ${pl.moduleName}`, pl.fiberPhase ?? "", null]);
|
|
1784
1797
|
}
|
|
@@ -1801,7 +1814,8 @@ export class ControlPanel extends Widget {
|
|
|
1801
1814
|
if (this.sel < this.scroll) this.scroll = this.sel;
|
|
1802
1815
|
else if (this.sel >= this.scroll + visible) this.scroll = this.sel - visible + 1;
|
|
1803
1816
|
this.scroll = Math.max(0, Math.min(Math.max(0, items.length - visible), this.scroll));
|
|
1804
|
-
if(this.page===0){s.text(this.x+2,this.y+1,"
|
|
1817
|
+
if(this.page===0){s.text(this.x+2,this.y+1,"KEY",{fg:T.PURPLE,bg:T.PANEL,attrs:1});s.text(this.x+13,this.y+1,"ACTION",{fg:T.OK,bg:T.PANEL,attrs:1});s.text(this.x+this.w-24,this.y+1,"等价命令",{fg:T.ACCENT,bg:T.PANEL,attrs:1});}
|
|
1818
|
+
if(this.page===1){s.text(this.x+2,this.y+1,"MODE",{fg:T.PURPLE,bg:T.PANEL,attrs:1});s.text(this.x+13,this.y+1,"KEY1",{fg:T.ACCENT,bg:T.PANEL,attrs:1});s.text(this.x+31,this.y+1,"KEY2",{fg:T.ACCENT,bg:T.PANEL,attrs:1});s.text(this.x+49,this.y+1,"FUNCTION",{fg:T.OK,bg:T.PANEL,attrs:1});}
|
|
1805
1819
|
if(this.page===3&&this.pluginFilter){s.text(this.x+2,this.y+1,`/ ${this.pluginQuery}`,{fg:T.ACCENT,bg:T.PANEL,attrs:1});}
|
|
1806
1820
|
for (let i = 0; i < visible; i++) {
|
|
1807
1821
|
const idx = this.scroll + i;
|
|
@@ -1810,10 +1824,11 @@ export class ControlPanel extends Widget {
|
|
|
1810
1824
|
const sel = idx === this.sel;
|
|
1811
1825
|
s.fillRect(this.x + 1, this.y + 2 + i, this.x + this.w - 2, this.y + 2 + i, " ", { bg: sel ? T.MENUSEL : T.PANEL });
|
|
1812
1826
|
const label = it[0];
|
|
1813
|
-
if(this.page===0){const [
|
|
1827
|
+
if(this.page===0){const [k, d2] = label.split(" ", 2);s.text(this.x+2,this.y+2+i,pad(k,9),{fg:T.PURPLE,bg:sel?T.MENUSEL:T.PANEL,attrs:sel?1:0});s.text(this.x+13,this.y+2+i,pad(truncate(d2, 30), 32),{fg:T.OK,bg:sel?T.MENUSEL:T.PANEL,attrs:sel?1:0});s.text(this.x+this.w-24,this.y+2+i,truncate(it[1]??"",22),{fg:T.ACCENT,bg:sel?T.MENUSEL:T.PANEL});}
|
|
1828
|
+
else if(this.page===1){const [mode,key1,key2]=label.split("\t");s.text(this.x+2,this.y+2+i,pad(mode,9),{fg:T.PURPLE,bg:sel?T.MENUSEL:T.PANEL,attrs:sel?1:0});s.text(this.x+13,this.y+2+i,pad(truncate(key1,16),17),{fg:T.ACCENT,bg:sel?T.MENUSEL:T.PANEL,attrs:sel?1:0});s.text(this.x+31,this.y+2+i,pad(truncate(key2,16),17),{fg:T.ACCENT,bg:sel?T.MENUSEL:T.PANEL,attrs:sel?1:0});s.text(this.x+49,this.y+2+i,truncate(it[1],this.w-52),{fg:T.OK,bg:sel?T.MENUSEL:T.PANEL,attrs:sel?1:0});}
|
|
1814
1829
|
else{s.text(this.x + 2, this.y + 2 + i, truncate(label, this.w - 34), { fg: sel ? T.BOLD : T.TXT, bg: sel ? T.MENUSEL : T.PANEL, attrs: sel ? 1 : 0 });if (it[1]) s.text(this.x + this.w - 30, this.y + 2 + i, truncate(it[1], 28), { fg: T.FAINT, bg: sel ? T.MENUSEL : T.PANEL });}
|
|
1815
1830
|
}
|
|
1816
|
-
s.text(this.x + 2, this.y + this.h - 1, this.page===0?"↑↓ 选择 · Enter 编辑 · Shift+Tab 轮换模式 · Alt+Enter 恢复默认 · Esc 关闭":this.page===3?`/ 筛选插件 · Ctrl+/ 清除 · ↑↓ 选择 · Esc 关闭${this.pluginQuery?` · ${this.pluginQuery}`:""}`:"↑↓ 选择 · Enter 执行 · Esc 关闭", { fg: T.FAINT });
|
|
1831
|
+
s.text(this.x + 2, this.y + this.h - 1, this.page===0?"按行首键执行(同 NORMAL 前缀层) · ↑↓/Enter 也可以 · Esc 关闭":this.page===1?"↑↓ 选择 · Enter 编辑 · Shift+Tab 轮换模式 · Alt+Enter 恢复默认 · Esc 关闭":this.page===3?`/ 筛选插件 · Ctrl+/ 清除 · ↑↓ 选择 · Esc 关闭${this.pluginQuery?` · ${this.pluginQuery}`:""}`:"↑↓ 选择 · Enter 执行 · Esc 关闭", { fg: T.FAINT });
|
|
1817
1832
|
}
|
|
1818
1833
|
onKey(ev) {
|
|
1819
1834
|
if(this.page===3&&this.pluginFilter){if(ev.type==="text"){this.pluginQuery+=ev.text;this.sel=0;return true;}if(ev.type==="key"&&ev.name==="backspace"){this.pluginQuery=this.pluginQuery.slice(0,-1);this.sel=0;return true;}if(ev.type==="key"&&ev.name==="enter"){this.pluginFilter=false;return true;}if(ev.type==="key"&&ev.ctrl&&(ev.key==="/"||ev.key==="_")){this.pluginFilter=false;this.pluginQuery="";return true;}}
|
|
@@ -1822,6 +1837,13 @@ export class ControlPanel extends Widget {
|
|
|
1822
1837
|
if(this.page===3&&ev.name==="char"&&ev.key==="/"){this.pluginFilter=true;this.pluginQuery="";return true;}
|
|
1823
1838
|
if(this.page===3&&ev.ctrl&&(ev.key==="/"||ev.key==="_")){this.pluginQuery="";return true;}
|
|
1824
1839
|
if (ev.name === "escape") { this.app.closeOverlay(); return true; }
|
|
1840
|
+
// Prefix page: a plain one-key press fires the mapped command — but ONLY
|
|
1841
|
+
// while the prefix page itself is on screen (leader-style EDITING logic).
|
|
1842
|
+
if (this.page === 0 && ev.name === "char" && !ev.ctrl && !ev.alt) {
|
|
1843
|
+
const key = ev.key ?? "";
|
|
1844
|
+
const row = this.prefixRows().find(([k]) => k === key || (k === key.toUpperCase() && ev.shift && k !== key));
|
|
1845
|
+
if (row) { row[3](); this.app.redraw(); return true; }
|
|
1846
|
+
}
|
|
1825
1847
|
if(this.page===0&&ev.name==="backtab"){const it=this.items()[this.sel],id=it?.[3];if(id){const b=keyBindings()[id],modes=["normal","insert","all"],mode=modes[(modes.indexOf(b.mode)+1)%3];setKeyBinding(id,{...b,mode});this.app.toast(`适用模式: ${mode.toUpperCase()}`);}return true;}
|
|
1826
1848
|
if(this.page===0&&ev.alt&&ev.name==="enter"){const id=this.items()[this.sel]?.[3];if(id){resetKeyBinding(id);this.app.toast("已恢复默认快捷键");}return true;}
|
|
1827
1849
|
if (ev.name === "tab" || ev.name === "right") {
|
|
@@ -2278,7 +2300,7 @@ export class QueuePanel extends Popup {
|
|
|
2278
2300
|
|
|
2279
2301
|
export class GoalPanel extends Popup {
|
|
2280
2302
|
constructor(app) {
|
|
2281
|
-
super({ x: 4, y: 2, w: Math.max(24, Math.min(
|
|
2303
|
+
super({ x: 4, y: 2, w: Math.max(24, Math.min(96, app.screen.w - 8)), h: Math.max(10, Math.min(app.screen.h - 4, 34)), title: "目标与任务", lines: [], buttons: [], scrollable: true });
|
|
2282
2304
|
this.app = app; this.busy = false; this.actionSel = 0; this.actions = []; this.actionRows = []; this.rebuild();
|
|
2283
2305
|
}
|
|
2284
2306
|
/** Called by App when a live goal/todo projection arrives while open. */
|
|
@@ -2292,6 +2314,14 @@ export class GoalPanel extends Popup {
|
|
|
2292
2314
|
else {
|
|
2293
2315
|
lines.push([{ t: ` 目标: ${goal.objective ?? goal}`, fg: K.TXT, bold: true }]);
|
|
2294
2316
|
lines.push([{ t: ` 阶段: ${goal.phase ?? "active"} · 轮次 ${this.app.goalData?.roundsStarted ?? 0}/${goal.maxGoalRounds ?? "∞"} · 修订 ${goal.revision ?? "?"}`, fg: K.DIM }]);
|
|
2317
|
+
const rounds = (this.app.chat?.nodes ?? []).filter((n) => n?.kind === "goal-round" || n?.source?.kind === "goal");
|
|
2318
|
+
if (rounds.length) {
|
|
2319
|
+
lines.push([{ t: "" }, { t: " ◆ 目标延续轮(自动进入)", fg: K.PURPLE, bold: true }]);
|
|
2320
|
+
for (const r of rounds) {
|
|
2321
|
+
const text = (r.blocks ?? []).filter((b) => b?.type === "text").map((b) => b.text ?? "").join("").replace(/\s+/g, " ").trim();
|
|
2322
|
+
lines.push([{ t: ` ◆ ${r.source?.round ?? "?"}轮 · ${truncate(text || r.id || "(无文本)", Math.max(12, this.w - 12))}`, fg: K.DIM }]);
|
|
2323
|
+
}
|
|
2324
|
+
}
|
|
2295
2325
|
if (goal.blockedReason?.message) lines.push([{ t: ` 阻塞: ${goal.blockedReason.message}`, fg: K.ERR }]);
|
|
2296
2326
|
}
|
|
2297
2327
|
this.actions = goal ? [
|
|
@@ -4742,3 +4772,381 @@ export class SkillsPanel extends Widget {
|
|
|
4742
4772
|
return false;
|
|
4743
4773
|
}
|
|
4744
4774
|
}
|
|
4775
|
+
|
|
4776
|
+
/** Open a scrollable detail Popup over `returnTo` (restored as app.overlay on
|
|
4777
|
+
* Escape/q). `lines` may be raw string lines or styled segment arrays. */
|
|
4778
|
+
function openDetailPopup(app, returnTo, title, lines) {
|
|
4779
|
+
const w = Math.max(40, Math.min(110, app.screen.w - 4));
|
|
4780
|
+
const h = Math.max(12, Math.min(36, app.screen.h - 4));
|
|
4781
|
+
const pop = new Popup({
|
|
4782
|
+
x: Math.max(0, Math.floor((app.screen.w - w) / 2)),
|
|
4783
|
+
y: Math.max(0, Math.floor((app.screen.h - h) / 2)),
|
|
4784
|
+
w, h, title, lines: [], buttons: [], scrollable: true,
|
|
4785
|
+
onAction: () => { app.overlay = returnTo; app.redraw(); },
|
|
4786
|
+
});
|
|
4787
|
+
for (const line of (lines ?? [])) {
|
|
4788
|
+
pop.lines.push(Array.isArray(line) ? line.map((seg) => ({ ...seg })) : [{ t: String(line), fg: K.TXT }]);
|
|
4789
|
+
}
|
|
4790
|
+
const base = pop.onKey.bind(pop);
|
|
4791
|
+
pop.onKey = (ev) => {
|
|
4792
|
+
if (ev.type === "key" && (ev.name === "escape" || (ev.name === "char" && ev.key === "q" && !ev.ctrl))) {
|
|
4793
|
+
app.overlay = returnTo; app.redraw(); return true;
|
|
4794
|
+
}
|
|
4795
|
+
return base(ev);
|
|
4796
|
+
};
|
|
4797
|
+
app.overlay = pop;
|
|
4798
|
+
app.redraw();
|
|
4799
|
+
return pop;
|
|
4800
|
+
}
|
|
4801
|
+
|
|
4802
|
+
/** Pretty, bounded JSON-ish summary of an entry for the detail popup. */
|
|
4803
|
+
function detailText(value) {
|
|
4804
|
+
const s = prettyJson(JSON.stringify(value));
|
|
4805
|
+
return (s ?? JSON.stringify(value, null, 2) ?? String(value)).slice(0, 6000);
|
|
4806
|
+
}
|
|
4807
|
+
|
|
4808
|
+
/** Message-source badge: ● direct prompt (user) · ◇ injected context
|
|
4809
|
+
* (plugin: cron/inject/skill…) · ◆ goal continuation round · ● unknown. */
|
|
4810
|
+
export function sourceBadge(kind) {
|
|
4811
|
+
return kind === "user" ? "●" : kind === "plugin" ? "◇" : kind === "goal" ? "◆" : "●";
|
|
4812
|
+
}
|
|
4813
|
+
export function sourceGroup(kind) {
|
|
4814
|
+
return kind === "user" ? "排队提问" : kind === "plugin" ? "注入上下文" : kind === "goal" ? "目标延续" : "排队提问";
|
|
4815
|
+
}
|
|
4816
|
+
/** 徽标图例(页脚小字)。 */
|
|
4817
|
+
export function sourceLegend() {
|
|
4818
|
+
return "● 提问 · ◇ 注入 · ◆ 目标轮";
|
|
4819
|
+
}
|
|
4820
|
+
|
|
4821
|
+
// ---- SubagentPage (main-area pane 子代理): single-column subagent list ----
|
|
4822
|
+
|
|
4823
|
+
export class SubagentPage extends Widget {
|
|
4824
|
+
constructor(app) {
|
|
4825
|
+
super({ x: 0, y: 1, w: app.screen.w, h: Math.max(2, app.screen.h - 1) });
|
|
4826
|
+
this.app = app;
|
|
4827
|
+
this.entries = [];
|
|
4828
|
+
this.error = null;
|
|
4829
|
+
this.loading = false;
|
|
4830
|
+
this.sel = 0;
|
|
4831
|
+
this.rows = []; // { kind:"header"|"item", item? } flattened selectable view
|
|
4832
|
+
this.selector = []; // entry indices selectable in order
|
|
4833
|
+
this.view = new ScrollView({ x: this.x, y: this.y, w: this.w, h: this.h, showScrollbar: true });
|
|
4834
|
+
this.loadToken = 0;
|
|
4835
|
+
this.inputMode = false;
|
|
4836
|
+
this.input = new Input({
|
|
4837
|
+
x: this.x, y: this.y + this.h - 1, w: this.w, h: 1,
|
|
4838
|
+
placeholder: "给选中子代理发消息…(continuable · i 聚焦 · Esc 回列表)",
|
|
4839
|
+
onEnter: (v) => this.send(v),
|
|
4840
|
+
});
|
|
4841
|
+
}
|
|
4842
|
+
relayout(x, y, w, h) {
|
|
4843
|
+
this.x = x; this.y = y; this.w = w; this.h = h;
|
|
4844
|
+
this.view.x = x; this.view.y = y; this.view.w = w; this.view.h = Math.max(1, h - 1);
|
|
4845
|
+
this.input.x = x; this.input.y = y + h - 1; this.input.w = w;
|
|
4846
|
+
}
|
|
4847
|
+
onActivate() { this.ensureLoaded(); }
|
|
4848
|
+
ensureLoaded() {
|
|
4849
|
+
if (this.loading || (this.entries.length > 0 && this.error == null)) return;
|
|
4850
|
+
this.load();
|
|
4851
|
+
}
|
|
4852
|
+
/** Live refresh (frame injection), throttled to one call per second. */
|
|
4853
|
+
reload() {
|
|
4854
|
+
const now = Date.now();
|
|
4855
|
+
if (this.loading) return;
|
|
4856
|
+
if (this.lastReloadAt != null && now - this.lastReloadAt < 1000) return;
|
|
4857
|
+
this.lastReloadAt = now;
|
|
4858
|
+
this.load();
|
|
4859
|
+
}
|
|
4860
|
+
async load() {
|
|
4861
|
+
const token = ++this.loadToken;
|
|
4862
|
+
this.loading = true;
|
|
4863
|
+
this.error = null;
|
|
4864
|
+
try {
|
|
4865
|
+
const res = await this.app.api.call("subagent.list", { parentSessionId: this.app.currentSession });
|
|
4866
|
+
if (token !== this.loadToken) return;
|
|
4867
|
+
this.entries = res.items ?? res.entries ?? [];
|
|
4868
|
+
} catch (e) {
|
|
4869
|
+
if (token !== this.loadToken) return;
|
|
4870
|
+
this.entries = [];
|
|
4871
|
+
this.error = e.message;
|
|
4872
|
+
this.app.toast(`子代理加载失败: ${e.message}`);
|
|
4873
|
+
}
|
|
4874
|
+
this.loading = false;
|
|
4875
|
+
this.sel = 0;
|
|
4876
|
+
this.#rebuild();
|
|
4877
|
+
this.app.redraw();
|
|
4878
|
+
}
|
|
4879
|
+
#parentOf(e) { return e.parentSessionId ?? e.parentId ?? e.parent ?? null; }
|
|
4880
|
+
#statusOf(e) {
|
|
4881
|
+
return e.activity ?? e.state ?? e.status ?? (e.done ? "inactive" : "idle");
|
|
4882
|
+
}
|
|
4883
|
+
#elapsedMs(e) {
|
|
4884
|
+
if (e.elapsed != null) return e.elapsed;
|
|
4885
|
+
if (e.duration != null) return e.duration;
|
|
4886
|
+
if (typeof e.settledMs === "number") return e.settledMs;
|
|
4887
|
+
if (typeof e.timing?.settledMs === "number") return e.timing.settledMs;
|
|
4888
|
+
if (typeof e.startedAt === "number" && typeof e.finishedAt === "number") return e.finishedAt - e.startedAt;
|
|
4889
|
+
if (typeof e.startedAt === "number") return Date.now() - e.startedAt;
|
|
4890
|
+
return null;
|
|
4891
|
+
}
|
|
4892
|
+
#modelOf(e) {
|
|
4893
|
+
if (typeof e.model === "string") return e.model;
|
|
4894
|
+
if (e.model?.id) return `${e.model.provider ? e.model.provider + "/" : ""}${e.model.id}`;
|
|
4895
|
+
if (typeof e.modelId === "string") return e.modelId;
|
|
4896
|
+
return null;
|
|
4897
|
+
}
|
|
4898
|
+
#tokensOf(e) {
|
|
4899
|
+
const t = e.tokenUsage ?? e.tokens ?? e.tokenCount;
|
|
4900
|
+
if (t == null) return null;
|
|
4901
|
+
const total = typeof t === "object" ? (t.total ?? t.input + (t.output ?? 0) ?? null) : t;
|
|
4902
|
+
return total == null ? null : total;
|
|
4903
|
+
}
|
|
4904
|
+
#exitReason(e) {
|
|
4905
|
+
return e.exitReason ?? e.stoppedReason ?? e.reason ?? e.error?.message ?? null;
|
|
4906
|
+
}
|
|
4907
|
+
#shortId(e, w) {
|
|
4908
|
+
const label = e.label ?? e.sessionId ?? e.id;
|
|
4909
|
+
return truncate(String(label ?? "子代理"), w);
|
|
4910
|
+
}
|
|
4911
|
+
#rebuild() {
|
|
4912
|
+
const w = Math.max(20, this.w - 2);
|
|
4913
|
+
const rows = [], selector = [];
|
|
4914
|
+
for (let i = 0; i < this.entries.length; i++) {
|
|
4915
|
+
const e = this.entries[i];
|
|
4916
|
+
const parent = this.#parentOf(e);
|
|
4917
|
+
if (parent != null) {
|
|
4918
|
+
const prev = i > 0 ? this.#parentOf(this.entries[i - 1]) : undefined;
|
|
4919
|
+
if (prev !== parent) rows.push({ kind: "header", text: `▼ 父会话 ${truncate(String(parent), 30)} / 子代理` });
|
|
4920
|
+
} else if (i === 0) {
|
|
4921
|
+
rows.push({ kind: "header", text: "▼ 父会话 / 子代理" });
|
|
4922
|
+
}
|
|
4923
|
+
const running = this.#statusOf(e) === "running";
|
|
4924
|
+
const model = this.#modelOf(e);
|
|
4925
|
+
const elapsed = this.#elapsedMs(e);
|
|
4926
|
+
const tokens = this.#tokensOf(e);
|
|
4927
|
+
const exit = this.#exitReason(e);
|
|
4928
|
+
const idw = Math.max(10, w - 14);
|
|
4929
|
+
let line = `${running ? "●" : "○"} ${this.#shortId(e, idw)}`;
|
|
4930
|
+
if (model) line += ` · ${truncate(String(model), 18)}`;
|
|
4931
|
+
if (elapsed != null) line += ` · ${fmtDuration(elapsed)}`;
|
|
4932
|
+
if (tokens != null) line += ` · ${tokens} tok`;
|
|
4933
|
+
if (exit) line += ` · ${truncate(String(exit), 22)}`;
|
|
4934
|
+
rows.push({ kind: "item", item: e, index: i, text: line, running });
|
|
4935
|
+
selector.push(i);
|
|
4936
|
+
}
|
|
4937
|
+
if (this.entries.length === 0) {
|
|
4938
|
+
rows.push({ kind: "empty", text: this.error ? ` 子代理加载失败: ${this.error}` : " 暂无子代理" });
|
|
4939
|
+
}
|
|
4940
|
+
this.rows = rows;
|
|
4941
|
+
this.selector = selector;
|
|
4942
|
+
if (this.sel >= selector.length) this.sel = Math.max(0, selector.length - 1);
|
|
4943
|
+
}
|
|
4944
|
+
render(screen) {
|
|
4945
|
+
screen.fillRect(this.x, this.y, this.x + this.w - 1, this.y + this.h - 1, " ", { bg: T.BG });
|
|
4946
|
+
if (this.entries.length === 0 && !this.loading) {
|
|
4947
|
+
const msg = this.error ? `子代理加载失败: ${this.error}` : "暂无子代理";
|
|
4948
|
+
screen.text(this.x + 1, this.y + 1, msg, { fg: this.error ? K.ERR : K.FAINT });
|
|
4949
|
+
return;
|
|
4950
|
+
}
|
|
4951
|
+
// Render rows through the shared ScrollView so scrolling works.
|
|
4952
|
+
this.view.setLines(this.rows.map((row) => {
|
|
4953
|
+
if (row.kind === "header") return [{ t: row.text, fg: K.ACCENT, bold: true }];
|
|
4954
|
+
if (row.kind === "empty") return [{ t: row.text, fg: row.text.startsWith("子代理加载失败") ? K.ERR : K.FAINT }];
|
|
4955
|
+
const sel = this.selector[this.sel] === row.index;
|
|
4956
|
+
const fg = row.running ? K.OK : sel ? T.SELFG : K.TXT;
|
|
4957
|
+
const bg = sel ? T.MENUSEL : -1;
|
|
4958
|
+
return [{ t: row.text, fg, bg, bold: sel }];
|
|
4959
|
+
}), { keep: true });
|
|
4960
|
+
this.view.render(screen);
|
|
4961
|
+
// selection highlight is handled above via bg; ensure selected row visible
|
|
4962
|
+
const selIdx = this.selector[this.sel];
|
|
4963
|
+
if (selIdx != null) {
|
|
4964
|
+
const rowPos = this.rows.findIndex((r) => r.kind === "item" && r.index === selIdx);
|
|
4965
|
+
const selLine = this.rows.slice(0, rowPos + 1).filter((r) => r.kind !== "empty").length - 1;
|
|
4966
|
+
if (selLine < this.view.scrollY) this.view.scrollY = selLine;
|
|
4967
|
+
else if (selLine >= this.view.scrollY + this.view.h) this.view.scrollY = selLine - this.view.h + 1;
|
|
4968
|
+
}
|
|
4969
|
+
screen.fillRect(this.x, this.y + this.h - 2, this.x + this.w - 1, this.y + this.h - 1, " ", { bg: this.inputMode ? T.BG2 : T.BG });
|
|
4970
|
+
screen.text(this.x + 1, this.y + this.h - 2, " ↑↓ 选择 · Enter 详情 · r 刷新 · i 给子代理发消息 · q/Esc 返回聊天", { fg: K.FAINT });
|
|
4971
|
+
this.input.render(screen);
|
|
4972
|
+
}
|
|
4973
|
+
/** Continuable message to the selected subagent (original panel parity). */
|
|
4974
|
+
async send(text) {
|
|
4975
|
+
const child = this.entries[this.selector[this.sel]];
|
|
4976
|
+
if (!child) { this.app.toast("先选择子代理"); return; }
|
|
4977
|
+
try {
|
|
4978
|
+
await this.app.api.call("subagent.prompt", {
|
|
4979
|
+
parentSessionId: this.app.currentSession,
|
|
4980
|
+
childSessionId: child.id,
|
|
4981
|
+
mode: "continuable",
|
|
4982
|
+
content: [{ type: "text", text }],
|
|
4983
|
+
clientTimeZone: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
|
4984
|
+
});
|
|
4985
|
+
this.app.toast(`已发送给 ${child.id.slice(0, 8)}`);
|
|
4986
|
+
this.input.setValue("");
|
|
4987
|
+
this.reload();
|
|
4988
|
+
} catch (e) { this.app.toast(`发送失败: ${e.message}`); }
|
|
4989
|
+
}
|
|
4990
|
+
openDetail() {
|
|
4991
|
+
const e = this.entries[this.selector[this.sel]];
|
|
4992
|
+
if (!e) return;
|
|
4993
|
+
openDetailPopup(this.app, this.app.overlay, "子代理详情", detailText(e).split("\n"));
|
|
4994
|
+
}
|
|
4995
|
+
onKey(ev) {
|
|
4996
|
+
if (ev.type !== "key") return false;
|
|
4997
|
+
if (this.inputMode) {
|
|
4998
|
+
if (ev.name === "escape") { this.inputMode = false; this.input.setValue(""); this.app.redraw(); return true; }
|
|
4999
|
+
this.input.onKey(ev);
|
|
5000
|
+
return true;
|
|
5001
|
+
}
|
|
5002
|
+
if (ev.name === "char" && (ev.key === "i" || ev.key === "I") && !ev.ctrl) { this.inputMode = true; this.app.redraw(); return true; }
|
|
5003
|
+
if (ev.name === "tab") { this.inputMode = true; this.app.redraw(); return true; }
|
|
5004
|
+
if (ev.name === "char" && ev.key === "r" && !ev.ctrl) { this.load(); return true; }
|
|
5005
|
+
if (ev.name === "up" || ev.name === "down") {
|
|
5006
|
+
if (this.selector.length === 0) return false;
|
|
5007
|
+
const next = wrapIndex(this.sel + (ev.name === "up" ? -1 : 1), this.selector.length);
|
|
5008
|
+
this.sel = next;
|
|
5009
|
+
this.app.redraw();
|
|
5010
|
+
return true;
|
|
5011
|
+
}
|
|
5012
|
+
if (ev.name === "enter") { this.openDetail(); return true; }
|
|
5013
|
+
if (ev.name === "pgup") { this.view.scroll(-this.view.h); this.app.redraw(); return true; }
|
|
5014
|
+
if (ev.name === "pgdn") { this.view.scroll(this.view.h); this.app.redraw(); return true; }
|
|
5015
|
+
return false;
|
|
5016
|
+
}
|
|
5017
|
+
onMouse(ev) {
|
|
5018
|
+
if (ev.kind === "press" && ev.button === 0) {
|
|
5019
|
+
const line = ev.y - this.view.y + this.view.scrollY;
|
|
5020
|
+
const row = this.rows[line];
|
|
5021
|
+
if (row?.kind === "item") {
|
|
5022
|
+
this.sel = this.selector.indexOf(row.index);
|
|
5023
|
+
this.openDetail();
|
|
5024
|
+
}
|
|
5025
|
+
return true;
|
|
5026
|
+
}
|
|
5027
|
+
return this.view.onMouse(ev);
|
|
5028
|
+
}
|
|
5029
|
+
}
|
|
5030
|
+
|
|
5031
|
+
// ---- TasksPage (main-area pane 后台任务): background jobs + queue, source-kind badges ----
|
|
5032
|
+
|
|
5033
|
+
export class TasksPage extends Widget {
|
|
5034
|
+
constructor(app) {
|
|
5035
|
+
super({ x: 0, y: 1, w: app.screen.w, h: Math.max(2, app.screen.h - 1) });
|
|
5036
|
+
this.app = app;
|
|
5037
|
+
this.sel = 0;
|
|
5038
|
+
this.groups = [];
|
|
5039
|
+
this.selector = [];
|
|
5040
|
+
this.view = new ScrollView({ x: this.x, y: this.y, w: this.w, h: this.h, showScrollbar: true });
|
|
5041
|
+
}
|
|
5042
|
+
relayout(x, y, w, h) {
|
|
5043
|
+
this.x = x; this.y = y; this.w = w; this.h = h;
|
|
5044
|
+
this.view.x = x; this.view.y = y; this.view.w = w; this.view.h = h;
|
|
5045
|
+
}
|
|
5046
|
+
onActivate() { this.#rebuild(); }
|
|
5047
|
+
/** One normalized entry: queue item or job. */
|
|
5048
|
+
#entries() {
|
|
5049
|
+
const out = [];
|
|
5050
|
+
for (const item of (this.app.queueItems ?? [])) {
|
|
5051
|
+
const src = item?.source?.kind ?? item?.message?.source?.kind ?? "unknown";
|
|
5052
|
+
out.push({ kind: "queue", badge: this.#badgeFor(src), group: this.#groupFor(src), src, item, text: this.#queueSummary(item) });
|
|
5053
|
+
}
|
|
5054
|
+
for (const job of (this.app.jobs ?? [])) {
|
|
5055
|
+
const src = job?.source?.kind ?? "unknown";
|
|
5056
|
+
out.push({ kind: "job", badge: this.#badgeFor(src), group: this.#groupFor(src), src, job, text: this.#jobSummary(job) });
|
|
5057
|
+
}
|
|
5058
|
+
return out;
|
|
5059
|
+
}
|
|
5060
|
+
#badgeFor(src) { return sourceBadge(src); }
|
|
5061
|
+
#groupFor(src) { return sourceGroup(src); }
|
|
5062
|
+
#queueSummary(item) {
|
|
5063
|
+
const text = partsText(item?.message?.content).replace(/\s+/g, " ").trim();
|
|
5064
|
+
return truncate(text || item?.id || "(无文本)", 56);
|
|
5065
|
+
}
|
|
5066
|
+
#jobSummary(job) {
|
|
5067
|
+
let s = truncate(job?.label ?? job?.kind ?? "任务", 56);
|
|
5068
|
+
if (job?.status) s += ` · ${job.status}`;
|
|
5069
|
+
return s;
|
|
5070
|
+
}
|
|
5071
|
+
#durationMs(entry) {
|
|
5072
|
+
const e = entry.kind === "job" ? entry.job : entry.item;
|
|
5073
|
+
if (e?.elapsed != null) return e.elapsed;
|
|
5074
|
+
if (e?.duration != null) return e.duration;
|
|
5075
|
+
if (typeof e?.startedAt === "number" && typeof e?.finishedAt === "number") return e.finishedAt - e.startedAt;
|
|
5076
|
+
if (typeof e?.startedAt === "number") return Date.now() - e.startedAt;
|
|
5077
|
+
return null;
|
|
5078
|
+
}
|
|
5079
|
+
#hasLogTail(entry) {
|
|
5080
|
+
const e = entry.kind === "job" ? entry.job : entry.item;
|
|
5081
|
+
return !!(e?.logTail ?? e?.tail ?? e?.result);
|
|
5082
|
+
}
|
|
5083
|
+
#exitCode(entry) {
|
|
5084
|
+
const e = entry.kind === "job" ? entry.job : entry.item;
|
|
5085
|
+
return e?.exitCode ?? e?.code ?? (e?.status === "failed" ? 1 : null);
|
|
5086
|
+
}
|
|
5087
|
+
#rebuild() {
|
|
5088
|
+
const entries = this.#entries();
|
|
5089
|
+
const groupOrder = [{ name: "排队提问", badge: "●" }, { name: "注入上下文", badge: "◇" }, { name: "目标延续", badge: "◆" }];
|
|
5090
|
+
const rows = [], selector = [];
|
|
5091
|
+
for (const g of groupOrder) {
|
|
5092
|
+
const members = entries.filter((e) => e.group === g.name);
|
|
5093
|
+
if (members.length === 0) continue;
|
|
5094
|
+
rows.push({ kind: "header", text: `${g.badge} ${g.name}(${members.length})` });
|
|
5095
|
+
for (const entry of members) {
|
|
5096
|
+
const srcTag = entry.src === "unknown" ? " (unknown source)" : "";
|
|
5097
|
+
let text = `${entry.badge} ${entry.text}${srcTag}`;
|
|
5098
|
+
const dur = this.#durationMs(entry);
|
|
5099
|
+
if (dur != null) text += ` · ${fmtDuration(dur)}`;
|
|
5100
|
+
const code = this.#exitCode(entry);
|
|
5101
|
+
if (code != null) text += ` · exit ${code}`;
|
|
5102
|
+
if (this.#hasLogTail(entry)) text += " …";
|
|
5103
|
+
rows.push({ kind: "item", entry, text, index: selector.length });
|
|
5104
|
+
selector.push(entries.indexOf(entry));
|
|
5105
|
+
}
|
|
5106
|
+
}
|
|
5107
|
+
if (rows.length === 0) rows.push({ kind: "empty", text: " 暂无后台任务或排队命令" });
|
|
5108
|
+
this.groups = rows;
|
|
5109
|
+
this.selector = selector;
|
|
5110
|
+
if (this.sel >= selector.length) this.sel = Math.max(0, selector.length - 1);
|
|
5111
|
+
}
|
|
5112
|
+
render(screen) {
|
|
5113
|
+
screen.fillRect(this.x, this.y, this.x + this.w - 1, this.y + this.h - 1, " ", { bg: T.BG });
|
|
5114
|
+
this.view.setLines(this.groups.map((row) => {
|
|
5115
|
+
if (row.kind === "header") return [{ t: row.text, fg: K.ACCENT, bold: true }];
|
|
5116
|
+
if (row.kind === "empty") return [{ t: row.text, fg: K.FAINT }];
|
|
5117
|
+
const sel = this.selector[this.sel] === row.index;
|
|
5118
|
+
return [{ t: row.text, fg: sel ? T.SELFG : K.TXT, bg: sel ? T.MENUSEL : -1, bold: sel }];
|
|
5119
|
+
}), { keep: true });
|
|
5120
|
+
this.view.render(screen);
|
|
5121
|
+
screen.text(this.x + 1, this.y + this.h - 1, " ↑↓ 选择 · Enter 详情 · q/Esc 返回聊天", { fg: K.FAINT });
|
|
5122
|
+
}
|
|
5123
|
+
openDetail() {
|
|
5124
|
+
const rowIdx = this.selector[this.sel];
|
|
5125
|
+
const entry = this.#entries()[rowIdx];
|
|
5126
|
+
if (!entry) return;
|
|
5127
|
+
const raw = entry.kind === "job" ? entry.job : entry.item;
|
|
5128
|
+
openDetailPopup(this.app, this.app.overlay, "任务详情", detailText(raw).split("\n"));
|
|
5129
|
+
}
|
|
5130
|
+
onKey(ev) {
|
|
5131
|
+
if (ev.type !== "key") return false;
|
|
5132
|
+
if (ev.name === "up" || ev.name === "down") {
|
|
5133
|
+
if (this.selector.length === 0) return false;
|
|
5134
|
+
this.sel = wrapIndex(this.sel + (ev.name === "up" ? -1 : 1), this.selector.length);
|
|
5135
|
+
this.app.redraw();
|
|
5136
|
+
return true;
|
|
5137
|
+
}
|
|
5138
|
+
if (ev.name === "enter") { this.openDetail(); return true; }
|
|
5139
|
+
if (ev.name === "pgup") { this.view.scroll(-this.view.h); this.app.redraw(); return true; }
|
|
5140
|
+
if (ev.name === "pgdn") { this.view.scroll(this.view.h); this.app.redraw(); return true; }
|
|
5141
|
+
return false;
|
|
5142
|
+
}
|
|
5143
|
+
onMouse(ev) {
|
|
5144
|
+
if (ev.type === "mouse" && ev.kind === "press" && ev.button === 0) {
|
|
5145
|
+
const line = ev.y - this.view.y + this.view.scrollY;
|
|
5146
|
+
const row = this.groups[line];
|
|
5147
|
+
if (row?.kind === "item") { this.sel = this.selector.indexOf(row.index); this.openDetail(); }
|
|
5148
|
+
return true;
|
|
5149
|
+
}
|
|
5150
|
+
return this.view.onMouse(ev);
|
|
5151
|
+
}
|
|
5152
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
// side-question.mjs — /btw 侧问的独立执行器(子进程,密钥一次性使用)。
|
|
2
|
+
// 只在子进程内存中读取 ~/.dsh/.credentials.yaml 的 DEEPSEEK_API_KEY,
|
|
3
|
+
// 直调 DeepSeek API(无工具、单轮),stdout 只输出答案 JSON。
|
|
4
|
+
// ── 重要 ── 绝不打印/回显密钥;错误消息不包含请求头内容。
|
|
5
|
+
import { readFileSync } from "node:fs";
|
|
6
|
+
import { homedir } from "node:os";
|
|
7
|
+
import { join } from "node:path";
|
|
8
|
+
|
|
9
|
+
/** env-style credential 文件解析(仅取键名列表,值不落日志)。 */
|
|
10
|
+
export function parseCredentials(text) {
|
|
11
|
+
const out = {};
|
|
12
|
+
for (const line of String(text).split("\n")) {
|
|
13
|
+
// YAML "KEY: value" 或 env "KEY=value" 两种形态
|
|
14
|
+
const m = /^\s*([A-Za-z_][A-Za-z0-9_]*)\s*(?::|=)\s*(.*?)\s*$/.exec(line);
|
|
15
|
+
if (m) out[m[1]] = m[2].replace(/^["']|["']$/g, "");
|
|
16
|
+
}
|
|
17
|
+
return out;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** 侧问题请求体(系统围栏 + 会话摘要上下文 + 问题)。 */
|
|
21
|
+
export function buildMessages(question, context = "") {
|
|
22
|
+
const system = `<system-reminder>This is a side question from the user. Answer it directly in a single response.
|
|
23
|
+
|
|
24
|
+
IMPORTANT CONTEXT:
|
|
25
|
+
- You are a separate, lightweight agent spawned to answer this one question
|
|
26
|
+
- The main agent is NOT interrupted - it continues working independently
|
|
27
|
+
- You share the conversation context but are a separate instance
|
|
28
|
+
|
|
29
|
+
CRITICAL CONSTRAINTS:
|
|
30
|
+
- You have NO tools - you cannot read files, run commands, search, or take actions
|
|
31
|
+
- This is a one-off response; there will be no follow-up turns
|
|
32
|
+
- Answer only with what you know from the conversation context
|
|
33
|
+
- Never promise to take any action; if you do not know, say so
|
|
34
|
+
|
|
35
|
+
Simply answer the question with the information you have.</system-reminder>`;
|
|
36
|
+
const msgs = [{ role: "system", content: system }];
|
|
37
|
+
if (context) msgs.push({ role: "user", content: `对话摘要(供参考,非问题本身):\n${context}` });
|
|
38
|
+
msgs.push({ role: "user", content: question });
|
|
39
|
+
return msgs;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** 直调 DeepSeek 聊天补全(无工具、单轮)。fetchImpl 可注入(测试)。 */
|
|
43
|
+
export async function callDeepSeek(apiKey, messages, { baseUrl = "https://api.deepseek.com", model = "deepseek-chat", fetchImpl = fetch } = {}) {
|
|
44
|
+
const res = await fetchImpl(`${baseUrl}/chat/completions`, {
|
|
45
|
+
method: "POST",
|
|
46
|
+
headers: { "Content-Type": "application/json", Authorization: `Bearer ${apiKey}` },
|
|
47
|
+
body: JSON.stringify({ model, messages, temperature: 0.6, max_tokens: 2048 }),
|
|
48
|
+
});
|
|
49
|
+
if (!res.ok) throw new Error(`API 响应 ${res.status}`);
|
|
50
|
+
const data = await res.json();
|
|
51
|
+
const answer = data?.choices?.[0]?.message?.content?.trim();
|
|
52
|
+
if (!answer) throw new Error("模型未返回内容");
|
|
53
|
+
return answer;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// ── CLI 入口:node src/side-question.mjs --question "..." [--context "..."] [--credentials path] ──
|
|
57
|
+
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
58
|
+
const args = process.argv.slice(2);
|
|
59
|
+
const opt = (name) => args.indexOf(name) >= 0 ? args[args.indexOf(name) + 1] : null;
|
|
60
|
+
const question = opt("--question");
|
|
61
|
+
const context = opt("--context") ?? "";
|
|
62
|
+
if (!question) { console.error("缺少 --question"); process.exit(1); }
|
|
63
|
+
const credPath = opt("--credentials") ?? join(process.env.DSH_HOME ?? join(homedir(), ".dsh"), ".credentials.yaml");
|
|
64
|
+
try {
|
|
65
|
+
const creds = parseCredentials(readFileSync(credPath, "utf8"));
|
|
66
|
+
const key = creds.DEEPSEEK_API_KEY;
|
|
67
|
+
if (!key) { console.error("credentials 中未找到 DEEPSEEK_API_KEY"); process.exit(1); }
|
|
68
|
+
const answer = await callDeepSeek(key, buildMessages(question, context));
|
|
69
|
+
process.stdout.write(JSON.stringify({ ok: true, answer }));
|
|
70
|
+
} catch (e) {
|
|
71
|
+
// 只输出通用错误(不含密钥与请求头)
|
|
72
|
+
console.error(`side-question 失败: ${e?.message ?? e}`);
|
|
73
|
+
process.exit(1);
|
|
74
|
+
}
|
|
75
|
+
}
|
package/src/term.js
CHANGED
|
@@ -30,6 +30,8 @@ export class Term {
|
|
|
30
30
|
this.onResize = onResize ?? (() => {});
|
|
31
31
|
this.kitty = kitty;
|
|
32
32
|
this.kittyActive = false; // set when the terminal answers the CSI ? u query
|
|
33
|
+
this.cellSize = null; // [w, h] chars + pixels from CSI 6/4 t replies
|
|
34
|
+
this.pixelSize = null; // [w, h] pixels
|
|
33
35
|
this.handoff = process.env.DSH_TUI_RESTART_HANDOFF === "1";
|
|
34
36
|
this.escTimer = null; // pending lone-ESC fallback (split-sequence safety)
|
|
35
37
|
this.decoder = new StringDecoder("utf8");
|
|
@@ -58,6 +60,8 @@ export class Term {
|
|
|
58
60
|
o.write("\x1b[?2004h"); // bracketed paste
|
|
59
61
|
o.write("\x1b[?7l"); // no autowrap (we clip ourselves)
|
|
60
62
|
if (this.kitty) { o.write("\x1b[>1u"); o.write("\x1b[?u"); }
|
|
63
|
+
// Cell/pixel geometry probes (XTerm rep): CSI 14 t → pixels, CSI 16 t → chars.
|
|
64
|
+
o.write("\x1b[14t\x1b[16t");
|
|
61
65
|
this.resizeHandler = () => this.#resize();
|
|
62
66
|
process.on("SIGWINCH", this.resizeHandler);
|
|
63
67
|
this.#resize();
|
|
@@ -285,6 +289,23 @@ export class Term {
|
|
|
285
289
|
return; // other private responses (cursor pos) stay ignored
|
|
286
290
|
}
|
|
287
291
|
if (prefix === ">") return;
|
|
292
|
+
if (prefix === "" && (final === "t") && params.startsWith("4;")) {
|
|
293
|
+
const [, ph, pw] = params.split(";").map(Number);
|
|
294
|
+
if (ph && pw) this.pixelSize = [pw, ph];
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
if (prefix === "" && final === "t" && params.startsWith("6;")) {
|
|
298
|
+
const [, chh, chw] = params.split(";").map(Number);
|
|
299
|
+
if (chh && chw) {
|
|
300
|
+
this.cellSize = [chw, chh];
|
|
301
|
+
if (this.pixelSize) {
|
|
302
|
+
// exact cell aspect ratio (pixels per char) — 实际几何而非假设 2:1
|
|
303
|
+
const [pw, ph] = this.pixelSize, [cw, ch] = this.cellSize;
|
|
304
|
+
this.cellAspect = { w: pw / cw, h: ph / ch, ratio: (pw / cw) / (ph / ch) };
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
288
309
|
if (final === "Z") { // Shift+Tab (backtab)
|
|
289
310
|
this.#emit({ type: "key", name: "backtab", ctrl: false, alt: false, shift: true });
|
|
290
311
|
return;
|