huddle-cli 0.1.0 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/CHANGELOG.md +69 -0
  2. package/QUICKSTART.md +135 -0
  3. package/README.md +26 -4
  4. package/dist/adapters/amp.js +1 -1
  5. package/dist/adapters/antigravity.js +2 -0
  6. package/dist/adapters/claude.js +19 -3
  7. package/dist/adapters/codex.js +9 -1
  8. package/dist/adapters/cursor.js +12 -5
  9. package/dist/adapters/custom.js +4 -2
  10. package/dist/adapters/droid.js +1 -0
  11. package/dist/adapters/goose.js +2 -0
  12. package/dist/adapters/index.js +44 -18
  13. package/dist/adapters/kiro.js +1 -1
  14. package/dist/adapters/opencode.js +1 -1
  15. package/dist/app/live.js +142 -0
  16. package/dist/app/page.js +701 -0
  17. package/dist/app/server.js +262 -0
  18. package/dist/args.js +18 -0
  19. package/dist/attach/connect.js +7 -4
  20. package/dist/attach/guard.js +21 -2
  21. package/dist/attach/live.js +5 -1
  22. package/dist/attach/plug.js +21 -3
  23. package/dist/attach/screen.js +150 -0
  24. package/dist/attach/session.js +7 -2
  25. package/dist/cli.js +134 -42
  26. package/dist/mcp/context-tools.js +79 -0
  27. package/dist/mcp/server.js +57 -11
  28. package/dist/orchestrator/ask.js +13 -10
  29. package/dist/orchestrator/brain.js +16 -4
  30. package/dist/orchestrator/budget.js +283 -0
  31. package/dist/orchestrator/busy.js +106 -0
  32. package/dist/orchestrator/council.js +434 -87
  33. package/dist/orchestrator/goal.js +196 -0
  34. package/dist/orchestrator/handoff.js +2 -2
  35. package/dist/orchestrator/launcher.js +31 -13
  36. package/dist/orchestrator/learn.js +101 -0
  37. package/dist/orchestrator/planner.js +54 -9
  38. package/dist/orchestrator/qa.js +71 -15
  39. package/dist/orchestrator/radar.js +719 -0
  40. package/dist/orchestrator/scheduler.js +575 -50
  41. package/dist/orchestrator/takeover.js +155 -0
  42. package/dist/platform.js +377 -0
  43. package/dist/room/context.js +1285 -0
  44. package/dist/room/contract.js +132 -0
  45. package/dist/room/db.js +79 -16
  46. package/dist/room/followup.js +233 -0
  47. package/dist/room/identity.js +2 -2
  48. package/dist/room/lessons.js +275 -0
  49. package/dist/room/live.js +75 -20
  50. package/dist/room/mail.js +225 -0
  51. package/dist/room/progress.js +40 -1
  52. package/dist/room/server.js +220 -39
  53. package/dist/room/tunnel.js +6 -5
  54. package/dist/room/user.js +54 -0
  55. package/dist/sandbox/sync.js +4 -4
  56. package/dist/sandbox/worktree.js +145 -14
  57. package/dist/ui/app.js +963 -159
  58. package/dist/ui/brief.js +494 -0
  59. package/dist/ui/catalog.js +62 -24
  60. package/dist/ui/dashboard.js +269 -55
  61. package/dist/ui/doctor.js +15 -11
  62. package/dist/ui/export.js +1194 -0
  63. package/dist/ui/lessons.js +81 -0
  64. package/dist/ui/narrator.js +56 -0
  65. package/dist/ui/paste.js +515 -0
  66. package/dist/ui/remote.js +11 -5
  67. package/dist/ui/setup.js +152 -0
  68. package/dist/ui/story.js +716 -94
  69. package/dist/ui/talk.js +176 -0
  70. package/dist/ui/term.js +182 -16
  71. package/dist/ui/transcript.js +434 -0
  72. package/dist/ui/watch.js +149 -48
  73. package/package.json +6 -3
