dsh-neotui 0.1.13 → 0.1.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/config.js +11 -0
- package/src/panels.js +34 -3
- package/src/text.js +14 -0
- package/src/views.js +87 -18
package/package.json
CHANGED
package/src/config.js
CHANGED
|
@@ -62,3 +62,14 @@ export function userName() {
|
|
|
62
62
|
export function userPrefix() {
|
|
63
63
|
return `${userName()} > `;
|
|
64
64
|
}
|
|
65
|
+
|
|
66
|
+
/** Fold defaults (settings → 默认展开/折叠): think/tool blocks and the
|
|
67
|
+
* todo list, with the shipped defaults when nothing is configured. */
|
|
68
|
+
export function foldDefaults() {
|
|
69
|
+
const fd = loadTuiConfig().foldDefaults ?? {};
|
|
70
|
+
return {
|
|
71
|
+
think: fd.think !== false, // think blocks default expanded
|
|
72
|
+
bash: fd.bash === true, // tool blocks default collapsed
|
|
73
|
+
todos: fd.todos !== false, // todo list default visible
|
|
74
|
+
};
|
|
75
|
+
}
|
package/src/panels.js
CHANGED
|
@@ -10,7 +10,7 @@ import { join, basename, extname } from "node:path";
|
|
|
10
10
|
import { spawn, execFileSync } from "node:child_process";
|
|
11
11
|
|
|
12
12
|
import { T, cycleTheme, themeName } from "./theme.js";
|
|
13
|
-
import { loadTuiConfig, saveTuiConfig, userPrefix, userName } from "./config.js";
|
|
13
|
+
import { loadTuiConfig, saveTuiConfig, userPrefix, userName, foldDefaults } from "./config.js";
|
|
14
14
|
// Live theme accessor: K.K.DIM etc. resolve against the active palette at render time.
|
|
15
15
|
const K = new Proxy({}, { get(_k, key) { return T[key]; } });
|
|
16
16
|
|
|
@@ -950,10 +950,13 @@ export class TrajectoryPanel extends Widget {
|
|
|
950
950
|
lines.push(segs);
|
|
951
951
|
this.stepLines[lines.length - 1] = si;
|
|
952
952
|
if (open) {
|
|
953
|
-
// 详细 mode: the step's events inline under its color block
|
|
953
|
+
// 详细 mode: the step's events inline under its color block, each
|
|
954
|
+
// with its deep-dive elapsed time since the step started (web-style).
|
|
955
|
+
const t0 = step.events[0]?.time;
|
|
954
956
|
const evs = step.events.slice(0, 12);
|
|
955
957
|
for (const e of evs) {
|
|
956
|
-
|
|
958
|
+
const dt = t0 != null && e.time != null ? ` +${fmtMs(e.time - t0)}` : "";
|
|
959
|
+
lines.push([{ t: ` #${String(e.seq).padStart(4)}${dt} ${truncate(this.#eventSummary(e), w - 12 - strWidth(dt))}`, fg: K.DIM, bg }]);
|
|
957
960
|
this.stepLines[lines.length - 1] = si;
|
|
958
961
|
}
|
|
959
962
|
if (step.events.length > evs.length) {
|
|
@@ -1588,6 +1591,13 @@ export class SettingsPanel extends Widget {
|
|
|
1588
1591
|
ns: "TUI 界面", applies: "live", local: true,
|
|
1589
1592
|
value: { userPrefix: userName() },
|
|
1590
1593
|
});
|
|
1594
|
+
// 默认展开/折叠: the fold-related defaults as a local sub-panel of
|
|
1595
|
+
// booleans (click to toggle), persisted to the TUI config file.
|
|
1596
|
+
const fd = foldDefaults();
|
|
1597
|
+
this.namespaces.splice(1, 0, {
|
|
1598
|
+
ns: "默认展开/折叠", applies: "live", local: true,
|
|
1599
|
+
value: { 思考块默认展开: fd.think, 工具块默认展开: fd.bash, 任务清单默认显示: fd.todos },
|
|
1600
|
+
});
|
|
1591
1601
|
this.selectNs(0);
|
|
1592
1602
|
}
|
|
1593
1603
|
selectNs(i) {
|
|
@@ -1716,6 +1726,27 @@ export class SettingsPanel extends Widget {
|
|
|
1716
1726
|
if (ns.local) {
|
|
1717
1727
|
// TUI-local config: write the config file, apply instantly.
|
|
1718
1728
|
const v = applyOps(ns.value, this.pendingOps);
|
|
1729
|
+
if (ns.ns === "默认展开/折叠") {
|
|
1730
|
+
const patch = { foldDefaults: { think: !!v.思考块默认展开, bash: !!v.工具块默认展开, todos: !!v.任务清单默认显示 } };
|
|
1731
|
+
if (saveTuiConfig(patch)) {
|
|
1732
|
+
this.pendingOps = [];
|
|
1733
|
+
this.app.toast("已保存展开/折叠默认值(即时生效)");
|
|
1734
|
+
// apply live to the current chat
|
|
1735
|
+
const chat = this.app.chat;
|
|
1736
|
+
if (chat) {
|
|
1737
|
+
chat.thinkMode = v.思考块默认展开 ? "expanded" : "collapsed";
|
|
1738
|
+
chat.bashMode = v.工具块默认展开 ? "expanded" : "collapsed";
|
|
1739
|
+
chat.todosVisible = !!v.任务清单默认显示;
|
|
1740
|
+
chat.expanded.clear();
|
|
1741
|
+
chat.collapsedBlocks.clear();
|
|
1742
|
+
chat.queueRebuild();
|
|
1743
|
+
}
|
|
1744
|
+
await this.load();
|
|
1745
|
+
} else {
|
|
1746
|
+
this.app.toast("保存失败:无法写入 TUI 配置文件");
|
|
1747
|
+
}
|
|
1748
|
+
return;
|
|
1749
|
+
}
|
|
1719
1750
|
const name = String(v.userPrefix ?? "").trim();
|
|
1720
1751
|
if (saveTuiConfig({ userPrefix: name })) {
|
|
1721
1752
|
this.pendingOps = [];
|
package/src/text.js
CHANGED
|
@@ -64,6 +64,20 @@ export function fmtDuration(ms) {
|
|
|
64
64
|
return `${sec}秒`;
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
+
/** Local wall-clock "HH:MM:SS" (status-bar live clock). */
|
|
68
|
+
export function fmtClock(ms) {
|
|
69
|
+
const d = new Date(ms);
|
|
70
|
+
const p = (n) => String(n).padStart(2, "0");
|
|
71
|
+
return `${p(d.getHours())}:${p(d.getMinutes())}:${p(d.getSeconds())}`;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Local wall-clock "MM-DD HH:MM" (session start time). */
|
|
75
|
+
export function fmtDateTime(ms) {
|
|
76
|
+
const d = new Date(ms);
|
|
77
|
+
const p = (n) => String(n).padStart(2, "0");
|
|
78
|
+
return `${p(d.getMonth() + 1)}-${p(d.getDate())} ${p(d.getHours())}:${p(d.getMinutes())}`;
|
|
79
|
+
}
|
|
80
|
+
|
|
67
81
|
export function hexRgb(hex) {
|
|
68
82
|
const m = /^#?([0-9a-f]{6})$/i.exec(String(hex).trim());
|
|
69
83
|
if (!m) return null;
|
package/src/views.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
// views.js — App composition: session list + chat timeline + approvals + status.
|
|
2
2
|
import { Screen } from "./screen.js";
|
|
3
3
|
import { renderMd, C } from "./md.js";
|
|
4
|
-
import { truncate, strWidth, bars, fmtDuration } from "./text.js";
|
|
4
|
+
import { truncate, strWidth, bars, fmtDuration, fmtClock, fmtDateTime } from "./text.js";
|
|
5
5
|
import { readFileSync, appendFileSync, mkdirSync } from "node:fs";
|
|
6
6
|
import { join } from "node:path";
|
|
7
7
|
import { Widget, List, ScrollView, Input, Popup, Menu, StatusBar } from "./widgets.js";
|
|
8
|
-
import { userPrefix, saveTuiConfig, loadTuiConfig, userName } from "./config.js";
|
|
9
|
-
export { userPrefix, saveTuiConfig, loadTuiConfig, userName } from "./config.js";
|
|
8
|
+
import { userPrefix, saveTuiConfig, loadTuiConfig, userName, foldDefaults } from "./config.js";
|
|
9
|
+
export { userPrefix, saveTuiConfig, loadTuiConfig, userName, foldDefaults } from "./config.js";
|
|
10
10
|
import {
|
|
11
11
|
Picker, buildCommandPalette, buildModelPicker, buildModePicker, buildPermissionPicker,
|
|
12
12
|
modeName, permName, WorkspacePanel, TrajectoryPanel, DirPicker,
|
|
@@ -147,12 +147,17 @@ function applyEvent(nodes, event, view, log, state = null) {
|
|
|
147
147
|
else blocks.push({ kind: "other", text: JSON.stringify(p).slice(0, 500) });
|
|
148
148
|
}
|
|
149
149
|
// Finalization stamps: endedAt marks when the message completed. The
|
|
150
|
-
//
|
|
151
|
-
//
|
|
150
|
+
// final message REPLACES the chunk-built blocks, so carry their start
|
|
151
|
+
// times by position (same rule as syncTail's inheritStarts) —
|
|
152
|
+
// otherwise every reloaded think block loses 耗时 after completion.
|
|
153
|
+
const last = cur();
|
|
154
|
+
const prevBlocks = last && last.kind === "assistant" ? last.blocks ?? [] : [];
|
|
152
155
|
for (const b of blocks) b.endedAt = event.time ?? Date.now();
|
|
156
|
+
for (let bi = 0; bi < blocks.length; bi++) {
|
|
157
|
+
if (blocks[bi].startedAt === undefined && prevBlocks[bi]?.startedAt !== undefined) blocks[bi].startedAt = prevBlocks[bi].startedAt;
|
|
158
|
+
}
|
|
153
159
|
const images = partsToImages(d.message?.content);
|
|
154
160
|
const id = d.message?.id ?? null;
|
|
155
|
-
const last = cur();
|
|
156
161
|
if (last && last.kind === "assistant" && last.streaming !== false) {
|
|
157
162
|
last.blocks = blocks;
|
|
158
163
|
last.images = images ?? last.images;
|
|
@@ -224,13 +229,38 @@ function applyEvent(nodes, event, view, log, state = null) {
|
|
|
224
229
|
const callId = d.message?.source?.callId;
|
|
225
230
|
const text = partsToText(d.message?.content);
|
|
226
231
|
const node = nodes.findLast((nd) => nd.kind === "assistant" && nd.blocks.some((b) => b.kind === "tool" && b.callId === callId));
|
|
227
|
-
|
|
232
|
+
let block = node?.blocks.find((b) => b.kind === "tool" && b.callId === callId);
|
|
233
|
+
if (!block) {
|
|
234
|
+
// the callId did not match (missed tool/call in the loaded window):
|
|
235
|
+
// attach to the most recent tool block still awaiting a result —
|
|
236
|
+
// otherwise a SUCCESSFUL bash renders as a red "无结果" failure
|
|
237
|
+
outer:
|
|
238
|
+
for (let ni = nodes.length - 1; ni >= 0; ni--) {
|
|
239
|
+
const nd = nodes[ni];
|
|
240
|
+
if (nd.kind !== "assistant") continue;
|
|
241
|
+
for (const b of nd.blocks ?? []) {
|
|
242
|
+
if (b.kind === "tool" && b.result == null) { block = b; break outer; }
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
228
246
|
if (block) {
|
|
229
247
|
block.result = text ?? JSON.stringify(d).slice(0, 400);
|
|
230
248
|
block.endedAt = event.time ?? Date.now();
|
|
231
249
|
}
|
|
232
250
|
break;
|
|
233
251
|
}
|
|
252
|
+
case "turn/start": {
|
|
253
|
+
st.turnStart = event.time ?? Date.now();
|
|
254
|
+
break;
|
|
255
|
+
}
|
|
256
|
+
case "turn/end": {
|
|
257
|
+
const node = cur();
|
|
258
|
+
if (node?.kind === "assistant" && st.turnStart !== undefined) {
|
|
259
|
+
node.turnMs = Math.max(0, (event.time ?? Date.now()) - st.turnStart);
|
|
260
|
+
}
|
|
261
|
+
st.turnStart = null;
|
|
262
|
+
break;
|
|
263
|
+
}
|
|
234
264
|
case "step/end": {
|
|
235
265
|
const node = cur();
|
|
236
266
|
if (node && node.kind === "assistant") {
|
|
@@ -254,7 +284,7 @@ function applyEvent(nodes, event, view, log, state = null) {
|
|
|
254
284
|
}
|
|
255
285
|
default: {
|
|
256
286
|
// known benign control events: silently ignored
|
|
257
|
-
const KNOWN = new Set(["
|
|
287
|
+
const KNOWN = new Set(["todo/write",
|
|
258
288
|
"agent/inbox/spliced", "request/header", "request/context", "permission/preset",
|
|
259
289
|
"sandbox/mode", "approval/policy", "session/title-llm-request", "command/done", "command/failed"]);
|
|
260
290
|
if (!KNOWN.has(event.type) && !SEEN_TYPES.has(event.type)) {
|
|
@@ -266,7 +296,7 @@ function applyEvent(nodes, event, view, log, state = null) {
|
|
|
266
296
|
}
|
|
267
297
|
|
|
268
298
|
/** Re-derive the whole node list from a complete event window (open/poll). */
|
|
269
|
-
function nodeForEvents(events, log) {
|
|
299
|
+
export function nodeForEvents(events, log) {
|
|
270
300
|
const nodes = [];
|
|
271
301
|
const state = { step: null };
|
|
272
302
|
for (const { event, view } of events) applyEvent(nodes, event, view, log, state);
|
|
@@ -565,14 +595,16 @@ export class ChatView extends Widget {
|
|
|
565
595
|
this.expanded = new Set(); // node indexes (user-message full text)
|
|
566
596
|
this.expandedTools = new Set();
|
|
567
597
|
this.collapsedBlocks = new Set(); // per-block COLLAPSE (default expanded): `${realIdx}:${bi}`
|
|
568
|
-
|
|
569
|
-
this.
|
|
570
|
-
this.
|
|
598
|
+
const fd = foldDefaults();
|
|
599
|
+
this.thinkMode = fd.think ? "expanded" : "collapsed"; // t toggles
|
|
600
|
+
this.bashMode = fd.bash ? "expanded" : "collapsed"; // b toggles
|
|
601
|
+
this.todosVisible = fd.todos; // Shift+T toggles
|
|
571
602
|
this.todoSeen = false; // once seen, the todo box keeps its height
|
|
572
603
|
this.running = false;
|
|
573
604
|
this.hasMore = false;
|
|
574
605
|
this.loadingOlder = false;
|
|
575
606
|
this.minSeq = null;
|
|
607
|
+
this.earliestTime = null; // earliest loaded event time ≈ session start
|
|
576
608
|
this.view = new ScrollView({
|
|
577
609
|
x: this.x, y: this.y, w: this.w, h: this.h - 2,
|
|
578
610
|
autoScroll: true, title: "",
|
|
@@ -633,10 +665,22 @@ export class ChatView extends Widget {
|
|
|
633
665
|
|
|
634
666
|
/** Idempotently re-derive the tail node(s) from the complete last message.
|
|
635
667
|
* Dedup by message id so already-loaded nodes are updated, never duplicated. */
|
|
668
|
+
/** Track the earliest event time ever loaded — the session's start time
|
|
669
|
+
* (converges to the true start as older pages load). */
|
|
670
|
+
#noteEarliest(events) {
|
|
671
|
+
let t = Infinity;
|
|
672
|
+
for (const e of events ?? []) {
|
|
673
|
+
const et = e?.event?.time;
|
|
674
|
+
if (typeof et === "number" && et < t) t = et;
|
|
675
|
+
}
|
|
676
|
+
if (t !== Infinity && (this.earliestTime == null || t < this.earliestTime)) this.earliestTime = t;
|
|
677
|
+
}
|
|
678
|
+
|
|
636
679
|
syncTail(events) {
|
|
637
680
|
const maxSeq = events[events.length - 1]?.event?.seq ?? 0;
|
|
638
681
|
if (maxSeq <= (this.lastSyncedSeq ?? -1)) return;
|
|
639
682
|
this.lastSyncedSeq = maxSeq;
|
|
683
|
+
this.#noteEarliest(events);
|
|
640
684
|
const nodes = nodeForEvents(events, this.app.log);
|
|
641
685
|
const lastAssistant = [...nodes].reverse().find((n) => n.kind === "assistant");
|
|
642
686
|
if (!lastAssistant) {
|
|
@@ -771,6 +815,7 @@ export class ChatView extends Widget {
|
|
|
771
815
|
this.lastSyncedSeq = -1;
|
|
772
816
|
this.pollSlow = false;
|
|
773
817
|
this.hasMore = hist.hasMore;
|
|
818
|
+
this.#noteEarliest(hist.events);
|
|
774
819
|
this.nodes = nodeForEvents(hist.events, this.app.log);
|
|
775
820
|
this.title = hist.projections?.values?.title ?? this.title;
|
|
776
821
|
if (hist.projections?.values) {
|
|
@@ -797,6 +842,7 @@ export class ChatView extends Widget {
|
|
|
797
842
|
const before = this.lines.length;
|
|
798
843
|
this.minSeq = hist.events[0]?.event?.seq ?? this.minSeq;
|
|
799
844
|
this.hasMore = hist.hasMore;
|
|
845
|
+
this.#noteEarliest(hist.events);
|
|
800
846
|
const more = nodeForEvents(hist.events, this.app.log);
|
|
801
847
|
this.nodes = [...more, ...this.nodes];
|
|
802
848
|
}
|
|
@@ -1138,7 +1184,7 @@ export class ChatView extends Widget {
|
|
|
1138
1184
|
return ".";
|
|
1139
1185
|
}).join("")
|
|
1140
1186
|
: "";
|
|
1141
|
-
const ckey = `${realIdx}|${w}|${expKey}|${blockKeys}|${this.thinkMode}|${this.bashMode}|${node.streaming ? "s" : "f"}|${themeName()}|${node.step ?? "-"}|${userPrefix()}`;
|
|
1187
|
+
const ckey = `${realIdx}|${w}|${expKey}|${blockKeys}|${this.thinkMode}|${this.bashMode}|${node.streaming ? "s" : "f"}|${themeName()}|${node.step ?? "-"}|${userPrefix()}|${node.turnMs ?? "-"}`;
|
|
1142
1188
|
// Streaming nodes re-render every frame: their text grows without any
|
|
1143
1189
|
// change to the cache key, so caching them freezes the live think/tool/text.
|
|
1144
1190
|
const hit = node.streaming ? undefined : this.cache.get(ckey);
|
|
@@ -1256,23 +1302,25 @@ export class ChatView extends Widget {
|
|
|
1256
1302
|
// otherwise the timer runs forever ("timing chaos").
|
|
1257
1303
|
const running = b.result == null && !b.done && node.streaming;
|
|
1258
1304
|
const orphan = b.result == null && !b.done && !node.streaming;
|
|
1259
|
-
|
|
1260
|
-
|
|
1305
|
+
// An orphan (result never matched) is NOT a failure — it renders
|
|
1306
|
+
// neutral (◌, TOOLBG), never the red ✗ of a failed exit code.
|
|
1307
|
+
const failed = !orphan && exitCode !== undefined && exitCode !== 0;
|
|
1308
|
+
const status = running ? "TOOLBG" : failed ? "TOOLERR" : "TOOLOK";
|
|
1309
|
+
const glyph = running ? "⏳" : failed ? "✗" : orphan ? "◌" : "✓";
|
|
1261
1310
|
const card = b.view ? renderToolCard(b.view, w, open) : [];
|
|
1262
1311
|
beginCard(status);
|
|
1263
1312
|
let timing = "";
|
|
1264
1313
|
if (running) {
|
|
1265
1314
|
timing = ` 已经过 ${fmtDuration(Date.now() - (b.startedAt ?? Date.now()))}`;
|
|
1266
1315
|
} else if (b.startedAt !== undefined && b.endedAt !== undefined) {
|
|
1267
|
-
|
|
1268
|
-
timing = ` ${failed ? "失败" : "已完成"},耗时 ${fmtDuration(b.endedAt - b.startedAt)}`;
|
|
1316
|
+
timing = ` ${failed ? "失败" : orphan ? "无结果" : "已完成"},耗时 ${fmtDuration(b.endedAt - b.startedAt)}`;
|
|
1269
1317
|
} else if (orphan) {
|
|
1270
1318
|
timing = " 无结果";
|
|
1271
1319
|
}
|
|
1272
1320
|
lines.push([
|
|
1273
1321
|
{ t: open ? "▾ " : "▸ ", fg: K.ACCENT },
|
|
1274
1322
|
{ t: ` ${b.name ?? "tool"}`, fg: K.TXT, bold: true },
|
|
1275
|
-
{ t: ` ${glyph}`, fg:
|
|
1323
|
+
{ t: ` ${glyph}`, fg: failed ? K.ERR : status === "TOOLOK" ? K.OK : K.WARN },
|
|
1276
1324
|
{ t: stepTag + timing, fg: K.DIM },
|
|
1277
1325
|
{ t: open ? " [b 折叠]" : " [b 展开]", fg: K.FAINT },
|
|
1278
1326
|
]);
|
|
@@ -1348,6 +1396,11 @@ export class ChatView extends Widget {
|
|
|
1348
1396
|
sep();
|
|
1349
1397
|
}
|
|
1350
1398
|
}
|
|
1399
|
+
// the turn's FINAL reply carries the whole turn duration
|
|
1400
|
+
if (node.turnMs != null) {
|
|
1401
|
+
lines.push([{ t: ` 🕐 本轮回答总耗时 ${fmtDuration(node.turnMs)}`, fg: T.WARN, bold: true }]);
|
|
1402
|
+
mark(realIdx);
|
|
1403
|
+
}
|
|
1351
1404
|
break;
|
|
1352
1405
|
}
|
|
1353
1406
|
default:
|
|
@@ -1810,6 +1863,7 @@ export class App {
|
|
|
1810
1863
|
this.jobs = [];
|
|
1811
1864
|
this.jobsBySession = new Map(); // sessionId → latest session/jobs snapshot
|
|
1812
1865
|
this.ctrlCUntil = null; // NORMAL-mode double-Ctrl+C exit window
|
|
1866
|
+
this.lastSec = 0; // status-bar clock second pulse
|
|
1813
1867
|
this.focused = null;
|
|
1814
1868
|
this.provider = "";
|
|
1815
1869
|
this.model = "";
|
|
@@ -2990,6 +3044,9 @@ export class App {
|
|
|
2990
3044
|
this.renderFrame();
|
|
2991
3045
|
}
|
|
2992
3046
|
if (this.toastMsg && Date.now() > this.toastUntil) { this.toastMsg = null; this.dirty = true; }
|
|
3047
|
+
// the status-bar clock ticks once per second
|
|
3048
|
+
const sec = Math.floor(Date.now() / 1000);
|
|
3049
|
+
if (sec !== this.lastSec) { this.lastSec = sec; this.dirty = true; }
|
|
2993
3050
|
} catch (e) {
|
|
2994
3051
|
this.log("render error (kept running):", e);
|
|
2995
3052
|
// stderr is invisible under the alt screen — record the stack where
|
|
@@ -3056,6 +3113,16 @@ export class App {
|
|
|
3056
3113
|
if (this.sidebarVisible) row0.left.push({ t: " " + truncate(t || "(未选择会话)", 40) + " ", fg: T.TXT, bg: T.STATUSBG });
|
|
3057
3114
|
else row0.left.push({ t: " " + truncate(t || "(未选择会话)", 40) + " ", fg: T.TXT, bg: T.STATUSBG });
|
|
3058
3115
|
if (cur?.running) row0.left.push({ t: " ●运行 ", fg: T.OK, bg: T.STATUSBG });
|
|
3116
|
+
// session elapsed/start: effective time (model+tool work, not wall clock)
|
|
3117
|
+
// right after the session name; start = the earliest event time loaded
|
|
3118
|
+
{
|
|
3119
|
+
const stats = this.projections.sessionStats ?? cur?.projections?.values?.sessionStats;
|
|
3120
|
+
const startMs = this.chat?.earliestTime;
|
|
3121
|
+
const parts = [];
|
|
3122
|
+
if (stats && stats.llmMs != null) parts.push(`有效 ${fmtDuration(stats.llmMs + (stats.toolMs ?? 0))}`);
|
|
3123
|
+
if (startMs != null) parts.push(`开始 ${fmtDateTime(startMs)}`);
|
|
3124
|
+
if (parts.length) row0.left.push({ t: ` ${parts.join(" · ")} `, fg: T.DIM, bg: T.STATUSBG });
|
|
3125
|
+
}
|
|
3059
3126
|
if (this.goalText) row0.right.push({ t: " 🎯" + truncate(this.goalText, 22) + " ", fg: T.SELFG, bg: T.WARN, bold: true });
|
|
3060
3127
|
const plan = this.projections.plan;
|
|
3061
3128
|
if (plan?.active || plan?.pending) row0.right.push({ t: plan.active ? " ✎计划中 " : " ✎计划待审 ", fg: T.SELFG, bg: T.ACCENT2 });
|
|
@@ -3095,6 +3162,8 @@ export class App {
|
|
|
3095
3162
|
// full working directory
|
|
3096
3163
|
const cwd = this.currentSession ? (this.sessions.find((x) => x.sessionId === this.currentSession)?.cwd) : process.cwd();
|
|
3097
3164
|
if (cwd) row1.left.push({ t: ` ${cwd} `, fg: T.FAINT, bg: T.STATUSBG });
|
|
3165
|
+
// live clock after the working directory (ticks once per second)
|
|
3166
|
+
row1.left.push({ t: ` ${fmtClock(Date.now())} `, fg: T.DIM, bg: T.STATUSBG });
|
|
3098
3167
|
const stats = this.projections.sessionStats;
|
|
3099
3168
|
if (stats) {
|
|
3100
3169
|
if (stats.steps) row1.right.push({ t: ` ⚙${stats.steps}步 `, fg: T.FAINT, bg: T.STATUSBG });
|