skyloom 1.13.5 → 1.13.7
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/.github/workflows/ci.yml +36 -36
- package/README.md +220 -159
- package/config/providers.yaml +39 -39
- package/config/skills/api_integrator/SKILL.md +15 -15
- package/config/skills/arch_designer/SKILL.md +13 -13
- package/config/skills/ci_cd_manager/SKILL.md +14 -14
- package/config/skills/code_analysis/SKILL.md +13 -13
- package/config/skills/code_generator/SKILL.md +12 -12
- package/config/skills/code_reviewer/SKILL.md +13 -13
- package/config/skills/content_writer/SKILL.md +14 -14
- package/config/skills/data_transformer/SKILL.md +15 -15
- package/config/skills/document_analysis/SKILL.md +13 -13
- package/config/skills/emotional_companion/SKILL.md +15 -15
- package/config/skills/performance_checker/SKILL.md +14 -14
- package/config/skills/security_auditor/SKILL.md +14 -14
- package/config/skills/self_evolve/SKILL.md +13 -13
- package/config/skills/sys_operator/SKILL.md +15 -15
- package/config/skills/task_planner/SKILL.md +14 -14
- package/config/skills/web_research/SKILL.md +14 -14
- package/config/skills/workflow_designer/SKILL.md +13 -13
- package/dist/agents/dew.js +52 -52
- package/dist/agents/fair.js +84 -84
- package/dist/agents/fog.js +30 -30
- package/dist/agents/frost.js +32 -32
- package/dist/agents/rain.js +32 -32
- package/dist/agents/snow.js +68 -68
- package/dist/cli/commands_md.d.ts +41 -0
- package/dist/cli/commands_md.d.ts.map +1 -0
- package/dist/cli/commands_md.js +140 -0
- package/dist/cli/commands_md.js.map +1 -0
- package/dist/cli/input_macros.d.ts +28 -0
- package/dist/cli/input_macros.d.ts.map +1 -0
- package/dist/cli/input_macros.js +120 -0
- package/dist/cli/input_macros.js.map +1 -0
- package/dist/cli/loom.d.ts +220 -0
- package/dist/cli/loom.d.ts.map +1 -0
- package/dist/cli/loom.js +1094 -0
- package/dist/cli/loom.js.map +1 -0
- package/dist/cli/loom_chat.d.ts +20 -0
- package/dist/cli/loom_chat.d.ts.map +1 -0
- package/dist/cli/loom_chat.js +685 -0
- package/dist/cli/loom_chat.js.map +1 -0
- package/dist/cli/main.js +310 -14
- package/dist/cli/main.js.map +1 -1
- package/dist/cli/tui.d.ts.map +1 -1
- package/dist/cli/tui.js +7 -1
- package/dist/cli/tui.js.map +1 -1
- package/dist/core/agent/guard.d.ts +45 -0
- package/dist/core/agent/guard.d.ts.map +1 -0
- package/dist/core/agent/guard.js +113 -0
- package/dist/core/agent/guard.js.map +1 -0
- package/dist/core/agent.d.ts +17 -0
- package/dist/core/agent.d.ts.map +1 -1
- package/dist/core/agent.js +182 -93
- package/dist/core/agent.js.map +1 -1
- package/dist/core/factory.d.ts.map +1 -1
- package/dist/core/factory.js +34 -2
- package/dist/core/factory.js.map +1 -1
- package/dist/core/file_checkpoint.d.ts +57 -0
- package/dist/core/file_checkpoint.d.ts.map +1 -0
- package/dist/core/file_checkpoint.js +162 -0
- package/dist/core/file_checkpoint.js.map +1 -0
- package/dist/core/hooks.d.ts +43 -0
- package/dist/core/hooks.d.ts.map +1 -0
- package/dist/core/hooks.js +110 -0
- package/dist/core/hooks.js.map +1 -0
- package/dist/core/llm.d.ts.map +1 -1
- package/dist/core/llm.js +15 -9
- package/dist/core/llm.js.map +1 -1
- package/dist/core/longdoc.js +5 -5
- package/dist/core/mcp.d.ts +16 -0
- package/dist/core/mcp.d.ts.map +1 -1
- package/dist/core/mcp.js +55 -0
- package/dist/core/mcp.js.map +1 -1
- package/dist/core/model_config.d.ts +40 -0
- package/dist/core/model_config.d.ts.map +1 -0
- package/dist/core/model_config.js +191 -0
- package/dist/core/model_config.js.map +1 -0
- package/dist/core/skill.d.ts +7 -0
- package/dist/core/skill.d.ts.map +1 -1
- package/dist/core/skill.js +47 -0
- package/dist/core/skill.js.map +1 -1
- package/dist/core/skymd.d.ts +39 -0
- package/dist/core/skymd.d.ts.map +1 -0
- package/dist/core/skymd.js +177 -0
- package/dist/core/skymd.js.map +1 -0
- package/dist/core/tool.d.ts +12 -0
- package/dist/core/tool.d.ts.map +1 -1
- package/dist/core/tool.js +30 -0
- package/dist/core/tool.js.map +1 -1
- package/dist/core/verify.d.ts +27 -0
- package/dist/core/verify.d.ts.map +1 -0
- package/dist/core/verify.js +62 -0
- package/dist/core/verify.js.map +1 -0
- package/dist/skills/loader.d.ts +22 -2
- package/dist/skills/loader.d.ts.map +1 -1
- package/dist/skills/loader.js +45 -15
- package/dist/skills/loader.js.map +1 -1
- package/dist/tools/builtin.d.ts.map +1 -1
- package/dist/tools/builtin.js +13 -3
- package/dist/tools/builtin.js.map +1 -1
- package/dist/tools/model_tool.d.ts +11 -0
- package/dist/tools/model_tool.d.ts.map +1 -0
- package/dist/tools/model_tool.js +71 -0
- package/dist/tools/model_tool.js.map +1 -0
- package/dist/tools/todo.d.ts +30 -0
- package/dist/tools/todo.d.ts.map +1 -0
- package/dist/tools/todo.js +78 -0
- package/dist/tools/todo.js.map +1 -0
- package/docs/AESTHETIC_DESIGN.md +152 -144
- package/docs/OPTIMIZATION_PLAN.md +178 -178
- package/package.json +1 -1
- package/scripts/install.js +48 -48
- package/scripts/link.js +10 -10
- package/setup.bat +79 -79
- package/skill-test-ty2fOA/test.md +10 -10
- package/src/agents/dew.ts +70 -70
- package/src/agents/fair.ts +102 -102
- package/src/agents/fog.ts +48 -48
- package/src/agents/frost.ts +50 -50
- package/src/agents/rain.ts +50 -50
- package/src/agents/snow.ts +239 -239
- package/src/cli/commands_md.ts +112 -0
- package/src/cli/input_macros.ts +83 -0
- package/src/cli/loom.ts +982 -0
- package/src/cli/loom_chat.ts +598 -0
- package/src/cli/main.ts +255 -9
- package/src/cli/mode.ts +58 -58
- package/src/cli/tui.ts +228 -222
- package/src/core/agent/guard.ts +134 -0
- package/src/core/agent/task.ts +100 -100
- package/src/core/agent.ts +177 -95
- package/src/core/arbitrate.ts +162 -162
- package/src/core/catalog.ts +178 -178
- package/src/core/checkpoint.ts +94 -94
- package/src/core/estimate.ts +104 -104
- package/src/core/evolve.ts +191 -191
- package/src/core/factory.ts +31 -2
- package/src/core/file_checkpoint.ts +136 -0
- package/src/core/filter.ts +103 -103
- package/src/core/graph.ts +156 -156
- package/src/core/hooks.ts +126 -0
- package/src/core/icons.ts +53 -53
- package/src/core/index.ts +37 -37
- package/src/core/learn.ts +146 -146
- package/src/core/llm.ts +15 -9
- package/src/core/longdoc.ts +155 -155
- package/src/core/mcp.ts +48 -0
- package/src/core/mcp_server.ts +176 -176
- package/src/core/model_config.ts +157 -0
- package/src/core/profile.ts +255 -255
- package/src/core/router.ts +124 -124
- package/src/core/sandbox.ts +142 -142
- package/src/core/security.ts +243 -243
- package/src/core/skill.ts +42 -0
- package/src/core/skymd.ts +143 -0
- package/src/core/theme.ts +65 -65
- package/src/core/tool.ts +30 -0
- package/src/core/tool_router.ts +193 -193
- package/src/core/vector.ts +152 -152
- package/src/core/verify.ts +71 -0
- package/src/core/workspace.ts +150 -150
- package/src/plugins/loader.ts +66 -66
- package/src/skills/loader.ts +45 -16
- package/src/sql.js.d.ts +29 -29
- package/src/tools/builtin.ts +13 -3
- package/src/tools/computer.ts +269 -269
- package/src/tools/delegate.ts +49 -49
- package/src/tools/model_tool.ts +74 -0
- package/src/tools/todo.ts +76 -0
- package/src/web/tts.ts +93 -93
- package/tests/agent.test.ts +159 -159
- package/tests/agent_helpers.test.ts +48 -48
- package/tests/bus.test.ts +121 -121
- package/tests/catalog.test.ts +86 -86
- package/tests/checkpoint_commands.test.ts +124 -0
- package/tests/claude_compat.test.ts +110 -0
- package/tests/config.test.ts +41 -41
- package/tests/guard.test.ts +75 -0
- package/tests/icons.test.ts +45 -45
- package/tests/loom.test.ts +248 -0
- package/tests/memory.test.ts +170 -170
- package/tests/model_config.test.ts +109 -0
- package/tests/router.test.ts +86 -86
- package/tests/schemas.test.ts +51 -51
- package/tests/semantic.test.ts +83 -83
- package/tests/setup.ts +10 -10
- package/tests/skill.test.ts +172 -172
- package/tests/skymd.test.ts +146 -0
- package/tests/task.test.ts +60 -60
- package/tests/todo_toolstats.test.ts +94 -0
- package/tests/tool.test.ts +108 -108
- package/tests/tool_router.test.ts +71 -71
- package/tests/tui.test.ts +67 -67
- package/vitest.config.ts +17 -17
package/dist/cli/loom.js
ADDED
|
@@ -0,0 +1,1094 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* 天空织机 · 立轴 — the full-screen ink-wash weather-station TUI.
|
|
4
|
+
*
|
|
5
|
+
* Architecture notes (why this one works where the old full-screen attempt
|
|
6
|
+
* failed):
|
|
7
|
+
* 1. Streamed text never touches the terminal directly. It lands in a
|
|
8
|
+
* virtual block buffer; every frame is composed in memory and a diff
|
|
9
|
+
* renderer repaints only the rows that changed. Streaming and animation
|
|
10
|
+
* therefore cannot fight over the cursor.
|
|
11
|
+
* 2. All width math goes through the CJK-aware helpers in tui.ts, so the
|
|
12
|
+
* hand-rolled input editor cannot mangle fullwidth glyphs.
|
|
13
|
+
* 3. The animation clock is the single writer: key events and stream events
|
|
14
|
+
* only mutate state; the frame timer (and explicit repaint requests)
|
|
15
|
+
* flush it.
|
|
16
|
+
*
|
|
17
|
+
* Layout (画轴 / hanging scroll):
|
|
18
|
+
* ┌─ 天空织机 ───────────────────────────── ▣ 霧 ─┐ header + seal
|
|
19
|
+
* │ ≋ ❉ ⸽ particles / shuttles │ sky band (2 rows)
|
|
20
|
+
* │ ▁▂▃▅▃▂▁▁▂▄▂▁ mountain grows with the session │
|
|
21
|
+
* │ ● 霧 fog │ conversation viewport │ rail │ viewport
|
|
22
|
+
* │ · 雨 rain │ … │
|
|
23
|
+
* ├─ 思忖 ··· ──────────────── model · cost · ctx ─┤ status divider
|
|
24
|
+
* │ ≋ ❯ input │ input line
|
|
25
|
+
* └─ /help · Tab 补全 · PgUp 翻页 ─────────────────┘
|
|
26
|
+
*
|
|
27
|
+
* Design rationale: docs/AESTHETIC_DESIGN.md §2.2 (方案三 · 立轴).
|
|
28
|
+
*/
|
|
29
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
30
|
+
if (k2 === undefined) k2 = k;
|
|
31
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
32
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
33
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
34
|
+
}
|
|
35
|
+
Object.defineProperty(o, k2, desc);
|
|
36
|
+
}) : (function(o, m, k, k2) {
|
|
37
|
+
if (k2 === undefined) k2 = k;
|
|
38
|
+
o[k2] = m[k];
|
|
39
|
+
}));
|
|
40
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
41
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
42
|
+
}) : function(o, v) {
|
|
43
|
+
o["default"] = v;
|
|
44
|
+
});
|
|
45
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
46
|
+
var ownKeys = function(o) {
|
|
47
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
48
|
+
var ar = [];
|
|
49
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
50
|
+
return ar;
|
|
51
|
+
};
|
|
52
|
+
return ownKeys(o);
|
|
53
|
+
};
|
|
54
|
+
return function (mod) {
|
|
55
|
+
if (mod && mod.__esModule) return mod;
|
|
56
|
+
var result = {};
|
|
57
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
58
|
+
__setModuleDefault(result, mod);
|
|
59
|
+
return result;
|
|
60
|
+
};
|
|
61
|
+
})();
|
|
62
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
63
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
64
|
+
};
|
|
65
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
66
|
+
exports.LoomUI = exports.OrchState = exports.SkyField = exports.Screen = void 0;
|
|
67
|
+
exports.cutVisual = cutVisual;
|
|
68
|
+
exports.padAnsi = padAnsi;
|
|
69
|
+
exports.wrapPlain = wrapPlain;
|
|
70
|
+
exports.mountainRow = mountainRow;
|
|
71
|
+
exports.circled = circled;
|
|
72
|
+
exports.overlay = overlay;
|
|
73
|
+
const readline = __importStar(require("readline"));
|
|
74
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
75
|
+
const theme_1 = require("../core/theme");
|
|
76
|
+
const tui_1 = require("./tui");
|
|
77
|
+
/* ════════════════════════════════════════
|
|
78
|
+
ANSI-aware string helpers (pure, tested)
|
|
79
|
+
════════════════════════════════════════ */
|
|
80
|
+
const ESC = "\x1b";
|
|
81
|
+
const ANSI_RE = /\x1b\[[0-9;]*m/;
|
|
82
|
+
/** Truncate a styled string to a visual width, keeping ANSI sequences intact. */
|
|
83
|
+
function cutVisual(s, maxW) {
|
|
84
|
+
let out = "";
|
|
85
|
+
let w = 0;
|
|
86
|
+
let i = 0;
|
|
87
|
+
let cut = false;
|
|
88
|
+
while (i < s.length) {
|
|
89
|
+
if (s[i] === ESC) {
|
|
90
|
+
const m = ANSI_RE.exec(s.slice(i));
|
|
91
|
+
if (m && m.index === 0) {
|
|
92
|
+
out += m[0];
|
|
93
|
+
i += m[0].length;
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
const cp = s.codePointAt(i);
|
|
98
|
+
const ch = String.fromCodePoint(cp);
|
|
99
|
+
const cw = (0, tui_1.charWidth)(cp);
|
|
100
|
+
if (w + cw > maxW) {
|
|
101
|
+
cut = true;
|
|
102
|
+
break;
|
|
103
|
+
}
|
|
104
|
+
out += ch;
|
|
105
|
+
w += cw;
|
|
106
|
+
i += ch.length;
|
|
107
|
+
}
|
|
108
|
+
return cut ? out + "\x1b[0m" : out;
|
|
109
|
+
}
|
|
110
|
+
/** Pad a styled string with spaces to an exact visual width (truncates if over). */
|
|
111
|
+
function padAnsi(s, w) {
|
|
112
|
+
const cutS = (0, tui_1.visualWidth)(s) > w ? cutVisual(s, w) : s;
|
|
113
|
+
const diff = w - (0, tui_1.visualWidth)(cutS);
|
|
114
|
+
return diff > 0 ? cutS + " ".repeat(diff) : cutS;
|
|
115
|
+
}
|
|
116
|
+
/** CJK-aware plain-text word wrap (latin wraps on spaces, CJK per glyph). */
|
|
117
|
+
function wrapPlain(text, width) {
|
|
118
|
+
const lines = [];
|
|
119
|
+
if (width < 4)
|
|
120
|
+
width = 4;
|
|
121
|
+
for (const raw of text.split("\n")) {
|
|
122
|
+
let line = "";
|
|
123
|
+
let col = 0;
|
|
124
|
+
let word = "";
|
|
125
|
+
const flushWord = () => {
|
|
126
|
+
if (!word)
|
|
127
|
+
return;
|
|
128
|
+
const w = (0, tui_1.visualWidth)(word);
|
|
129
|
+
if (col > 0 && col + w > width) {
|
|
130
|
+
lines.push(line.trimEnd());
|
|
131
|
+
line = "";
|
|
132
|
+
col = 0;
|
|
133
|
+
}
|
|
134
|
+
// hard-break monster tokens (plain glyph slicing — no ANSI involved)
|
|
135
|
+
while ((0, tui_1.visualWidth)(word) > width) {
|
|
136
|
+
let head = "", hw = 0, i = 0;
|
|
137
|
+
for (const ch of word) {
|
|
138
|
+
const cw = (0, tui_1.charWidth)(ch.codePointAt(0));
|
|
139
|
+
if (hw + cw > width - col)
|
|
140
|
+
break;
|
|
141
|
+
head += ch;
|
|
142
|
+
hw += cw;
|
|
143
|
+
i += ch.length;
|
|
144
|
+
}
|
|
145
|
+
lines.push(line + head);
|
|
146
|
+
word = word.slice(i);
|
|
147
|
+
line = "";
|
|
148
|
+
col = 0;
|
|
149
|
+
}
|
|
150
|
+
line += word;
|
|
151
|
+
col += (0, tui_1.visualWidth)(word);
|
|
152
|
+
word = "";
|
|
153
|
+
};
|
|
154
|
+
for (const ch of raw) {
|
|
155
|
+
const cp = ch.codePointAt(0);
|
|
156
|
+
if (ch === " " || ch === "\t") {
|
|
157
|
+
flushWord();
|
|
158
|
+
if (col > 0 && col < width) {
|
|
159
|
+
line += " ";
|
|
160
|
+
col += 1;
|
|
161
|
+
}
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
if ((0, tui_1.charWidth)(cp) === 2) {
|
|
165
|
+
flushWord();
|
|
166
|
+
if (col + 2 > width) {
|
|
167
|
+
lines.push(line.trimEnd());
|
|
168
|
+
line = "";
|
|
169
|
+
col = 0;
|
|
170
|
+
}
|
|
171
|
+
line += ch;
|
|
172
|
+
col += 2;
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
word += ch;
|
|
176
|
+
}
|
|
177
|
+
flushWord();
|
|
178
|
+
lines.push(line);
|
|
179
|
+
}
|
|
180
|
+
// trim trailing blank produced by terminal newline at very end
|
|
181
|
+
while (lines.length > 1 && lines[lines.length - 1] === "" && text.endsWith("\n"))
|
|
182
|
+
lines.pop();
|
|
183
|
+
return lines;
|
|
184
|
+
}
|
|
185
|
+
/** Repaints only rows whose content changed since the previous frame. */
|
|
186
|
+
class Screen {
|
|
187
|
+
constructor(out) {
|
|
188
|
+
this.out = out;
|
|
189
|
+
this.prev = [];
|
|
190
|
+
}
|
|
191
|
+
/** Force the next flush to repaint everything (resize / resume). */
|
|
192
|
+
invalidate() { this.prev = []; }
|
|
193
|
+
flush(rows, cursor) {
|
|
194
|
+
let seq = "\x1b[?25l"; // hide cursor while painting
|
|
195
|
+
for (let i = 0; i < rows.length; i++) {
|
|
196
|
+
if (this.prev[i] !== rows[i]) {
|
|
197
|
+
seq += `\x1b[${i + 1};1H\x1b[2K` + rows[i];
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
if (this.prev.length > rows.length) {
|
|
201
|
+
for (let i = rows.length; i < this.prev.length; i++)
|
|
202
|
+
seq += `\x1b[${i + 1};1H\x1b[2K`;
|
|
203
|
+
}
|
|
204
|
+
if (cursor)
|
|
205
|
+
seq += `\x1b[${cursor.row + 1};${cursor.col + 1}H\x1b[?25h`;
|
|
206
|
+
this.out.write(seq);
|
|
207
|
+
this.prev = rows.slice();
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
exports.Screen = Screen;
|
|
211
|
+
/** Per-agent weather motion over a w×2 field. drift/fall/glint/float/bead/rise. */
|
|
212
|
+
class SkyField {
|
|
213
|
+
constructor(h = 2) {
|
|
214
|
+
this.h = h;
|
|
215
|
+
this.particles = [];
|
|
216
|
+
this.w = 0;
|
|
217
|
+
}
|
|
218
|
+
resize(w) {
|
|
219
|
+
this.w = Math.max(8, w);
|
|
220
|
+
const n = Math.max(3, Math.floor(this.w / 7));
|
|
221
|
+
this.particles = Array.from({ length: n }, (_, i) => ({
|
|
222
|
+
x: (i * 7.3 + (i * i % 5)) % this.w,
|
|
223
|
+
y: (i * 13) % this.h,
|
|
224
|
+
phase: (i * 37) % 17,
|
|
225
|
+
}));
|
|
226
|
+
}
|
|
227
|
+
step(motion, tick) {
|
|
228
|
+
for (const p of this.particles) {
|
|
229
|
+
switch (motion) {
|
|
230
|
+
case "drift":
|
|
231
|
+
p.x += 0.45;
|
|
232
|
+
p.y = (Math.sin((tick + p.phase) / 6) > 0 ? 0 : 1);
|
|
233
|
+
break;
|
|
234
|
+
case "fall":
|
|
235
|
+
p.y += 0.55;
|
|
236
|
+
p.x += 0.12;
|
|
237
|
+
break;
|
|
238
|
+
case "glint": /* static, blink via phase at render */ break;
|
|
239
|
+
case "float":
|
|
240
|
+
p.y += 0.28;
|
|
241
|
+
p.x += Math.sin((tick + p.phase) / 4) * 0.5;
|
|
242
|
+
break;
|
|
243
|
+
case "bead": /* static, brightness breathes */ break;
|
|
244
|
+
case "rise":
|
|
245
|
+
p.y -= 0.3;
|
|
246
|
+
break;
|
|
247
|
+
}
|
|
248
|
+
if (p.x >= this.w)
|
|
249
|
+
p.x -= this.w;
|
|
250
|
+
if (p.x < 0)
|
|
251
|
+
p.x += this.w;
|
|
252
|
+
if (p.y >= this.h)
|
|
253
|
+
p.y -= this.h;
|
|
254
|
+
if (p.y < 0)
|
|
255
|
+
p.y += this.h;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
/** Render the two sky rows. Shuttles (orchestration) overlay the weather. */
|
|
259
|
+
render(w, motion, symbol, hex, tick, shuttles) {
|
|
260
|
+
if (w !== this.w)
|
|
261
|
+
this.resize(w);
|
|
262
|
+
const grid = Array.from({ length: this.h }, () => Array.from({ length: w }, () => ({ ch: " ", style: (s) => s })));
|
|
263
|
+
const pigment = chalk_1.default.hex(hex);
|
|
264
|
+
for (const p of this.particles) {
|
|
265
|
+
const visible = motion === "glint" ? Math.sin((tick + p.phase) / 3) > -0.2 : true;
|
|
266
|
+
if (!visible)
|
|
267
|
+
continue;
|
|
268
|
+
const dimmed = motion === "bead" ? Math.sin((tick + p.phase) / 5) < 0 : (p.phase % 3 === 0);
|
|
269
|
+
const x = Math.min(w - 1, Math.round(p.x));
|
|
270
|
+
const y = Math.min(this.h - 1, Math.round(p.y));
|
|
271
|
+
grid[y][x] = { ch: symbol, style: dimmed ? (s) => pigment.dim(s) : (s) => pigment(s) };
|
|
272
|
+
}
|
|
273
|
+
// Loom shuttles: a thread of ┄ in the agent's pigment, shuttle glyph at the head.
|
|
274
|
+
for (const sh of shuttles) {
|
|
275
|
+
const row = sh.row % this.h;
|
|
276
|
+
const head = Math.round(sh.x) % w;
|
|
277
|
+
const thread = chalk_1.default.hex(sh.hex).dim;
|
|
278
|
+
for (let x = 0; x < head; x++)
|
|
279
|
+
if (grid[row][x].ch === " ")
|
|
280
|
+
grid[row][x] = { ch: "┄", style: thread };
|
|
281
|
+
grid[row][head] = { ch: sh.symbol, style: chalk_1.default.hex(sh.hex).bold };
|
|
282
|
+
}
|
|
283
|
+
return grid.map((cells) => {
|
|
284
|
+
let line = "";
|
|
285
|
+
for (const c of cells)
|
|
286
|
+
line += c.ch === " " ? " " : c.style(c.ch);
|
|
287
|
+
return line;
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
exports.SkyField = SkyField;
|
|
292
|
+
/** Distant-mountain silhouette; grows slowly as the session lengthens. */
|
|
293
|
+
function mountainRow(width, turns) {
|
|
294
|
+
const GLYPHS = [" ", "▁", "▂", "▃", "▄", "▅"];
|
|
295
|
+
const growth = Math.min(1, turns / 30) * 0.7 + 0.3;
|
|
296
|
+
let out = "";
|
|
297
|
+
for (let x = 0; x < width; x++) {
|
|
298
|
+
// layered sines make a credible ridge; deterministic, so the diff renderer
|
|
299
|
+
// only repaints this row when `turns` changes.
|
|
300
|
+
const r = Math.sin(x / 6.1) * 0.5 + Math.sin(x / 13.7 + 2) * 0.35 + Math.sin(x / 3.3 + 5) * 0.15;
|
|
301
|
+
const h = Math.max(0, Math.round((r * 0.5 + 0.5) * (GLYPHS.length - 1) * growth));
|
|
302
|
+
out += GLYPHS[h];
|
|
303
|
+
}
|
|
304
|
+
return chalk_1.default.hex(theme_1.PALETTE.inkFaint).dim(out);
|
|
305
|
+
}
|
|
306
|
+
const CIRCLED = "①②③④⑤⑥⑦⑧⑨⑩⑪⑫⑬⑭⑮⑯⑰⑱⑲⑳";
|
|
307
|
+
function circled(i) { return CIRCLED[i] ?? `(${i + 1})`; }
|
|
308
|
+
class OrchState {
|
|
309
|
+
constructor() {
|
|
310
|
+
this.tasks = new Map();
|
|
311
|
+
this.order = [];
|
|
312
|
+
this.active = false;
|
|
313
|
+
/** moving shuttle x-position per running agent */
|
|
314
|
+
this.shuttleX = new Map();
|
|
315
|
+
}
|
|
316
|
+
plan(raw) {
|
|
317
|
+
this.active = true;
|
|
318
|
+
for (const t of raw) {
|
|
319
|
+
if (this.tasks.has(t.id))
|
|
320
|
+
continue;
|
|
321
|
+
this.tasks.set(t.id, {
|
|
322
|
+
id: t.id,
|
|
323
|
+
index: this.order.length,
|
|
324
|
+
agent: t.assignedTo || "fog",
|
|
325
|
+
desc: String(t.description || "").split("\n")[0],
|
|
326
|
+
deps: (t.allDeps || []).slice(),
|
|
327
|
+
state: "wait",
|
|
328
|
+
});
|
|
329
|
+
this.order.push(t.id);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
start(id) {
|
|
333
|
+
const t = this.tasks.get(id);
|
|
334
|
+
if (t) {
|
|
335
|
+
t.state = "run";
|
|
336
|
+
t.startedAt = Date.now();
|
|
337
|
+
this.shuttleX.set(t.agent, 0);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
done(id, ok) {
|
|
341
|
+
const t = this.tasks.get(id);
|
|
342
|
+
if (!t)
|
|
343
|
+
return;
|
|
344
|
+
t.state = ok ? "ok" : "fail";
|
|
345
|
+
t.ms = t.startedAt ? Date.now() - t.startedAt : undefined;
|
|
346
|
+
if (![...this.tasks.values()].some((x) => x.state === "run" && x.agent === t.agent)) {
|
|
347
|
+
this.shuttleX.delete(t.agent);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
finish() { this.active = false; this.shuttleX.clear(); }
|
|
351
|
+
runningAgents() {
|
|
352
|
+
return [...new Set([...this.tasks.values()].filter((t) => t.state === "run").map((t) => t.agent))];
|
|
353
|
+
}
|
|
354
|
+
/** Per-agent ✓/✗ tally for the rail. */
|
|
355
|
+
tally(agent) {
|
|
356
|
+
let ok = 0, fail = 0, run = false;
|
|
357
|
+
for (const t of this.tasks.values()) {
|
|
358
|
+
if (t.agent !== agent)
|
|
359
|
+
continue;
|
|
360
|
+
if (t.state === "ok")
|
|
361
|
+
ok++;
|
|
362
|
+
else if (t.state === "fail")
|
|
363
|
+
fail++;
|
|
364
|
+
else if (t.state === "run")
|
|
365
|
+
run = true;
|
|
366
|
+
}
|
|
367
|
+
return { ok, fail, run };
|
|
368
|
+
}
|
|
369
|
+
progress() {
|
|
370
|
+
let done = 0;
|
|
371
|
+
for (const t of this.tasks.values())
|
|
372
|
+
if (t.state === "ok" || t.state === "fail")
|
|
373
|
+
done++;
|
|
374
|
+
return { done, total: this.tasks.size };
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
exports.OrchState = OrchState;
|
|
378
|
+
const RAIL_W = 15; // visual columns of the left rail (inside borders)
|
|
379
|
+
const SKY_H = 2;
|
|
380
|
+
class LoomUI {
|
|
381
|
+
constructor(opts) {
|
|
382
|
+
this.sky = new SkyField(SKY_H);
|
|
383
|
+
this.blocks = [];
|
|
384
|
+
this.byId = new Map();
|
|
385
|
+
this.tick = 0;
|
|
386
|
+
this.timer = null;
|
|
387
|
+
this.destroyed = false;
|
|
388
|
+
this.agentName = "fog";
|
|
389
|
+
this.turns = 0;
|
|
390
|
+
this.busy = false;
|
|
391
|
+
this.busyLabel = "";
|
|
392
|
+
this.orch = new OrchState();
|
|
393
|
+
/** status providers (wired by the chat loop) */
|
|
394
|
+
this.statusRight = () => "";
|
|
395
|
+
// input editor state
|
|
396
|
+
this.inputGlyphs = []; // glyphs
|
|
397
|
+
this.cursor = 0;
|
|
398
|
+
this.history = [];
|
|
399
|
+
this.histIdx = -1;
|
|
400
|
+
this.histStash = "";
|
|
401
|
+
this.scrollOff = 0; // 0 = follow tail
|
|
402
|
+
this.paletteIdx = 0;
|
|
403
|
+
this.pendingResolve = null;
|
|
404
|
+
this.modal = null;
|
|
405
|
+
this.sigintAt = 0;
|
|
406
|
+
this.onInterrupt = null;
|
|
407
|
+
/** Shift+Tab cycles interactive modes (default/plan/auto); wired by the chat loop. */
|
|
408
|
+
this.onModeCycle = null;
|
|
409
|
+
/** Styled mode badge shown in the status divider when idle ('' = default). */
|
|
410
|
+
this.modeBadge = "";
|
|
411
|
+
/** User-defined slash commands shown in the palette ([name, description]). */
|
|
412
|
+
this.extraCommands = [];
|
|
413
|
+
this.keypressHandler = null;
|
|
414
|
+
this.resizeHandler = null;
|
|
415
|
+
/* ── streaming ── */
|
|
416
|
+
this.openBlock = null;
|
|
417
|
+
this.bleedLen = 0;
|
|
418
|
+
this.flashHint = "";
|
|
419
|
+
this.viewportCache = null;
|
|
420
|
+
this.out = opts?.out ?? process.stdout;
|
|
421
|
+
this.inp = opts?.inp === undefined ? process.stdin : opts.inp;
|
|
422
|
+
this.headless = opts?.headless ?? false;
|
|
423
|
+
this.screen = new Screen(this.out);
|
|
424
|
+
}
|
|
425
|
+
/* ── lifecycle ── */
|
|
426
|
+
start() {
|
|
427
|
+
if (!this.headless) {
|
|
428
|
+
this.out.write("\x1b[?1049h\x1b[2J"); // alternate screen
|
|
429
|
+
if (this.inp && this.inp.isTTY) {
|
|
430
|
+
readline.emitKeypressEvents(this.inp);
|
|
431
|
+
this.inp.setRawMode(true);
|
|
432
|
+
this.inp.resume();
|
|
433
|
+
this.keypressHandler = (str, key) => this.onKey(str, key);
|
|
434
|
+
this.inp.on("keypress", this.keypressHandler);
|
|
435
|
+
}
|
|
436
|
+
this.resizeHandler = () => { this.screen.invalidate(); this.invalidateWraps(); this.paint(); };
|
|
437
|
+
process.stdout.on?.("resize", this.resizeHandler);
|
|
438
|
+
this.timer = setInterval(() => this.frame(), 120);
|
|
439
|
+
}
|
|
440
|
+
this.paint();
|
|
441
|
+
}
|
|
442
|
+
destroy() {
|
|
443
|
+
if (this.destroyed)
|
|
444
|
+
return;
|
|
445
|
+
this.destroyed = true;
|
|
446
|
+
if (this.timer)
|
|
447
|
+
clearInterval(this.timer);
|
|
448
|
+
if (this.inp && this.keypressHandler)
|
|
449
|
+
this.inp.removeListener("keypress", this.keypressHandler);
|
|
450
|
+
if (this.resizeHandler)
|
|
451
|
+
process.stdout.removeListener?.("resize", this.resizeHandler);
|
|
452
|
+
if (!this.headless) {
|
|
453
|
+
if (this.inp && this.inp.isTTY)
|
|
454
|
+
this.inp.setRawMode(false);
|
|
455
|
+
this.out.write("\x1b[?1049l\x1b[?25h");
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
/** Temporarily leave the loom (setup wizard etc.), then restore. */
|
|
459
|
+
async suspend(fn) {
|
|
460
|
+
if (this.inp && this.inp.isTTY)
|
|
461
|
+
this.inp.setRawMode(false);
|
|
462
|
+
if (this.inp && this.keypressHandler)
|
|
463
|
+
this.inp.removeListener("keypress", this.keypressHandler);
|
|
464
|
+
if (this.timer) {
|
|
465
|
+
clearInterval(this.timer);
|
|
466
|
+
this.timer = null;
|
|
467
|
+
}
|
|
468
|
+
this.out.write("\x1b[?1049l\x1b[?25h");
|
|
469
|
+
try {
|
|
470
|
+
return await fn();
|
|
471
|
+
}
|
|
472
|
+
finally {
|
|
473
|
+
this.out.write("\x1b[?1049h\x1b[2J");
|
|
474
|
+
if (this.inp && this.inp.isTTY) {
|
|
475
|
+
this.inp.setRawMode(true);
|
|
476
|
+
this.inp.resume();
|
|
477
|
+
if (this.keypressHandler)
|
|
478
|
+
this.inp.on("keypress", this.keypressHandler);
|
|
479
|
+
}
|
|
480
|
+
this.screen.invalidate();
|
|
481
|
+
if (!this.headless)
|
|
482
|
+
this.timer = setInterval(() => this.frame(), 120);
|
|
483
|
+
this.paint();
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
/* ── block API ── */
|
|
487
|
+
push(b) {
|
|
488
|
+
const blk = { version: 0, ...b };
|
|
489
|
+
this.blocks.push(blk);
|
|
490
|
+
if (blk.id)
|
|
491
|
+
this.byId.set(blk.id, blk);
|
|
492
|
+
if (this.blocks.length > 3000) {
|
|
493
|
+
const drop = this.blocks.splice(0, 500);
|
|
494
|
+
for (const d of drop)
|
|
495
|
+
if (d.id)
|
|
496
|
+
this.byId.delete(d.id);
|
|
497
|
+
}
|
|
498
|
+
this.scrollOff = 0; // new content snaps to tail
|
|
499
|
+
return blk;
|
|
500
|
+
}
|
|
501
|
+
blank() { this.push({ kind: "blank", text: "" }); }
|
|
502
|
+
/** Pre-styled single line (truncated, never wrapped). */
|
|
503
|
+
line(text, id) {
|
|
504
|
+
if (id && this.byId.has(id)) {
|
|
505
|
+
this.update(id, text);
|
|
506
|
+
return;
|
|
507
|
+
}
|
|
508
|
+
this.push({ kind: "line", text, id });
|
|
509
|
+
}
|
|
510
|
+
update(id, text) {
|
|
511
|
+
const b = this.byId.get(id);
|
|
512
|
+
if (b && b.text !== text) {
|
|
513
|
+
b.text = text;
|
|
514
|
+
b.version++;
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
/** Wrapped plain-text block. With an id, later calls update it in place. */
|
|
518
|
+
text(text, style, head, id) {
|
|
519
|
+
if (id && this.byId.has(id)) {
|
|
520
|
+
this.update(id, text);
|
|
521
|
+
return;
|
|
522
|
+
}
|
|
523
|
+
this.push({ kind: "text", text, style, head, id });
|
|
524
|
+
}
|
|
525
|
+
beginStream(agentName) {
|
|
526
|
+
const t = (0, theme_1.agentTheme)(agentName);
|
|
527
|
+
this.blank();
|
|
528
|
+
this.line(chalk_1.default.bold.hex(t.hex)(`${t.symbol} ${t.kanji} `) + chalk_1.default.hex(t.hex)(t.name));
|
|
529
|
+
this.blank();
|
|
530
|
+
this.openBlock = this.push({ kind: "text", text: "", open: true });
|
|
531
|
+
this.bleedLen = 0;
|
|
532
|
+
}
|
|
533
|
+
/** Re-open a fresh stream block (after a tool event), without the header. */
|
|
534
|
+
continueStream() {
|
|
535
|
+
this.endStream();
|
|
536
|
+
this.openBlock = this.push({ kind: "text", text: "", open: true });
|
|
537
|
+
this.bleedLen = 0;
|
|
538
|
+
}
|
|
539
|
+
streamWrite(s) {
|
|
540
|
+
if (!this.openBlock)
|
|
541
|
+
this.beginStream(this.agentName);
|
|
542
|
+
const b = this.openBlock;
|
|
543
|
+
b.text += s.replace(/\r/g, "");
|
|
544
|
+
b.version++;
|
|
545
|
+
this.bleedLen = Math.min(12, this.bleedLen + [...s].length);
|
|
546
|
+
}
|
|
547
|
+
endStream() {
|
|
548
|
+
if (this.openBlock) {
|
|
549
|
+
this.openBlock.open = false;
|
|
550
|
+
this.openBlock.version++;
|
|
551
|
+
this.openBlock = null;
|
|
552
|
+
}
|
|
553
|
+
this.bleedLen = 0;
|
|
554
|
+
}
|
|
555
|
+
clearViewport() { this.blocks = []; this.byId.clear(); this.scrollOff = 0; this.viewportCache = null; this.paint(); }
|
|
556
|
+
/** Transient hint in the status divider. */
|
|
557
|
+
flash(msg, ms = 1600) {
|
|
558
|
+
this.flashHint = msg;
|
|
559
|
+
this.paint();
|
|
560
|
+
setTimeout(() => { if (this.flashHint === msg) {
|
|
561
|
+
this.flashHint = "";
|
|
562
|
+
this.paint();
|
|
563
|
+
} }, ms);
|
|
564
|
+
}
|
|
565
|
+
/* ── input ── */
|
|
566
|
+
/** Read one submitted line (the editor stays live during streaming). */
|
|
567
|
+
readInput() {
|
|
568
|
+
return new Promise((resolve) => { this.pendingResolve = resolve; });
|
|
569
|
+
}
|
|
570
|
+
/** Modal y/N confirmation (tool approval). */
|
|
571
|
+
confirm(text) {
|
|
572
|
+
return new Promise((resolve) => { this.modal = { text, resolve }; this.paint(); });
|
|
573
|
+
}
|
|
574
|
+
setHistory(h) { this.history = h.slice(); }
|
|
575
|
+
onKey(str, key) {
|
|
576
|
+
if (this.destroyed)
|
|
577
|
+
return;
|
|
578
|
+
if (this.modal) {
|
|
579
|
+
const k = (str || "").toLowerCase();
|
|
580
|
+
if (k === "y") {
|
|
581
|
+
const m = this.modal;
|
|
582
|
+
this.modal = null;
|
|
583
|
+
m.resolve(true);
|
|
584
|
+
}
|
|
585
|
+
else if (k === "n" || key?.name === "return" || key?.name === "escape") {
|
|
586
|
+
const m = this.modal;
|
|
587
|
+
this.modal = null;
|
|
588
|
+
m.resolve(false);
|
|
589
|
+
}
|
|
590
|
+
this.paint();
|
|
591
|
+
return;
|
|
592
|
+
}
|
|
593
|
+
const name = key?.name;
|
|
594
|
+
if (key?.ctrl && name === "c") {
|
|
595
|
+
this.handleSigint();
|
|
596
|
+
return;
|
|
597
|
+
}
|
|
598
|
+
if (name === "pageup") {
|
|
599
|
+
this.scrollOff += Math.max(1, this.bodyH() - 2);
|
|
600
|
+
this.clampScroll();
|
|
601
|
+
this.paint();
|
|
602
|
+
return;
|
|
603
|
+
}
|
|
604
|
+
if (name === "pagedown") {
|
|
605
|
+
this.scrollOff -= Math.max(1, this.bodyH() - 2);
|
|
606
|
+
this.clampScroll();
|
|
607
|
+
this.paint();
|
|
608
|
+
return;
|
|
609
|
+
}
|
|
610
|
+
if (name === "return") {
|
|
611
|
+
if (this.busy)
|
|
612
|
+
return; // a reply is being woven; ignore submit
|
|
613
|
+
let text = this.inputGlyphs.join("").trim();
|
|
614
|
+
// Palette open: Enter runs the ↑↓-highlighted command (Claude Code
|
|
615
|
+
// style). Commands that take arguments fill the input instead so the
|
|
616
|
+
// user can type them.
|
|
617
|
+
const matches = this.paletteMatches();
|
|
618
|
+
if (matches.length > 0 && text.startsWith("/")) {
|
|
619
|
+
const [cmd] = matches[Math.max(0, Math.min(this.paletteIdx, matches.length - 1))];
|
|
620
|
+
if (cmd.endsWith(" ")) {
|
|
621
|
+
// argument-taking command: fill the input and wait for arguments
|
|
622
|
+
// (the palette closes once the line contains a space; a second
|
|
623
|
+
// Enter then submits as typed)
|
|
624
|
+
this.inputGlyphs = [...cmd];
|
|
625
|
+
this.cursor = this.inputGlyphs.length;
|
|
626
|
+
this.paletteIdx = 0;
|
|
627
|
+
this.paint();
|
|
628
|
+
return;
|
|
629
|
+
}
|
|
630
|
+
text = cmd.trimEnd();
|
|
631
|
+
}
|
|
632
|
+
this.inputGlyphs = [];
|
|
633
|
+
this.cursor = 0;
|
|
634
|
+
this.histIdx = -1;
|
|
635
|
+
this.paletteIdx = 0;
|
|
636
|
+
if (text) {
|
|
637
|
+
this.history.unshift(text);
|
|
638
|
+
if (this.history.length > 200)
|
|
639
|
+
this.history.pop();
|
|
640
|
+
}
|
|
641
|
+
const r = this.pendingResolve;
|
|
642
|
+
this.pendingResolve = null;
|
|
643
|
+
this.paint();
|
|
644
|
+
if (r)
|
|
645
|
+
r(text);
|
|
646
|
+
return;
|
|
647
|
+
}
|
|
648
|
+
const paletteOpen = this.paletteMatches().length > 0 && this.inputGlyphs[0] === "/";
|
|
649
|
+
if (name === "up") {
|
|
650
|
+
if (paletteOpen) {
|
|
651
|
+
this.paletteIdx = Math.max(0, this.paletteIdx - 1);
|
|
652
|
+
}
|
|
653
|
+
else if (this.histIdx < this.history.length - 1) {
|
|
654
|
+
if (this.histIdx === -1)
|
|
655
|
+
this.histStash = this.inputGlyphs.join("");
|
|
656
|
+
this.histIdx++;
|
|
657
|
+
this.inputGlyphs = [...this.history[this.histIdx]];
|
|
658
|
+
this.cursor = this.inputGlyphs.length;
|
|
659
|
+
}
|
|
660
|
+
this.paint();
|
|
661
|
+
return;
|
|
662
|
+
}
|
|
663
|
+
if (name === "down") {
|
|
664
|
+
if (paletteOpen) {
|
|
665
|
+
this.paletteIdx = Math.min(this.paletteMatches().length - 1, this.paletteIdx + 1);
|
|
666
|
+
}
|
|
667
|
+
else if (this.histIdx >= 0) {
|
|
668
|
+
this.histIdx--;
|
|
669
|
+
this.inputGlyphs = [...(this.histIdx === -1 ? this.histStash : this.history[this.histIdx])];
|
|
670
|
+
this.cursor = this.inputGlyphs.length;
|
|
671
|
+
}
|
|
672
|
+
this.paint();
|
|
673
|
+
return;
|
|
674
|
+
}
|
|
675
|
+
if (name === "tab" && key?.shift) {
|
|
676
|
+
this.onModeCycle?.();
|
|
677
|
+
this.paint();
|
|
678
|
+
return;
|
|
679
|
+
}
|
|
680
|
+
if (name === "tab") {
|
|
681
|
+
if (paletteOpen) {
|
|
682
|
+
const m = this.paletteMatches();
|
|
683
|
+
const pick = m[Math.min(this.paletteIdx, m.length - 1)];
|
|
684
|
+
if (pick) {
|
|
685
|
+
this.inputGlyphs = [...pick[0].trimEnd()];
|
|
686
|
+
this.cursor = this.inputGlyphs.length;
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
this.paint();
|
|
690
|
+
return;
|
|
691
|
+
}
|
|
692
|
+
if (name === "escape") {
|
|
693
|
+
// Esc closes the palette by clearing the slash input; otherwise it
|
|
694
|
+
// just resets selection / jumps back to the tail.
|
|
695
|
+
if (paletteOpen) {
|
|
696
|
+
this.inputGlyphs = [];
|
|
697
|
+
this.cursor = 0;
|
|
698
|
+
}
|
|
699
|
+
this.paletteIdx = 0;
|
|
700
|
+
this.scrollOff = 0;
|
|
701
|
+
this.paint();
|
|
702
|
+
return;
|
|
703
|
+
}
|
|
704
|
+
if (name === "backspace") {
|
|
705
|
+
if (this.cursor > 0) {
|
|
706
|
+
this.inputGlyphs.splice(this.cursor - 1, 1);
|
|
707
|
+
this.cursor--;
|
|
708
|
+
}
|
|
709
|
+
this.paletteIdx = 0; // filter changed — selection restarts at the top
|
|
710
|
+
this.paint();
|
|
711
|
+
return;
|
|
712
|
+
}
|
|
713
|
+
if (name === "delete") {
|
|
714
|
+
if (this.cursor < this.inputGlyphs.length)
|
|
715
|
+
this.inputGlyphs.splice(this.cursor, 1);
|
|
716
|
+
this.paint();
|
|
717
|
+
return;
|
|
718
|
+
}
|
|
719
|
+
if (name === "left") {
|
|
720
|
+
if (this.cursor > 0)
|
|
721
|
+
this.cursor--;
|
|
722
|
+
this.paint();
|
|
723
|
+
return;
|
|
724
|
+
}
|
|
725
|
+
if (name === "right") {
|
|
726
|
+
if (this.cursor < this.inputGlyphs.length)
|
|
727
|
+
this.cursor++;
|
|
728
|
+
this.paint();
|
|
729
|
+
return;
|
|
730
|
+
}
|
|
731
|
+
if (key?.ctrl && name === "a") {
|
|
732
|
+
this.cursor = 0;
|
|
733
|
+
this.paint();
|
|
734
|
+
return;
|
|
735
|
+
}
|
|
736
|
+
if (key?.ctrl && name === "e") {
|
|
737
|
+
this.cursor = this.inputGlyphs.length;
|
|
738
|
+
this.paint();
|
|
739
|
+
return;
|
|
740
|
+
}
|
|
741
|
+
if (key?.ctrl && name === "u") {
|
|
742
|
+
this.inputGlyphs.splice(0, this.cursor);
|
|
743
|
+
this.cursor = 0;
|
|
744
|
+
this.paint();
|
|
745
|
+
return;
|
|
746
|
+
}
|
|
747
|
+
if (key?.ctrl && name === "w") {
|
|
748
|
+
let i = this.cursor;
|
|
749
|
+
while (i > 0 && this.inputGlyphs[i - 1] === " ")
|
|
750
|
+
i--;
|
|
751
|
+
while (i > 0 && this.inputGlyphs[i - 1] !== " ")
|
|
752
|
+
i--;
|
|
753
|
+
this.inputGlyphs.splice(i, this.cursor - i);
|
|
754
|
+
this.cursor = i;
|
|
755
|
+
this.paint();
|
|
756
|
+
return;
|
|
757
|
+
}
|
|
758
|
+
if (key?.ctrl && name === "l") {
|
|
759
|
+
this.clearViewport();
|
|
760
|
+
return;
|
|
761
|
+
}
|
|
762
|
+
if (str && !key?.ctrl && !key?.meta) {
|
|
763
|
+
const glyphs = [...str].filter((c) => c >= " " || (0, tui_1.charWidth)(c.codePointAt(0)) > 0);
|
|
764
|
+
if (glyphs.length) {
|
|
765
|
+
this.inputGlyphs.splice(this.cursor, 0, ...glyphs);
|
|
766
|
+
this.cursor += glyphs.length;
|
|
767
|
+
this.histIdx = -1;
|
|
768
|
+
this.paletteIdx = 0; // filter changed — selection restarts at the top
|
|
769
|
+
this.paint();
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
handleSigint() {
|
|
774
|
+
const now = Date.now();
|
|
775
|
+
if (this.busy && this.onInterrupt) {
|
|
776
|
+
this.onInterrupt();
|
|
777
|
+
return;
|
|
778
|
+
}
|
|
779
|
+
if (now - this.sigintAt < 1500) {
|
|
780
|
+
this.destroy();
|
|
781
|
+
process.stdout.write(chalk_1.default.dim(" 再会。\n"));
|
|
782
|
+
process.exit(0);
|
|
783
|
+
}
|
|
784
|
+
this.sigintAt = now;
|
|
785
|
+
this.flash("再按一次 Ctrl-C 退出");
|
|
786
|
+
}
|
|
787
|
+
paletteMatches() {
|
|
788
|
+
const l = this.inputGlyphs.join("");
|
|
789
|
+
if (!l.startsWith("/") || l.includes(" "))
|
|
790
|
+
return [];
|
|
791
|
+
return [...tui_1.SLASH_COMMANDS, ...this.extraCommands].filter(([c]) => c.trimEnd().startsWith(l));
|
|
792
|
+
}
|
|
793
|
+
/* ── geometry ── */
|
|
794
|
+
cols() { return Math.max(40, this.out.columns || 80); }
|
|
795
|
+
rows() { return Math.max(12, this.out.rows || 24); }
|
|
796
|
+
// header(1) + sky(2) + body + divider(1) + input(1) + bottom(1) = rows
|
|
797
|
+
bodyH() { return this.rows() - SKY_H - 4; }
|
|
798
|
+
clampScroll() {
|
|
799
|
+
const total = this.viewportLines().length;
|
|
800
|
+
const maxOff = Math.max(0, total - this.bodyH());
|
|
801
|
+
this.scrollOff = Math.max(0, Math.min(this.scrollOff, maxOff));
|
|
802
|
+
}
|
|
803
|
+
invalidateWraps() { for (const b of this.blocks)
|
|
804
|
+
b.cache = undefined; }
|
|
805
|
+
/* ── frame composition ── */
|
|
806
|
+
frame() {
|
|
807
|
+
this.tick++;
|
|
808
|
+
const animate = this.busy || this.orch.active;
|
|
809
|
+
// advance shuttles
|
|
810
|
+
if (this.orch.active) {
|
|
811
|
+
for (const [a, x] of this.orch.shuttleX)
|
|
812
|
+
this.orch.shuttleX.set(a, x + 1.3);
|
|
813
|
+
}
|
|
814
|
+
// ink "dries": the bleed tail shrinks even when no new tokens arrive
|
|
815
|
+
if (this.openBlock && this.bleedLen > 0 && this.tick % 2 === 0)
|
|
816
|
+
this.bleedLen = Math.max(0, this.bleedLen - 2);
|
|
817
|
+
if (animate || this.tick % 5 === 0) {
|
|
818
|
+
const t = (0, theme_1.agentTheme)(this.agentName);
|
|
819
|
+
this.sky.step(t.motion, this.tick);
|
|
820
|
+
this.paint();
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
viewportLines() {
|
|
824
|
+
const w = this.viewW();
|
|
825
|
+
// the open block's cursor pulse + bleed tail animate with the clock
|
|
826
|
+
const anim = this.openBlock ? `|b${this.bleedLen}|t${this.tick & 7}` : "";
|
|
827
|
+
const key = this.blocks.map((b) => b.version).join(",") + `|${w}|${this.blocks.length}` + anim;
|
|
828
|
+
if (this.viewportCache && this.viewportCache.key === key)
|
|
829
|
+
return this.viewportCache.lines;
|
|
830
|
+
const lines = [];
|
|
831
|
+
for (const b of this.blocks) {
|
|
832
|
+
if (b.kind === "blank") {
|
|
833
|
+
lines.push("");
|
|
834
|
+
continue;
|
|
835
|
+
}
|
|
836
|
+
if (b.kind === "line") {
|
|
837
|
+
lines.push(cutVisual(b.text, w));
|
|
838
|
+
continue;
|
|
839
|
+
}
|
|
840
|
+
if (!b.cache || b.cache.width !== w || b.cache.version !== b.version) {
|
|
841
|
+
const wrapped = wrapPlain(b.text, b.head ? w - (0, tui_1.visualWidth)(b.head) : w);
|
|
842
|
+
b.cache = { width: w, version: b.version, lines: wrapped };
|
|
843
|
+
}
|
|
844
|
+
const style = b.style ?? ((s) => s);
|
|
845
|
+
b.cache.lines.forEach((ln, i) => {
|
|
846
|
+
const head = b.head ? (i === 0 ? b.head : " ".repeat((0, tui_1.visualWidth)(b.head))) : "";
|
|
847
|
+
let body = style(ln);
|
|
848
|
+
if (b.open && i === b.cache.lines.length - 1) {
|
|
849
|
+
// ink-bleed: the freshest glyphs render faint, "drying" into full ink
|
|
850
|
+
const glyphs = [...ln];
|
|
851
|
+
const bleed = Math.min(this.bleedLen, glyphs.length);
|
|
852
|
+
if (bleed > 0) {
|
|
853
|
+
const headPart = glyphs.slice(0, glyphs.length - bleed).join("");
|
|
854
|
+
const tailPart = glyphs.slice(glyphs.length - bleed).join("");
|
|
855
|
+
body = style(headPart) + chalk_1.default.hex(theme_1.PALETTE.inkLight)(tailPart);
|
|
856
|
+
}
|
|
857
|
+
body += this.tick % 8 < 4 ? chalk_1.default.hex((0, theme_1.agentTheme)(this.agentName).hex)("▍") : chalk_1.default.dim("▏");
|
|
858
|
+
}
|
|
859
|
+
lines.push(head + body);
|
|
860
|
+
});
|
|
861
|
+
}
|
|
862
|
+
this.viewportCache = { lines, key };
|
|
863
|
+
return lines;
|
|
864
|
+
}
|
|
865
|
+
// borders(2) + rail + rail-border(1) + gutter(1)
|
|
866
|
+
viewW() { return this.cols() - 2 - RAIL_W - 2; }
|
|
867
|
+
railLines(h) {
|
|
868
|
+
const out = [];
|
|
869
|
+
const W = RAIL_W;
|
|
870
|
+
out.push("");
|
|
871
|
+
for (const name of theme_1.AGENT_ORDER) {
|
|
872
|
+
const t = (0, theme_1.agentTheme)(name);
|
|
873
|
+
const active = name === this.agentName;
|
|
874
|
+
const tally = this.orch.tally(name);
|
|
875
|
+
let marker;
|
|
876
|
+
if (tally.run)
|
|
877
|
+
marker = this.tick % 2 ? chalk_1.default.hex(t.hex)(t.symbol) : chalk_1.default.hex(t.hex).dim(t.symbol);
|
|
878
|
+
else
|
|
879
|
+
marker = active ? chalk_1.default.hex(t.hex)("●") : chalk_1.default.hex(theme_1.PALETTE.inkFaint)("·");
|
|
880
|
+
let badge = "";
|
|
881
|
+
if (tally.ok)
|
|
882
|
+
badge += chalk_1.default.hex("#3a7a6e")(` ✓${tally.ok}`);
|
|
883
|
+
if (tally.fail)
|
|
884
|
+
badge += chalk_1.default.hex("#b3342d")(` ✗${tally.fail}`);
|
|
885
|
+
const label = active ? chalk_1.default.bold.hex(t.hex)(`${t.kanji} ${t.name}`) : chalk_1.default.hex(t.hex).dim(`${t.kanji} ${t.name}`);
|
|
886
|
+
out.push(padAnsi(` ${marker} ${label}${badge}`, W));
|
|
887
|
+
}
|
|
888
|
+
out.push(chalk_1.default.hex(theme_1.PALETTE.inkFaint)(" " + "╌".repeat(W - 2)));
|
|
889
|
+
const t = (0, theme_1.agentTheme)(this.agentName);
|
|
890
|
+
for (const ln of wrapPlain(t.poem, W - 2).slice(0, 2)) {
|
|
891
|
+
out.push(" " + chalk_1.default.hex(theme_1.PALETTE.inkLight).italic(ln));
|
|
892
|
+
}
|
|
893
|
+
out.push(" " + chalk_1.default.hex(theme_1.PALETTE.inkLight).dim(t.pigment));
|
|
894
|
+
if (this.orch.active) {
|
|
895
|
+
const p = this.orch.progress();
|
|
896
|
+
out.push("");
|
|
897
|
+
out.push(" " + chalk_1.default.hex(t.hex)(`織 ${p.done}/${p.total}`) + chalk_1.default.dim(" 梭"));
|
|
898
|
+
}
|
|
899
|
+
while (out.length < h)
|
|
900
|
+
out.push("");
|
|
901
|
+
return out.slice(0, h);
|
|
902
|
+
}
|
|
903
|
+
/** Compose and flush a frame. Returns the composed rows (used by tests). */
|
|
904
|
+
paint() {
|
|
905
|
+
if (this.destroyed)
|
|
906
|
+
return [];
|
|
907
|
+
const cols = this.cols();
|
|
908
|
+
const rows = this.rows();
|
|
909
|
+
const innerW = cols - 2;
|
|
910
|
+
const t = (0, theme_1.agentTheme)(this.agentName);
|
|
911
|
+
const frame = [];
|
|
912
|
+
const faint = chalk_1.default.hex(theme_1.PALETTE.inkFaint);
|
|
913
|
+
const B = (s) => faint(s);
|
|
914
|
+
if (cols < 60 || rows < 14) {
|
|
915
|
+
const small = [chalk_1.default.yellow(" 窗口太小 · 请放大终端 (≥60×14) ")];
|
|
916
|
+
this.screen.flush(small, null);
|
|
917
|
+
return small;
|
|
918
|
+
}
|
|
919
|
+
// ── header: title + seal ──
|
|
920
|
+
{
|
|
921
|
+
const seal = chalk_1.default.bgHex(t.hex).hex(theme_1.PALETTE.paper).bold(` ${t.kanji} `);
|
|
922
|
+
const title = chalk_1.default.bold(" 天空织机 ") + chalk_1.default.dim("Skyloom ");
|
|
923
|
+
// ┌─ title ───…─ seal ─┐ → 2 + w(title) + fill + 4 + 2 = cols
|
|
924
|
+
const fill = innerW - (0, tui_1.visualWidth)(title) - 6;
|
|
925
|
+
frame.push(B("┌─") + title + B("─".repeat(Math.max(0, fill))) + seal + B("─┐"));
|
|
926
|
+
}
|
|
927
|
+
// ── sky band ──
|
|
928
|
+
{
|
|
929
|
+
const shuttles = this.orch.active
|
|
930
|
+
? this.orch.runningAgents().map((a, i) => {
|
|
931
|
+
const th = (0, theme_1.agentTheme)(a);
|
|
932
|
+
return { symbol: th.symbol, hex: th.hex, x: (this.orch.shuttleX.get(a) || 0) % innerW, row: i };
|
|
933
|
+
})
|
|
934
|
+
: [];
|
|
935
|
+
const skyRows = this.sky.render(innerW, t.motion, t.symbol, t.hex, this.tick, shuttles);
|
|
936
|
+
const mountain = mountainRow(innerW, this.turns);
|
|
937
|
+
frame.push(B("│") + padAnsi(skyRows[0], innerW) + B("│"));
|
|
938
|
+
// mountain sits behind the lower particle row: particles overlay where present
|
|
939
|
+
frame.push(B("│") + overlay(mountain, skyRows[1], innerW) + B("│"));
|
|
940
|
+
}
|
|
941
|
+
// ── body: rail │ viewport ──
|
|
942
|
+
const bodyH = this.bodyH();
|
|
943
|
+
const rail = this.railLines(bodyH);
|
|
944
|
+
const view = this.viewportLines();
|
|
945
|
+
this.clampScroll();
|
|
946
|
+
const start = Math.max(0, view.length - bodyH - this.scrollOff);
|
|
947
|
+
const visible = view.slice(start, start + bodyH);
|
|
948
|
+
for (let i = 0; i < bodyH; i++) {
|
|
949
|
+
const left = padAnsi(rail[i] ?? "", RAIL_W);
|
|
950
|
+
const right = padAnsi(visible[i] ?? "", this.viewW());
|
|
951
|
+
frame.push(B("│") + left + B("│") + " " + right + B("│"));
|
|
952
|
+
}
|
|
953
|
+
// ── status divider ──
|
|
954
|
+
{
|
|
955
|
+
let leftLabel = "";
|
|
956
|
+
if (this.modal)
|
|
957
|
+
leftLabel = "";
|
|
958
|
+
else if (this.busy && this.busyLabel) {
|
|
959
|
+
const dots = ["· ", "·· ", "···", " ··", " ·", " "][this.tick % 6];
|
|
960
|
+
leftLabel = ` ${chalk_1.default.hex(t.hex)(t.symbol)} ${chalk_1.default.dim(this.busyLabel + " " + dots)} `;
|
|
961
|
+
}
|
|
962
|
+
else if (this.flashHint)
|
|
963
|
+
leftLabel = " " + chalk_1.default.yellow(this.flashHint) + " ";
|
|
964
|
+
else if (this.scrollOff > 0)
|
|
965
|
+
leftLabel = " " + chalk_1.default.dim(`↑ 回看中 · Esc 回到末尾`) + " ";
|
|
966
|
+
else if (this.modeBadge)
|
|
967
|
+
leftLabel = " " + this.modeBadge + " ";
|
|
968
|
+
const right = this.statusRight();
|
|
969
|
+
const rightLabel = right ? ` ${right} ` : "";
|
|
970
|
+
const fill = innerW - (0, tui_1.visualWidth)(leftLabel) - (0, tui_1.visualWidth)(rightLabel);
|
|
971
|
+
frame.push(B("├") + leftLabel + B("─".repeat(Math.max(0, fill))) + rightLabel + B("┤"));
|
|
972
|
+
}
|
|
973
|
+
// ── palette overlay (replaces tail viewport rows visually — drawn over input-adjacent rows) ──
|
|
974
|
+
// (kept simple: palette renders inside the viewport's final rows via paint order below)
|
|
975
|
+
// ── input row ──
|
|
976
|
+
let cursorPos = null;
|
|
977
|
+
{
|
|
978
|
+
let content;
|
|
979
|
+
if (this.modal) {
|
|
980
|
+
content = " " + chalk_1.default.yellow("⚠ ") + cutVisual(this.modal.text, innerW - 14) + chalk_1.default.bold(" 允许? ") + chalk_1.default.dim("[y/N]");
|
|
981
|
+
cursorPos = { row: rows - 2, col: Math.min(innerW, (0, tui_1.visualWidth)(content) + 1) };
|
|
982
|
+
}
|
|
983
|
+
else {
|
|
984
|
+
const promptStr = chalk_1.default.hex(t.hex)(` ${t.symbol} `) + chalk_1.default.hex(theme_1.PALETTE.inkLight)("❯ ");
|
|
985
|
+
const promptW = (0, tui_1.visualWidth)(promptStr);
|
|
986
|
+
const avail = innerW - promptW - 1;
|
|
987
|
+
// horizontal scroll window around the cursor
|
|
988
|
+
const glyphs = this.inputGlyphs;
|
|
989
|
+
let beforeW = 0;
|
|
990
|
+
for (let i = 0; i < this.cursor; i++)
|
|
991
|
+
beforeW += (0, tui_1.charWidth)(glyphs[i].codePointAt(0));
|
|
992
|
+
let startIdx = 0, skipW = 0;
|
|
993
|
+
while (beforeW - skipW > avail - 2 && startIdx < glyphs.length) {
|
|
994
|
+
skipW += (0, tui_1.charWidth)(glyphs[startIdx].codePointAt(0));
|
|
995
|
+
startIdx++;
|
|
996
|
+
}
|
|
997
|
+
let shown = "", shownW = 0, cursorCol = promptW + (beforeW - skipW);
|
|
998
|
+
for (let i = startIdx; i < glyphs.length; i++) {
|
|
999
|
+
const cw = (0, tui_1.charWidth)(glyphs[i].codePointAt(0));
|
|
1000
|
+
if (shownW + cw > avail)
|
|
1001
|
+
break;
|
|
1002
|
+
shown += glyphs[i];
|
|
1003
|
+
shownW += cw;
|
|
1004
|
+
}
|
|
1005
|
+
content = promptStr + shown;
|
|
1006
|
+
cursorPos = { row: rows - 2, col: 1 + cursorCol };
|
|
1007
|
+
}
|
|
1008
|
+
frame.push(B("│") + padAnsi(content, innerW) + B("│"));
|
|
1009
|
+
}
|
|
1010
|
+
// ── bottom border with hints ──
|
|
1011
|
+
{
|
|
1012
|
+
const paletteUp = this.paletteMatches().length > 0 && this.inputGlyphs[0] === "/";
|
|
1013
|
+
const hint = this.busy
|
|
1014
|
+
? " Ctrl-C 中断本轮 "
|
|
1015
|
+
: paletteUp
|
|
1016
|
+
? " ↑↓ 选命令 · Enter 执行 · Tab 补全 · Esc 收起 "
|
|
1017
|
+
: " / 命令 · PgUp 回看 · Shift+Tab 切模式 · Ctrl-C 退出 ";
|
|
1018
|
+
// └─ hint ───…┘ → 2 + w(hint) + fill + 1 = cols
|
|
1019
|
+
const fill = innerW - (0, tui_1.visualWidth)(hint) - 1;
|
|
1020
|
+
frame.push(B("└─") + chalk_1.default.dim(hint) + B("─".repeat(Math.max(0, fill)) + "┘"));
|
|
1021
|
+
}
|
|
1022
|
+
// ── slash palette: overlay onto the rows just above the divider ──
|
|
1023
|
+
const matches = this.paletteMatches();
|
|
1024
|
+
if (matches.length > 0 && this.inputGlyphs[0] === "/" && !this.modal) {
|
|
1025
|
+
const maxShow = Math.min(8, bodyH - 1);
|
|
1026
|
+
this.paletteIdx = Math.max(0, Math.min(this.paletteIdx, matches.length - 1));
|
|
1027
|
+
// scroll window that keeps the ↑↓ selection visible
|
|
1028
|
+
const start = Math.max(0, Math.min(this.paletteIdx - maxShow + 1, matches.length - maxShow));
|
|
1029
|
+
const show = matches.slice(start, start + maxShow);
|
|
1030
|
+
const baseRow = 1 + SKY_H + bodyH - show.length; // first overlay row index in frame
|
|
1031
|
+
show.forEach(([cmd, desc], i) => {
|
|
1032
|
+
const sel = start + i === this.paletteIdx;
|
|
1033
|
+
const agentCmd = ["/fog", "/rain", "/frost", "/snow", "/dew", "/fair"].includes(cmd.trim());
|
|
1034
|
+
const color = agentCmd ? chalk_1.default.hex((0, theme_1.agentTheme)(cmd.trim().slice(1)).hex) : chalk_1.default.hex(theme_1.PALETTE.inkLight);
|
|
1035
|
+
const mark = sel ? chalk_1.default.hex(t.hex)(" ▸ ") : " ";
|
|
1036
|
+
const counter = sel && matches.length > maxShow ? chalk_1.default.dim(` ${this.paletteIdx + 1}/${matches.length}`) : "";
|
|
1037
|
+
const lineStr = mark + (sel ? chalk_1.default.bold(color(cmd.padEnd(11))) : color(cmd.padEnd(11))) + chalk_1.default.dim(cutVisual(desc, this.viewW() - 22)) + counter;
|
|
1038
|
+
const row = baseRow + i;
|
|
1039
|
+
frame[row] = B("│") + padAnsi(this.railLines(bodyH)[row - 1 - SKY_H] ?? "", RAIL_W) + B("│") + " " + padAnsi(lineStr, this.viewW()) + B("│");
|
|
1040
|
+
});
|
|
1041
|
+
}
|
|
1042
|
+
this.screen.flush(frame, cursorPos);
|
|
1043
|
+
return frame;
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
exports.LoomUI = LoomUI;
|
|
1047
|
+
/** Overlay `top` onto `base` (top's non-space glyphs win), to a fixed width. */
|
|
1048
|
+
function overlay(base, top, width) {
|
|
1049
|
+
// Both strings are styled; walk them in parallel by visual column.
|
|
1050
|
+
const cells = (s) => {
|
|
1051
|
+
const out = [];
|
|
1052
|
+
let i = 0;
|
|
1053
|
+
let pending = "";
|
|
1054
|
+
while (i < s.length && out.length < width * 2) {
|
|
1055
|
+
if (s[i] === ESC) {
|
|
1056
|
+
const m = ANSI_RE.exec(s.slice(i));
|
|
1057
|
+
if (m && m.index === 0) {
|
|
1058
|
+
pending += m[0];
|
|
1059
|
+
i += m[0].length;
|
|
1060
|
+
continue;
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
1063
|
+
const cp = s.codePointAt(i);
|
|
1064
|
+
const ch = String.fromCodePoint(cp);
|
|
1065
|
+
const cw = (0, tui_1.charWidth)(cp);
|
|
1066
|
+
out.push(pending + ch);
|
|
1067
|
+
pending = "";
|
|
1068
|
+
if (cw === 2)
|
|
1069
|
+
out.push(""); // wide glyph occupies two cells
|
|
1070
|
+
i += ch.length;
|
|
1071
|
+
}
|
|
1072
|
+
return out;
|
|
1073
|
+
};
|
|
1074
|
+
const b = cells(base);
|
|
1075
|
+
const t = cells(top);
|
|
1076
|
+
let res = "";
|
|
1077
|
+
for (let x = 0; x < width; x++) {
|
|
1078
|
+
const tc = t[x];
|
|
1079
|
+
const bc = b[x];
|
|
1080
|
+
const topVisible = tc !== undefined && tc.replace(/\x1b\[[0-9;]*m/g, "") !== " " && tc !== "";
|
|
1081
|
+
if (topVisible)
|
|
1082
|
+
res += tc + "\x1b[0m";
|
|
1083
|
+
else if (tc === "")
|
|
1084
|
+
continue; // second cell of a wide top glyph
|
|
1085
|
+
else if (bc !== undefined && bc !== "")
|
|
1086
|
+
res += bc + "\x1b[0m";
|
|
1087
|
+
else if (bc === "")
|
|
1088
|
+
continue;
|
|
1089
|
+
else
|
|
1090
|
+
res += " ";
|
|
1091
|
+
}
|
|
1092
|
+
return padAnsi(res, width);
|
|
1093
|
+
}
|
|
1094
|
+
//# sourceMappingURL=loom.js.map
|