@@ -0,0 +1,176 @@
1
+ // You and the room, during a run: the agents' questions for you
2
+ // (huddle_ask_user) and what you type to them.
3
+ //
4
+ // A question shows like the ones Claude Code, Codex and Cursor ask, with
5
+ // numbered options, the agent's recommendation first:
6
+ //
7
+ // ⚑ claude asks you · should it also convert Rankine?
8
+ // 1. No, just C, F and K (recommended)
9
+ // 2. Yes, add Rankine too
10
+ // type 1-2 · Enter = 1 · or answer in your own words · picks 1 in 90s
11
+ //
12
+ // Unanswered, it goes with option 1 when the time runs out: the run never
13
+ // waits on someone who walked away. Anything else you type during a run goes
14
+ // to every working agent, sent as "you" (room/user.ts); "@codex …" to one.
15
+ import { fmtDuration } from "../adapters/usage.js";
16
+ import { C } from "./term.js";
17
+ export class Talk {
18
+ now;
19
+ queue = [];
20
+ /** A question is on screen (the first one asked; the rest wait their turn). */
21
+ onShow = () => { };
22
+ /** It was answered, by you or by the clock. */
23
+ onSettled = () => { };
24
+ /** Set by the run while it's on: what you typed for the agents, and `details`. */
25
+ say = null;
26
+ details = null;
27
+ constructor(now = () => Date.now()) {
28
+ this.now = now;
29
+ }
30
+ /** The question on screen and when it times out, if any. */
31
+ current() {
32
+ const p = this.queue[0];
33
+ return p ? { q: p.q, until: p.until } : null;
34
+ }
35
+ ask(q) {
36
+ return new Promise((resolve) => {
37
+ const p = { q, until: 0, settle: resolve, timer: undefined };
38
+ this.queue.push(p);
39
+ if (this.queue.length === 1)
40
+ this.show();
41
+ });
42
+ }
43
+ /** The time starts when a question reaches the screen, not when it was asked. */
44
+ show() {
45
+ const p = this.queue[0];
46
+ if (!p)
47
+ return;
48
+ p.until = this.now() + p.q.waitMs;
49
+ p.timer = setTimeout(() => this.settle({ choice: 1, text: "", by: "default" }), p.q.waitMs);
50
+ this.onShow(p.q, p.until);
51
+ }
52
+ settle(a) {
53
+ const p = this.queue.shift();
54
+ if (!p)
55
+ return;
56
+ clearTimeout(p.timer);
57
+ p.settle(a);
58
+ this.onSettled(p.q, a);
59
+ this.show();
60
+ }
61
+ /**
62
+ * A line you typed. With a question up, it's the answer: a number picks that
63
+ * option, Enter picks 1, anything else is your answer in your own words.
64
+ * Returns false when no question is up (the line is for someone else).
65
+ */
66
+ answer(line) {
67
+ const p = this.queue[0];
68
+ if (!p)
69
+ return false;
70
+ const t = line.trim();
71
+ const k = Number(t);
72
+ if (!t)
73
+ this.settle({ choice: 1, text: "", by: "you" });
74
+ else if (/^\d$/.test(t) && k >= 1 && k <= p.q.options.length)
75
+ this.settle({ choice: k, text: "", by: "you" });
76
+ else
77
+ this.settle({ choice: null, text: t, by: "you" });
78
+ return true;
79
+ }
80
+ /** Every open question goes with option 1 (the run ended or was stopped). */
81
+ close() {
82
+ while (this.queue.length)
83
+ this.settle({ choice: 1, text: "", by: "default" });
84
+ }
85
+ }
86
+ /** The question as it's printed. */
87
+ export function questionLines(q, until, now = Date.now()) {
88
+ const out = [` ${C.yellow("⚑")} ${C.bold(q.from)} ${C.yellow("asks you")} ${C.faint("·")} ${q.question}`];
89
+ if (q.why)
90
+ out.push(C.faint(` ${q.why}`));
91
+ q.options.forEach((o, i) => out.push(` ${C.bold(String(i + 1) + ".")} ${o}${i === 0 ? C.faint(" (recommended)") : ""}`));
92
+ out.push(C.faint(` type 1-${q.options.length} · Enter = 1 · or answer in your own words · picks 1 in ${fmtDuration(Math.max(0, until - now))}`));
93
+ return out;
94
+ }
95
+ /** " → you chose 2: Yes, add Rankine too" / " → no answer in 1m30s: claude goes with 1: …" */
96
+ export function settledLine(q, a) {
97
+ if (a.by === "default")
98
+ return C.faint(` → no answer in ${fmtDuration(q.waitMs)}: ${q.from} goes with 1 · ${q.options[0]}`);
99
+ return ` ${C.green("→")} ${a.choice ? `you chose ${a.choice} · ${q.options[a.choice - 1]}` : `you said: ${a.text}`} ${C.faint(`· sent to ${q.from}`)}`;
100
+ }
101
+ /** "@codex use 2 decimals" → to codex; anything else → everyone working. */
102
+ export function addressee(line) {
103
+ const m = /^@([\w#./-]+)[,:]?\s+([\s\S]+)$/.exec(line.trim());
104
+ return m ? { to: m[1], text: m[2].trim() } : { text: line.trim() };
105
+ }
106
+ /**
107
+ * A typing line under a one-shot run's pinned block (`huddle join` on a
108
+ * terminal), so you can answer questions and talk to the agents there too.
109
+ * Raw keys: Enter sends, Backspace and ctrl-u edit, ctrl-c stops the run.
110
+ */
111
+ export class KeyLine {
112
+ onLine;
113
+ onChange;
114
+ onInterrupt;
115
+ input;
116
+ buf = "";
117
+ on = false;
118
+ onData = (d) => this.feed(String(d));
119
+ constructor(onLine, onChange,
120
+ /** ctrl-c: raw mode keeps the terminal from sending SIGINT, so the run decides. */
121
+ onInterrupt = () => process.exit(130), input = process.stdin) {
122
+ this.onLine = onLine;
123
+ this.onChange = onChange;
124
+ this.onInterrupt = onInterrupt;
125
+ this.input = input;
126
+ }
127
+ start() {
128
+ if (this.on || !this.input.isTTY)
129
+ return this;
130
+ this.on = true;
131
+ this.input.setRawMode?.(true);
132
+ this.input.on("data", this.onData);
133
+ this.input.resume();
134
+ return this;
135
+ }
136
+ stop() {
137
+ if (!this.on)
138
+ return;
139
+ this.on = false;
140
+ this.input.off("data", this.onData);
141
+ this.input.setRawMode?.(false);
142
+ this.input.pause();
143
+ }
144
+ /** What the line shows: what you've typed so far, or how to use it. */
145
+ line() {
146
+ if (!this.on)
147
+ return "";
148
+ return this.buf
149
+ ? ` ${C.violet("›")} ${this.buf}${C.faint("▏")}`
150
+ : ` ${C.violet("›")} ${C.faint("type to talk to the agents (@claude for one) · details · ctrl-c stops")}`;
151
+ }
152
+ feed(s) {
153
+ // Arrows and other escape keys: not text.
154
+ for (const ch of s.replace(/\x1b\[[0-9;?]*[A-Za-z~]|\x1bO?./g, "")) {
155
+ if (ch === "\r" || ch === "\n") {
156
+ const l = this.buf;
157
+ this.buf = "";
158
+ this.onLine(l);
159
+ }
160
+ else if (ch === "\x7f" || ch === "\b")
161
+ this.buf = [...this.buf].slice(0, -1).join("");
162
+ else if (ch === "\x15")
163
+ this.buf = "";
164
+ else if (ch === "\x03") {
165
+ this.buf = "";
166
+ this.onInterrupt();
167
+ if (!this.on)
168
+ return;
169
+ }
170
+ else if (ch >= " ")
171
+ this.buf += ch;
172
+ }
173
+ this.onChange();
174
+ }
175
+ }
176
+ //# sourceMappingURL=talk.js.map
package/dist/ui/term.js CHANGED
@@ -1,6 +1,41 @@
1
1
  // Terminal primitives: color, width-aware padding, the rounded box, spinners.
2
2
  // Huddle lives in the terminal only, so this file is the whole "UI toolkit".
3
3
  const color = process.stdout.isTTY && !process.env.NO_COLOR;
4
+ /**
5
+ * A console that can't draw Huddle's glyphs: the old Windows console host
6
+ * (conhost: cmd.exe or PowerShell outside Windows Terminal) shows ⤳, ⇉,
7
+ * braille spinners and emoji as boxes or "?". HUDDLE_ASCII=1 forces plain
8
+ * ASCII anywhere; HUDDLE_ASCII=0 turns it off. Windows Terminal sets
9
+ * WT_SESSION, VS Code's terminal sets TERM_PROGRAM, ConEmu sets ConEmuANSI.
10
+ */
11
+ export function legacyConsole(env = process.env, platform = process.platform) {
12
+ if (env.HUDDLE_ASCII === "1")
13
+ return true;
14
+ if (env.HUDDLE_ASCII === "0")
15
+ return false;
16
+ return platform === "win32" && !env.WT_SESSION && !env.TERM_PROGRAM && env.ConEmuANSI !== "ON";
17
+ }
18
+ export const ASCII = legacyConsole();
19
+ const ASCII_MAP = {
20
+ "●": "*", "○": "o", "✓": "v", "✗": "x", "▸": ">", "◎": "@", "◆": "#", "◇": "#", "⚠": "!", "⇉": "=>", "⇢": "->",
21
+ "⤳": "~>", "⇄": "<>", "→": "->", "←": "<-", "↑": "^", "↓": "v", "…": "...", "·": "-", "—": "-", "–": "-", "“": '"', "”": '"',
22
+ "‘": "'", "’": "'", "─": "-", "━": "=", "│": "|", "╭": "+", "╮": "+", "╰": "+", "╯": "+", "┌": "+", "┐": "+", "└": "+", "┘": "+",
23
+ "█": "#", "░": ".", "▶": ">", "■": "#", "✎": "*", "⬢": "#", "⏱": "", "⚡": "!", "×": "x", "≥": ">=", "≤": "<=", "\uFE0F": "",
24
+ };
25
+ const ASCII_RE = new RegExp(`[${Object.keys(ASCII_MAP).join("")}]|[\u{1F000}-\u{1FAFF}\u{2600}-\u{27BF}]\uFE0F?|\uFE0F|[\u2800-\u28FF]`, "gu");
26
+ /** Huddle's glyphs → ASCII, for legacy consoles (escape codes pass through). */
27
+ export function toAscii(s) {
28
+ return s.replace(ASCII_RE, (c) => ASCII_MAP[c] ?? ASCII_MAP[c.replace("\uFE0F", "")] ?? (c >= "\u2800" && c <= "\u28FF" ? "|" : "*"));
29
+ }
30
+ /** In a legacy console, every write to stdout/stderr goes through toAscii (once, at startup). */
31
+ export function useAsciiOutput(on = ASCII) {
32
+ if (!on)
33
+ return;
34
+ for (const stream of [process.stdout, process.stderr]) {
35
+ const write = stream.write.bind(stream);
36
+ stream.write = (chunk, ...rest) => write(typeof chunk === "string" ? toAscii(chunk) : chunk, ...rest);
37
+ }
38
+ }
4
39
  const sgr = (open) => (s) => (color ? `\x1b[${open}m${s}\x1b[0m` : s);
5
40
  const hex = (h) => {
6
41
  const n = parseInt(h.slice(1), 16);
@@ -33,9 +68,21 @@ export const AGENT_COLOR = {
33
68
  };
34
69
  /** Colored dot for an agent; "claude#2" gets Claude's color. */
35
70
  export const dot = (agentId) => hex(AGENT_COLOR[agentId.split("#")[0]] ?? "#8A857C")("●");
36
- /** Visible width of a string (ignores ANSI escapes). */
71
+ /** Terminal cells for one character: emoji and CJK take two, combining marks none. */
72
+ function cells(ch) {
73
+ const c = ch.codePointAt(0);
74
+ if (c === 0xfe0f || c === 0x200d || (c >= 0x300 && c <= 0x36f))
75
+ return 0;
76
+ if ((c >= 0x1f300 && c <= 0x1faff) || (c >= 0x1100 && c <= 0x115f) || (c >= 0x2e80 && c <= 0xa4cf) || (c >= 0xac00 && c <= 0xd7a3) || (c >= 0xf900 && c <= 0xfaff) || (c >= 0xff00 && c <= 0xff60))
77
+ return 2;
78
+ return 1;
79
+ }
80
+ /** Visible width of a string in terminal cells (ignores ANSI escapes). */
37
81
  export function width(s) {
38
- return [...s.replace(/\x1b\[[0-9;]*m/g, "")].length;
82
+ let n = 0;
83
+ for (const ch of s.replace(/\x1b\[[0-9;]*m/g, ""))
84
+ n += cells(ch);
85
+ return n;
39
86
  }
40
87
  /** Cut to `n` visible characters, keeping color codes intact. */
41
88
  export function truncate(s, n) {
@@ -51,17 +98,71 @@ export function truncate(s, n) {
51
98
  continue;
52
99
  }
53
100
  const ch = String.fromCodePoint(s.codePointAt(i));
54
- if (seen >= n - 1)
55
- return out + "…\x1b[0m";
101
+ const w = cells(ch);
102
+ if (seen + w > n - 1)
103
+ return out + "…" + (out.includes("\x1b[") ? "\x1b[0m" : "");
56
104
  out += ch;
57
- seen++;
105
+ seen += w;
58
106
  i += ch.length;
59
107
  }
60
108
  return out;
61
109
  }
110
+ /** Word-wrap plain or colored text to `n` cells; `indent` starts every line after the first. */
111
+ export function wrap(s, n, indent = "") {
112
+ const lead = /^ */.exec(s)[0];
113
+ if (!indent.startsWith(lead))
114
+ indent = lead + indent;
115
+ const out = [];
116
+ let line = lead;
117
+ for (const word of s.slice(lead.length).split(/ +/)) {
118
+ if (line.trim() && width(line) + 1 + width(word) > n) {
119
+ out.push(line);
120
+ line = indent + word;
121
+ }
122
+ else
123
+ line = line.trim() ? line + " " + word : line + word;
124
+ }
125
+ out.push(line);
126
+ return out.map((l) => truncate(l, n + 1));
127
+ }
128
+ /** Columns to fit printed lines to: the terminal's (minus one, so nothing auto-wraps), or none when piped. */
129
+ export function lineWidth(out = process.stdout) {
130
+ return out.isTTY ? Math.max(20, (out.columns || 80) - 1) : null;
131
+ }
132
+ /** One printed line, cut to the terminal (no-op when piped: logs keep everything). */
133
+ export function fit(s, out = process.stdout) {
134
+ const w = lineWidth(out);
135
+ return w === null ? s : s.split("\n").map((l) => truncate(l, w)).join("\n");
136
+ }
62
137
  export function pad(s, n) {
63
138
  return s + " ".repeat(Math.max(0, n - width(s)));
64
139
  }
140
+ /**
141
+ * Whether progress bars draw block characters. Off when colors are (NO_COLOR,
142
+ * or output piped to a log): there a bar is just its label, "6/9 tasks".
143
+ * Tests and samples flip it to see the bars.
144
+ */
145
+ export const BARS = { on: color, ascii: ASCII };
146
+ /**
147
+ * A progress bar exactly `w` cells wide — "█████░░░" — or "" when bars are off
148
+ * or there's no room (under 2 cells). `frac` null = unknown: a faint dotted
149
+ * track, never a made-up 0%. Anything above 0 shows at least one block and
150
+ * anything below 1 leaves at least one empty, so 1% isn't nothing and 99%
151
+ * isn't done. Legacy consoles get "#####...".
152
+ */
153
+ export function bar(frac, w, o = {}) {
154
+ w = Math.floor(w);
155
+ if (!(o.on ?? BARS.on) || !(w >= 2))
156
+ return "";
157
+ const ascii = o.ascii ?? BARS.ascii;
158
+ const [full, empty, unknown] = ascii ? ["#", ".", "-"] : ["█", "░", "·"];
159
+ if (frac === null || !Number.isFinite(frac))
160
+ return C.faint(unknown.repeat(w));
161
+ const f = o.tone === "done" ? 1 : Math.min(1, Math.max(0, frac));
162
+ const n = f <= 0 ? 0 : f >= 1 ? w : Math.min(w - 1, Math.max(1, Math.round(f * w)));
163
+ const fill = o.tone === "done" ? C.green : o.tone === "failed" ? C.red : C.violet;
164
+ return (n ? fill(full.repeat(n)) : "") + (n < w ? C.faint(empty.repeat(w - n)) : "");
165
+ }
65
166
  /** Rounded box with a colored border, Claude-Code style. */
66
167
  export function box(lines, opts = {}) {
67
168
  const border = opts.border ?? C.violet;
@@ -71,44 +172,109 @@ export function box(lines, opts = {}) {
71
172
  const mid = lines.map((l) => border("│") + " " + pad(l, inner - 2) + " " + border("│"));
72
173
  return [top, ...mid, bot].join("\n");
73
174
  }
74
- const FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
75
- /** A set of lines that re-render in place while async work settles. */
175
+ const FRAMES = ASCII ? ["|", "/", "-", "\\"] : ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
176
+ /**
177
+ * A set of lines that re-render in place while async work settles. Each row
178
+ * is cut to the terminal's width: a row that wraps takes two screen lines,
179
+ * so moving the cursor up "one per row" would land mid-list and every redraw
180
+ * would print the list again further down (seen on Windows: rows doubled and
181
+ * spliced, "API_KEYodex · openai").
182
+ */
76
183
  export class LiveLines {
77
184
  render;
185
+ out;
78
186
  lines;
79
187
  frame = 0;
80
188
  timer;
81
189
  drawn = false;
82
- constructor(render) {
190
+ constructor(render, out = process.stdout) {
83
191
  this.render = render;
192
+ this.out = out;
84
193
  this.lines = [];
85
194
  }
86
195
  start() {
87
196
  this.draw();
88
- if (process.stdout.isTTY)
197
+ if (this.out.isTTY)
89
198
  this.timer = setInterval(() => this.draw(), 80);
90
199
  return this;
91
200
  }
201
+ /** One screen line per row: never wider than the terminal (minus one, so no auto-wrap). */
202
+ fit(l) {
203
+ return truncate(l.replace(/[\r\n]+/g, " "), Math.max(10, (this.out.columns || 80) - 1));
204
+ }
92
205
  draw() {
93
- if (!process.stdout.isTTY)
206
+ if (!this.out.isTTY)
94
207
  return; // non-TTY: draw once at stop()
95
- const next = this.render(C.violet(FRAMES[this.frame++ % FRAMES.length]));
96
- if (this.drawn)
97
- process.stdout.write(`\x1b[${this.lines.length}A`);
208
+ const next = this.render(C.violet(FRAMES[this.frame++ % FRAMES.length])).map((l) => this.fit(l));
209
+ let s = this.drawn && this.lines.length ? `\x1b[${this.lines.length}A` : "";
98
210
  for (const l of next)
99
- process.stdout.write("\x1b[2K" + l + "\n");
211
+ s += "\x1b[2K" + l + "\n";
212
+ this.out.write(s); // one write: no half-drawn frame
100
213
  this.lines = next;
101
214
  this.drawn = true;
102
215
  }
103
216
  stop() {
104
217
  if (this.timer)
105
218
  clearInterval(this.timer);
106
- if (!process.stdout.isTTY) {
219
+ if (!this.out.isTTY) {
107
220
  for (const l of this.render(" "))
108
- process.stdout.write(l + "\n");
221
+ this.out.write(l + "\n");
109
222
  return;
110
223
  }
111
224
  this.draw();
112
225
  }
113
226
  }
227
+ /**
228
+ * Lines pinned under scrolling output — the run's status block. log() prints
229
+ * a line above them and redraws them; nothing else should write to `out`
230
+ * while the dock is up. TTY only: piped output (a log file) gets the logged
231
+ * lines and never the dock.
232
+ */
233
+ export class Dock {
234
+ render;
235
+ out;
236
+ n = 0;
237
+ timer;
238
+ up = false;
239
+ constructor(render, out = process.stdout) {
240
+ this.render = render;
241
+ this.out = out;
242
+ }
243
+ paint() {
244
+ const w = Math.max(10, (this.out.columns || 80) - 1);
245
+ const ls = this.render().map((l) => truncate(l.replace(/[\r\n]+/g, " "), w));
246
+ this.n = ls.length;
247
+ return ls.map((l) => "\x1b[2K" + l + "\n").join("");
248
+ }
249
+ clear() {
250
+ return this.n ? `\x1b[${this.n}A\x1b[J` : "";
251
+ }
252
+ start() {
253
+ if (!this.out.isTTY || this.up)
254
+ return this;
255
+ this.up = true;
256
+ this.out.write(this.paint());
257
+ this.timer = setInterval(() => this.out.write(this.clear() + this.paint()), 1000);
258
+ this.timer.unref?.();
259
+ return this;
260
+ }
261
+ /** Repaint now (something it shows changed, e.g. a key was typed). */
262
+ redraw() {
263
+ if (this.up)
264
+ this.out.write(this.clear() + this.paint());
265
+ }
266
+ log(line) {
267
+ if (!this.up)
268
+ return void this.out.write(line + "\n");
269
+ this.out.write(this.clear() + line + "\n" + this.paint());
270
+ }
271
+ stop() {
272
+ if (this.timer)
273
+ clearInterval(this.timer);
274
+ if (this.up)
275
+ this.out.write(this.clear());
276
+ this.up = false;
277
+ this.n = 0;
278
+ }
279
+ }
114
280
  //# sourceMappingURL=term.js.map