dsh-neotui 0.0.33 → 0.1.0
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/md.js +15 -10
- package/src/theme.js +1 -1
- package/src/views.js +62 -9
package/package.json
CHANGED
package/src/md.js
CHANGED
|
@@ -252,26 +252,31 @@ export function renderMd(text, width, sink = null, opts = {}) {
|
|
|
252
252
|
const lang = inCode || "text";
|
|
253
253
|
const hw = Math.max(2, width - 4);
|
|
254
254
|
const codeLines = codeBuf.length === 0 ? [""] : codeBuf;
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
//
|
|
258
|
-
//
|
|
259
|
-
// to the corner looked like a rendering artifact)
|
|
255
|
+
if (sink?.codeBlocks) sink.codeBlocks.push({ text: codeBuf.join("\n"), lineIdx: lines.length, lang });
|
|
256
|
+
// Fixed-width box: EVERY row is exactly (hw + 4) columns wide —
|
|
257
|
+
// top `┌…[复制]…┐`, content `│ … │`, bottom `└…┘` — so the corners
|
|
258
|
+
// always sit above the vertical bars, never above the code text.
|
|
260
259
|
const btn = "[复制]";
|
|
261
260
|
const btnW = strWidth(btn);
|
|
262
|
-
const
|
|
261
|
+
const inner = hw + 2; // columns between the corners
|
|
262
|
+
const leftLen = Math.max(1, inner - btnW - 2);
|
|
263
263
|
const tag = lang && lang !== "text" ? " " + lang : "";
|
|
264
|
-
if (sink?.codeBlocks) sink.codeBlocks.push({ text: codeBuf.join("\n"), lineIdx: lines.length, lang });
|
|
265
264
|
lines.push([
|
|
266
|
-
SEG("┌" + "─".repeat(
|
|
265
|
+
SEG("┌" + "─".repeat(leftLen) + " ", { fg: C.hr }),
|
|
267
266
|
SEG(btn, { fg: C.link, bold: true, copyCode: codeBuf.join("\n") }),
|
|
268
267
|
SEG(" ┐" + tag, { fg: C.hr }),
|
|
269
268
|
]);
|
|
270
269
|
for (const cl of codeLines) {
|
|
271
270
|
const hls = highlightLine(cl, lang);
|
|
272
|
-
|
|
271
|
+
for (const row of wrapSegs(hls, hw)) {
|
|
272
|
+
const rowW = strWidth(row.map((g) => g.t ?? "").join(""));
|
|
273
|
+
const segs = [SEG("│ ", { fg: C.hr }), ...row];
|
|
274
|
+
if (rowW < hw) segs.push(SEG(" ".repeat(hw - rowW)));
|
|
275
|
+
segs.push(SEG(" │", { fg: C.hr }));
|
|
276
|
+
lines.push(segs);
|
|
277
|
+
}
|
|
273
278
|
}
|
|
274
|
-
lines.push([SEG("└" + "─".repeat(
|
|
279
|
+
lines.push([SEG("└" + "─".repeat(inner) + "┘", { fg: C.hr })]);
|
|
275
280
|
inCode = null;
|
|
276
281
|
}
|
|
277
282
|
i++;
|
package/src/theme.js
CHANGED
package/src/views.js
CHANGED
|
@@ -566,7 +566,7 @@ export class ChatView extends Widget {
|
|
|
566
566
|
this.expandedTools = new Set();
|
|
567
567
|
this.collapsedBlocks = new Set(); // per-block COLLAPSE (default expanded): `${realIdx}:${bi}`
|
|
568
568
|
this.thinkMode = "expanded"; // think blocks: expanded by default (t toggles)
|
|
569
|
-
this.bashMode = "
|
|
569
|
+
this.bashMode = "collapsed"; // tool blocks: collapsed by default (b toggles)
|
|
570
570
|
this.todosVisible = true; // todo block above the input (Shift+T toggles)
|
|
571
571
|
this.todoSeen = false; // once seen, the todo box keeps its height
|
|
572
572
|
this.running = false;
|
|
@@ -897,7 +897,20 @@ export class ChatView extends Widget {
|
|
|
897
897
|
// the segment under the cursor at PRESS time (code-block [复制] hit test):
|
|
898
898
|
// re-resolving it at release would let a streaming rebuild move the line
|
|
899
899
|
const pressSeg = pressY !== null && pressY !== undefined && pressY >= 0 ? this.#segAtLine(pressY, pressX) : null;
|
|
900
|
-
|
|
900
|
+
// press-time BLOCK signature: the streaming tail re-derives between press
|
|
901
|
+
// and release (syncTail replaces the block objects), so the positional
|
|
902
|
+
// nodeIdx:blockIdx key can drift; the signature lets release re-locate
|
|
903
|
+
// the same block by content instead.
|
|
904
|
+
let pressSig = null;
|
|
905
|
+
if (info && info.blockIdx !== null) {
|
|
906
|
+
const b = this.nodes[info.nodeIdx]?.blocks?.[info.blockIdx];
|
|
907
|
+
if (b) pressSig = {
|
|
908
|
+
nodeId: this.nodes[info.nodeIdx]?.id ?? null,
|
|
909
|
+
kind: b.kind,
|
|
910
|
+
prefix: String(b.text ?? b.args ?? "").slice(0, 40),
|
|
911
|
+
};
|
|
912
|
+
}
|
|
913
|
+
return { lineKey, topKey, topFirst, topOffset, match, firstNonEmpty, preHeaderIdx, preHeaderRow, pressY, pressRow, pressX, pressSeg, pressSig };
|
|
901
914
|
}
|
|
902
915
|
|
|
903
916
|
/** The line-segment under a screen x on a rendered line (for the code
|
|
@@ -924,9 +937,9 @@ export class ChatView extends Widget {
|
|
|
924
937
|
const ref = node?.images?.[info.imgIdx];
|
|
925
938
|
if (ref) { this.app.openImage(ref, { all: node.images, index: info.imgIdx }); return true; }
|
|
926
939
|
}
|
|
927
|
-
|
|
940
|
+
let node = this.nodes[info.nodeIdx];
|
|
928
941
|
if (!node) return false;
|
|
929
|
-
const { lineKey, topKey, topFirst, topOffset, match, firstNonEmpty, preHeaderIdx, preHeaderRow, pressY, pressRow, pressX, pressSeg } = ctx ?? this.#anchorCtx(info);
|
|
942
|
+
const { lineKey, topKey, topFirst, topOffset, match, firstNonEmpty, preHeaderIdx, preHeaderRow, pressY, pressRow, pressX, pressSeg, pressSig } = ctx ?? this.#anchorCtx(info);
|
|
930
943
|
// code block [复制] button: copy the raw code, no toggle (hit identity
|
|
931
944
|
// and segment locked at press time)
|
|
932
945
|
if (pressSeg?.copyCode) {
|
|
@@ -934,6 +947,31 @@ export class ChatView extends Widget {
|
|
|
934
947
|
this.app.toast("已复制代码块");
|
|
935
948
|
return true;
|
|
936
949
|
}
|
|
950
|
+
// The stream re-derives between press and release: if the positional
|
|
951
|
+
// block no longer matches the press-time signature, re-locate the SAME
|
|
952
|
+
// block by kind + content prefix (node id when the node carries one).
|
|
953
|
+
if (pressSig && info.blockIdx !== null) {
|
|
954
|
+
const cur = node.blocks?.[info.blockIdx];
|
|
955
|
+
const same = cur && cur.kind === pressSig.kind && String(cur.text ?? cur.args ?? "").slice(0, 40) === pressSig.prefix;
|
|
956
|
+
if (!same) {
|
|
957
|
+
let found = null;
|
|
958
|
+
for (let ni = 0; ni < this.nodes.length && !found; ni++) {
|
|
959
|
+
const n = this.nodes[ni];
|
|
960
|
+
if (pressSig.nodeId && n?.id && n.id !== pressSig.nodeId) continue;
|
|
961
|
+
for (let bi = 0; bi < (n?.blocks ?? []).length; bi++) {
|
|
962
|
+
const b = n.blocks[bi];
|
|
963
|
+
if (b.kind === pressSig.kind && String(b.text ?? b.args ?? "").slice(0, 40) === pressSig.prefix) {
|
|
964
|
+
found = { nodeIdx: ni, blockIdx: bi };
|
|
965
|
+
break;
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
if (found) {
|
|
970
|
+
info = { ...info, ...found };
|
|
971
|
+
node = this.nodes[found.nodeIdx];
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
}
|
|
937
975
|
// formal text blocks are NOT collapsible: clicking them is a no-op
|
|
938
976
|
if (node.kind === "assistant" && info.blockIdx !== null && node.blocks[info.blockIdx]?.kind === "text") {
|
|
939
977
|
return true;
|
|
@@ -1222,18 +1260,33 @@ export class ChatView extends Widget {
|
|
|
1222
1260
|
beginCard("CARD");
|
|
1223
1261
|
// FORMAL text output is NOT collapsible — the user's message
|
|
1224
1262
|
// content must stay readable; only think/tool blocks fold.
|
|
1225
|
-
//
|
|
1263
|
+
// The 🐳 marker distinguishes formal output from 💭 think and
|
|
1264
|
+
// ▸ tool blocks at a glance. Code blocks inside render as boxes
|
|
1265
|
+
// with a [复制] button.
|
|
1226
1266
|
const key = `${realIdx}:${bi}`;
|
|
1227
1267
|
const text = b.text ?? "";
|
|
1228
1268
|
const mdW = Math.max(10, w - 6 - strWidth(stepTag));
|
|
1229
1269
|
const sink = { codeBlocks: [] };
|
|
1230
1270
|
const md = renderMd(text, mdW, sink);
|
|
1271
|
+
const whale = { t: " 🐳", fg: K.ACCENT, bold: true };
|
|
1272
|
+
const step = { t: stepTag || " ", fg: K.FAINT };
|
|
1231
1273
|
if (md.length > 0) {
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1274
|
+
// When the message STARTS with a code box, the whale+step
|
|
1275
|
+
// marker gets its own line so the box's top border keeps the
|
|
1276
|
+
// same indent as its content rows (corners over bars, not
|
|
1277
|
+
// shifted right by the marker).
|
|
1278
|
+
const firstIsBoxTop = md[0].some((g) => g.copyCode);
|
|
1279
|
+
if (firstIsBoxTop) {
|
|
1280
|
+
lines.push([whale, step]);
|
|
1281
|
+
mark(realIdx, bi);
|
|
1282
|
+
for (const ln of md) { lines.push([{ t: " " }, ...ln]); mark(realIdx, bi); }
|
|
1283
|
+
} else {
|
|
1284
|
+
lines.push([whale, step, ...md[0]]);
|
|
1285
|
+
mark(realIdx, bi);
|
|
1286
|
+
for (const ln of md.slice(1)) { lines.push([{ t: " " }, ...ln]); mark(realIdx, bi); }
|
|
1287
|
+
}
|
|
1235
1288
|
} else {
|
|
1236
|
-
lines.push([
|
|
1289
|
+
lines.push([whale, step]);
|
|
1237
1290
|
mark(realIdx, bi);
|
|
1238
1291
|
}
|
|
1239
1292
|
sep();
|