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.
Files changed (195) hide show
  1. package/.github/workflows/ci.yml +36 -36
  2. package/README.md +220 -159
  3. package/config/providers.yaml +39 -39
  4. package/config/skills/api_integrator/SKILL.md +15 -15
  5. package/config/skills/arch_designer/SKILL.md +13 -13
  6. package/config/skills/ci_cd_manager/SKILL.md +14 -14
  7. package/config/skills/code_analysis/SKILL.md +13 -13
  8. package/config/skills/code_generator/SKILL.md +12 -12
  9. package/config/skills/code_reviewer/SKILL.md +13 -13
  10. package/config/skills/content_writer/SKILL.md +14 -14
  11. package/config/skills/data_transformer/SKILL.md +15 -15
  12. package/config/skills/document_analysis/SKILL.md +13 -13
  13. package/config/skills/emotional_companion/SKILL.md +15 -15
  14. package/config/skills/performance_checker/SKILL.md +14 -14
  15. package/config/skills/security_auditor/SKILL.md +14 -14
  16. package/config/skills/self_evolve/SKILL.md +13 -13
  17. package/config/skills/sys_operator/SKILL.md +15 -15
  18. package/config/skills/task_planner/SKILL.md +14 -14
  19. package/config/skills/web_research/SKILL.md +14 -14
  20. package/config/skills/workflow_designer/SKILL.md +13 -13
  21. package/dist/agents/dew.js +52 -52
  22. package/dist/agents/fair.js +84 -84
  23. package/dist/agents/fog.js +30 -30
  24. package/dist/agents/frost.js +32 -32
  25. package/dist/agents/rain.js +32 -32
  26. package/dist/agents/snow.js +68 -68
  27. package/dist/cli/commands_md.d.ts +41 -0
  28. package/dist/cli/commands_md.d.ts.map +1 -0
  29. package/dist/cli/commands_md.js +140 -0
  30. package/dist/cli/commands_md.js.map +1 -0
  31. package/dist/cli/input_macros.d.ts +28 -0
  32. package/dist/cli/input_macros.d.ts.map +1 -0
  33. package/dist/cli/input_macros.js +120 -0
  34. package/dist/cli/input_macros.js.map +1 -0
  35. package/dist/cli/loom.d.ts +220 -0
  36. package/dist/cli/loom.d.ts.map +1 -0
  37. package/dist/cli/loom.js +1094 -0
  38. package/dist/cli/loom.js.map +1 -0
  39. package/dist/cli/loom_chat.d.ts +20 -0
  40. package/dist/cli/loom_chat.d.ts.map +1 -0
  41. package/dist/cli/loom_chat.js +685 -0
  42. package/dist/cli/loom_chat.js.map +1 -0
  43. package/dist/cli/main.js +310 -14
  44. package/dist/cli/main.js.map +1 -1
  45. package/dist/cli/tui.d.ts.map +1 -1
  46. package/dist/cli/tui.js +7 -1
  47. package/dist/cli/tui.js.map +1 -1
  48. package/dist/core/agent/guard.d.ts +45 -0
  49. package/dist/core/agent/guard.d.ts.map +1 -0
  50. package/dist/core/agent/guard.js +113 -0
  51. package/dist/core/agent/guard.js.map +1 -0
  52. package/dist/core/agent.d.ts +17 -0
  53. package/dist/core/agent.d.ts.map +1 -1
  54. package/dist/core/agent.js +182 -93
  55. package/dist/core/agent.js.map +1 -1
  56. package/dist/core/factory.d.ts.map +1 -1
  57. package/dist/core/factory.js +34 -2
  58. package/dist/core/factory.js.map +1 -1
  59. package/dist/core/file_checkpoint.d.ts +57 -0
  60. package/dist/core/file_checkpoint.d.ts.map +1 -0
  61. package/dist/core/file_checkpoint.js +162 -0
  62. package/dist/core/file_checkpoint.js.map +1 -0
  63. package/dist/core/hooks.d.ts +43 -0
  64. package/dist/core/hooks.d.ts.map +1 -0
  65. package/dist/core/hooks.js +110 -0
  66. package/dist/core/hooks.js.map +1 -0
  67. package/dist/core/llm.d.ts.map +1 -1
  68. package/dist/core/llm.js +15 -9
  69. package/dist/core/llm.js.map +1 -1
  70. package/dist/core/longdoc.js +5 -5
  71. package/dist/core/mcp.d.ts +16 -0
  72. package/dist/core/mcp.d.ts.map +1 -1
  73. package/dist/core/mcp.js +55 -0
  74. package/dist/core/mcp.js.map +1 -1
  75. package/dist/core/model_config.d.ts +40 -0
  76. package/dist/core/model_config.d.ts.map +1 -0
  77. package/dist/core/model_config.js +191 -0
  78. package/dist/core/model_config.js.map +1 -0
  79. package/dist/core/skill.d.ts +7 -0
  80. package/dist/core/skill.d.ts.map +1 -1
  81. package/dist/core/skill.js +47 -0
  82. package/dist/core/skill.js.map +1 -1
  83. package/dist/core/skymd.d.ts +39 -0
  84. package/dist/core/skymd.d.ts.map +1 -0
  85. package/dist/core/skymd.js +177 -0
  86. package/dist/core/skymd.js.map +1 -0
  87. package/dist/core/tool.d.ts +12 -0
  88. package/dist/core/tool.d.ts.map +1 -1
  89. package/dist/core/tool.js +30 -0
  90. package/dist/core/tool.js.map +1 -1
  91. package/dist/core/verify.d.ts +27 -0
  92. package/dist/core/verify.d.ts.map +1 -0
  93. package/dist/core/verify.js +62 -0
  94. package/dist/core/verify.js.map +1 -0
  95. package/dist/skills/loader.d.ts +22 -2
  96. package/dist/skills/loader.d.ts.map +1 -1
  97. package/dist/skills/loader.js +45 -15
  98. package/dist/skills/loader.js.map +1 -1
  99. package/dist/tools/builtin.d.ts.map +1 -1
  100. package/dist/tools/builtin.js +13 -3
  101. package/dist/tools/builtin.js.map +1 -1
  102. package/dist/tools/model_tool.d.ts +11 -0
  103. package/dist/tools/model_tool.d.ts.map +1 -0
  104. package/dist/tools/model_tool.js +71 -0
  105. package/dist/tools/model_tool.js.map +1 -0
  106. package/dist/tools/todo.d.ts +30 -0
  107. package/dist/tools/todo.d.ts.map +1 -0
  108. package/dist/tools/todo.js +78 -0
  109. package/dist/tools/todo.js.map +1 -0
  110. package/docs/AESTHETIC_DESIGN.md +152 -144
  111. package/docs/OPTIMIZATION_PLAN.md +178 -178
  112. package/package.json +1 -1
  113. package/scripts/install.js +48 -48
  114. package/scripts/link.js +10 -10
  115. package/setup.bat +79 -79
  116. package/skill-test-ty2fOA/test.md +10 -10
  117. package/src/agents/dew.ts +70 -70
  118. package/src/agents/fair.ts +102 -102
  119. package/src/agents/fog.ts +48 -48
  120. package/src/agents/frost.ts +50 -50
  121. package/src/agents/rain.ts +50 -50
  122. package/src/agents/snow.ts +239 -239
  123. package/src/cli/commands_md.ts +112 -0
  124. package/src/cli/input_macros.ts +83 -0
  125. package/src/cli/loom.ts +982 -0
  126. package/src/cli/loom_chat.ts +598 -0
  127. package/src/cli/main.ts +255 -9
  128. package/src/cli/mode.ts +58 -58
  129. package/src/cli/tui.ts +228 -222
  130. package/src/core/agent/guard.ts +134 -0
  131. package/src/core/agent/task.ts +100 -100
  132. package/src/core/agent.ts +177 -95
  133. package/src/core/arbitrate.ts +162 -162
  134. package/src/core/catalog.ts +178 -178
  135. package/src/core/checkpoint.ts +94 -94
  136. package/src/core/estimate.ts +104 -104
  137. package/src/core/evolve.ts +191 -191
  138. package/src/core/factory.ts +31 -2
  139. package/src/core/file_checkpoint.ts +136 -0
  140. package/src/core/filter.ts +103 -103
  141. package/src/core/graph.ts +156 -156
  142. package/src/core/hooks.ts +126 -0
  143. package/src/core/icons.ts +53 -53
  144. package/src/core/index.ts +37 -37
  145. package/src/core/learn.ts +146 -146
  146. package/src/core/llm.ts +15 -9
  147. package/src/core/longdoc.ts +155 -155
  148. package/src/core/mcp.ts +48 -0
  149. package/src/core/mcp_server.ts +176 -176
  150. package/src/core/model_config.ts +157 -0
  151. package/src/core/profile.ts +255 -255
  152. package/src/core/router.ts +124 -124
  153. package/src/core/sandbox.ts +142 -142
  154. package/src/core/security.ts +243 -243
  155. package/src/core/skill.ts +42 -0
  156. package/src/core/skymd.ts +143 -0
  157. package/src/core/theme.ts +65 -65
  158. package/src/core/tool.ts +30 -0
  159. package/src/core/tool_router.ts +193 -193
  160. package/src/core/vector.ts +152 -152
  161. package/src/core/verify.ts +71 -0
  162. package/src/core/workspace.ts +150 -150
  163. package/src/plugins/loader.ts +66 -66
  164. package/src/skills/loader.ts +45 -16
  165. package/src/sql.js.d.ts +29 -29
  166. package/src/tools/builtin.ts +13 -3
  167. package/src/tools/computer.ts +269 -269
  168. package/src/tools/delegate.ts +49 -49
  169. package/src/tools/model_tool.ts +74 -0
  170. package/src/tools/todo.ts +76 -0
  171. package/src/web/tts.ts +93 -93
  172. package/tests/agent.test.ts +159 -159
  173. package/tests/agent_helpers.test.ts +48 -48
  174. package/tests/bus.test.ts +121 -121
  175. package/tests/catalog.test.ts +86 -86
  176. package/tests/checkpoint_commands.test.ts +124 -0
  177. package/tests/claude_compat.test.ts +110 -0
  178. package/tests/config.test.ts +41 -41
  179. package/tests/guard.test.ts +75 -0
  180. package/tests/icons.test.ts +45 -45
  181. package/tests/loom.test.ts +248 -0
  182. package/tests/memory.test.ts +170 -170
  183. package/tests/model_config.test.ts +109 -0
  184. package/tests/router.test.ts +86 -86
  185. package/tests/schemas.test.ts +51 -51
  186. package/tests/semantic.test.ts +83 -83
  187. package/tests/setup.ts +10 -10
  188. package/tests/skill.test.ts +172 -172
  189. package/tests/skymd.test.ts +146 -0
  190. package/tests/task.test.ts +60 -60
  191. package/tests/todo_toolstats.test.ts +94 -0
  192. package/tests/tool.test.ts +108 -108
  193. package/tests/tool_router.test.ts +71 -71
  194. package/tests/tui.test.ts +67 -67
  195. package/vitest.config.ts +17 -17
@@ -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