dsh-neotui 0.1.5 → 0.1.6
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/panels.js +7 -2
- package/src/views.js +22 -11
- package/src/widgets.js +51 -5
package/package.json
CHANGED
package/src/panels.js
CHANGED
|
@@ -1218,6 +1218,7 @@ export class ControlPanel extends Widget {
|
|
|
1218
1218
|
["G", "滚动到底", () => { this.app.closeOverlay(); this.app.chat.view.scrollY = this.app.chat.view.maxScroll(); }],
|
|
1219
1219
|
["[", "上一提问的终点", () => { this.app.closeOverlay(); this.app.focus(this.app.chat); this.app.chat.onKey({ type: "key", name: "char", key: "[", text: "[", ctrl: false, alt: false, shift: false }); }],
|
|
1220
1220
|
["]", "下一提问的终点", () => { this.app.closeOverlay(); this.app.focus(this.app.chat); this.app.chat.onKey({ type: "key", name: "char", key: "]", text: "]", ctrl: false, alt: false, shift: false }); }],
|
|
1221
|
+
["Ctrl+L", "输入栏 展开/折叠", () => { this.app.closeOverlay(); this.app.focus(this.app.chat.input); this.app.chat.input.onKey({ type: "key", name: "char", key: "l", text: "l", ctrl: true, alt: false, shift: false }); }],
|
|
1221
1222
|
["Ctrl+P", "控制面板", () => { this.page = 1; this.sel = 0; this.app.redraw(); }],
|
|
1222
1223
|
["Ctrl+M", "切换模型", () => { this.app.overlay = buildModelPicker(this.app); }],
|
|
1223
1224
|
["Ctrl+T", "轨迹视图", () => { this.app.closeOverlay(); this.app.setMode("trajectory"); }],
|
|
@@ -1408,16 +1409,20 @@ export class JobsPanel extends Popup {
|
|
|
1408
1409
|
#detailLines(j) {
|
|
1409
1410
|
// Expanded = EVERYTHING: every field, full values — long commands wrap
|
|
1410
1411
|
// across lines instead of being truncated (the web clips them; we don't).
|
|
1412
|
+
// `label` carries the full command, so it is shown here too (the header
|
|
1413
|
+
// row keeps only a 36-column preview of it).
|
|
1414
|
+
const names = { label: "命令", detail: "结果", startedAt: "开始于", finishedAt: "结束于" };
|
|
1411
1415
|
const lines = [];
|
|
1412
1416
|
const budget = Math.max(20, this.w - 10);
|
|
1413
1417
|
for (const [k, v] of Object.entries(j)) {
|
|
1414
|
-
if (["status", "kind"
|
|
1418
|
+
if (["status", "kind"].includes(k)) continue;
|
|
1415
1419
|
const s = v !== null && typeof v === "object" ? JSON.stringify(v) : String(v ?? "");
|
|
1416
1420
|
if (s === "") continue;
|
|
1421
|
+
const key = names[k] ?? k;
|
|
1417
1422
|
let rest = s;
|
|
1418
1423
|
let first = true;
|
|
1419
1424
|
while (rest.length > 0 || first) {
|
|
1420
|
-
const head = first ? `${
|
|
1425
|
+
const head = first ? `${key}: ` : " ";
|
|
1421
1426
|
const take = JobsPanel.#cutWidth(rest, budget - strWidth(head));
|
|
1422
1427
|
lines.push([{ t: ` ${head}${take}`, fg: K.DIM }]);
|
|
1423
1428
|
rest = rest.slice(take.length);
|
package/src/views.js
CHANGED
|
@@ -580,9 +580,9 @@ export class ChatView extends Widget {
|
|
|
580
580
|
});
|
|
581
581
|
this.input = new Input({
|
|
582
582
|
x: this.x, y: this.y + this.h - 2, w: this.w, h: 1,
|
|
583
|
-
multi: true, maxLines: 6,
|
|
583
|
+
multi: true, maxLines: 6, app: this.app,
|
|
584
584
|
bg: T.PANEL,
|
|
585
|
-
placeholder: "输入消息…(Shift+Enter
|
|
585
|
+
placeholder: "输入消息…(Shift+Enter/Ctrl+J 换行,Ctrl+L 展开,Enter 发送)",
|
|
586
586
|
onEnter: (v) => this.send(v),
|
|
587
587
|
onChange: () => this.inputChanged(),
|
|
588
588
|
});
|
|
@@ -731,10 +731,12 @@ export class ChatView extends Widget {
|
|
|
731
731
|
|
|
732
732
|
inputChanged() {
|
|
733
733
|
// Multi-line input grew/shrunk → reflow view vs input, keep the tail visible.
|
|
734
|
-
|
|
734
|
+
// The expanded (Ctrl+L) input may claim the whole window: clamp so the
|
|
735
|
+
// scroll view keeps at least one row.
|
|
736
|
+
const th = this.todoHeight();
|
|
737
|
+
const ih = Math.min(this.input.height(), Math.max(1, this.h - th - 2));
|
|
735
738
|
const prevIh = this.input.h;
|
|
736
739
|
this.input.h = ih;
|
|
737
|
-
const th = this.todoHeight();
|
|
738
740
|
this.view.h = this.h - ih - th - 1;
|
|
739
741
|
this.input.y = this.y + this.h - ih;
|
|
740
742
|
if (ih !== prevIh) this.app.layout();
|
|
@@ -743,9 +745,9 @@ export class ChatView extends Widget {
|
|
|
743
745
|
|
|
744
746
|
resize(x, y, w, h) {
|
|
745
747
|
this.x = x; this.y = y; this.w = w; this.h = h;
|
|
746
|
-
const ih = this.input.height();
|
|
747
|
-
this.input.h = ih;
|
|
748
748
|
const th = this.todoHeight();
|
|
749
|
+
const ih = Math.min(this.input.height(), Math.max(1, h - th - 2));
|
|
750
|
+
this.input.h = ih;
|
|
749
751
|
this.view.x = x; this.view.y = y; this.view.w = w; this.view.h = h - ih - th - 1;
|
|
750
752
|
this.input.x = x; this.input.y = y + h - ih; this.input.w = w;
|
|
751
753
|
this.cache.clear();
|
|
@@ -1970,10 +1972,16 @@ export class App {
|
|
|
1970
1972
|
this.model = host.model ?? "";
|
|
1971
1973
|
} catch (e) { this.log(`[app] host.describe: ${e.message}`); }
|
|
1972
1974
|
await this.refreshSessions();
|
|
1973
|
-
//
|
|
1974
|
-
//
|
|
1975
|
-
//
|
|
1976
|
-
|
|
1975
|
+
// A /restart handoff carries the session to reopen: resume it instead of
|
|
1976
|
+
// minting a fresh blank session (which was leaking a stray "new session"
|
|
1977
|
+
// into 未分组 on every restart).
|
|
1978
|
+
const resumeId = process.env.DSH_TUI_RESUME_SESSION;
|
|
1979
|
+
if (resumeId && this.sessions.some((s) => s.sessionId === resumeId)) {
|
|
1980
|
+
await this.openSession(resumeId);
|
|
1981
|
+
} else if (!this.currentSession) {
|
|
1982
|
+
// Open on a blank session at the launch directory (reusing an existing
|
|
1983
|
+
// draft when available) so the blank-session homepage shows immediately
|
|
1984
|
+
// instead of a fully empty chat area.
|
|
1977
1985
|
await this.newSessionIn(null);
|
|
1978
1986
|
}
|
|
1979
1987
|
this.api.connectMux();
|
|
@@ -2488,7 +2496,10 @@ export class App {
|
|
|
2488
2496
|
try {
|
|
2489
2497
|
const { spawn } = await import("node:child_process");
|
|
2490
2498
|
const argv = process.argv.slice(1);
|
|
2491
|
-
|
|
2499
|
+
// hand the CURRENT session to the new instance so it reopens it instead
|
|
2500
|
+
// of minting a fresh blank session (the "strange new session" in 未分组)
|
|
2501
|
+
const env = { ...process.env, DSH_TUI_RESUME_SESSION: this.currentSession ?? "" };
|
|
2502
|
+
const child = spawn("sh", ["-c", 'sleep 1; exec "$@"', "sh", ...argv], { detached: true, stdio: "inherit", env });
|
|
2492
2503
|
child.unref();
|
|
2493
2504
|
} catch (e) {
|
|
2494
2505
|
this.toast(`重启失败: ${e.message}(请手动重启)`);
|
package/src/widgets.js
CHANGED
|
@@ -253,6 +253,11 @@ export class Input extends Widget {
|
|
|
253
253
|
this.border = opts.border ?? T.BORDER2;
|
|
254
254
|
this.multi = opts.multi ?? false;
|
|
255
255
|
this.maxLines = opts.maxLines ?? 6;
|
|
256
|
+
this.baseMaxLines = this.maxLines; // restored when the input collapses
|
|
257
|
+
this.expanded = false; // Ctrl+L: drop the line cap and fill the window
|
|
258
|
+
this.app = opts.app ?? null;
|
|
259
|
+
this.pendingPaste = null; // large-paste stage 1: held-back clipboard text
|
|
260
|
+
this.pendingPlaceholder = null; // the "[已复制 N 行内容]" marker currently shown
|
|
256
261
|
this.onChange = opts.onChange ?? null;
|
|
257
262
|
this.allowEmptyEnter = opts.allowEmptyEnter ?? false;
|
|
258
263
|
this.history = [];
|
|
@@ -313,6 +318,7 @@ export class Input extends Widget {
|
|
|
313
318
|
/** Rendered height: 1, or wrapped rows capped at maxLines when multi. */
|
|
314
319
|
height() { return this.multi ? Math.max(1, Math.min(this.maxLines, this.#visualRows().length)) : 1; }
|
|
315
320
|
setValue(v, opts = {}) {
|
|
321
|
+
this.#touch();
|
|
316
322
|
this.value = String(v);
|
|
317
323
|
this.cursor = this.#cps().length;
|
|
318
324
|
this.selectAll = Boolean(opts.select); // first insert/text replaces the whole value
|
|
@@ -416,15 +422,45 @@ export class Input extends Widget {
|
|
|
416
422
|
}
|
|
417
423
|
return false;
|
|
418
424
|
}
|
|
425
|
+
/** Any manual edit cancels the held-back paste (the placeholder stays as
|
|
426
|
+
* ordinary text). */
|
|
427
|
+
#touch() { this.pendingPaste = null; this.pendingPlaceholder = null; }
|
|
428
|
+
/** Claude-Code-style two-stage paste: the first Ctrl+Shift+V of a large
|
|
429
|
+
* clipboard shows a "[已复制 N 行内容]" placeholder; pasting the same
|
|
430
|
+
* content again inserts it in full, like a normal paste. */
|
|
431
|
+
#paste(text) {
|
|
432
|
+
text = String(text ?? "");
|
|
433
|
+
const large = text.includes("\n") || text.length > 300;
|
|
434
|
+
if (large) {
|
|
435
|
+
if (this.pendingPaste && this.pendingPaste.text === text) {
|
|
436
|
+
const full = this.pendingPaste.text;
|
|
437
|
+
const placeholder = this.pendingPlaceholder;
|
|
438
|
+
this.#touch();
|
|
439
|
+
if (this.value === placeholder) this.setValue(full);
|
|
440
|
+
else this.insert(full);
|
|
441
|
+
this.app?.toast?.("已粘贴完整内容");
|
|
442
|
+
return true;
|
|
443
|
+
}
|
|
444
|
+
const lines = text.split("\n").length;
|
|
445
|
+
this.pendingPaste = { text };
|
|
446
|
+
this.pendingPlaceholder = `[已复制 ${lines} 行内容]`;
|
|
447
|
+
this.insert(this.pendingPlaceholder);
|
|
448
|
+
this.app?.toast?.("再次 Ctrl+Shift+V 粘贴完整内容(Ctrl+L 展开输入栏)");
|
|
449
|
+
return true;
|
|
450
|
+
}
|
|
451
|
+
this.#touch();
|
|
452
|
+
this.insert(text);
|
|
453
|
+
return true;
|
|
454
|
+
}
|
|
419
455
|
onKey(ev) {
|
|
420
|
-
if (ev.type === "text") { this
|
|
456
|
+
if (ev.type === "text") { return this.#paste(ev.text ?? ""); }
|
|
421
457
|
if (ev.type !== "key") return false;
|
|
422
458
|
switch (ev.name) {
|
|
423
459
|
case "backspace":
|
|
424
|
-
if (this.cursor > 0) { this.#deleteAt(this.cursor - 1); this.cursor--; this.onChange?.(); }
|
|
460
|
+
if (this.cursor > 0) { this.#touch(); this.#deleteAt(this.cursor - 1); this.cursor--; this.onChange?.(); }
|
|
425
461
|
return true;
|
|
426
462
|
case "delete":
|
|
427
|
-
if (this.cursor < this.#cps().length) { this.#deleteAt(this.cursor); this.onChange?.(); }
|
|
463
|
+
if (this.cursor < this.#cps().length) { this.#touch(); this.#deleteAt(this.cursor); this.onChange?.(); }
|
|
428
464
|
return true;
|
|
429
465
|
case "left": this.selectAll = false; this.cursor = Math.max(0, this.cursor - 1); return true;
|
|
430
466
|
case "right": this.selectAll = false; this.cursor = Math.min(this.#cps().length, this.cursor + 1); return true;
|
|
@@ -463,8 +499,17 @@ export class Input extends Widget {
|
|
|
463
499
|
if (ev.ctrl) {
|
|
464
500
|
switch (ev.key) {
|
|
465
501
|
case "j": if (this.multi) { this.insert("\n"); return true; } return false;
|
|
466
|
-
case "u": this.value = this.#cps().slice(this.cursor).join(""); this.cursor = 0; this.onChange?.(); return true;
|
|
467
|
-
case "k": this.value = this.#cps().slice(0, this.cursor).join(""); this.onChange?.(); return true;
|
|
502
|
+
case "u": this.#touch(); this.value = this.#cps().slice(this.cursor).join(""); this.cursor = 0; this.onChange?.(); return true;
|
|
503
|
+
case "k": this.#touch(); this.value = this.#cps().slice(0, this.cursor).join(""); this.onChange?.(); return true;
|
|
504
|
+
case "l": {
|
|
505
|
+
// expand/collapse the input: expanded drops the 6-line cap and
|
|
506
|
+
// lets the editor fill the window above the input
|
|
507
|
+
this.expanded = !this.expanded;
|
|
508
|
+
this.maxLines = this.expanded ? 1000 : this.baseMaxLines;
|
|
509
|
+
this.onChange?.();
|
|
510
|
+
this.app?.toast?.(this.expanded ? "输入栏已展开(Ctrl+L 折叠)" : "输入栏已折叠(Ctrl+L 展开)");
|
|
511
|
+
return true;
|
|
512
|
+
}
|
|
468
513
|
case "a": this.cursor = 0; return true;
|
|
469
514
|
case "e": this.cursor = this.#cps().length; return true;
|
|
470
515
|
case "w": {
|
|
@@ -481,6 +526,7 @@ export class Input extends Widget {
|
|
|
481
526
|
}
|
|
482
527
|
return false;
|
|
483
528
|
}
|
|
529
|
+
this.#touch();
|
|
484
530
|
this.insert(ev.text);
|
|
485
531
|
return true;
|
|
486
532
|
case "enter":
|