dsh-neotui 0.4.1 → 0.4.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/CHANGELOG.md +6 -0
- package/README.md +2 -0
- package/bin/dsh-tui.js +5 -1
- package/package.json +1 -1
- package/src/ascii-logo.js +43 -0
- package/src/file-picker.js +11 -3
- package/src/index.js +1 -1
- package/src/keybindings.js +2 -1
- package/src/logo.js +43 -0
- package/src/panels.js +1 -1
- package/src/views.js +545 -62
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
|
|
4
|
+
### Added
|
|
5
|
+
|
|
6
|
+
- 欢迎页改版:**吉祥物 logo 默认关闭**(此前的 40×19 象限块/58×27 半块等实现保留,`Ctrl+R` 面板可随时启用预设/自定义/关闭;视觉迭代未能达成理想效果,先回退到仅标题)(真实色 255 色调色板,数据内嵌 `src/logo.js`,去背景、无残块、裁剪掉原图底部黑条);自定义 logo JSON 支持象限格式(`[mask,fg,bg]`)与旧半块格式(`[top,bottom]`);品牌行用 **`█▀▄` 半块像素字** 绘制(`DEEPSEEK` / `DSH NEOTUI` 两行大字标,5×7 像素字模),**每约 3.5 秒一道斜向高光扫过字标**(0.9s 扫程,纯 TUI 渲染并随主题语义色,扫过字标全宽含末尾 `I`;`DSH NEOTUI` 用深灰底保证白色高光可见);**logo 三态** `Ctrl+R` buffer:预设(内置)/ 自定义(复用 Ctrl+O 与新建工作区的 `UploadPicker` 单选 `{palette, grid}` 40×19 JSON)/ 关闭(仅标题),持久化到配置文件;版本更新提示收敛到底部(仅「可更新/失败/检查中」时显示,可点击检查,**最新则不提示**);模式选择从内联 4 列表收敛为底部提示「模式: <当前> · F9 打开模式选择(支持自定义)」——与 WebUI 鼓励自定义预设的方向一致;矮视口(<32 行或 <62 列)回退为普通文本品牌行。
|
|
7
|
+
- 启动动画(经典游戏启动器风格,**opt-in meme**):纯黑 → 白色从屏幕中心圆形扩散至全屏(1s)→ 四行健康游戏忠告逐句淡入(2s)→ 忠告淡出的同时 `DEEPSEEK` 半块大字交叉淡入(2s)→ 底部闪烁「按任意键进入游戏」,任意键进入主界面——全程纯 ANSI(`█▀▄`),无图片协议;默认关闭,`--launcher-anime`(或 `DSH_TUI_LAUNCHER_ANIME=1`)显式启用,`DSH_TUI_NO_SPLASH=1` 强制关闭;`--script` 模式不播放,窄小终端自动跳过。
|
|
8
|
+
|
|
3
9
|
## 0.4.1 — 2026-08-22
|
|
4
10
|
|
|
5
11
|
### Added
|
package/README.md
CHANGED
|
@@ -210,7 +210,9 @@ TUI 支持:
|
|
|
210
210
|
- 工具审批、AskUser 单选/多选、Plan Review;
|
|
211
211
|
- Goal、TODO、后台任务和 Subagent;
|
|
212
212
|
- 独立插件清单及 `/` 筛选;
|
|
213
|
+
- 空白会话欢迎页:**默认不显示吉祥物 logo**(`Ctrl+R` 打开 logo 面板:预设(40×19 象限块 80×38 真实色)/ 自定义 JSON / 关闭);品牌行用 **`█▀▄` 半块像素字**绘制(`DEEPSEEK` / `DSH NEOTUI` 两行大字标),**每约 3.5 秒一道斜向高光扫过字标**(纯 TUI 渲染,随主题语义色);**logo 三态**(`Ctrl+R` 打开 buffer):预设(内置)/ 自定义(Yazi 风格文件选择器单选 JSON,格式 `{palette, grid}` 40×19)/ 关闭(仅标题),选择持久化到配置文件;版本更新提示收敛到底部(`DeepSeek Harness 可更新 v…` / `dsh-neotui 可更新 v…`,可点击检查,**最新时不显示**);模式选择为底部提示(`F9 打开模式选择(支持自定义)`);矮视口(<29 行或 <60 列)回退为普通文本品牌行;
|
|
213
214
|
- 11 套配色主题:dark、light、gruvbox、nord、solarized-dark、solarized-light、dracula、onedark、catppuccin-mocha、tokyonight、monokai;`Ctrl+D`、`/theme` 或命令面板打开**配色选择器**——每行带该方案的色板(面板/用户/强调/成功/警告/错误六色),**光标移动即对整套 TUI 即时预览**(未持久化,标题栏显示「预览: <name>」),`Enter` 或双击应用并停留继续调整,`Esc` 恢复已提交主题并关闭;选择持久化到 `$DSH_HOME/tui-theme.txt`。
|
|
215
|
+
- 启动动画(经典游戏启动器风格,**默认关闭的 meme**):纯黑 → 白色从屏幕中心圆扩散到全屏(约 1s)→ 四行健康游戏忠告("抵制不良游戏,拒绝盗版游戏。"等,逐句淡入,约 2s)→ 忠告淡出同时 `DEEPSEEK` 半块大字交叉淡入(约 2s)→ 底部闪烁「按任意键进入游戏」,**按任意键进入**。全程纯 ANSI(`█▀▄`);`dsh-tui --launcher-anime`(或 `DSH_TUI_LAUNCHER_ANIME=1`)显式启用,`DSH_TUI_NO_SPLASH=1` 强制关闭,`--script` 模式不播放。
|
|
214
216
|
|
|
215
217
|
工作区(`Ctrl+W`)、设置(`Ctrl+S`)、模型供应商(模型选择器里的 `⚙ 管理供应商…`)、子代理(`Ctrl+A`)和技能(`Ctrl+H`)都是**全屏模态 Buffer**:打开后覆盖整个界面,`Esc` 逐级返回并最终关闭。它们不再占用“标签页模式”,因此关闭后 `Ctrl+←/→` 的 pane 聚焦立即恢复,两者互不冲突。轨迹仍是 pane 序列的一部分,由 `Ctrl+T` 或 `Ctrl+←/→` 进入。
|
|
216
218
|
|
package/bin/dsh-tui.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
// Usage:
|
|
4
4
|
// dsh-tui interactive mode (needs a real terminal)
|
|
5
5
|
// dsh-tui --base http://host:port
|
|
6
|
+
// dsh-tui --launcher-anime opt-in white fade-in boot animation
|
|
6
7
|
// dsh-tui --script <file> scripted mode: feed events from a file, dump frames
|
|
7
8
|
// dsh-tui --plain with --script: dump plain text frames (no ANSI)
|
|
8
9
|
import { Term, detectKitty } from "../src/term.js";
|
|
@@ -29,9 +30,12 @@ async function main() {
|
|
|
29
30
|
return;
|
|
30
31
|
}
|
|
31
32
|
// ---- interactive ----
|
|
33
|
+
// The boot animation is opt-in meme fuel: --launcher-anime enables it
|
|
34
|
+
// (DSH_TUI_LAUNCHER_ANIME=1 works too); it stays OFF by default.
|
|
35
|
+
const launcherAnime = has("--launcher-anime") || process.env.DSH_TUI_LAUNCHER_ANIME === "1";
|
|
32
36
|
const screen = new Screen(process.stdout.columns || 80, process.stdout.rows || 24);
|
|
33
37
|
const api = new Api({ base, log, onFrame: () => {}, onHostFrame: () => {} });
|
|
34
|
-
const app = new App({ screen, term: null, api, log });
|
|
38
|
+
const app = new App({ screen, term: null, api, log, launcherAnime });
|
|
35
39
|
const term = new Term({
|
|
36
40
|
output: process.stdout,
|
|
37
41
|
kitty: detectKitty(),
|
package/package.json
CHANGED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// ascii-logo.js — Welcome-page ASCII-art logo (52x24 character grid).
|
|
2
|
+
// Aspect-true for 1:2 terminal cells: 52 cols / 24 rows = 2.17, matching the
|
|
3
|
+
// source face (740x680 px). Each cell = [rampIndex, colorIndex] (hex byte
|
|
4
|
+
// each; 0,0 = empty). RAMP lightest '.' (no space keeps bright areas textured).
|
|
5
|
+
export const ASCII_LOGO_COLS = 52;
|
|
6
|
+
export const ASCII_LOGO_ROWS = 24;
|
|
7
|
+
export const ASCII_LOGO_RAMP = ".:-=+*#%@";
|
|
8
|
+
export const ASCII_LOGO_PALETTE = [
|
|
9
|
+
"fefeff","fefef7","fdfcfd","fdfbfd","fef9fd","fcfafe","fcfafb","fff2ed",
|
|
10
|
+
"ffefe8","feeee2","feecdf","feebe0","ffeae1","ffeadc","fdeadc","fceadd",
|
|
11
|
+
"fee9e2","ffe9de","fde9de","ffe9dd","ffe9dc","ffe9db","fce9d8","ffe8e3",
|
|
12
|
+
"ffe7de","fee8dd","fce8dd","ffe8dc","fee8d9","fde6da","fee5d9","fde3d8",
|
|
13
|
+
"ffe2d8","ffe1d9","fedacf","fed0c5","f9fefc","fafcfb","f6fdfc","fbfbfd",
|
|
14
|
+
"fbfbfb","fbfbf8","fbfafd","fafaf8","f6f9fd","f8f7fa","f7ede6","f8e6dc",
|
|
15
|
+
"f7dfd5","f9dbd1","f8d5cd","f1f1f7","e9eaf5","ece5e6","e9dbd9","d7d8e7",
|
|
16
|
+
"eed2ce","d9d5e5","d4d1e0","d0d5e8","d0cee0","cfcddc","cecbe2","cfcbda",
|
|
17
|
+
"b5e4fa","c0d7f4","b9ceeb","d0cadc","ccc9d8","c9c8da","c7c8de","c8c8d8",
|
|
18
|
+
"e2bcbc","e8b5b1","c8c5da","c8c3d0","bfc6e2","c5c6d8","c0bed5","abc1e9",
|
|
19
|
+
"a7b8e0","a5b5e1","e7b1af","ebaaaa","dab2b5","c2abbc","f4a19d","d49ea0",
|
|
20
|
+
"bf93a2","c58381","a3b0d6","9baddf","96aedb","95abdb","96aadc","95a9d8",
|
|
21
|
+
"97a6ce","9e9fc6","94a1c4","8faee1","91a7d8","8fa3d8","89a2d0","8c9bc7",
|
|
22
|
+
"8c96c3","839bcf","7697c3","8692c0","888ebf","8090bf","808cbf","7d8cbe",
|
|
23
|
+
"7d8ab7","778dc1","778bbe","718db9","8383a7","7a84b7","7685b4","7585ba",
|
|
24
|
+
"7188bc","7185b8","7085bb","7281b6","6e85b6","6d82b9","6885b3","6c81b5",
|
|
25
|
+
"6c80b6","6780b8","cc716d","9a7684","7778a5","707eb2","717ca8","72769e",
|
|
26
|
+
"6c7fb5","697eb9","697eb2","6b7bb1","687eb5","677eb1","677db7","687db2",
|
|
27
|
+
"677daf","687cb7","677cb3","677cb1","6774a6","657fb7","657fb3","667db2",
|
|
28
|
+
"647db6","647db3","667cb6","667cb4","657bb5","667bb2","657ab3","647ab4",
|
|
29
|
+
"657baf","657aaf","6679b0","6578b0","6479b0","6478ad","637db5","637cb5",
|
|
30
|
+
"627cb5","627bb4","607eb2","607cb3","6379b3","617ab3","6279ae","6278ae",
|
|
31
|
+
"6278a8","5f79ab","5b77a6","6175aa","5e74ae","5f72a7","5974a1","5d70a7",
|
|
32
|
+
"5c6f98","586fa5","576da4","576ba4","586a9b","586799","536ba2","53689f",
|
|
33
|
+
"4f689d","6b6486","586495","54659c","53628f","4e639c","4f6389","505e94",
|
|
34
|
+
"495e99","715861","7a4945","594d64","4c5b92","4c5781","485a8f","475084",
|
|
35
|
+
"455a94","445991","435891","435792","42578c","425691","43568d","41558e",
|
|
36
|
+
"42538f","465286","3f538d","3f5087","3c4c86","3a487e","32477e","724245",
|
|
37
|
+
"523e4d","3e4678","3c4184","5f3640","3f3857","552b30","391d2b","33457d",
|
|
38
|
+
"2e4076","313c6e","293672","2c345f","253163","252a57","221d38","1b2e61",
|
|
39
|
+
"11295c","152350","0c2357","0f1f56","0d1d50","121d46","0a1e53","101a48",
|
|
40
|
+
"0b1849","161536","071746","030e3c","080e29","090728","0c0605",
|
|
41
|
+
];
|
|
42
|
+
export const ASCII_LOGO_GRID =
|
|
43
|
+
"0000000000000000000000000000000000000000000000000000000004b9002c0005002c002a0034013e036205c805c905c60147013e000105de0367025d0367002800040005002705ce000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000005ce002d0029002e013d0035003507f607f505c905c9013c013807f904a6049606f00481047e024f013a00040003024c0034024c04b90000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002d0003013b0006002e05c20250049e0498049a06df06df05dd05c905d404ae06e906df06dd07f1048a0372049704ab024301380145000100280006000305cc0000000000000000000000000000000000000000000000000000000000000000000000000487002c002e000200290475035f04ac049904ad06df049204a805bb04a904a7049904a704a7049b06f007f304b506eb04a70499049a049a025c07ef0144013e00340006003500000000000000000000000000000000000000000000000000000000000000000000002600290029013d0252037204a604aa0482049d049b04aa04b504a004aa04aa04aa04aa04ac04ae04a00499049904990499048204990499025e0360025c013b013e05cc05c707fb0000000000000000000000000000000000000000000000000000000000000001025601460138025d04b1036004b604a904b0049e04ae06f0049d04ad04ad04ad04ad04ae04ae04ae04ae04aa04aa04a8025f036a0497025f036004a60473048c07f8014b0256013d013800000000000000000000000000000000000000000000000000000001002b014c0138025204b104b604a504b604a20365049104a504b804b10360036b0365025c0252037203650365049b049c025c04a1036704a205d8047904b90023002106e105c3013f013e013e06e500000000000000000000000000000000000000000000000001440035013a025204a20498049d04a004ba036a04ae04a505d704b004b1036004a60366036604a2048e04ad048f049b04b504960496049904aa049907f704ac04ab06e7001900150011013a014005cd0000000000000000000000000000000000000000000005d50144013f025004b004aa04ac04aa0482049d04a80482049b07f10493048d0496048f048f047e04940481048a049e04800256047e05bf048f048f049b0492049b049a04ac04ad05c2000b0019000805cc07f2001e001105cb000f01320000000000000000000007fb014b0252049f05c904aa04aa049905db048a0492048a04b602550480047a047a0480047a047a04b1047804780491047a0255025504860478048800300031000b05cc048704a604a205c606e1001a001e000c035a0249000b024a024a00000000000000000000025204a304af05d204ae04a804a8049905d4047e05dd047b05ce06e507ef036904860370036f036f036f036e036f06ed036d01390013001202580473036e047706e101390019001f000c000906e105c200110031001b00300124024a05cb000007f7048e04b405d105c407f405cd0499049b0499048a047e05d1047905bc04c2000a000e0019048503690477036c036c036c05d00476025604750016001500150015000f025806ec05c404770480048605c205cb0022001a001505cb000c00310254025402530000000000000000000007f306f0049b05d10493048d047e047e05bc06e707fa001807ff07ff07ff0008013d04880137047703690369036205cc000f0031024a0132013907e7000f0014025804710478047804bd05bf05c006f005e0025400100015001c000f00150000000000000000000005da06f0049c05d1049e04930480047806ea013307e70140002d06e206e906d9024c0009002f03710484024906e504c2000906e006e600310132003105e006e60484036d0371048c04a605bc05c005c606e806e906e402530014001a001b00000000000007fc05da05da07f4048d05d6048a048b05c80477036c013906e0000406df02500141025000020010000f000f0017000f00120015001c00200023012301240124012401330368047605bd0486049407f405dc07f9036106ed06de06de06de06de06ea000006ed036805da05da05cf06ea025c07f505cd0252048605c3036f036c0009013301230133013200320020001200120012000900170012001300150020002100230023002106e207fe05cc05cd047c036006f006eb05c4025b0363014d024d014d024d014d06ec05da05d706e2036103610369036305ce07f107f805d104a4036605be05c506ec048707ff002101230032001f001c0015001500120012000f000f000f000f001e001e0020001e001707ef04860486047805db05d807f405d405d805d704ba05bb07f500000000000001420142014207fc0143026107f505cf05cd05d505d106df05c4047c047c036e06e1001e001f001f001e000f001506e00383045a035a04830383038300130012001200180249024a05c507fc05d905d9048c05d306eb05d404a305d804ba04b804ba05cd0000000000000000000005c007f505d505d104b804b805d805c005c4049805dc05d805d605d905c20016000b001200120015001505cb0257025702570257001a000d00120012000b06e105c105d305d70492048e049005d105d607f705d604b405d705bc04b406f004b807f7000007fc07fc0141025105c9047e04a305db05bc05db05d604b40490049304a405bc05d905c405be06e00030001300120012001200130015001200120019013105c305c005c605bf04b6047b04980490049005d105d405d607f107fc07fa048b05d507f70264025c036401430141014105c6048d049105d805c005d805d805d605dc0366048004940372047b06df04ba05bf04ba05bf05cb06e40139000a000d0139035a035a024a048804b304b704b205c0048b0366047d06e80142014d002500060006002a0001048b048607f9026404b601410141025204b004a105d205bb00010007002c024f013f013f036a0366047b047b06f004b304b304b3014d07ef02530254035a035a02540254025301380147024d04b204b20373047d047407f20147014e002c05c7003505c304b9002507fd07fc06e205c304b3025d047f047f048b047905c107f2002900290006000600060148024b06f0047d047406ea05be07fc024f0147014b014b0148014806e701470148014601460148024f024f07f6036b036b07f307f6013e00060034002a05ce05bf0027002a07fa06ee06e206e20474047407f7047f037405c405c007f6002a00290004013b013b0034014804b70252036b07f207fd06ef0147014701470146014b04750362036204880144013c07fa07fa07fd013f036e036a0491047d00340148002900030025002a000602560006002d06ec06e2";
|
package/src/file-picker.js
CHANGED
|
@@ -49,10 +49,10 @@ class YnPopup extends Popup {
|
|
|
49
49
|
}
|
|
50
50
|
|
|
51
51
|
export class UploadPicker extends Widget {
|
|
52
|
-
constructor(app, { startPath, onUpload, onCancel, selectDirectories = false, onPickDirectory = null }) {
|
|
52
|
+
constructor(app, { startPath, onUpload, onCancel, selectDirectories = false, onPickDirectory = null, single = false, onPickFile = null }) {
|
|
53
53
|
const w = Math.min(app.screen.w - 4, 120), h = Math.min(app.screen.h - 4, 34);
|
|
54
54
|
super({ x: Math.floor((app.screen.w - w) / 2), y: Math.floor((app.screen.h - h) / 2), w, h });
|
|
55
|
-
this.app = app; this.path = startPath; this.onUpload = onUpload; this.onCancel = onCancel; this.selectDirectories = selectDirectories; this.onPickDirectory = onPickDirectory;
|
|
55
|
+
this.app = app; this.path = startPath; this.onUpload = onUpload; this.onCancel = onCancel; this.selectDirectories = selectDirectories; this.onPickDirectory = onPickDirectory; this.single = single; this.onPickFile = onPickFile;
|
|
56
56
|
this.all = []; this.sel = 0; this.selected = new Map(); this.filter = ''; this.filterInput = null; this.showHidden = false; this.pathPopup = null; this.imagePreview = null;
|
|
57
57
|
this.load();
|
|
58
58
|
}
|
|
@@ -81,6 +81,7 @@ export class UploadPicker extends Widget {
|
|
|
81
81
|
this.confirmDirectory(it.path); return;
|
|
82
82
|
}
|
|
83
83
|
if (it.dir) { this.app.toast('不可选择文件夹'); return; }
|
|
84
|
+
if (this.single) { this.clearKitty(); this.onPickFile?.(it.path); return; }
|
|
84
85
|
if (this.selected.has(it.path)) this.selected.delete(it.path); else this.selected.set(it.path, it);
|
|
85
86
|
this.app.redraw();
|
|
86
87
|
}
|
|
@@ -187,7 +188,14 @@ export class UploadPicker extends Widget {
|
|
|
187
188
|
if (ev.name === 'down') { this.clearKitty(); this.sel = wrapIndex(this.sel + 1, this.items().length); return true; }
|
|
188
189
|
if (ev.name === 'left') { this.goParent(); return true; }
|
|
189
190
|
if (ev.name === 'right') { this.enterDir(); return true; }
|
|
190
|
-
if (ev.name === 'enter') {
|
|
191
|
+
if (ev.name === 'enter') {
|
|
192
|
+
if (this.single) {
|
|
193
|
+
const it = this.current();
|
|
194
|
+
if (it?.dir) { this.enterDir(); return true; }
|
|
195
|
+
if (it) { this.clearKitty(); this.onPickFile?.(it.path); return true; }
|
|
196
|
+
}
|
|
197
|
+
this.confirmUpload(); return true;
|
|
198
|
+
}
|
|
191
199
|
if (ev.name === 'char' && ev.key === ' ') { this.toggle(); return true; }
|
|
192
200
|
if (ev.name === 'char' && ev.key === '/') { this.startFilter(); return true; }
|
|
193
201
|
return false;
|
package/src/index.js
CHANGED
|
@@ -23,7 +23,7 @@ export function launchTui(opts = {}) {
|
|
|
23
23
|
}
|
|
24
24
|
const screen = new Screen(process.stdout.columns || 80, process.stdout.rows || 24);
|
|
25
25
|
const api = new Api({ base: opts.base ?? "http://127.0.0.1:1", log, onFrame: () => {}, onHostFrame: () => {} });
|
|
26
|
-
const app = new App({ screen, term: null, api, log });
|
|
26
|
+
const app = new App({ screen, term: null, api, log, launcherAnime: opts.launcherAnime ?? process.env.DSH_TUI_LAUNCHER_ANIME === "1" });
|
|
27
27
|
const term = new Term({
|
|
28
28
|
output: process.stdout,
|
|
29
29
|
kitty: detectKitty(),
|
package/src/keybindings.js
CHANGED
|
@@ -51,13 +51,14 @@ export const DEFAULT_KEYBINDINGS = {
|
|
|
51
51
|
commandPalette: { mode: "normal", key: "Ctrl+P", key2: "" },
|
|
52
52
|
modePicker: { mode: "normal", key: "F9", key2: "" },
|
|
53
53
|
themePicker: { mode: "normal", key: "Ctrl+D", key2: "" },
|
|
54
|
+
logoPicker: { mode: "normal", key: "Ctrl+R", key2: "" },
|
|
54
55
|
quitDouble: { mode: "normal", key: "Ctrl+C", key2: "" },
|
|
55
56
|
quit: { mode: "all", key: "Ctrl+Q", key2: "" },
|
|
56
57
|
};
|
|
57
58
|
|
|
58
59
|
/** App-level dispatch precedence: the first matching binding wins. */
|
|
59
60
|
export const KEYBINDING_ORDER = [
|
|
60
|
-
"sessionFilter", "panel", "panePrev", "paneNext", "permissionRotate", "editConfig", "addWorkspace", "commandPalette", "modePicker", "themePicker", "copySelection", "quitDouble", "quit",
|
|
61
|
+
"sessionFilter", "panel", "panePrev", "paneNext", "permissionRotate", "editConfig", "addWorkspace", "commandPalette", "modePicker", "themePicker", "logoPicker", "copySelection", "quitDouble", "quit",
|
|
61
62
|
"model", "trajectory", "workspace", "settings", "subagent", "skills", "goal",
|
|
62
63
|
"jobs", "queue", "busyEnter", "attachments", "stepJump", "sidebar",
|
|
63
64
|
];
|
package/src/logo.js
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// logo.js — Welcome-page logo data (DeepSeek mascot face, 40x19 quadrant).
|
|
2
|
+
// 40x19 cells × 2x2 quadrant pixels = 80x38 effective resolution.
|
|
3
|
+
// Each cell = 4 bytes: [type, maskByte, fg, bg]; type 0 = quadrant with
|
|
4
|
+
// maskByte low nibble = character quads, high nibble = second-color quads.
|
|
5
|
+
// Ordered luminance pairs (bright = fg), near-identical pairs collapsed.
|
|
6
|
+
export const LOGO_COLS = 40;
|
|
7
|
+
export const LOGO_ROWS = 19;
|
|
8
|
+
export const LOGO_PALETTE = [
|
|
9
|
+
"fefffd","fcfcfc","fcfbfb","fcfafc","fef2ea","feede3","feebde","feeae4",
|
|
10
|
+
"fee9de","ffe9dd","fdeae1","fdeadd","fdeadc","ffe8de","ffe8dc","fce8dc",
|
|
11
|
+
"fee6df","fde7da","fee5dc","fbfcfc","fbfdf9","fbfbfb","fbfafc","fbfaf5",
|
|
12
|
+
"fbebe2","fbe6df","f7fbfc","f7f8f9","f7f2ee","f3f4f3","eeecee","e2e9f1",
|
|
13
|
+
"fee3d9","fee2d7","fae2d8","fdded4","fddcd2","f6e0d7","ebe4e0","e1dfe8",
|
|
14
|
+
"f8d8cf","f8d2c8","ebd7d6","dfd8e1","f9d0c7","f4cac4","e7cecb","e7c8c2",
|
|
15
|
+
"d5d9ea","d4d2e1","d1d0dd","d0cede","ceccda","b6def8","bed4f3","cbcee2",
|
|
16
|
+
"c2ccec","cdcadc","cbc8d9","cac9db","c9c6d5","c7c7d7","c6c6d8","c0c8e1",
|
|
17
|
+
"eabbb9","dbb9c0","e4adaf","d6b0ae","c5c4d9","bfbecf","b3bbda","b5b5ce",
|
|
18
|
+
"b6acb7","a6b6e1","97b1e6","94aedf","a0acd4","97acdf","9aa9d2","94a9db",
|
|
19
|
+
"92a9d8","e9a3a3","de9896","b39da5","c78d8b","96a5d5","9ba5c3","98a0c3",
|
|
20
|
+
"978fab","91a6d6","8fa3d7","8ea0ca","8f9dcb","8c96c2","8c91b9","7ea1cc",
|
|
21
|
+
"879ac3","8498cb","8294c6","7893c2","818ebe","8590b5","7b8dbe","738db6",
|
|
22
|
+
"6d8cb9","7b88ba","7c85b8","7d85af","7787ba","7089bb","7386b8","7385b8",
|
|
23
|
+
"7084b6","6a84b4","7782ab","7282b4","7180af","6f82b5","707eb1","6b81b6",
|
|
24
|
+
"6b80b3","6a7eb6","6980b7","687fb7","687eb6","697fb2","6680b7","647eb6",
|
|
25
|
+
"af6c68","8c748a","77799a","6e7caf","6d78a8","6d6783","6a7db6","687db3",
|
|
26
|
+
"687cb1","6979ad","696d90","677db6","667cb6","667cb0","677bb4","667aad",
|
|
27
|
+
"6673a4","647db6","647cb5","637cb5","627cb4","627bb4","647cb1","617caa",
|
|
28
|
+
"657ab3","647ab0","637ab2","6279b2","6277b0","6278ab","6275ac","63739f",
|
|
29
|
+
"607aae","5f76ad","5d75ad","5d72a8","5973a8","5d70a6","5d6f98","576fa2",
|
|
30
|
+
"5c6b9e","576b9e","556b9f","59679c","576898","566888","526aa1","4c689e",
|
|
31
|
+
"5166a0","586498","5d6280","4e6497","4d619e","4d609a","4b5e96","495a94",
|
|
32
|
+
"525a84","4f5c71","475c93","455993","455892","445792","44588d","42587c",
|
|
33
|
+
"794e4d","654c51","5c506e","78403d","5f3b51","51557a","4d4e7a","4d3f5d",
|
|
34
|
+
"523745","45558d","42558f","41558d","45528a","40538d","415088","434572",
|
|
35
|
+
"44436c","423c5a","3e538c","3c5285","3d4f83","384b83","354881","394677",
|
|
36
|
+
"354275","30447a","393f75","3a4064","323f6d","303a6e","34395c","2b3e72",
|
|
37
|
+
"293a6c","29386f","583136","38324d","2f3155","263363","592527","432025",
|
|
38
|
+
"2d2945","2b1b33","213266","193166","1e2b5a","192c5f","1f244b","182755",
|
|
39
|
+
"1c1a38","132a5e","12255d","122256","0b2258","0e1e52","131e44","081c49",
|
|
40
|
+
"10173c","11152c","06174d","071444","0e0f2b","010c39","07061f",
|
|
41
|
+
];
|
|
42
|
+
export const LOGO_GRID =
|
|
43
|
+
"0000000000000000000000000000000000000000000000000000000000000000000000000000000000088600002e17c6000f150000cf1635000f1600001f284900af34b200ef64b6008fb5ed003f35d5001f1e34001f38c7004f51df004f516e009f40dc008f023d004f0435002f16ba0004330000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004c18ba000c1e00002e183200cf1c33000f1c0000cf33b400ef2875003f93fc003fa4ed004fb6e200df31b500ef31a0005fccf400cf7fd7002fa1f5000f7b00002f9df600cf2862004f28fd004f2c86008f2854001f1454001d1486008c1e6100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008ba00001f16b3000f1c00000f1600008f33ff00ef01ac00ef4b95000f8d0000cfa3e2003f8dd7001fa3d7000f9300000f9600000f9400008f94af008f8daf003fb5ec008f7ad7004fa3f3000f8000000f780000ef50ad008f32fd00cf033a00cf0433000f0200009d17b300000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000005f1bba000f1400002f1733006f47b9004f4ea6000f7d00008f94ec002f9cbe000f9300000f9d00000f8c00000f9500000f9500000f9500000f9500000f9600000f8000000f8000000f9200000f7f00000f9400005f4c95002f5a7e002f50fa00cf1447004f1f83002f3ce5008cb4ec000000000000000000000000000000000000000000000000000000000000000000000000000000000008c000002e0234003f0446009f2046001f4e83000f5100000f5100008f78d3001f6deb00bf639b008f99ec000f7b00000f9d00000f9c00000fa200000f9c00000f9c00007f4e9c000fa500000f7800003f5193001f5ba1000f9700006f4b9e00ef5c9a00cf64e400df4b84008f48ef000f3b00000f3a00009d38c3000000000000000000000000000000000000000000000000000000000000000000000000002a1b20000f0300003f173500ef205600ef508e000f780000df4e8d000f9d0000cf5a9e00cf509d004f9db700af4e9e000f5a00000f5100000f5100005f5090005f5a8e00cf5a9a00ef5b7800df4ea400ef6297000f880000ef5b9b008f9bbc000f8000001f5fe8004f242f003f13b3001f2cdf000f3400008f3bdc0004fe000000000000000000000000000000000000000000000000000000000000000000006e3ddd003f3a83006f66fc00ef5697000f9600000f960000af7fda000f9900000fa300000f9d00005f99e2000f880000ef6288000f8000000f7d00000f7b00000f7d00000f7800000f7b0000df7aef000f7b00000f8c00000f8d00000f8d00001fa3f2000f9300000f850000df108b004f1ae3002f2154008f2ce8008da0f6004c07c2000c0e00001f2bc5009d13c200000000000000000000000000000000005f58f300af47b900ef519b000f8d00000f9400000f8000005f87d6000f8c00000f8d00007f44b200af42ac000f7600000f7100000f7000000f7000008f71a6000f6f00000f7700000f6f0000df43c600cf73c700cf88cf00cf85c6003f08c3003f248b003f138e008f58c9000f8500000fb20000cf30b3004f102e002f23e8005f2d81009f42c400ef1a5500ef26430008f40000000000008cdafe008c5d79001f6ffe008fa8ee001f9bd3000f9600000f8d00000f7c00005f7abe008f88e2008f6fe9003f2fd0003f2bdc007f65d1008f6ade000f6500000f6500002f65dd004f6584008f75e5008f65df001f0742000f0900001f2953003f446a008f6ab7002f85e300cf1ab200cf0f83004f29e8000f1000003f12c3003f0f82000f2300000f0900002f2ac4008f41c400df415200000000000269000003f6000003fb00005fcbf6007f99da000fa200000f8c00000f7800000f7100005fb7d600af83d1005f2fae000f2600000f0b00002f2ad2008f5885005f4de5004f5e6c000f5e00003f5e6b001f426b001f1a66000f0d00000f1200000f0700000f0600002f1244003f2adc005f6fe6000f7400008f77a700df86b1001f59e300cf21c5002f2ec2000f1000002f0ac1002f29c1001f29550000000000000000000000000008fc00002fcdf4001f67b1000fb700000f8800000f8e00000f7000009f90e1008f2ae8007f3de4007f1efa003fdbdf003fd7ea005f2bc9003f06e5008f2b6b007f42b9003f425e005f46c6004f104900cf2ac1002f19ff003f25ff003f24ff00af54e8004f2681000f6600008f6ade000f7700000fa600004fb1e000afb8db002fdbe300cf2fc800cf0f43000f2300000f10000000000000000000004c5d8a00ef56cd000fcd0000af508f008fbbf5004f7eb7002f79aa004f6a91002f65c2007f25e3004f1f81009f37e6007f2060003f3760001f05df000f0900000f0c00000f0600000f0d00000f0900000f0f00000f2100000f2500000f2d00000f2d00000f2a0000ef24ae008f5ead001f74f0007f4e77008f90f3001f90f4003f4ffb003f57d8003f4fd600bf5cd9000fd800007f47c8004c5b6300cf6dcd00ef5ecd00cfcdde00efcae6001f56f8005f5db700bf85b8006f4e9f002f71ee008f77de002f6bd2004f295e000f2e00000f2900000f2500000f2200000f0a00000f0a00000f0b00000f0d00000f0c00000f0c00000f1300000f2200000f2500000f2500003f21d100bf30c3003f6aa600bf637400ef50d700afbcf6001fbff400cf58bf008f58fc000f4700008f48e600cf40fc00df47dd003f40cd003f39d8003f394f004f39fa009f394d00ef58e100ef4bf0000fbf00004fccfb007f6d8a002f74a9003f84a900df44c9000f0800000f2400000f2200000f1200000f0d0000cf0ae3004f2154008f30e800cf0c5500cf0d81000f0600000f0a00000f0900000f230000af2bd100cf6db600cf74ce001fceee003f8acc00afbeeb000fbd0000bf8fbd007f9db1000fa50000ef5bcc0004fb00000000000000000000000000006eb1f8007f4ad5001fbbfb007f9fd300af8ecf001fbdf6008fb8f4007f89bd00bf7ed500cf74cb00cf6dce00ef6cef001f19f1000f0900000f0a00000f0a00003f12c4000f5200000f5200000f5200002f2de7000f0e00000f0900000f060000cf10dc00ef86b0004fbdf3007f7dd6000f790000af8ebb004fbcfb001fcbf5005fa4cb00bf9ebd004fa3cb009f89ec000faa00008e5bf30008fc00007f36d5001f36d500af36e0007f6ed7000f9a0000efb1cc009fb1ce008fcef2005f9fb8000f8900002f88d600bf8fcb002fb5f4002fb0b3001f5fe300cf09c6004f0c2f000f1100000f0a00000f1200000f0900000f0700008f1ae700cf2682001fafc3002fafec008fabfb002f8ad5000f8900000f790000afa4e0000fbe00000fbe00007f1cdd003f149a006f7ee6008f9fd3004f4bab002f4c7700af63e0000f3600008f36e0001f67bf000f8e0000ef8ebc007f02cf003f20d3004fd3f7007f39b4004f9ef000bf628e00bf6488001f78d7005fa5eb000fa400000fa800003f4dac004f52e3001f43c100cf105500cf0855001f53e7001f4355008f42f1009f5ced000fa500002f8ed3007f4c71004f6298008f67d8003f48e6007f0145001f1535008f1649008f1c8b002f14f9000f7900001f74aa00cf4bef002fa4f800ef36aa00ef3772008f7ea8007f6ea6008fabed000f1700000f0400002f042800bf013e000f3800009f73f600cf5b68002f71bf005f98d8009f9eeb003f47d4000f4500002f45ea003f3c41003f3f52008f43f1003f3f52001f3aea000f3b0000af3f48001f58ef00cfa8f700ef5064008f69b4005f48ad007f143f006f1eae003f15af000faf00005f1baf000f1400005fe5fa007fd0e5000fd000000e6800004f72ee006f68b4009f69ad000fad0000efb4f9000f0200000f1600003f1c2c00cf1d33006f1e3500af3fad001f68ee000f6400008feffa001f38ff00cf3efd00cf3ffa00cf3cf1000f3f00004f48f1004f58ff00df3ee500cf3cfd00cf3cdf00cf3fd100cf20d1002f3aef004f6068001f98f5008f61dd009f2882001f143d003f02a7009f1b6c008f1fc3000f0400003f03fd00bf1be5000fd000";
|
package/src/panels.js
CHANGED
|
@@ -1749,7 +1749,7 @@ export class ControlPanel extends Widget {
|
|
|
1749
1749
|
const b=keyBindings();
|
|
1750
1750
|
const row=(id,desc)=>[`${(b[id]?.mode??"all").toUpperCase()}\t${describeSpec(b[id]?.key)}\t${describeSpec(b[id]?.key2)}`,desc,null,id];
|
|
1751
1751
|
return [
|
|
1752
|
-
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("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","退出"),
|
|
1752
|
+
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
1753
|
];
|
|
1754
1754
|
}
|
|
1755
1755
|
items() {
|
package/src/views.js
CHANGED
|
@@ -13,12 +13,14 @@ import { bindingMatchFor, matchKeyBinding, CHAT_BINDING_ORDER, SIDEBAR_BINDING_O
|
|
|
13
13
|
export { userPrefix, saveTuiConfig, loadTuiConfig, userName, busyEnter, foldDefaults } from "./config.js";
|
|
14
14
|
import {
|
|
15
15
|
Picker, buildCommandPalette, buildModelPicker, buildModePicker, buildPermissionPicker,
|
|
16
|
-
modeName, permName, WorkspacePanel, TrajectoryPanel, DirPicker,
|
|
16
|
+
modeName, permName, WorkspacePanel, TrajectoryPanel, DirPicker, AttachmentPanel,
|
|
17
17
|
ImagePopup, kittyCapable, buildGoalPopup, GoalPanel, SettingsPanel, SubagentPanel,
|
|
18
18
|
SkillsPanel, ControlPanel, JobsPanel, QueuePanel, ModelPanel, fmtMs, ThemePickerBuffer,
|
|
19
19
|
} from "./panels.js";
|
|
20
20
|
|
|
21
21
|
import { T, themeName, cycleTheme } from "./theme.js";
|
|
22
|
+
import { LOGO_COLS, LOGO_ROWS, LOGO_PALETTE, LOGO_GRID } from "./logo.js";
|
|
23
|
+
import { ASCII_LOGO_COLS, ASCII_LOGO_ROWS, ASCII_LOGO_RAMP, ASCII_LOGO_PALETTE, ASCII_LOGO_GRID } from "./ascii-logo.js";
|
|
22
24
|
// Live theme accessor: K.K.DIM etc. resolve against the active palette at render time.
|
|
23
25
|
const K = new Proxy({}, { get(_k, key) { return T[key]; } });
|
|
24
26
|
|
|
@@ -193,6 +195,144 @@ function compareSemver(left, right) {
|
|
|
193
195
|
return 0;
|
|
194
196
|
}
|
|
195
197
|
|
|
198
|
+
// ---- Boot splash (retro game-launcher animation) ----
|
|
199
|
+
|
|
200
|
+
const SPLASH_WHITE = 0xffffff;
|
|
201
|
+
const SPLASH_BLACK = 0x000000;
|
|
202
|
+
const SPLASH_BLUE = 0x2b5bd7; // DEEPSEEK wordmark color on white
|
|
203
|
+
const SPLASH_INK = 0x14181d; // caution-lines ink
|
|
204
|
+
const SPLASH_HINT = 0x2f3540; // bottom hint ink
|
|
205
|
+
/** Classic four-line healthy-gaming warning, one sentence per line. */
|
|
206
|
+
const SPLASH_LINES = [
|
|
207
|
+
"抵制不良游戏,拒绝盗版游戏。",
|
|
208
|
+
"注意自我保护,谨防受骗上当。",
|
|
209
|
+
"适度游戏益脑,沉迷游戏伤身。",
|
|
210
|
+
"合理安排时间,享受健康生活。",
|
|
211
|
+
];
|
|
212
|
+
// 5x6 pixel glyphs rendered with half-blocks (3 terminal rows per letter);
|
|
213
|
+
// "█" full, "▀" upper half, "▄" lower half — pure-TUI, no image protocol.
|
|
214
|
+
const SPLASH_GLYPHS = {
|
|
215
|
+
D: ["11110", "10001", "10001", "10001", "10001", "11110"],
|
|
216
|
+
E: ["11111", "10000", "11110", "10000", "10000", "11111"],
|
|
217
|
+
P: ["11110", "10001", "10001", "11110", "10000", "10000"],
|
|
218
|
+
S: ["01111", "10000", "01110", "00001", "00001", "11110"],
|
|
219
|
+
K: ["10001", "10010", "10100", "11000", "10100", "10001"],
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
function lerpColor(a, b, t) {
|
|
223
|
+
const ar = (a >> 16) & 255, ag = (a >> 8) & 255, ab = a & 255;
|
|
224
|
+
const br = (b >> 16) & 255, bg2 = (b >> 8) & 255, bb = b & 255;
|
|
225
|
+
return ((Math.round(ar + (br - ar) * t) << 16) | (Math.round(ag + (bg2 - ag) * t) << 8) | Math.round(ab + (bb - ab) * t)) >>> 0;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// ---- Welcome brand wordmarks (TUI-drawn half-block letters) ----
|
|
229
|
+
|
|
230
|
+
/** 5x7 pixel font for the welcome brand lines (7 pixel rows = 4 terminal
|
|
231
|
+
* rows with ▀/▄ half-blocks, the last row single-half). */
|
|
232
|
+
const BRAND_GLYPHS = {
|
|
233
|
+
A: ["01110", "10001", "10001", "11111", "10001", "10001", "10001"],
|
|
234
|
+
D: ["11110", "10001", "10001", "10001", "10001", "10001", "11110"],
|
|
235
|
+
E: ["11111", "10000", "10000", "11110", "10000", "10000", "11111"],
|
|
236
|
+
H: ["10001", "10001", "10001", "11111", "10001", "10001", "10001"],
|
|
237
|
+
I: ["01110", "00100", "00100", "00100", "00100", "00100", "01110"], // 工-style serif I
|
|
238
|
+
K: ["10001", "10010", "10100", "11000", "10100", "10010", "10001"],
|
|
239
|
+
N: ["10001", "11001", "10101", "10011", "10001", "10001", "10001"],
|
|
240
|
+
O: ["01110", "10001", "10001", "10001", "10001", "10001", "01110"],
|
|
241
|
+
P: ["11110", "10001", "10001", "11110", "10000", "10000", "10000"],
|
|
242
|
+
R: ["11110", "10001", "10001", "11110", "10100", "10010", "10001"],
|
|
243
|
+
S: ["01111", "10000", "10000", "01110", "00001", "00001", "11110"],
|
|
244
|
+
T: ["11111", "00100", "00100", "00100", "00100", "00100", "00100"],
|
|
245
|
+
U: ["10001", "10001", "10001", "10001", "10001", "10001", "01110"],
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
/** Draw a half-block wordmark (5-wide glyphs, 6-col advance). `bandX`
|
|
249
|
+
* (nullable) places a diagonal shimmer sweep: pixels near the band mix
|
|
250
|
+
* toward white, tilted ~0.7 cols/row. */
|
|
251
|
+
function drawHalfBlockWordmark(screen, x0, y0, word, color, bandX, bg) {
|
|
252
|
+
const mix = (col, px, py) => {
|
|
253
|
+
if (bandX == null) return col;
|
|
254
|
+
const d = Math.abs((px + py * 0.6) - bandX);
|
|
255
|
+
if (d > 2.5) return col;
|
|
256
|
+
return lerpColor(col, 0xffffff, ((2.5 - d) / 2.5) * 0.7);
|
|
257
|
+
};
|
|
258
|
+
let cx = x0;
|
|
259
|
+
for (const ch of word) {
|
|
260
|
+
if (ch === " ") { cx += 5; continue; }
|
|
261
|
+
const rows = BRAND_GLYPHS[ch] ?? ["00000", "00000", "00000", "00000", "00000", "00000", "00000"];
|
|
262
|
+
for (let pr = 0; pr < 7; pr += 2) {
|
|
263
|
+
const topBits = rows[pr], botBits = rows[pr + 1] ?? "00000";
|
|
264
|
+
const y = y0 + pr / 2;
|
|
265
|
+
for (let px = 0; px < 5; px++) {
|
|
266
|
+
const t = topBits[px] === "1", b = botBits[px] === "1";
|
|
267
|
+
if (!t && !b) continue;
|
|
268
|
+
const fc = mix(color, cx + px, y);
|
|
269
|
+
if (t && b) screen.put(cx + px, y, "▀", { fg: fc, bg: fc });
|
|
270
|
+
else if (t) screen.put(cx + px, y, "▀", { fg: fc, bg });
|
|
271
|
+
else screen.put(cx + px, y, "▄", { fg: fc, bg });
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
cx += 6;
|
|
275
|
+
}
|
|
276
|
+
return cx - x0 - 1;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/** One frame of the opt-in boot animation. Phases are driven by opaque
|
|
280
|
+
* numbers so the renderer stays pure and testable:
|
|
281
|
+
* diffuse 0..1 white circle expanding from the screen center over black;
|
|
282
|
+
* lineAlpha[4] per-line opacity of the healthy-gaming warning;
|
|
283
|
+
* wordAlpha 0..1 DEEPSEEK wordmark crossfading in;
|
|
284
|
+
* blink show/hide the 按任意键进入游戏 prompt (toggling = blinking). */
|
|
285
|
+
export function drawBootSplash(screen, { diffuse = 0, lineAlpha = [0, 0, 0, 0], wordAlpha = 0, blink = false } = {}) {
|
|
286
|
+
const w = screen.w, h = screen.h;
|
|
287
|
+
const p = Math.max(0, Math.min(1, diffuse));
|
|
288
|
+
const cx = (w - 1) / 2, cy = (h - 1) / 2;
|
|
289
|
+
const maxR = Math.hypot(cx, cy * 2);
|
|
290
|
+
for (let y = 0; y < h; y++) {
|
|
291
|
+
for (let x = 0; x < w; x++) {
|
|
292
|
+
const r = Math.hypot(x - cx, (y - cy) * 2);
|
|
293
|
+
screen.put(x, y, " ", { bg: p >= 1 || r <= maxR * p ? SPLASH_WHITE : SPLASH_BLACK });
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
const inkFor = (a) => lerpColor(SPLASH_WHITE, SPLASH_INK, Math.max(0, Math.min(1, a)));
|
|
297
|
+
for (let i = 0; i < SPLASH_LINES.length; i++) {
|
|
298
|
+
const a = Math.max(0, Math.min(1, lineAlpha[i] ?? 0));
|
|
299
|
+
if (a <= 0) continue;
|
|
300
|
+
const spaced = [...SPLASH_LINES[i]].join(" ");
|
|
301
|
+
const x = Math.max(0, Math.floor((w - strWidth(spaced)) / 2));
|
|
302
|
+
const y = Math.max(1, Math.floor(h / 2) - 4 + i);
|
|
303
|
+
screen.text(x, y, spaced, { fg: inkFor(a), attrs: 1 });
|
|
304
|
+
}
|
|
305
|
+
if (wordAlpha > 0 && w >= 20) {
|
|
306
|
+
const word = "DEEPSEEK";
|
|
307
|
+
const ink = lerpColor(SPLASH_WHITE, SPLASH_BLUE, Math.max(0, Math.min(1, wordAlpha)));
|
|
308
|
+
if (w >= 52) {
|
|
309
|
+
const glyphW = 6, totalW = word.length * glyphW - 1;
|
|
310
|
+
const x0 = Math.max(0, Math.floor((w - totalW) / 2));
|
|
311
|
+
const y0 = Math.max(0, Math.floor(h / 2));
|
|
312
|
+
for (let li = 0; li < word.length; li++) {
|
|
313
|
+
const rows = SPLASH_GLYPHS[word[li]] ?? ["00000", "00000", "00000", "00000", "00000", "00000"];
|
|
314
|
+
for (let pr = 0; pr < 6; pr += 2) {
|
|
315
|
+
const top = rows[pr], bottom = rows[pr + 1];
|
|
316
|
+
const y = y0 + pr / 2;
|
|
317
|
+
for (let px2 = 0; px2 < 5; px2++) {
|
|
318
|
+
const t = top[px2] === "1", b = bottom[px2] === "1";
|
|
319
|
+
const x = x0 + li * glyphW + px2;
|
|
320
|
+
if (t && b) screen.text(x, y, "█", { fg: ink });
|
|
321
|
+
else if (t) screen.text(x, y, "▀", { fg: ink, bg: SPLASH_WHITE });
|
|
322
|
+
else if (b) screen.text(x, y, "▄", { fg: ink, bg: SPLASH_WHITE });
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
} else {
|
|
327
|
+
screen.text(Math.max(0, Math.floor((w - word.length) / 2)), Math.max(1, Math.floor(h / 2)), word, { fg: ink, attrs: 1 });
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
if (blink && (p >= 1 || wordAlpha > 0)) {
|
|
331
|
+
const hint = "按任意键进入游戏";
|
|
332
|
+
screen.text(Math.max(0, Math.floor((w - strWidth(hint)) / 2)), Math.max(1, h - 2), hint, { fg: lerpColor(SPLASH_WHITE, SPLASH_HINT, 1), attrs: 1 });
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
196
336
|
// ---- Tool card renderers (host-computed view models) ----
|
|
197
337
|
|
|
198
338
|
function renderToolCard(view, width, expanded) {
|
|
@@ -1200,9 +1340,6 @@ export class ChatView extends Widget {
|
|
|
1200
1340
|
this.rebuildQueued = false;
|
|
1201
1341
|
this.cache = new Map(); // node render cache: key → { lines, marks }
|
|
1202
1342
|
this.cardRanges = []; // absolute line ranges of card-backed message blocks
|
|
1203
|
-
this.welcomeModes = []; // absolute row y → agent preset id (welcome screen)
|
|
1204
|
-
this.welcomeModeIds = ["standard", "code", "minimal", "cordis"];
|
|
1205
|
-
this.welcomeModeSel = 0;
|
|
1206
1343
|
this.pressY = null;
|
|
1207
1344
|
this.pressInfo = null; // hit identity locked at press time
|
|
1208
1345
|
this.pressCtx = null;
|
|
@@ -1416,7 +1553,6 @@ export class ChatView extends Widget {
|
|
|
1416
1553
|
async open(sessionId, epoch = this.app.sessionEpoch, maxMessages = 80) {
|
|
1417
1554
|
this.sessionId = sessionId;
|
|
1418
1555
|
this.nodes = [];
|
|
1419
|
-
this.welcomeModeSel = 0;
|
|
1420
1556
|
this.blockSel = -1;
|
|
1421
1557
|
this.cursorMode = "block";
|
|
1422
1558
|
this.visualAnchor = null;
|
|
@@ -2612,58 +2748,207 @@ export class ChatView extends Widget {
|
|
|
2612
2748
|
return true;
|
|
2613
2749
|
}
|
|
2614
2750
|
|
|
2615
|
-
/** Blank session:
|
|
2751
|
+
/** Blank session: logo + brand/version rows + a bottom mode hint. The 4-mode
|
|
2752
|
+
* inline list is gone: mode selection lives in the F9 picker buffer, which
|
|
2753
|
+
* also supports custom presets (web parity: self-made modes encouraged). */
|
|
2616
2754
|
#renderWelcome(screen) {
|
|
2617
2755
|
const x = this.view.x;
|
|
2618
2756
|
const cx = x + Math.max(0, Math.floor((this.view.w - 40) / 2));
|
|
2619
|
-
|
|
2620
|
-
const put = (t, fg, bold) => { if (
|
|
2621
|
-
|
|
2757
|
+
const h = this.view.h, top = this.view.y + 1;
|
|
2758
|
+
const put = (t, fg, bold, yy) => { if (yy < top + h) screen.text(cx, yy, t, { fg, attrs: bold ? 1 : 0 }); };
|
|
2759
|
+
// 40x19 half-block logo, per mode: preset (bundled), custom (JSON file)
|
|
2760
|
+
// or none (title only). Centered on the 40-col block.
|
|
2622
2761
|
this.welcomeVersionRows = [];
|
|
2623
|
-
|
|
2762
|
+
// the logo only fits when the view is tall enough for its own grid;
|
|
2763
|
+
// otherwise the welcome falls back to the plain text brand lines.
|
|
2764
|
+
const needRows = this.app.logoMode === "none" ? 0
|
|
2765
|
+
: this.app.logoStyle === "ascii" && this.app.logoMode !== "custom" ? ASCII_LOGO_ROWS
|
|
2766
|
+
: this.app.logoMode === "custom" ? (this.app.logoData?.grid?.length ?? LOGO_ROWS)
|
|
2767
|
+
: (this.app.logoStyle === "ascii" ? ASCII_LOGO_ROWS : LOGO_ROWS);
|
|
2768
|
+
const logoH = this.view.w >= 62 && h >= needRows + 6 ? this.#renderWelcomeLogo(screen, cx, top) : 0;
|
|
2769
|
+
const logoDrawn = logoH > 0;
|
|
2770
|
+
// Brand lines: TUI-drawn half-block wordmarks (like the splash), each with
|
|
2771
|
+
// its clickable version/update row on the right; the diagonal shimmer
|
|
2772
|
+
// sweeps across every ~4s. Falls back to spaced text on short/narrow views.
|
|
2773
|
+
const brandY = top + (logoDrawn ? logoH + 1 : 0);
|
|
2774
|
+
const useWordmarks = h >= (logoH > 0 ? logoH + 13 : 32) && this.view.w >= 62;
|
|
2775
|
+
if (useWordmarks) {
|
|
2776
|
+
// The band must run PAST the right edge: the final I of DSH NEOTUI sits
|
|
2777
|
+
// at x0+57 (plus the ~0.6/row diagonal offset), so phase 1 lands ~x0+72.
|
|
2778
|
+
const bandX = this.app.brandShimmer >= 0
|
|
2779
|
+
? (x + Math.floor((this.view.w - 58) / 2) - 8 + this.app.brandShimmer * 88)
|
|
2780
|
+
: null;
|
|
2781
|
+
const glow = lerpColor(T.HEADING, 0xffffff, 0.18);
|
|
2782
|
+
const w1Y = brandY;
|
|
2783
|
+
drawHalfBlockWordmark(screen, x + Math.max(0, Math.floor((this.view.w - 47) / 2)), w1Y, "DEEPSEEK", glow, bandX, T.BG);
|
|
2784
|
+
this.#putVersionRight(screen, w1Y + 1, "dsh", this.app.dshVersion ?? "unknown", T.HEADING, true);
|
|
2785
|
+
const w2Y = w1Y + 4;
|
|
2786
|
+
const glide = T.FAINT; // dark enough for the white glint to pop
|
|
2787
|
+
drawHalfBlockWordmark(screen, x + Math.max(0, Math.floor((this.view.w - 58) / 2)), w2Y, "DSH NEOTUI", glide, bandX, T.BG);
|
|
2788
|
+
this.#putVersionRight(screen, w2Y + 1, "tui", TUI_VERSION, T.FAINT, false);
|
|
2789
|
+
// Update/check notices live at the bottom (only when not latest).
|
|
2790
|
+
this.#putUpdateHints(screen, top, h);
|
|
2791
|
+
} else {
|
|
2792
|
+
const versionLine = (name, version, key, fg, bold, yy) => {
|
|
2793
|
+
const spaced = [...name].join(" ");
|
|
2794
|
+
const text = `${spaced} ${version === "unknown" ? "版本未知" : `v${version}`}`;
|
|
2795
|
+
const tx = x + Math.max(0, Math.floor((this.view.w - strWidth(text)) / 2));
|
|
2796
|
+
if (yy < top + h) screen.text(tx, yy, text, { fg, attrs: bold ? 1 : 0 });
|
|
2797
|
+
this.welcomeVersionRows[yy] = { key, x1: tx, x2: tx + strWidth(text) - 1 };
|
|
2798
|
+
};
|
|
2799
|
+
versionLine("DEEPSEEK HARNESS", this.app.dshVersion ?? "unknown", "dsh", T.HEADING, true, brandY);
|
|
2800
|
+
versionLine("DSH NEOTUI", TUI_VERSION, "tui", T.FAINT, false, brandY + 1);
|
|
2801
|
+
this.#putUpdateHints(screen, top, h);
|
|
2802
|
+
}
|
|
2803
|
+
// Bottom hint: current preset + F9 opens the (custom-mode friendly) picker.
|
|
2804
|
+
if (this.app.currentSession != null) {
|
|
2805
|
+
const currentPreset = this.app.sessions.find((s) => s.sessionId === this.app.currentSession)?.agentPreset;
|
|
2806
|
+
const hint = `模式: ${modeName(currentPreset ?? "standard")} · F9 打开模式选择(支持自定义) · Ctrl+R 欢迎页 logo`;
|
|
2807
|
+
this.welcomeModeRow = top + h - 2;
|
|
2808
|
+
const hx = x + Math.max(0, Math.floor((this.view.w - strWidth(hint)) / 2));
|
|
2809
|
+
screen.text(hx, this.welcomeModeRow, hint, { fg: T.WARN, attrs: 1 });
|
|
2810
|
+
}
|
|
2811
|
+
}
|
|
2812
|
+
|
|
2813
|
+
/** The welcome mascot for the current logo mode/style. Pixel style renders
|
|
2814
|
+
* 40x19 2x2 quadrant blocks (80x38 effective); ascii style renders the
|
|
2815
|
+
* 52x24 character grid (aspect-true for 1:2 terminal cells). Returns the
|
|
2816
|
+
* drawn height in rows (0 = not drawn). */
|
|
2817
|
+
#renderWelcomeLogo(screen, cx, top) {
|
|
2818
|
+
if (this.app.logoMode === "none") return 0;
|
|
2819
|
+
const hr = this.view.y + this.view.h;
|
|
2820
|
+
if (this.app.logoStyle === "ascii" && this.app.logoMode !== "custom") {
|
|
2821
|
+
for (let r = 0; r < ASCII_LOGO_ROWS && top + r < hr; r++) {
|
|
2822
|
+
for (let c = 0; c < ASCII_LOGO_COLS; c++) {
|
|
2823
|
+
const i = (r * ASCII_LOGO_COLS + c) * 4;
|
|
2824
|
+
// empty cell = ramp 0 AND color 0; ramp 0 with a color = the '.' glyph
|
|
2825
|
+
const ci = parseInt(ASCII_LOGO_GRID.slice(i, i + 2), 16);
|
|
2826
|
+
const pi = parseInt(ASCII_LOGO_GRID.slice(i + 2, i + 4), 16);
|
|
2827
|
+
if (ci === 0 && pi === 0) continue;
|
|
2828
|
+
screen.put(cx - Math.floor((ASCII_LOGO_COLS - LOGO_COLS) / 2) + c, top + r,
|
|
2829
|
+
ASCII_LOGO_RAMP[ci], { fg: pi ? parseInt(ASCII_LOGO_PALETTE[pi - 1], 16) : T.FAINT, attrs: 1 });
|
|
2830
|
+
}
|
|
2831
|
+
}
|
|
2832
|
+
return ASCII_LOGO_ROWS;
|
|
2833
|
+
}
|
|
2834
|
+
const custom = this.app.logoMode === "custom";
|
|
2835
|
+
let pal, grid, quadrant = !custom;
|
|
2836
|
+
if (custom) {
|
|
2837
|
+
if (!this.app.logoData) {
|
|
2838
|
+
if (this.app.logoPath) {
|
|
2839
|
+
try { this.app.logoData = this.app.loadCustomLogo(this.app.logoPath); }
|
|
2840
|
+
catch { this.app.logoData = null; } // invalid file → title-only
|
|
2841
|
+
}
|
|
2842
|
+
if (!this.app.logoData) return false;
|
|
2843
|
+
}
|
|
2844
|
+
pal = this.app.logoData.palette; grid = this.app.logoData.grid;
|
|
2845
|
+
quadrant = this.app.logoData.quadrant !== false;
|
|
2846
|
+
} else {
|
|
2847
|
+
pal = LOGO_PALETTE; grid = LOGO_GRID;
|
|
2848
|
+
}
|
|
2849
|
+
// mask → glyph: bits tl/tr/bl/br (quadrant blocks render width 1).
|
|
2850
|
+
const GL = [" ", "▘", "▝", "▀", "▖", "▌", "▞", "▛", "▗", "▚", "▐", "▜", "▄", "▙", "▟", "█"];
|
|
2851
|
+
const rows = custom ? grid.length : LOGO_ROWS;
|
|
2852
|
+
const cols = custom ? (grid[0]?.length ?? LOGO_COLS) : LOGO_COLS;
|
|
2853
|
+
for (let r = 0; r < rows && top + r < hr; r++) {
|
|
2854
|
+
for (let c = 0; c < cols; c++) {
|
|
2855
|
+
let kind, mask, fi, bi;
|
|
2856
|
+
if (quadrant) {
|
|
2857
|
+
const cell = custom ? grid[r]?.[c] : null;
|
|
2858
|
+
if (cell) { kind = cell.length === 4 ? cell[0] : 0; mask = cell.length === 4 ? cell[1] : cell[0]; fi = cell.length === 4 ? cell[2] : cell[1]; bi = cell.length === 4 ? cell[3] : cell[2]; }
|
|
2859
|
+
else {
|
|
2860
|
+
// each cell = 4 bytes (type,mask,fg,bg) = 8 hex chars; type 0 =
|
|
2861
|
+
// quadrant (low nibble character quads, high nibble 2nd-color
|
|
2862
|
+
// quads), type 1 = braille (mouth region, 2x4 dots)
|
|
2863
|
+
const i = (r * LOGO_COLS + c) * 8;
|
|
2864
|
+
kind = parseInt(grid.slice(i, i + 2), 16);
|
|
2865
|
+
mask = parseInt(grid.slice(i + 2, i + 4), 16);
|
|
2866
|
+
fi = parseInt(grid.slice(i + 4, i + 6), 16);
|
|
2867
|
+
bi = parseInt(grid.slice(i + 6, i + 8), 16);
|
|
2868
|
+
}
|
|
2869
|
+
if (kind === 0 && mask === 0 && fi === 0 && bi === 0) continue;
|
|
2870
|
+
const fgc = fi ? (custom ? pal[fi] : parseInt(pal[fi - 1], 16)) : null;
|
|
2871
|
+
const bgc = bi ? (custom ? pal[bi] : parseInt(pal[bi - 1], 16)) : null;
|
|
2872
|
+
if (kind === 1) {
|
|
2873
|
+
// braille mouth cell: 8 dots (2 cols x 4 rows), fg/bg pair
|
|
2874
|
+
screen.put(cx + c, top + r, String.fromCharCode(0x2800 | (mask & 0xff)),
|
|
2875
|
+
{ fg: fgc ?? T.BG, bg: bgc ?? T.BG });
|
|
2876
|
+
continue;
|
|
2877
|
+
}
|
|
2878
|
+
if (kind === 2) {
|
|
2879
|
+
// half-block: one cell = one full-width pixel pair (top/bottom)
|
|
2880
|
+
screen.put(cx + c, top + r, "▀", { fg: fgc ?? T.BG, bg: bgc ?? T.BG });
|
|
2881
|
+
continue;
|
|
2882
|
+
}
|
|
2883
|
+
const fm = mask & 0x0f, cm = (mask >> 4) & 0x0f;
|
|
2884
|
+
if (fm === 0) continue;
|
|
2885
|
+
if (fm === 0x0f && bgc != null) {
|
|
2886
|
+
// full cell: second-color quads render as glyph bg → true 2×2.
|
|
2887
|
+
const glyph = GL[(~cm) & 0x0f];
|
|
2888
|
+
screen.put(cx + c, top + r, glyph, { fg: fgc ?? T.BG, bg: bgc });
|
|
2889
|
+
} else {
|
|
2890
|
+
// partial: character quads in fg over the transparent background.
|
|
2891
|
+
screen.put(cx + c, top + r, GL[fm], { fg: fgc ?? T.BG, bg: T.BG });
|
|
2892
|
+
}
|
|
2893
|
+
} else {
|
|
2894
|
+
const cell = custom ? grid[r][c] : null;
|
|
2895
|
+
let topIdx, botIdx;
|
|
2896
|
+
if (cell) { topIdx = cell[0]; botIdx = cell[1]; }
|
|
2897
|
+
else {
|
|
2898
|
+
const i = (r * LOGO_COLS + c) * 4;
|
|
2899
|
+
topIdx = parseInt(grid.slice(i, i + 2), 16);
|
|
2900
|
+
botIdx = parseInt(grid.slice(i + 2, i + 4), 16);
|
|
2901
|
+
}
|
|
2902
|
+
const emptyT = custom ? topIdx < 0 : topIdx === 0;
|
|
2903
|
+
const emptyB = custom ? botIdx < 0 : botIdx === 0;
|
|
2904
|
+
if (emptyT && emptyB) continue;
|
|
2905
|
+
const pal32 = (pi) => (custom ? pal[pi] : parseInt(pal[pi], 16));
|
|
2906
|
+
const tcol = emptyT ? null : pal32(custom ? topIdx : topIdx - 1);
|
|
2907
|
+
const bcol = emptyB ? null : pal32(custom ? botIdx : botIdx - 1);
|
|
2908
|
+
const fgc = tcol ?? bcol ?? T.BG;
|
|
2909
|
+
const bgc = bcol ?? T.BG;
|
|
2910
|
+
screen.put(cx + c, top + r, tcol != null ? "▀" : "▄", { fg: fgc, bg: bgc });
|
|
2911
|
+
}
|
|
2912
|
+
}
|
|
2913
|
+
}
|
|
2914
|
+
return LOGO_ROWS;
|
|
2915
|
+
}
|
|
2916
|
+
|
|
2917
|
+
/** Bottom update notices: only shown when a product has a pending update
|
|
2918
|
+
* (or a failed/ongoing check) — "已是最新" stays quiet. Clickable. */
|
|
2919
|
+
#putUpdateHints(screen, top, h) {
|
|
2920
|
+
this.welcomeUpdateRows = [];
|
|
2921
|
+
const names = { dsh: "DeepSeek Harness", tui: "dsh-neotui" };
|
|
2922
|
+
const parts = [];
|
|
2923
|
+
for (const key of ["dsh", "tui"]) {
|
|
2624
2924
|
const check = this.app.versionChecks?.[key];
|
|
2625
|
-
|
|
2626
|
-
|
|
2627
|
-
|
|
2628
|
-
|
|
2629
|
-
: "← 检查更新";
|
|
2630
|
-
const text = ` ${name} ${version === "unknown" ? "版本未知" : `v${version}`} ${status}`;
|
|
2631
|
-
put(text, fg, bold);
|
|
2632
|
-
this.welcomeVersionRows[y - 1] = { key, x1: cx, x2: cx + strWidth(text) - 1 };
|
|
2633
|
-
};
|
|
2634
|
-
versionLine("DeepSeek Harness", this.app.dshVersion ?? "unknown", "dsh", T.HEADING, true);
|
|
2635
|
-
versionLine("dsh-neotui", TUI_VERSION, "tui", T.FAINT, false);
|
|
2636
|
-
put("", 0, false);
|
|
2637
|
-
if (this.app.currentSession == null) {
|
|
2638
|
-
put(" 打开一个会话开始,或 Ctrl+N 新建", T.DIM, false);
|
|
2639
|
-
return;
|
|
2925
|
+
if (!check) continue;
|
|
2926
|
+
if (check.state === "update") parts.push({ key, text: `${names[key]} 可更新 ${check.latest}` });
|
|
2927
|
+
else if (check.state === "error") parts.push({ key, text: `${names[key]} 检查失败(点击重试)` });
|
|
2928
|
+
else if (check.state === "checking") parts.push({ key, text: `${names[key]} 检查中…` });
|
|
2640
2929
|
}
|
|
2641
|
-
|
|
2642
|
-
|
|
2643
|
-
this.
|
|
2644
|
-
const
|
|
2645
|
-
const
|
|
2646
|
-
|
|
2647
|
-
|
|
2648
|
-
|
|
2649
|
-
|
|
2650
|
-
|
|
2651
|
-
const currentIdx = presets.findIndex(([id]) => id === currentPreset);
|
|
2652
|
-
if (this.welcomeModeSel == null || this.welcomeModeSel >= presets.length) this.welcomeModeSel = currentIdx >= 0 ? currentIdx : 0;
|
|
2653
|
-
for (let i = 0; i < presets.length; i++) {
|
|
2654
|
-
const [id, name, desc] = presets[i];
|
|
2655
|
-
if (y < this.view.y + this.view.h) {
|
|
2656
|
-
const active = id === currentPreset;
|
|
2657
|
-
const cursor = this.app.focused === this && i === this.welcomeModeSel;
|
|
2658
|
-
const label = `${cursor ? "=>" : " "} ${active ? "●" : "○"} ${name}${active ? " [当前]" : ""}`;
|
|
2659
|
-
screen.text(cx, y, ` ${label}`, { fg: active ? T.OK : cursor ? T.ACCENT : T.DIM, bg: cursor ? T.MENUSEL : -1, attrs: active || cursor ? 1 : 0 });
|
|
2660
|
-
screen.text(cx + 2 + strWidth(label) + 1, y, truncate(desc, Math.max(1, this.view.w - strWidth(label) - 8)), { fg: cursor ? T.TXT : T.DIM, bg: cursor ? T.MENUSEL : -1 });
|
|
2661
|
-
this.welcomeModes[y] = id;
|
|
2662
|
-
}
|
|
2663
|
-
y++;
|
|
2930
|
+
if (!parts.length) return;
|
|
2931
|
+
const row = top + h - 3;
|
|
2932
|
+
this.welcomeUpdateRow = row;
|
|
2933
|
+
const full = parts.map((p) => p.text).join(" · ");
|
|
2934
|
+
const tx = this.view.x + Math.max(0, Math.floor((this.view.w - strWidth(full)) / 2));
|
|
2935
|
+
let at = tx;
|
|
2936
|
+
for (const p of parts) {
|
|
2937
|
+
if (row < top + h) screen.text(at, row, p.text, { fg: T.WARN, attrs: 1 });
|
|
2938
|
+
this.welcomeUpdateRows.push({ key: p.key, x1: at, x2: at + strWidth(p.text) - 1 });
|
|
2939
|
+
at += strWidth(p.text) + 3;
|
|
2664
2940
|
}
|
|
2665
2941
|
}
|
|
2666
2942
|
|
|
2943
|
+
/** Right-aligned version row for the wordmark brand lines (clickable check;
|
|
2944
|
+
* update notices themselves live at the bottom of the welcome). */
|
|
2945
|
+
#putVersionRight(screen, y, key, version, fg, bold) {
|
|
2946
|
+
const text = version === "unknown" ? "版本未知" : `v${version}`;
|
|
2947
|
+
const tx = this.view.x + Math.max(0, this.view.w - strWidth(text) - 2);
|
|
2948
|
+
if (y < this.view.y + this.view.h) screen.text(tx, y, text, { fg, attrs: bold ? 1 : 0 });
|
|
2949
|
+
this.welcomeVersionRows[y] = { key, x1: tx, x2: tx + strWidth(text) - 1 };
|
|
2950
|
+
}
|
|
2951
|
+
|
|
2667
2952
|
render(screen) {
|
|
2668
2953
|
// Blank session: show the welcome + mode prompt instead of empty chat.
|
|
2669
2954
|
const isBlank = this.app.sessions.find((s) => s.sessionId === this.sessionId)?.blank ?? false;
|
|
@@ -2837,12 +3122,14 @@ export class ChatView extends Widget {
|
|
|
2837
3122
|
if (this.view.inside(ev.x, ev.y)) {
|
|
2838
3123
|
// clicks act, but never exit INSERT mode — Esc is the only way out
|
|
2839
3124
|
if (this.app.focused !== this.app.chat?.input) this.app.focus(this);
|
|
2840
|
-
// Welcome-screen
|
|
3125
|
+
// Welcome-screen clicks: version rows check updates; the mode hint opens
|
|
3126
|
+
// the custom-mode-friendly picker buffer.
|
|
2841
3127
|
if (this.nodes.length === 0 && ev.kind === "press" && ev.button === 0) {
|
|
2842
3128
|
const versionHit = this.welcomeVersionRows?.[ev.y];
|
|
2843
3129
|
if (versionHit && ev.x >= versionHit.x1 && ev.x <= versionHit.x2) { this.app.checkUpdates(versionHit.key, true); return true; }
|
|
2844
|
-
const
|
|
2845
|
-
if (
|
|
3130
|
+
const updateHit = this.welcomeUpdateRows?.find((r) => ev.y === this.welcomeUpdateRow && ev.x >= r.x1 && ev.x <= r.x2);
|
|
3131
|
+
if (updateHit) { this.app.checkUpdates(updateHit.key, true); return true; }
|
|
3132
|
+
if (ev.y === this.welcomeModeRow && ev.x >= cx && ev.x <= cx + 40) { this.app.showModePicker(); return true; }
|
|
2846
3133
|
}
|
|
2847
3134
|
if (ev.kind === "wheel-up" && this.view.scrollY <= 3 && this.hasMore) { void this.loadOlder(); return true; }
|
|
2848
3135
|
if (ev.kind === "press" && ev.button === 0) {
|
|
@@ -2953,15 +3240,6 @@ export class ChatView extends Widget {
|
|
|
2953
3240
|
}
|
|
2954
3241
|
|
|
2955
3242
|
onKey(ev) {
|
|
2956
|
-
const blankWelcome = this.nodes.length === 0 && (this.app.sessions.find((s) => s.sessionId === this.sessionId)?.blank ?? false);
|
|
2957
|
-
if (blankWelcome && ev.type === "key" && (ev.name === "up" || ev.name === "down")) {
|
|
2958
|
-
this.welcomeModeSel = wrapIndex(this.welcomeModeSel + (ev.name === "up" ? -1 : 1), this.welcomeModeIds.length);
|
|
2959
|
-
return true;
|
|
2960
|
-
}
|
|
2961
|
-
if (blankWelcome && ev.type === "key" && ev.name === "enter") {
|
|
2962
|
-
this.app.selectPreset(this.welcomeModeIds[this.welcomeModeSel]);
|
|
2963
|
-
return true;
|
|
2964
|
-
}
|
|
2965
3243
|
if (ev.type === "text" || ev.type === "paste") {
|
|
2966
3244
|
// In cursor/Visual modes text is never inserted into the transcript or
|
|
2967
3245
|
// silently redirected to INSERT. Only explicit i enters the input editor.
|
|
@@ -3335,12 +3613,24 @@ export class ApprovalPopup extends Popup {
|
|
|
3335
3613
|
// ---- App ----
|
|
3336
3614
|
|
|
3337
3615
|
export class App {
|
|
3338
|
-
constructor({ screen, term, api, base, log, versionFetcher = latestNpmVersion }) {
|
|
3616
|
+
constructor({ screen, term, api, base, log, versionFetcher = latestNpmVersion, launcherAnime = false }) {
|
|
3339
3617
|
this.screen = screen;
|
|
3340
3618
|
this.term = term;
|
|
3341
3619
|
this.api = api;
|
|
3342
3620
|
this.log = log ?? (() => {});
|
|
3343
3621
|
this.versionFetcher = versionFetcher;
|
|
3622
|
+
this.launcherAnime = launcherAnime; // opt-in boot animation (--launcher-anime)
|
|
3623
|
+
// Welcome logo mode: "preset" (bundled mascot), "custom" (JSON file) or
|
|
3624
|
+
// "none" (title only). Persisted in the tui config file.
|
|
3625
|
+
const logoCfg = loadTuiConfig().logo ?? {};
|
|
3626
|
+
this.logoMode = ["preset", "custom", "none"].includes(logoCfg.mode) ? logoCfg.mode : "none"; // mascot off by default; Ctrl+R re-enables
|
|
3627
|
+
this.logoStyle = logoCfg.style === "ascii" ? "ascii" : "pixel"; // pixel = quadrant blocks
|
|
3628
|
+
this.logoPath = logoCfg.path ?? null;
|
|
3629
|
+
this.logoData = null; // decoded custom logo: { palette, grid }
|
|
3630
|
+
this.logoLoadError = null;
|
|
3631
|
+
this.brandShimmer = -1; // welcome wordmark diagonal sweep phase (-1 = idle)
|
|
3632
|
+
this.brandSweep0 = -1;
|
|
3633
|
+
this.brandShimmerNext = null;
|
|
3344
3634
|
this.popup = null;
|
|
3345
3635
|
this.activePrompt = null;
|
|
3346
3636
|
this.promptQueue = [];
|
|
@@ -4237,7 +4527,108 @@ export class App {
|
|
|
4237
4527
|
showGoal() { this.overlay = buildGoalPopup(this); this.redraw(); }
|
|
4238
4528
|
showModePicker() { this.overlay = buildModePicker(this); this.redraw(); }
|
|
4239
4529
|
showThemePicker() { this.overlay = new ThemePickerBuffer(this); this.redraw(); }
|
|
4240
|
-
|
|
4530
|
+
|
|
4531
|
+
/** Welcome-logo picker (Ctrl+R / 控制面板): preset, custom file, or none. */
|
|
4532
|
+
showLogoPicker() {
|
|
4533
|
+
const mark = (m) => (this.logoMode === m ? "● " : "○ ");
|
|
4534
|
+
const items = [
|
|
4535
|
+
{ label: `${mark("preset")}预设`, hint: "DeepSeek 吉祥物(内置)", action: "preset" },
|
|
4536
|
+
{ label: `${mark("custom")}自定义…`, hint: `选择 JSON 文件(当前: ${this.logoPath ?? "—"})`, action: "custom" },
|
|
4537
|
+
{ label: `${mark("none")}关闭`, hint: "不显示 logo,仅保留品牌字标", action: "none" },
|
|
4538
|
+
{ label: `${this.logoStyle === "ascii" ? "◉" : "○"} 字符样式`, hint: this.logoStyle === "ascii" ? "当前: ASCII 52×24(回车切回像素)" : "当前: 像素 80×38(回车切换为字符画)", action: "style" },
|
|
4539
|
+
];
|
|
4540
|
+
const pick = async (action) => {
|
|
4541
|
+
if (action === "preset" || action === "none") {
|
|
4542
|
+
this.logoMode = action;
|
|
4543
|
+
this.logoData = null;
|
|
4544
|
+
this.logoLoadError = null;
|
|
4545
|
+
saveTuiConfig({ logo: { mode: action, path: action === "preset" ? null : this.logoPath, style: this.logoStyle } });
|
|
4546
|
+
this.closeOverlay();
|
|
4547
|
+
this.redraw();
|
|
4548
|
+
} else if (action === "style") {
|
|
4549
|
+
this.logoStyle = this.logoStyle === "ascii" ? "pixel" : "ascii";
|
|
4550
|
+
saveTuiConfig({ logo: { mode: this.logoMode, path: this.logoPath, style: this.logoStyle } });
|
|
4551
|
+
this.closeOverlay();
|
|
4552
|
+
this.redraw();
|
|
4553
|
+
} else if (action === "custom") {
|
|
4554
|
+
// Reuse the shared Ctrl+O / 新建工作区 picker (UploadPicker) in its
|
|
4555
|
+
// single-select mode — same UI, same navigation, one file only.
|
|
4556
|
+
const dir = this.logoPath ? dirname(this.logoPath) : (this.sessions.find((s) => s.sessionId === this.currentSession)?.cwd ?? process.cwd());
|
|
4557
|
+
const picker = new UploadPicker(this, {
|
|
4558
|
+
startPath: dir,
|
|
4559
|
+
single: true,
|
|
4560
|
+
onPickFile: (path) => this.#applyCustomLogo(path),
|
|
4561
|
+
onCancel: () => { this.overlay = null; this.focus(this.chat); this.redraw(); },
|
|
4562
|
+
});
|
|
4563
|
+
this.overlay = picker; this.focus(picker); this.redraw();
|
|
4564
|
+
}
|
|
4565
|
+
};
|
|
4566
|
+
const w = Math.max(1, Math.min(66, this.screen.w - 4)), ph = Math.max(1, Math.min(12, this.screen.h - 4));
|
|
4567
|
+
this.overlay = new Picker({
|
|
4568
|
+
x: Math.floor((this.screen.w - w) / 2), y: Math.floor((this.screen.h - ph) / 2),
|
|
4569
|
+
w, h: ph, title: "欢迎页 logo(Ctrl+R)",
|
|
4570
|
+
items,
|
|
4571
|
+
onPick: (it) => void pick(it.action),
|
|
4572
|
+
onCancel: () => this.closeOverlay(),
|
|
4573
|
+
});
|
|
4574
|
+
this.redraw();
|
|
4575
|
+
}
|
|
4576
|
+
|
|
4577
|
+
/** Load + validate a custom logo JSON ({ palette, grid }, 40x19) and switch
|
|
4578
|
+
* to it. Accepted schema mirrors the generator's logo-data.json; palette
|
|
4579
|
+
* entries may be [r,g,b] arrays or "#rrggbb"/"rrggbb" strings. Returns the
|
|
4580
|
+
* normalized data or throws with a user-facing message. */
|
|
4581
|
+
loadCustomLogo(path) {
|
|
4582
|
+
const data = JSON.parse(readFileSync(path, "utf8"));
|
|
4583
|
+
const pal = data?.palette, grid = data?.grid;
|
|
4584
|
+
const rows = grid?.length, cols = grid?.[0]?.length;
|
|
4585
|
+
if (!Array.isArray(pal) || !Array.isArray(grid) || (rows !== LOGO_ROWS && rows !== 19)
|
|
4586
|
+
|| grid.some((row) => !Array.isArray(row) || row.length !== cols)
|
|
4587
|
+
|| (cols !== LOGO_COLS && cols !== 40)
|
|
4588
|
+
|| pal.length < 1 || pal.length > 65535) {
|
|
4589
|
+
throw new Error(`需要 { palette:[…], grid:[...] } 且为 58×27 或 40×19(得到 ${rows ?? "?"}×${cols ?? "?"})`);
|
|
4590
|
+
}
|
|
4591
|
+
const norm = pal.map((c) => {
|
|
4592
|
+
if (Array.isArray(c) && c.length >= 3) return ((c[0] << 16) | (c[1] << 8) | c[2]) >>> 0;
|
|
4593
|
+
if (typeof c === "string") return parseInt(c.replace("#", ""), 16);
|
|
4594
|
+
return NaN;
|
|
4595
|
+
});
|
|
4596
|
+
if (norm.some((c) => Number.isNaN(c) || c < 0 || c > 0xffffff)) throw new Error("调色板条目必须是 [r,g,b] 或 #rrggbb");
|
|
4597
|
+
// Cells: [top, bottom] = half-block (legacy) or [mask, fg, bg] = quadrant.
|
|
4598
|
+
const fmt = grid[0][0]?.length === 3 ? "quadrant" : "half";
|
|
4599
|
+
for (const row of grid) {
|
|
4600
|
+
for (const cell of row) {
|
|
4601
|
+
if (!Array.isArray(cell) || !Number.isInteger(cell[0])) throw new Error("grid 单元必须是数组");
|
|
4602
|
+
if (fmt === "quadrant") {
|
|
4603
|
+
if (cell.length !== 3 || cell[0] < 0 || cell[0] > 0xff || cell[1] < -1 || cell[2] < -1
|
|
4604
|
+
|| cell[1] >= norm.length || cell[2] >= norm.length) {
|
|
4605
|
+
throw new Error("quadrant 单元必须是 [maskByte, fg, bg] 且索引在调色板范围内(mask 低四位=角色象限,高四位=第二色象限)");
|
|
4606
|
+
}
|
|
4607
|
+
} else {
|
|
4608
|
+
if (cell.length !== 2 || cell[0] < -1 || cell[1] < -1
|
|
4609
|
+
|| cell[0] >= norm.length || cell[1] >= norm.length || !Number.isInteger(cell[1])) {
|
|
4610
|
+
throw new Error("half 单元必须是 [top, bottom] 且索引在调色板范围内");
|
|
4611
|
+
}
|
|
4612
|
+
}
|
|
4613
|
+
}
|
|
4614
|
+
}
|
|
4615
|
+
return { palette: norm, grid, quadrant: fmt === "quadrant" };
|
|
4616
|
+
}
|
|
4617
|
+
|
|
4618
|
+
#applyCustomLogo(path) {
|
|
4619
|
+
try {
|
|
4620
|
+
this.logoData = this.loadCustomLogo(path);
|
|
4621
|
+
this.logoMode = "custom";
|
|
4622
|
+
this.logoPath = path;
|
|
4623
|
+
this.logoLoadError = null;
|
|
4624
|
+
saveTuiConfig({ logo: { mode: "custom", path, style: this.logoStyle } });
|
|
4625
|
+
this.closeOverlay();
|
|
4626
|
+
this.redraw();
|
|
4627
|
+
this.toast("自定义 logo 已应用");
|
|
4628
|
+
} catch (e) {
|
|
4629
|
+
this.toast(`logo 文件无效: ${e.message}`);
|
|
4630
|
+
}
|
|
4631
|
+
}
|
|
4241
4632
|
|
|
4242
4633
|
/** /reload: in-place soft reload — fresh session list, fresh chat history,
|
|
4243
4634
|
* panels rebuilt, screen re-rendered. No process churn, no terminal
|
|
@@ -4446,6 +4837,7 @@ export class App {
|
|
|
4446
4837
|
case "commandPalette": this.overlay = new ControlPanel(this, { startPage: 1 }); this.redraw(); return true;
|
|
4447
4838
|
case "modePicker": this.showModePicker(); return true;
|
|
4448
4839
|
case "themePicker": this.showThemePicker(); return true;
|
|
4840
|
+
case "logoPicker": this.showLogoPicker(); return true;
|
|
4449
4841
|
default: return false;
|
|
4450
4842
|
}
|
|
4451
4843
|
}
|
|
@@ -4551,6 +4943,16 @@ export class App {
|
|
|
4551
4943
|
|
|
4552
4944
|
onEvent(ev) {
|
|
4553
4945
|
if (ev.type === "resize") { this.resize(ev.w, ev.h); return; }
|
|
4946
|
+
// Retro launcher splash: every key/mouse press enters the game. Events
|
|
4947
|
+
// are swallowed while the blinking 按任意键提示 is on screen.
|
|
4948
|
+
if (this.splashWaiting) {
|
|
4949
|
+
if (ev.type === "key" || ev.type === "text" || ev.type === "paste" || ev.type === "mouse") {
|
|
4950
|
+
this.splashWaiting = false;
|
|
4951
|
+
this.screen.prev = null; // force a full repaint of the real UI
|
|
4952
|
+
this.redraw();
|
|
4953
|
+
}
|
|
4954
|
+
return;
|
|
4955
|
+
}
|
|
4554
4956
|
if (this.swallowRelease && ev.type === "mouse" && ev.kind === "release") {
|
|
4555
4957
|
this.swallowRelease = false;
|
|
4556
4958
|
return; // a press just closed an overlay; eat its matching release
|
|
@@ -5429,9 +5831,63 @@ export class App {
|
|
|
5429
5831
|
|
|
5430
5832
|
// ---- main loop ----
|
|
5431
5833
|
|
|
5834
|
+
/** DeepSeek-style boot splash: white background, DEEPSEEK wordmark fading
|
|
5835
|
+
* in, pulsing 启动中 dot, then dissolve into the theme background.
|
|
5836
|
+
* Opt-in meme: enable with --launcher-anime / DSH_TUI_LAUNCHER_ANIME=1;
|
|
5837
|
+
* DSH_TUI_NO_SPLASH=1 always wins. */
|
|
5838
|
+
playSplash() {
|
|
5839
|
+
if (!this.launcherAnime) return;
|
|
5840
|
+
if (process.env.DSH_TUI_NO_SPLASH === "1") return;
|
|
5841
|
+
if (!this.term?.output?.write) return;
|
|
5842
|
+
const w = this.screen.w, h = this.screen.h;
|
|
5843
|
+
if (w < 30 || h < 12) return;
|
|
5844
|
+
const wait = (ms) => { if (this.splashDelay ?? 30) { const sab = new Int32Array(new SharedArrayBuffer(4)); Atomics.wait(sab, 0, 0, ms); } };
|
|
5845
|
+
const frames = (steps, dur, draw) => {
|
|
5846
|
+
for (let i = 0; i < steps; i++) {
|
|
5847
|
+
draw(i / Math.max(1, steps - 1), i);
|
|
5848
|
+
this.term.output.write(this.screen.render() + "\x1b[?25l");
|
|
5849
|
+
wait(dur);
|
|
5850
|
+
}
|
|
5851
|
+
};
|
|
5852
|
+
const ease = (t) => (t < 0.5 ? 2 * t * t : 1 - Math.pow(-2 * t + 2, 2) / 2);
|
|
5853
|
+
const clamp01 = (t) => Math.max(0, Math.min(1, t));
|
|
5854
|
+
// 1s: white circle diffuses from the screen center over pure black.
|
|
5855
|
+
frames(25, this.splashDelay ?? 40, (t) => drawBootSplash(this.screen, { diffuse: ease(t) }));
|
|
5856
|
+
// 2s: the four healthy-gaming lines fade in one sentence at a time.
|
|
5857
|
+
frames(40, this.splashDelay ?? 50, (t) => {
|
|
5858
|
+
const lineAlpha = [];
|
|
5859
|
+
for (let i = 0; i < SPLASH_LINES.length; i++) lineAlpha.push(ease(clamp01((t - i * 0.25) / 0.25)));
|
|
5860
|
+
drawBootSplash(this.screen, { diffuse: 1, lineAlpha });
|
|
5861
|
+
});
|
|
5862
|
+
// 2s: lines dissolve out while DEEPSEEK fades in (crossfade).
|
|
5863
|
+
frames(40, this.splashDelay ?? 50, (t) => drawBootSplash(this.screen, {
|
|
5864
|
+
diffuse: 1, lineAlpha: SPLASH_LINES.map(() => 1 - t), wordAlpha: ease(t),
|
|
5865
|
+
}));
|
|
5866
|
+
// Wait for any key: blinking prompt at the bottom; the tick loop toggles it.
|
|
5867
|
+
this.splashWaiting = true;
|
|
5868
|
+
this.splashBlink = true;
|
|
5869
|
+
this.splashBlinkAt = Date.now();
|
|
5870
|
+
drawBootSplash(this.screen, { diffuse: 1, wordAlpha: 1, blink: this.splashBlink });
|
|
5871
|
+
this.term.output.write(this.screen.render() + "\x1b[?25l");
|
|
5872
|
+
}
|
|
5873
|
+
|
|
5432
5874
|
run() {
|
|
5875
|
+
this.playSplash();
|
|
5433
5876
|
const tick = () => {
|
|
5434
5877
|
try {
|
|
5878
|
+
this.tickWelcomeShimmer(Date.now());
|
|
5879
|
+
// While the splash waits for a key, only the blinking prompt is
|
|
5880
|
+
// repainted — the real UI must not render underneath it.
|
|
5881
|
+
if (this.splashWaiting) {
|
|
5882
|
+
const now = Date.now();
|
|
5883
|
+
if (now - (this.splashBlinkAt ?? 0) >= 400) {
|
|
5884
|
+
this.splashBlinkAt = now;
|
|
5885
|
+
this.splashBlink = !this.splashBlink;
|
|
5886
|
+
drawBootSplash(this.screen, { diffuse: 1, wordAlpha: 1, blink: this.splashBlink });
|
|
5887
|
+
this.term?.output?.write?.(this.screen.render() + "\x1b[?25l");
|
|
5888
|
+
}
|
|
5889
|
+
return;
|
|
5890
|
+
}
|
|
5435
5891
|
if (this.dirty) {
|
|
5436
5892
|
this.dirty = false;
|
|
5437
5893
|
this.renderFrame();
|
|
@@ -5463,6 +5919,33 @@ export class App {
|
|
|
5463
5919
|
tick();
|
|
5464
5920
|
}
|
|
5465
5921
|
|
|
5922
|
+
/** Welcome brand shimmer: a diagonal sweep across the wordmarks (~0.85s every
|
|
5923
|
+
* ~3.5s) while a tall blank welcome is on screen. Separated from the tick
|
|
5924
|
+
* loop so tests can drive it with fake timestamps. */
|
|
5925
|
+
tickWelcomeShimmer(now) {
|
|
5926
|
+
const logoH = this.logoStyle === "ascii" ? ASCII_LOGO_ROWS : this.logoMode === "none" ? 0 : LOGO_ROWS;
|
|
5927
|
+
const welcomeUp = this.chat.nodes.length === 0
|
|
5928
|
+
&& (this.sessions.find((s) => s.sessionId === this.currentSession)?.blank ?? false)
|
|
5929
|
+
&& this.chat.view.h >= logoH + 13 && this.chat.view.w >= 62;
|
|
5930
|
+
if (!welcomeUp) {
|
|
5931
|
+
if (this.brandSweep0 >= 0 || this.brandShimmer >= 0) { this.brandSweep0 = -1; this.brandShimmer = -1; }
|
|
5932
|
+
return;
|
|
5933
|
+
}
|
|
5934
|
+
if (this.brandSweep0 >= 0) {
|
|
5935
|
+
// 900ms so the very last tick reaches phase ~1.0 (the band's far end).
|
|
5936
|
+
if (now - this.brandSweep0 > 900) { this.brandSweep0 = -1; this.brandShimmer = -1; this.dirty = true; }
|
|
5937
|
+
else {
|
|
5938
|
+
const p = (now - this.brandSweep0) / 900;
|
|
5939
|
+
if (p !== this.brandShimmer) { this.brandShimmer = p; this.dirty = true; }
|
|
5940
|
+
}
|
|
5941
|
+
} else {
|
|
5942
|
+
if (this.brandShimmerNext == null) this.brandShimmerNext = now + 1500;
|
|
5943
|
+
if (now >= this.brandShimmerNext) {
|
|
5944
|
+
this.brandSweep0 = now; this.brandShimmer = 0; this.brandShimmerNext = now + 3500; this.dirty = true;
|
|
5945
|
+
}
|
|
5946
|
+
}
|
|
5947
|
+
}
|
|
5948
|
+
|
|
5466
5949
|
/** Force one render (also used by the scripted test harness). */
|
|
5467
5950
|
renderFrame() {
|
|
5468
5951
|
this.chat.flushRebuild();
|