@wolfstar/cli 2.2.0 → 2.3.0-next-20261004142621
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/LICENSE +201 -201
- package/README.md +465 -304
- package/dist/{_shared-DJrpalNQ.js → _shared-BIQK0Bhm.js} +2 -2
- package/dist/_shared-BIQK0Bhm.js.map +1 -0
- package/dist/bridge-BvkmlAOZ.js +410 -0
- package/dist/bridge-BvkmlAOZ.js.map +1 -0
- package/dist/{build-CluOR8dJ.js → build-DJcc95Yq.js} +5 -5
- package/dist/build-DJcc95Yq.js.map +1 -0
- package/dist/cli.js +1 -1
- package/dist/cli.js.map +1 -1
- package/dist/{codegen-D6V1i_fa.js → codegen-aJSyPz-J.js} +4 -4
- package/dist/codegen-aJSyPz-J.js.map +1 -0
- package/dist/command-diff-BK912Hjc.js +78 -0
- package/dist/command-diff-BK912Hjc.js.map +1 -0
- package/dist/commands-CIPWQtF0.js +3 -0
- package/dist/commands-Clpbqq3K.js +15 -0
- package/dist/commands-Clpbqq3K.js.map +1 -0
- package/dist/{commands-BJ589_AC.js → commands-DVC7ky6f.js} +259 -9
- package/dist/commands-DVC7ky6f.js.map +1 -0
- package/dist/completions-D2KYtrrv.js +161 -0
- package/dist/completions-D2KYtrrv.js.map +1 -0
- package/dist/{dev-DJJZN4NJ.js → dev-oPPVR7d_.js} +434 -28
- package/dist/dev-oPPVR7d_.js.map +1 -0
- package/dist/{diagnostics-3JRAy8Xj.js → diagnostics-BysbNnPD.js} +22 -1
- package/dist/diagnostics-BysbNnPD.js.map +1 -0
- package/dist/doctor-DkVUwgDd.js +334 -0
- package/dist/doctor-DkVUwgDd.js.map +1 -0
- package/dist/errors-COFWKKKw.js.map +1 -1
- package/dist/external-CxFKxwi3.js.map +1 -1
- package/dist/framework-auto-imports-Gu_PXSyr.js.map +1 -1
- package/dist/{hooks-BfDFM38k.js → hooks-Cy6F2bhH.js} +20 -4
- package/dist/hooks-Cy6F2bhH.js.map +1 -0
- package/dist/index.d.ts +53 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -3
- package/dist/{info-C1Kx_4Og.js → info-CKJVMWIT.js} +15 -4
- package/dist/info-CKJVMWIT.js.map +1 -0
- package/dist/{locales-BzOLRBVg.js → locales-BJAP4HYf.js} +5 -5
- package/dist/locales-BJAP4HYf.js.map +1 -0
- package/dist/log-view-B7uJEnMR.js +342 -0
- package/dist/log-view-B7uJEnMR.js.map +1 -0
- package/dist/{nitro-yjqZBh5L.js → nitro-B8u1iVgf.js} +2 -2
- package/dist/nitro-B8u1iVgf.js.map +1 -0
- package/dist/none-uC_wi_l4.js.map +1 -1
- package/dist/output-mode-Bp7Da8vJ.js.map +1 -1
- package/dist/{plain-B6z85uQP.js → plain-DG7cy0Kk.js} +7 -2
- package/dist/plain-DG7cy0Kk.js.map +1 -0
- package/dist/{prepare-Cb45Z4re.js → prepare-BM93i3Rm.js} +4 -4
- package/dist/prepare-BM93i3Rm.js.map +1 -0
- package/dist/{project-D-8VIcyF.js → project-zlQStwsf.js} +2 -2
- package/dist/project-zlQStwsf.js.map +1 -0
- package/dist/{run-BiLZqib3.js → run-DMM2YlP3.js} +2 -12
- package/dist/run-DMM2YlP3.js.map +1 -0
- package/dist/{theme-3Fc_LI-2.js → theme-yS2h5dd_.js} +25 -1
- package/dist/theme-yS2h5dd_.js.map +1 -0
- package/dist/tsc-C5l5TQGC.js +3 -0
- package/dist/{tsc-DEBcwIO0.js → tsc-puVAcwmo.js} +24 -5
- package/dist/tsc-puVAcwmo.js.map +1 -0
- package/dist/{tsdown-DrWXld54.js → tsdown-Dwt3JhPw.js} +2 -2
- package/dist/tsdown-Dwt3JhPw.js.map +1 -0
- package/dist/{tui-BRddrLQP.js → tui-nldGSD4c.js} +913 -141
- package/dist/tui-nldGSD4c.js.map +1 -0
- package/dist/version-BzclCE19.js.map +1 -1
- package/dist/{vite-Bf7nrJx7.js → vite-YZUtv2Hk.js} +2 -2
- package/dist/vite-YZUtv2Hk.js.map +1 -0
- package/package.json +5 -5
- package/dist/_shared-DJrpalNQ.js.map +0 -1
- package/dist/build-CluOR8dJ.js.map +0 -1
- package/dist/codegen-D6V1i_fa.js.map +0 -1
- package/dist/commands-BJ589_AC.js.map +0 -1
- package/dist/dev-DJJZN4NJ.js.map +0 -1
- package/dist/diagnostics-3JRAy8Xj.js.map +0 -1
- package/dist/hooks-BfDFM38k.js.map +0 -1
- package/dist/info-C1Kx_4Og.js.map +0 -1
- package/dist/locales-BzOLRBVg.js.map +0 -1
- package/dist/log-buffer-CY82aqnb.js +0 -72
- package/dist/log-buffer-CY82aqnb.js.map +0 -1
- package/dist/nitro-yjqZBh5L.js.map +0 -1
- package/dist/plain-B6z85uQP.js.map +0 -1
- package/dist/prepare-Cb45Z4re.js.map +0 -1
- package/dist/project-D-8VIcyF.js.map +0 -1
- package/dist/run-BiLZqib3.js.map +0 -1
- package/dist/theme-3Fc_LI-2.js.map +0 -1
- package/dist/tsc-B931Ezh7.js +0 -3
- package/dist/tsc-DEBcwIO0.js.map +0 -1
- package/dist/tsdown-DrWXld54.js.map +0 -1
- package/dist/tui-BRddrLQP.js.map +0 -1
- package/dist/tunnel-DHSxOZKr.js +0 -175
- package/dist/tunnel-DHSxOZKr.js.map +0 -1
- package/dist/vite-Bf7nrJx7.js.map +0 -1
|
@@ -1,13 +1,34 @@
|
|
|
1
1
|
import { t as readOwnPackageJson } from "./version-BzclCE19.js";
|
|
2
|
-
import { n as THEME_SETTINGS, o as resolveTheme, r as detectTerminalBackground, t as THEMES } from "./theme-
|
|
3
|
-
import {
|
|
2
|
+
import { n as THEME_SETTINGS, o as resolveTheme, r as detectTerminalBackground, t as THEMES } from "./theme-yS2h5dd_.js";
|
|
3
|
+
import { t as findInstalledVersion } from "./project-zlQStwsf.js";
|
|
4
|
+
import { t as describeCommand } from "./command-diff-BK912Hjc.js";
|
|
5
|
+
import { S as isErrorDetail, _ as windowAround, a as initialLogView, c as levelBadge, d as pinAt, f as pinnedEnd, g as toggleLevel, h as toggleChannel, i as highlight, l as matchesView, m as soloLevel, n as buildRows, o as isChannelVisible, p as soloChannel, r as formatClock, t as VIEW_LEVELS, u as newestErrorRow, v as wrapLabels, x as classifyAppLine } from "./log-view-B7uJEnMR.js";
|
|
6
|
+
import { displayPath } from "@wolfstar/schema";
|
|
4
7
|
import { execFile } from "node:child_process";
|
|
5
8
|
import { stripVTControlCharacters } from "node:util";
|
|
6
9
|
import { Box, Text, render, useInput, useWindowSize } from "ink";
|
|
7
|
-
import { createContext, useContext, useEffect, useMemo, useState } from "react";
|
|
10
|
+
import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from "react";
|
|
8
11
|
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
9
12
|
import { StringDecoder } from "node:string_decoder";
|
|
10
13
|
|
|
14
|
+
//#region src/dev/tui/ink/hooks/useLogVersion.ts
|
|
15
|
+
/** Bumps whenever the buffer changes, so a consumer can re-derive a filtered view without re-filtering on every render. */
|
|
16
|
+
function useLogVersion(logs) {
|
|
17
|
+
const [version, setVersion] = useState(0);
|
|
18
|
+
useEffect(() => {
|
|
19
|
+
const bump = () => setVersion((current) => current + 1);
|
|
20
|
+
logs.on("entry", bump);
|
|
21
|
+
logs.on("clear", bump);
|
|
22
|
+
bump();
|
|
23
|
+
return () => {
|
|
24
|
+
logs.off("entry", bump);
|
|
25
|
+
logs.off("clear", bump);
|
|
26
|
+
};
|
|
27
|
+
}, [logs]);
|
|
28
|
+
return version;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
//#endregion
|
|
11
32
|
//#region src/dev/tui/ink/theme.tsx
|
|
12
33
|
/**
|
|
13
34
|
* `stars dev` decides once whether the UI may use colour (`NO_COLOR`, `FORCE_COLOR`, TTY detection) and which theme
|
|
@@ -37,19 +58,808 @@ function useColorEnabled() {
|
|
|
37
58
|
return useContext(ThemeContext).color;
|
|
38
59
|
}
|
|
39
60
|
|
|
61
|
+
//#endregion
|
|
62
|
+
//#region src/dev/tui/ink/components/LogPane.tsx
|
|
63
|
+
const LEVEL_TOKENS$1 = {
|
|
64
|
+
trace: "trace",
|
|
65
|
+
debug: "debug",
|
|
66
|
+
info: "info",
|
|
67
|
+
success: "success",
|
|
68
|
+
warn: "warning",
|
|
69
|
+
error: "error"
|
|
70
|
+
};
|
|
71
|
+
const SEGMENT_TOKENS = {
|
|
72
|
+
url: "url",
|
|
73
|
+
path: null,
|
|
74
|
+
number: "accent",
|
|
75
|
+
name: "trace",
|
|
76
|
+
dim: null,
|
|
77
|
+
ok: "success",
|
|
78
|
+
warn: "warning",
|
|
79
|
+
error: "error"
|
|
80
|
+
};
|
|
81
|
+
/** The colours a channel's dot is picked from, by name, so a channel keeps its colour for the whole session. */
|
|
82
|
+
const CHANNEL_TOKENS = [
|
|
83
|
+
"info",
|
|
84
|
+
"debug",
|
|
85
|
+
"trace",
|
|
86
|
+
"accent",
|
|
87
|
+
"url",
|
|
88
|
+
"tunnel"
|
|
89
|
+
];
|
|
90
|
+
function channelToken(channel) {
|
|
91
|
+
let hash = 0;
|
|
92
|
+
for (const character of channel) hash = hash * 31 + character.codePointAt(0) >>> 0;
|
|
93
|
+
return CHANNEL_TOKENS[hash % CHANNEL_TOKENS.length];
|
|
94
|
+
}
|
|
95
|
+
/** The log stream: one line per row, the newest at the bottom unless the view is scrolled. */
|
|
96
|
+
function LogPane({ rows, end, width, height, channelWidth, marked }) {
|
|
97
|
+
const paint = usePaint();
|
|
98
|
+
const color = useColorEnabled();
|
|
99
|
+
const visible = rows.slice(Math.max(0, end - height), end);
|
|
100
|
+
const gutter = " ".repeat(15 + channelWidth + 1);
|
|
101
|
+
return /* @__PURE__ */ jsxs(Box, {
|
|
102
|
+
flexDirection: "column",
|
|
103
|
+
width,
|
|
104
|
+
height,
|
|
105
|
+
overflow: "hidden",
|
|
106
|
+
justifyContent: "flex-end",
|
|
107
|
+
children: [visible.length === 0 && /* @__PURE__ */ jsx(Text, {
|
|
108
|
+
dimColor: true,
|
|
109
|
+
children: " no matching logs"
|
|
110
|
+
}), visible.map((row, index) => {
|
|
111
|
+
const key = `${end - visible.length + index}`;
|
|
112
|
+
if (row.kind === "rule") return /* @__PURE__ */ jsx(Text, {
|
|
113
|
+
dimColor: true,
|
|
114
|
+
wrap: "truncate-end",
|
|
115
|
+
children: "─".repeat(width)
|
|
116
|
+
}, key);
|
|
117
|
+
if (row.kind === "header") return /* @__PURE__ */ jsxs(Text, {
|
|
118
|
+
wrap: "truncate-end",
|
|
119
|
+
children: [/* @__PURE__ */ jsx(Text, {
|
|
120
|
+
bold: true,
|
|
121
|
+
color: paint(channelToken(row.channel)),
|
|
122
|
+
children: ` ${row.channel}`
|
|
123
|
+
}), /* @__PURE__ */ jsx(Text, {
|
|
124
|
+
dimColor: true,
|
|
125
|
+
children: ` · ${row.count}`
|
|
126
|
+
})]
|
|
127
|
+
}, key);
|
|
128
|
+
const { entry } = row;
|
|
129
|
+
const failed = entry.level === "error";
|
|
130
|
+
if (row.kind === "detail") return /* @__PURE__ */ jsxs(Text, {
|
|
131
|
+
wrap: "truncate-end",
|
|
132
|
+
children: [
|
|
133
|
+
gutter,
|
|
134
|
+
/* @__PURE__ */ jsx(Text, {
|
|
135
|
+
dimColor: true,
|
|
136
|
+
children: "│ "
|
|
137
|
+
}),
|
|
138
|
+
/* @__PURE__ */ jsx(Message, {
|
|
139
|
+
text: row.text,
|
|
140
|
+
channel: entry.channel,
|
|
141
|
+
color: failed ? paint("error") : void 0,
|
|
142
|
+
dim: !failed
|
|
143
|
+
})
|
|
144
|
+
]
|
|
145
|
+
}, key);
|
|
146
|
+
const level = LEVEL_TOKENS$1[entry.level];
|
|
147
|
+
return /* @__PURE__ */ jsxs(Text, {
|
|
148
|
+
wrap: "truncate-end",
|
|
149
|
+
children: [
|
|
150
|
+
color ? /* @__PURE__ */ jsx(Text, {
|
|
151
|
+
bold: true,
|
|
152
|
+
backgroundColor: paint(level),
|
|
153
|
+
color: paint(failed ? "onError" : "onBright"),
|
|
154
|
+
children: ` ${levelBadge(entry.level)} `
|
|
155
|
+
}) : /* @__PURE__ */ jsx(Text, {
|
|
156
|
+
bold: true,
|
|
157
|
+
children: `[${levelBadge(entry.level)}]`
|
|
158
|
+
}),
|
|
159
|
+
/* @__PURE__ */ jsx(Text, {
|
|
160
|
+
color: paint("brand"),
|
|
161
|
+
children: entry.id === marked ? "▎" : " "
|
|
162
|
+
}),
|
|
163
|
+
" ",
|
|
164
|
+
/* @__PURE__ */ jsx(Text, {
|
|
165
|
+
dimColor: !failed,
|
|
166
|
+
color: failed ? paint("error") : void 0,
|
|
167
|
+
children: formatClock(entry.time)
|
|
168
|
+
}),
|
|
169
|
+
" ",
|
|
170
|
+
/* @__PURE__ */ jsx(Text, {
|
|
171
|
+
bold: failed,
|
|
172
|
+
color: failed ? paint("error") : void 0,
|
|
173
|
+
children: entry.channel.slice(0, channelWidth).padStart(channelWidth)
|
|
174
|
+
}),
|
|
175
|
+
" ",
|
|
176
|
+
/* @__PURE__ */ jsx(Text, {
|
|
177
|
+
color: paint(channelToken(entry.channel)),
|
|
178
|
+
children: "●"
|
|
179
|
+
}),
|
|
180
|
+
" ",
|
|
181
|
+
/* @__PURE__ */ jsx(Message, {
|
|
182
|
+
text: row.text,
|
|
183
|
+
channel: entry.channel,
|
|
184
|
+
color: entry.level === "warn" ? paint("warning") : void 0
|
|
185
|
+
})
|
|
186
|
+
]
|
|
187
|
+
}, key);
|
|
188
|
+
})]
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
function Message({ text, channel, color, dim = false }) {
|
|
192
|
+
const paint = usePaint();
|
|
193
|
+
return /* @__PURE__ */ jsx(Text, {
|
|
194
|
+
color,
|
|
195
|
+
dimColor: dim,
|
|
196
|
+
children: highlight(text, channel).map((segment, index) => {
|
|
197
|
+
if (segment.token === void 0) return segment.text;
|
|
198
|
+
const token = SEGMENT_TOKENS[segment.token];
|
|
199
|
+
return /* @__PURE__ */ jsx(Text, {
|
|
200
|
+
color: token === null ? color : paint(token),
|
|
201
|
+
dimColor: token === null || dim,
|
|
202
|
+
bold: segment.token === "name",
|
|
203
|
+
children: segment.text
|
|
204
|
+
}, index);
|
|
205
|
+
})
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
//#endregion
|
|
210
|
+
//#region src/dev/tui/ink/components/PromptCard.tsx
|
|
211
|
+
/** The rows a prompt takes, border included, so the log pane above it can be sized. */
|
|
212
|
+
function promptHeight(prompt, available) {
|
|
213
|
+
return Math.min(Math.max(4, available), prompt.changes.length + 4);
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* The card under the log stream that asks before the bot's commands are deployed again: a redeploy changes what
|
|
217
|
+
* every user of the application sees, so it is never done behind the developer's back.
|
|
218
|
+
*/
|
|
219
|
+
function PromptCard({ prompt, width, height }) {
|
|
220
|
+
const paint = usePaint();
|
|
221
|
+
const room = Math.max(0, height - 4);
|
|
222
|
+
const shown = prompt.changes.slice(0, prompt.changes.length > room ? Math.max(0, room - 1) : room);
|
|
223
|
+
const more = prompt.changes.length - shown.length;
|
|
224
|
+
return /* @__PURE__ */ jsxs(Box, {
|
|
225
|
+
flexDirection: "column",
|
|
226
|
+
width,
|
|
227
|
+
height,
|
|
228
|
+
borderStyle: "single",
|
|
229
|
+
borderColor: paint("accent"),
|
|
230
|
+
paddingX: 1,
|
|
231
|
+
children: [
|
|
232
|
+
/* @__PURE__ */ jsx(Text, {
|
|
233
|
+
bold: true,
|
|
234
|
+
color: paint("accent"),
|
|
235
|
+
wrap: "truncate-end",
|
|
236
|
+
children: "Commands updated:"
|
|
237
|
+
}),
|
|
238
|
+
shown.map((change) => /* @__PURE__ */ jsxs(Text, {
|
|
239
|
+
wrap: "truncate-end",
|
|
240
|
+
children: [
|
|
241
|
+
/* @__PURE__ */ jsx(Text, {
|
|
242
|
+
dimColor: true,
|
|
243
|
+
children: "- "
|
|
244
|
+
}),
|
|
245
|
+
/* @__PURE__ */ jsx(Text, {
|
|
246
|
+
color: paint(change.kind === "removed" ? "error" : change.kind === "added" ? "success" : "url"),
|
|
247
|
+
children: describeCommand(change)
|
|
248
|
+
}),
|
|
249
|
+
/* @__PURE__ */ jsx(Text, {
|
|
250
|
+
dimColor: true,
|
|
251
|
+
children: ` ${change.kind}`
|
|
252
|
+
})
|
|
253
|
+
]
|
|
254
|
+
}, `${change.guild}:${change.type}:${change.name}`)),
|
|
255
|
+
more > 0 && /* @__PURE__ */ jsx(Text, {
|
|
256
|
+
dimColor: true,
|
|
257
|
+
children: ` and ${more} more`
|
|
258
|
+
}),
|
|
259
|
+
/* @__PURE__ */ jsxs(Text, {
|
|
260
|
+
wrap: "truncate-end",
|
|
261
|
+
children: ["Refresh commands? ", /* @__PURE__ */ jsx(Text, {
|
|
262
|
+
dimColor: true,
|
|
263
|
+
children: "(y/n)"
|
|
264
|
+
})]
|
|
265
|
+
})
|
|
266
|
+
]
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
//#endregion
|
|
271
|
+
//#region src/dev/tui/panel-logic.ts
|
|
272
|
+
/** Whether the badge describes work in progress, as opposed to a state the session rests in. */
|
|
273
|
+
function isBusy(badge) {
|
|
274
|
+
return badge === "starting" || badge === "building" || badge === "restarting";
|
|
275
|
+
}
|
|
276
|
+
/**
|
|
277
|
+
* The badge and standing description for the panel's status row, mirroring the states Nuxt CLI v4's own dev panel
|
|
278
|
+
* cycles through (`STARTING`/`BUILDING`/`RESTART`/`READY`/`ERROR`).
|
|
279
|
+
*/
|
|
280
|
+
function describeBadge(status) {
|
|
281
|
+
if (status.build === "building") return {
|
|
282
|
+
badge: status.startedAt === null ? "starting" : "building",
|
|
283
|
+
note: status.progress.message
|
|
284
|
+
};
|
|
285
|
+
if (status.process === "starting") return {
|
|
286
|
+
badge: "starting",
|
|
287
|
+
note: "starting the bot"
|
|
288
|
+
};
|
|
289
|
+
if (status.paused) return {
|
|
290
|
+
badge: "stopped",
|
|
291
|
+
note: "the bot is stopped, press r to start it"
|
|
292
|
+
};
|
|
293
|
+
if (status.process === "stopping") return {
|
|
294
|
+
badge: "restarting",
|
|
295
|
+
note: "restarting the bot"
|
|
296
|
+
};
|
|
297
|
+
if (status.build === "failed") return {
|
|
298
|
+
badge: "error",
|
|
299
|
+
note: "build failed, waiting for changes"
|
|
300
|
+
};
|
|
301
|
+
if (status.process === "crashed") return {
|
|
302
|
+
badge: "error",
|
|
303
|
+
note: "the bot crashed, press r to restart"
|
|
304
|
+
};
|
|
305
|
+
if (status.health === "down") return {
|
|
306
|
+
badge: "error",
|
|
307
|
+
note: "health check failed"
|
|
308
|
+
};
|
|
309
|
+
if (status.typecheck === "failed") return {
|
|
310
|
+
badge: "error",
|
|
311
|
+
note: "type check failed, press e to view it"
|
|
312
|
+
};
|
|
313
|
+
if (status.process === "stopped") return {
|
|
314
|
+
badge: "error",
|
|
315
|
+
note: "the bot stopped, press r to restart"
|
|
316
|
+
};
|
|
317
|
+
if (status.progress.fraction < 1) return {
|
|
318
|
+
badge: "starting",
|
|
319
|
+
note: status.progress.message
|
|
320
|
+
};
|
|
321
|
+
if (status.process === "running") return {
|
|
322
|
+
badge: "ready",
|
|
323
|
+
note: "watching for changes"
|
|
324
|
+
};
|
|
325
|
+
return {
|
|
326
|
+
badge: "starting",
|
|
327
|
+
note: "waiting for the first build"
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
/** Formats an uptime the way a status line reads it: `35s`, `3m 35s`, `2h 04m`. */
|
|
331
|
+
function formatUptime(milliseconds) {
|
|
332
|
+
const total = Math.max(0, Math.floor(milliseconds / 1e3));
|
|
333
|
+
const seconds = total % 60;
|
|
334
|
+
const minutes = Math.floor(total / 60) % 60;
|
|
335
|
+
const hours = Math.floor(total / 3600);
|
|
336
|
+
if (hours > 0) return `${hours}h ${minutes.toString().padStart(2, "0")}m`;
|
|
337
|
+
return minutes > 0 ? `${minutes}m ${seconds}s` : `${seconds}s`;
|
|
338
|
+
}
|
|
339
|
+
/** Formats a duration the way a dev session reads it: `mm:ss` under an hour, `h:mm:ss` above. */
|
|
340
|
+
function formatDuration(milliseconds) {
|
|
341
|
+
const total = Math.max(0, Math.floor(milliseconds / 1e3));
|
|
342
|
+
const seconds = total % 60;
|
|
343
|
+
const minutes = Math.floor(total / 60) % 60;
|
|
344
|
+
const hours = Math.floor(total / 3600);
|
|
345
|
+
const pad = (value) => value.toString().padStart(2, "0");
|
|
346
|
+
return hours > 0 ? `${hours}:${pad(minutes)}:${pad(seconds)}` : `${pad(minutes)}:${pad(seconds)}`;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
//#endregion
|
|
350
|
+
//#region src/dev/tui/ink/hooks/useUptime.ts
|
|
351
|
+
/**
|
|
352
|
+
* Re-renders once a second while the bot is up, so the uptime in the header keeps counting.
|
|
353
|
+
*/
|
|
354
|
+
function useUptime(startedAt) {
|
|
355
|
+
const [now, setNow] = useState(() => Date.now());
|
|
356
|
+
useEffect(() => {
|
|
357
|
+
if (startedAt === null) return;
|
|
358
|
+
setNow(Date.now());
|
|
359
|
+
const timer = setInterval(() => setNow(Date.now()), 1e3);
|
|
360
|
+
return () => clearInterval(timer);
|
|
361
|
+
}, [startedAt]);
|
|
362
|
+
return startedAt === null ? null : now - startedAt;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
//#endregion
|
|
366
|
+
//#region src/dev/tui/ink/components/Sidebar.tsx
|
|
367
|
+
const LEVEL_TOKENS = {
|
|
368
|
+
error: "error",
|
|
369
|
+
warn: "warning",
|
|
370
|
+
info: "info",
|
|
371
|
+
debug: "debug",
|
|
372
|
+
trace: "trace"
|
|
373
|
+
};
|
|
374
|
+
const SPINNER = [
|
|
375
|
+
"⠋",
|
|
376
|
+
"⠙",
|
|
377
|
+
"⠹",
|
|
378
|
+
"⠸",
|
|
379
|
+
"⠼",
|
|
380
|
+
"⠴",
|
|
381
|
+
"⠦",
|
|
382
|
+
"⠧",
|
|
383
|
+
"⠇",
|
|
384
|
+
"⠏"
|
|
385
|
+
];
|
|
386
|
+
const KEYS$1 = [[
|
|
387
|
+
["←→", "select"],
|
|
388
|
+
["tab", "group"],
|
|
389
|
+
["space", "filter"],
|
|
390
|
+
["s", "solo"]
|
|
391
|
+
], [
|
|
392
|
+
["q", "quit"],
|
|
393
|
+
["r", "restart"],
|
|
394
|
+
["d", "disconnect"],
|
|
395
|
+
["c", "clear"],
|
|
396
|
+
["↑↓", "scroll"],
|
|
397
|
+
["g/G", "top/bottom"],
|
|
398
|
+
["?", "help"]
|
|
399
|
+
]];
|
|
400
|
+
let frameworkVersion = null;
|
|
401
|
+
/** Looked up once per project: it walks `node_modules`, and the sidebar repaints on every log entry. */
|
|
402
|
+
function readFrameworkVersion(root) {
|
|
403
|
+
if (frameworkVersion?.root !== root) frameworkVersion = {
|
|
404
|
+
root,
|
|
405
|
+
version: findInstalledVersion(root, "@wolfstar/http-framework")
|
|
406
|
+
};
|
|
407
|
+
return frameworkVersion.version;
|
|
408
|
+
}
|
|
409
|
+
/**
|
|
410
|
+
* The left column of the dashboard: what the session is doing, the filters of the log stream, and the keys. Under a
|
|
411
|
+
* short terminal the key help goes first, then the filters' own hints: the status and the filters are what it is for.
|
|
412
|
+
*/
|
|
413
|
+
function Sidebar(props) {
|
|
414
|
+
const { status, config, counters, filter, channels, focus, collapsed, live, grouped, frame, elapsedMs, width, height, confirmQuit } = props;
|
|
415
|
+
const paint = usePaint();
|
|
416
|
+
const uptime = useUptime(status.startedAt);
|
|
417
|
+
const state = describeBadge(status);
|
|
418
|
+
const busy = isBusy(state.badge);
|
|
419
|
+
const failed = state.badge === "error";
|
|
420
|
+
const name = config.packageJson?.name ?? "stars project";
|
|
421
|
+
const inner = Math.max(1, width - 2);
|
|
422
|
+
const row = (label, value, key = label) => /* @__PURE__ */ jsxs(Text, {
|
|
423
|
+
wrap: "truncate-end",
|
|
424
|
+
children: [/* @__PURE__ */ jsx(Text, {
|
|
425
|
+
dimColor: true,
|
|
426
|
+
children: label.padEnd(8)
|
|
427
|
+
}), value]
|
|
428
|
+
}, key);
|
|
429
|
+
const stateColor = paint(failed ? "error" : busy || state.badge === "stopped" ? "busy" : "success");
|
|
430
|
+
const tunnel = status.tunnel === "up" ? /* @__PURE__ */ jsx(Text, {
|
|
431
|
+
color: paint("tunnel"),
|
|
432
|
+
children: "live"
|
|
433
|
+
}) : status.tunnel === "starting" ? /* @__PURE__ */ jsx(Text, {
|
|
434
|
+
color: paint("busy"),
|
|
435
|
+
children: "connecting"
|
|
436
|
+
}) : status.tunnel === "failed" ? /* @__PURE__ */ jsx(Text, {
|
|
437
|
+
color: paint("error"),
|
|
438
|
+
children: "failed"
|
|
439
|
+
}) : /* @__PURE__ */ jsx(Text, {
|
|
440
|
+
dimColor: true,
|
|
441
|
+
children: "off"
|
|
442
|
+
});
|
|
443
|
+
const port = status.port ?? (status.url ? new URL(status.url).port || null : null);
|
|
444
|
+
const framework = readFrameworkVersion(config.root);
|
|
445
|
+
const head = [
|
|
446
|
+
/* @__PURE__ */ jsxs(Text, {
|
|
447
|
+
wrap: "truncate-end",
|
|
448
|
+
children: [/* @__PURE__ */ jsx(Text, {
|
|
449
|
+
bold: true,
|
|
450
|
+
color: paint("brand"),
|
|
451
|
+
children: name
|
|
452
|
+
}), /* @__PURE__ */ jsx(Text, {
|
|
453
|
+
dimColor: true,
|
|
454
|
+
children: ` • stars v${readOwnPackageJson().version}`
|
|
455
|
+
})]
|
|
456
|
+
}, "title"),
|
|
457
|
+
/* @__PURE__ */ jsx(Text, { children: " " }, "gap-title"),
|
|
458
|
+
/* @__PURE__ */ jsxs(Text, {
|
|
459
|
+
wrap: "truncate-end",
|
|
460
|
+
children: [
|
|
461
|
+
/* @__PURE__ */ jsx(Text, {
|
|
462
|
+
color: stateColor,
|
|
463
|
+
children: busy ? SPINNER[frame % SPINNER.length] : failed ? "✖" : "●"
|
|
464
|
+
}),
|
|
465
|
+
" ",
|
|
466
|
+
/* @__PURE__ */ jsx(Text, {
|
|
467
|
+
bold: true,
|
|
468
|
+
color: stateColor,
|
|
469
|
+
children: confirmQuit ? "quit?" : state.badge === "ready" ? "running" : state.badge
|
|
470
|
+
}),
|
|
471
|
+
busy && /* @__PURE__ */ jsx(Text, {
|
|
472
|
+
dimColor: true,
|
|
473
|
+
children: ` ${Math.round(status.progress.fraction * 100)}% · ${(elapsedMs / 1e3).toFixed(1)}s`
|
|
474
|
+
})
|
|
475
|
+
]
|
|
476
|
+
}, "state"),
|
|
477
|
+
/* @__PURE__ */ jsx(Text, {
|
|
478
|
+
wrap: "truncate-end",
|
|
479
|
+
children: confirmQuit ? /* @__PURE__ */ jsx(Text, {
|
|
480
|
+
dimColor: true,
|
|
481
|
+
children: "press y to confirm, esc to stay"
|
|
482
|
+
}) : state.badge === "ready" ? /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(Text, {
|
|
483
|
+
bold: true,
|
|
484
|
+
children: name
|
|
485
|
+
}), " is ready!"] }) : /* @__PURE__ */ jsx(Text, {
|
|
486
|
+
dimColor: true,
|
|
487
|
+
children: state.note
|
|
488
|
+
})
|
|
489
|
+
}, "note"),
|
|
490
|
+
row("http", framework ? `v${framework}` : /* @__PURE__ */ jsx(Text, {
|
|
491
|
+
dimColor: true,
|
|
492
|
+
children: "not installed"
|
|
493
|
+
})),
|
|
494
|
+
row("up", uptime === null ? /* @__PURE__ */ jsx(Text, {
|
|
495
|
+
dimColor: true,
|
|
496
|
+
children: "—"
|
|
497
|
+
}) : formatUptime(uptime)),
|
|
498
|
+
row("port", port ?? /* @__PURE__ */ jsx(Text, {
|
|
499
|
+
dimColor: true,
|
|
500
|
+
children: "—"
|
|
501
|
+
})),
|
|
502
|
+
row("tunnel", tunnel),
|
|
503
|
+
row("logs", config.dev.logFile ? displayPath(config.root, config.dev.logFile) : /* @__PURE__ */ jsx(Text, {
|
|
504
|
+
dimColor: true,
|
|
505
|
+
children: "off"
|
|
506
|
+
})),
|
|
507
|
+
...config.dev.typecheck.enabled ? [row("types", status.typecheck === "failed" ? /* @__PURE__ */ jsx(Text, {
|
|
508
|
+
color: paint("error"),
|
|
509
|
+
children: `${status.typeErrors} ${status.typeErrors === 1 ? "error" : "errors"}`
|
|
510
|
+
}) : status.typecheck)] : [],
|
|
511
|
+
...counters.errors > 0 || counters.warnings > 0 ? [/* @__PURE__ */ jsxs(Text, {
|
|
512
|
+
wrap: "truncate-end",
|
|
513
|
+
children: [counters.errors > 0 && /* @__PURE__ */ jsx(Text, {
|
|
514
|
+
bold: true,
|
|
515
|
+
color: paint("error"),
|
|
516
|
+
children: `✖ ${counters.errors} `
|
|
517
|
+
}), counters.warnings > 0 && /* @__PURE__ */ jsx(Text, {
|
|
518
|
+
color: paint("warning"),
|
|
519
|
+
children: `⚠ ${counters.warnings}`
|
|
520
|
+
})]
|
|
521
|
+
}, "counters")] : []
|
|
522
|
+
];
|
|
523
|
+
const groupHeader = (group, suffix = "") => /* @__PURE__ */ jsxs(Text, {
|
|
524
|
+
wrap: "truncate-end",
|
|
525
|
+
children: [
|
|
526
|
+
/* @__PURE__ */ jsx(Text, {
|
|
527
|
+
color: focus.group === group ? paint("brand") : void 0,
|
|
528
|
+
children: collapsed[group] ? "▸" : "▾"
|
|
529
|
+
}),
|
|
530
|
+
" ",
|
|
531
|
+
/* @__PURE__ */ jsx(Text, {
|
|
532
|
+
bold: focus.group === group,
|
|
533
|
+
dimColor: focus.group !== group,
|
|
534
|
+
children: group
|
|
535
|
+
}),
|
|
536
|
+
group === "channels" && grouped && /* @__PURE__ */ jsx(Text, {
|
|
537
|
+
dimColor: true,
|
|
538
|
+
children: " · grouped"
|
|
539
|
+
}),
|
|
540
|
+
suffix && /* @__PURE__ */ jsx(Text, {
|
|
541
|
+
dimColor: true,
|
|
542
|
+
children: suffix
|
|
543
|
+
})
|
|
544
|
+
]
|
|
545
|
+
}, `${group}-header`);
|
|
546
|
+
const item = (group, index, label, on, token) => {
|
|
547
|
+
const focused = focus.group === group && focus.index === index;
|
|
548
|
+
return /* @__PURE__ */ jsx(Text, {
|
|
549
|
+
color: on ? paint(token) : void 0,
|
|
550
|
+
dimColor: !on,
|
|
551
|
+
strikethrough: !on,
|
|
552
|
+
underline: focused,
|
|
553
|
+
bold: focused,
|
|
554
|
+
children: label
|
|
555
|
+
}, label);
|
|
556
|
+
};
|
|
557
|
+
const budget = Math.max(1, height - 1);
|
|
558
|
+
const labelWidth = Math.max(1, inner - 2);
|
|
559
|
+
const lines = (group, labels, nodes, range = [0, Infinity]) => wrapLabels(labels, labelWidth).slice(...range).map((line, index) => /* @__PURE__ */ jsxs(Text, {
|
|
560
|
+
wrap: "truncate-end",
|
|
561
|
+
children: [" ", line.flatMap((label, position) => position > 0 ? [" ", nodes[label]] : [nodes[label]])]
|
|
562
|
+
}, `${group}-${index}`));
|
|
563
|
+
const levelNodes = VIEW_LEVELS.map((level, index) => item("levels", index, level, filter.levels.has(level), LEVEL_TOKENS[level]));
|
|
564
|
+
const levelLines = collapsed.levels ? [] : lines("levels", VIEW_LEVELS, levelNodes);
|
|
565
|
+
const channelNodes = channels.map((channel, index) => item("channels", index, channel, isChannelVisible(filter, channel), channelToken(channel)));
|
|
566
|
+
const channelLayout = wrapLabels(channels, labelWidth);
|
|
567
|
+
const focusedLine = focus.group === "channels" ? Math.max(0, channelLayout.findIndex((line) => line.includes(focus.index))) : 0;
|
|
568
|
+
const range = windowAround(channelLayout.length, focusedLine, Math.max(1, budget - head.length - 3 - levelLines.length));
|
|
569
|
+
const hiddenChannels = channelLayout.length - (range[1] - range[0]);
|
|
570
|
+
const filters = [
|
|
571
|
+
/* @__PURE__ */ jsx(Text, { children: " " }, "gap-filters"),
|
|
572
|
+
groupHeader("channels", !collapsed.channels && hiddenChannels > 0 ? ` · ${range[0] + 1}-${range[1]}/${channelLayout.length}` : ""),
|
|
573
|
+
...collapsed.channels ? [] : lines("channels", channels, channelNodes, range),
|
|
574
|
+
groupHeader("levels"),
|
|
575
|
+
...levelLines
|
|
576
|
+
];
|
|
577
|
+
const keys = KEYS$1.map((section, sectionIndex) => [/* @__PURE__ */ jsx(Text, { children: " " }, `gap-keys-${sectionIndex}`), ...section.map(([key, label]) => /* @__PURE__ */ jsxs(Text, {
|
|
578
|
+
wrap: "truncate-end",
|
|
579
|
+
children: [
|
|
580
|
+
/* @__PURE__ */ jsx(Text, {
|
|
581
|
+
bold: true,
|
|
582
|
+
color: paint("url"),
|
|
583
|
+
children: key
|
|
584
|
+
}),
|
|
585
|
+
" ",
|
|
586
|
+
/* @__PURE__ */ jsx(Text, {
|
|
587
|
+
dimColor: true,
|
|
588
|
+
children: label
|
|
589
|
+
})
|
|
590
|
+
]
|
|
591
|
+
}, key))]);
|
|
592
|
+
let body = [
|
|
593
|
+
...head,
|
|
594
|
+
...filters,
|
|
595
|
+
...keys.flat()
|
|
596
|
+
];
|
|
597
|
+
if (body.length > budget) body = [
|
|
598
|
+
...head,
|
|
599
|
+
...filters,
|
|
600
|
+
...keys[1]
|
|
601
|
+
];
|
|
602
|
+
if (body.length > budget) body = [...head, ...filters];
|
|
603
|
+
body = body.slice(0, budget);
|
|
604
|
+
return /* @__PURE__ */ jsxs(Box, {
|
|
605
|
+
flexDirection: "column",
|
|
606
|
+
width,
|
|
607
|
+
height,
|
|
608
|
+
paddingX: 1,
|
|
609
|
+
children: [
|
|
610
|
+
body.map((node, index) => /* @__PURE__ */ jsx(Box, {
|
|
611
|
+
height: 1,
|
|
612
|
+
width: inner,
|
|
613
|
+
children: node
|
|
614
|
+
}, index)),
|
|
615
|
+
/* @__PURE__ */ jsx(Box, { flexGrow: 1 }),
|
|
616
|
+
/* @__PURE__ */ jsx(Box, {
|
|
617
|
+
height: 1,
|
|
618
|
+
width: inner,
|
|
619
|
+
justifyContent: "flex-end",
|
|
620
|
+
children: /* @__PURE__ */ jsxs(Text, {
|
|
621
|
+
wrap: "truncate-end",
|
|
622
|
+
children: [
|
|
623
|
+
/* @__PURE__ */ jsx(Text, {
|
|
624
|
+
color: paint(live ? "error" : "busy"),
|
|
625
|
+
children: "●"
|
|
626
|
+
}),
|
|
627
|
+
" ",
|
|
628
|
+
/* @__PURE__ */ jsx(Text, {
|
|
629
|
+
dimColor: true,
|
|
630
|
+
children: live ? "live" : "paused"
|
|
631
|
+
})
|
|
632
|
+
]
|
|
633
|
+
})
|
|
634
|
+
})
|
|
635
|
+
]
|
|
636
|
+
});
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
//#endregion
|
|
640
|
+
//#region src/dev/tui/ink/components/Dashboard.tsx
|
|
641
|
+
/** The sidebar takes about a quarter of the terminal, within what its longest lines need and can spare. */
|
|
642
|
+
function sidebarWidth(columns) {
|
|
643
|
+
return Math.max(24, Math.min(34, Math.floor(columns * .26)));
|
|
644
|
+
}
|
|
645
|
+
/**
|
|
646
|
+
* The full-screen dev view: the session's state and the log filters on the left, the log stream on the right, and a
|
|
647
|
+
* card under the stream when `stars dev` has something to ask. Everything it shows comes from the service; the keys
|
|
648
|
+
* here only change what is looked at.
|
|
649
|
+
*/
|
|
650
|
+
function Dashboard(props) {
|
|
651
|
+
const { service, status, counters, frame, elapsedMs, width, height, confirmQuit, initialFilter, active, onCapture } = props;
|
|
652
|
+
const paint = usePaint();
|
|
653
|
+
const [filter, setFilter] = useState(initialFilter);
|
|
654
|
+
const [focus, setFocus] = useState({
|
|
655
|
+
group: "channels",
|
|
656
|
+
index: 0
|
|
657
|
+
});
|
|
658
|
+
const [collapsed, setCollapsed] = useState({
|
|
659
|
+
channels: false,
|
|
660
|
+
levels: false
|
|
661
|
+
});
|
|
662
|
+
const [grouped, setGrouped] = useState(false);
|
|
663
|
+
const [searching, setSearching] = useState(false);
|
|
664
|
+
const [pinned, setPinned] = useState(null);
|
|
665
|
+
const [marked, setMarked] = useState(null);
|
|
666
|
+
const version = useLogVersion(service.logs);
|
|
667
|
+
const channels = useMemo(() => [.../* @__PURE__ */ new Set([...initialFilter.allow ?? [], ...service.logs.channels()])].sort(), [
|
|
668
|
+
service,
|
|
669
|
+
initialFilter,
|
|
670
|
+
version
|
|
671
|
+
]);
|
|
672
|
+
const rows = useMemo(() => buildRows(service.logs.entries().filter((entry) => matchesView(entry, filter)), { group: grouped }), [
|
|
673
|
+
service,
|
|
674
|
+
filter,
|
|
675
|
+
grouped,
|
|
676
|
+
version
|
|
677
|
+
]);
|
|
678
|
+
useEffect(() => {
|
|
679
|
+
onCapture(searching);
|
|
680
|
+
return () => onCapture(false);
|
|
681
|
+
}, [searching, onCapture]);
|
|
682
|
+
const side = sidebarWidth(width);
|
|
683
|
+
const mainWidth = Math.max(1, width - side - 1);
|
|
684
|
+
const searchRows = searching || filter.query ? 1 : 0;
|
|
685
|
+
const cardRows = status.prompt ? promptHeight(status.prompt, Math.max(4, height - searchRows - 3)) : 0;
|
|
686
|
+
const paneHeight = Math.max(1, height - searchRows - cardRows);
|
|
687
|
+
const end = pinnedEnd(rows, pinned, paneHeight);
|
|
688
|
+
const channelWidth = Math.min(12, Math.max(3, ...channels.map((channel) => channel.length)));
|
|
689
|
+
const items = focus.group === "channels" ? channels : VIEW_LEVELS;
|
|
690
|
+
const change = (next) => {
|
|
691
|
+
setFilter(next);
|
|
692
|
+
setPinned(null);
|
|
693
|
+
};
|
|
694
|
+
const scroll = (delta) => {
|
|
695
|
+
setPinned(pinAt(rows, Math.max(Math.min(paneHeight, rows.length), end + delta)));
|
|
696
|
+
};
|
|
697
|
+
useInput((input, key) => {
|
|
698
|
+
if (key.ctrl || confirmQuit) return;
|
|
699
|
+
if (searching) {
|
|
700
|
+
if (key.escape) {
|
|
701
|
+
setSearching(false);
|
|
702
|
+
change({
|
|
703
|
+
...filter,
|
|
704
|
+
query: ""
|
|
705
|
+
});
|
|
706
|
+
} else if (key.return) setSearching(false);
|
|
707
|
+
else if (key.backspace || key.delete) change({
|
|
708
|
+
...filter,
|
|
709
|
+
query: [...filter.query].slice(0, -1).join("")
|
|
710
|
+
});
|
|
711
|
+
else if (input && !key.upArrow && !key.downArrow && !key.leftArrow && !key.rightArrow && !key.tab) change({
|
|
712
|
+
...filter,
|
|
713
|
+
query: filter.query + input
|
|
714
|
+
});
|
|
715
|
+
return;
|
|
716
|
+
}
|
|
717
|
+
if (key.escape) {
|
|
718
|
+
if (filter.query) change({
|
|
719
|
+
...filter,
|
|
720
|
+
query: ""
|
|
721
|
+
});
|
|
722
|
+
else setPinned(null);
|
|
723
|
+
return setMarked(null);
|
|
724
|
+
}
|
|
725
|
+
if (input === "/") return setSearching(true);
|
|
726
|
+
if (key.tab) return setFocus((current) => ({
|
|
727
|
+
group: current.group === "channels" ? "levels" : "channels",
|
|
728
|
+
index: 0
|
|
729
|
+
}));
|
|
730
|
+
if (key.leftArrow || key.rightArrow) {
|
|
731
|
+
if (items.length === 0) return;
|
|
732
|
+
const delta = key.leftArrow ? -1 : 1;
|
|
733
|
+
return setFocus((current) => ({
|
|
734
|
+
...current,
|
|
735
|
+
index: (current.index + delta + items.length) % items.length
|
|
736
|
+
}));
|
|
737
|
+
}
|
|
738
|
+
if (key.return) return setCollapsed((current) => ({
|
|
739
|
+
...current,
|
|
740
|
+
[focus.group]: !current[focus.group]
|
|
741
|
+
}));
|
|
742
|
+
if (input === " " || input === "s") {
|
|
743
|
+
const target = items[Math.min(focus.index, items.length - 1)];
|
|
744
|
+
if (target === void 0) return;
|
|
745
|
+
if (focus.group === "channels") return change(input === " " ? toggleChannel(filter, target) : soloChannel(filter, target));
|
|
746
|
+
const level = target;
|
|
747
|
+
return change(input === " " ? toggleLevel(filter, level) : soloLevel(filter, level));
|
|
748
|
+
}
|
|
749
|
+
if (input === "a") return change({
|
|
750
|
+
...filter,
|
|
751
|
+
allow: null,
|
|
752
|
+
hidden: /* @__PURE__ */ new Set(),
|
|
753
|
+
levels: new Set(VIEW_LEVELS),
|
|
754
|
+
query: ""
|
|
755
|
+
});
|
|
756
|
+
if (input === "b") {
|
|
757
|
+
setGrouped((current) => !current);
|
|
758
|
+
return setPinned(null);
|
|
759
|
+
}
|
|
760
|
+
if (key.upArrow || input === "k") return scroll(-1);
|
|
761
|
+
if (key.downArrow || input === "j") return scroll(1);
|
|
762
|
+
if (key.pageUp) return scroll(-Math.max(1, paneHeight - 1));
|
|
763
|
+
if (key.pageDown) return scroll(Math.max(1, paneHeight - 1));
|
|
764
|
+
if (key.home || input === "g") return setPinned(pinAt(rows, Math.min(paneHeight, rows.length)));
|
|
765
|
+
if (key.end || input === "G") return setPinned(null);
|
|
766
|
+
if (input === "e") {
|
|
767
|
+
const index = newestErrorRow(rows);
|
|
768
|
+
if (index < 0) return;
|
|
769
|
+
const row = rows[index];
|
|
770
|
+
setMarked(row.kind === "entry" ? row.entry.id : null);
|
|
771
|
+
setPinned(pinAt(rows, Math.max(paneHeight, index + Math.ceil(paneHeight / 2))));
|
|
772
|
+
}
|
|
773
|
+
}, { isActive: active });
|
|
774
|
+
return /* @__PURE__ */ jsxs(Box, {
|
|
775
|
+
width,
|
|
776
|
+
height,
|
|
777
|
+
children: [/* @__PURE__ */ jsx(Sidebar, {
|
|
778
|
+
status,
|
|
779
|
+
config: service.config,
|
|
780
|
+
counters,
|
|
781
|
+
filter,
|
|
782
|
+
channels,
|
|
783
|
+
focus,
|
|
784
|
+
collapsed,
|
|
785
|
+
live: pinned === null,
|
|
786
|
+
grouped,
|
|
787
|
+
frame,
|
|
788
|
+
elapsedMs,
|
|
789
|
+
width: side,
|
|
790
|
+
height,
|
|
791
|
+
confirmQuit
|
|
792
|
+
}), /* @__PURE__ */ jsxs(Box, {
|
|
793
|
+
flexDirection: "column",
|
|
794
|
+
width: mainWidth + 1,
|
|
795
|
+
height,
|
|
796
|
+
borderStyle: "single",
|
|
797
|
+
borderTop: false,
|
|
798
|
+
borderRight: false,
|
|
799
|
+
borderBottom: false,
|
|
800
|
+
borderDimColor: true,
|
|
801
|
+
children: [
|
|
802
|
+
/* @__PURE__ */ jsx(LogPane, {
|
|
803
|
+
rows,
|
|
804
|
+
end,
|
|
805
|
+
width: mainWidth,
|
|
806
|
+
height: paneHeight,
|
|
807
|
+
channelWidth,
|
|
808
|
+
marked
|
|
809
|
+
}),
|
|
810
|
+
status.prompt && /* @__PURE__ */ jsx(PromptCard, {
|
|
811
|
+
prompt: status.prompt,
|
|
812
|
+
width: mainWidth,
|
|
813
|
+
height: cardRows
|
|
814
|
+
}),
|
|
815
|
+
searchRows > 0 && /* @__PURE__ */ jsxs(Text, {
|
|
816
|
+
wrap: "truncate-end",
|
|
817
|
+
children: [
|
|
818
|
+
/* @__PURE__ */ jsx(Text, {
|
|
819
|
+
bold: true,
|
|
820
|
+
color: paint("url"),
|
|
821
|
+
children: " / "
|
|
822
|
+
}),
|
|
823
|
+
filter.query,
|
|
824
|
+
searching ? "▏" : /* @__PURE__ */ jsx(Text, {
|
|
825
|
+
dimColor: true,
|
|
826
|
+
children: " · esc clears"
|
|
827
|
+
})
|
|
828
|
+
]
|
|
829
|
+
})
|
|
830
|
+
]
|
|
831
|
+
})]
|
|
832
|
+
});
|
|
833
|
+
}
|
|
834
|
+
|
|
40
835
|
//#endregion
|
|
41
836
|
//#region src/dev/tui/ink/components/HelpOverlay.tsx
|
|
42
837
|
const KEYS = [
|
|
43
838
|
["r", "restart the bot now"],
|
|
839
|
+
["d", "disconnect: stop the bot until the next r"],
|
|
44
840
|
["o", "open the local URL in a browser"],
|
|
45
841
|
["t", "toggle the public tunnel"],
|
|
46
842
|
["i", "show versions, URLs and session info"],
|
|
47
843
|
["T", "pick a colour theme (dark, light, colour-blind friendly, ANSI)"],
|
|
48
|
-
["
|
|
844
|
+
["v", "switch between the dashboard and the compact panel"],
|
|
845
|
+
["l", "open the logs (select, copy, filter by source or level)"],
|
|
49
846
|
["e", "jump to the last error, keeping its context"],
|
|
847
|
+
["y / n", "answer a prompt (refresh the commands)"],
|
|
50
848
|
["c / Ctrl+L", "clear the log history"],
|
|
51
849
|
["q / Ctrl+C", "stop the bot and quit"],
|
|
52
|
-
["?", "toggle this help"]
|
|
850
|
+
["?", "toggle this help"],
|
|
851
|
+
["", ""],
|
|
852
|
+
["dashboard", ""],
|
|
853
|
+
["← / →", "select a channel or a level"],
|
|
854
|
+
["tab", "switch between channels and levels"],
|
|
855
|
+
["space", "show or hide the selected one"],
|
|
856
|
+
["s", "solo: only the selected one, again for all"],
|
|
857
|
+
["a", "show every channel and level"],
|
|
858
|
+
["enter", "fold or unfold the selected group"],
|
|
859
|
+
["b", "group the stream by channel"],
|
|
860
|
+
["↑ / ↓", "scroll (PgUp/PgDn by page)"],
|
|
861
|
+
["g / G", "top / back to live"],
|
|
862
|
+
["/", "search (Esc clears)"]
|
|
53
863
|
];
|
|
54
864
|
/** The static keys reference, opened with `?` from the pinned panel. */
|
|
55
865
|
function HelpOverlay({ height, width, onClose }) {
|
|
@@ -73,7 +883,7 @@ function HelpOverlay({ height, width, onClose }) {
|
|
|
73
883
|
dimColor: true,
|
|
74
884
|
children: "─".repeat(width)
|
|
75
885
|
}),
|
|
76
|
-
KEYS.slice(0, Math.max(0, height - 3)).map(([key, description]) => /* @__PURE__ */ jsxs(Text, {
|
|
886
|
+
KEYS.slice(0, Math.max(0, height - 3)).map(([key, description], index) => /* @__PURE__ */ jsxs(Text, {
|
|
77
887
|
wrap: "truncate-end",
|
|
78
888
|
children: [
|
|
79
889
|
" ",
|
|
@@ -87,7 +897,7 @@ function HelpOverlay({ height, width, onClose }) {
|
|
|
87
897
|
children: description
|
|
88
898
|
})
|
|
89
899
|
]
|
|
90
|
-
},
|
|
900
|
+
}, index)),
|
|
91
901
|
/* @__PURE__ */ jsx(Box, { flexGrow: 1 }),
|
|
92
902
|
/* @__PURE__ */ jsx(Text, {
|
|
93
903
|
dimColor: true,
|
|
@@ -97,24 +907,6 @@ function HelpOverlay({ height, width, onClose }) {
|
|
|
97
907
|
});
|
|
98
908
|
}
|
|
99
909
|
|
|
100
|
-
//#endregion
|
|
101
|
-
//#region src/dev/tui/ink/hooks/useLogVersion.ts
|
|
102
|
-
/** Bumps whenever the buffer changes, so a consumer can re-derive a filtered view without re-filtering on every render. */
|
|
103
|
-
function useLogVersion(logs) {
|
|
104
|
-
const [version, setVersion] = useState(0);
|
|
105
|
-
useEffect(() => {
|
|
106
|
-
const bump = () => setVersion((current) => current + 1);
|
|
107
|
-
logs.on("entry", bump);
|
|
108
|
-
logs.on("clear", bump);
|
|
109
|
-
bump();
|
|
110
|
-
return () => {
|
|
111
|
-
logs.off("entry", bump);
|
|
112
|
-
logs.off("clear", bump);
|
|
113
|
-
};
|
|
114
|
-
}, [logs]);
|
|
115
|
-
return version;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
910
|
//#endregion
|
|
119
911
|
//#region src/dev/tui/ink/components/Hints.tsx
|
|
120
912
|
const PANEL_HINTS = [
|
|
@@ -350,84 +1142,6 @@ function useDevStatus(service) {
|
|
|
350
1142
|
return status;
|
|
351
1143
|
}
|
|
352
1144
|
|
|
353
|
-
//#endregion
|
|
354
|
-
//#region src/dev/tui/ink/hooks/useUptime.ts
|
|
355
|
-
/**
|
|
356
|
-
* Re-renders once a second while the bot is up, so the uptime in the header keeps counting.
|
|
357
|
-
*/
|
|
358
|
-
function useUptime(startedAt) {
|
|
359
|
-
const [now, setNow] = useState(() => Date.now());
|
|
360
|
-
useEffect(() => {
|
|
361
|
-
if (startedAt === null) return;
|
|
362
|
-
setNow(Date.now());
|
|
363
|
-
const timer = setInterval(() => setNow(Date.now()), 1e3);
|
|
364
|
-
return () => clearInterval(timer);
|
|
365
|
-
}, [startedAt]);
|
|
366
|
-
return startedAt === null ? null : now - startedAt;
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
//#endregion
|
|
370
|
-
//#region src/dev/tui/panel-logic.ts
|
|
371
|
-
/**
|
|
372
|
-
* The badge and standing description for the panel's status row, mirroring the states Nuxt CLI v4's own dev panel
|
|
373
|
-
* cycles through (`STARTING`/`BUILDING`/`RESTART`/`READY`/`ERROR`).
|
|
374
|
-
*/
|
|
375
|
-
function describeBadge(status) {
|
|
376
|
-
if (status.build === "building") return {
|
|
377
|
-
badge: status.startedAt === null ? "starting" : "building",
|
|
378
|
-
note: status.progress.message
|
|
379
|
-
};
|
|
380
|
-
if (status.process === "starting") return {
|
|
381
|
-
badge: "starting",
|
|
382
|
-
note: "starting the bot"
|
|
383
|
-
};
|
|
384
|
-
if (status.process === "stopping") return {
|
|
385
|
-
badge: "restarting",
|
|
386
|
-
note: "restarting the bot"
|
|
387
|
-
};
|
|
388
|
-
if (status.build === "failed") return {
|
|
389
|
-
badge: "error",
|
|
390
|
-
note: "build failed, waiting for changes"
|
|
391
|
-
};
|
|
392
|
-
if (status.process === "crashed") return {
|
|
393
|
-
badge: "error",
|
|
394
|
-
note: "the bot crashed, press r to restart"
|
|
395
|
-
};
|
|
396
|
-
if (status.health === "down") return {
|
|
397
|
-
badge: "error",
|
|
398
|
-
note: "health check failed"
|
|
399
|
-
};
|
|
400
|
-
if (status.typecheck === "failed") return {
|
|
401
|
-
badge: "error",
|
|
402
|
-
note: "type check failed, press e to view it"
|
|
403
|
-
};
|
|
404
|
-
if (status.process === "stopped") return {
|
|
405
|
-
badge: "error",
|
|
406
|
-
note: "the bot stopped, press r to restart"
|
|
407
|
-
};
|
|
408
|
-
if (status.progress.fraction < 1) return {
|
|
409
|
-
badge: "starting",
|
|
410
|
-
note: status.progress.message
|
|
411
|
-
};
|
|
412
|
-
if (status.process === "running") return {
|
|
413
|
-
badge: "ready",
|
|
414
|
-
note: "watching for changes"
|
|
415
|
-
};
|
|
416
|
-
return {
|
|
417
|
-
badge: "starting",
|
|
418
|
-
note: "waiting for the first build"
|
|
419
|
-
};
|
|
420
|
-
}
|
|
421
|
-
/** Formats a duration the way a dev session reads it: `mm:ss` under an hour, `h:mm:ss` above. */
|
|
422
|
-
function formatDuration(milliseconds) {
|
|
423
|
-
const total = Math.max(0, Math.floor(milliseconds / 1e3));
|
|
424
|
-
const seconds = total % 60;
|
|
425
|
-
const minutes = Math.floor(total / 60) % 60;
|
|
426
|
-
const hours = Math.floor(total / 3600);
|
|
427
|
-
const pad = (value) => value.toString().padStart(2, "0");
|
|
428
|
-
return hours > 0 ? `${hours}:${pad(minutes)}:${pad(seconds)}` : `${pad(minutes)}:${pad(seconds)}`;
|
|
429
|
-
}
|
|
430
|
-
|
|
431
1145
|
//#endregion
|
|
432
1146
|
//#region src/dev/tui/ink/components/InfoOverlay.tsx
|
|
433
1147
|
function InfoOverlay({ service, height, width, onClose }) {
|
|
@@ -492,9 +1206,9 @@ function InfoOverlay({ service, height, width, onClose }) {
|
|
|
492
1206
|
function Panel({ status, config, counters, frame, elapsedMs, width, height, confirmQuit }) {
|
|
493
1207
|
const paint = usePaint();
|
|
494
1208
|
const state = describeBadge(status);
|
|
495
|
-
const busy = state.badge
|
|
1209
|
+
const busy = isBusy(state.badge);
|
|
496
1210
|
const badge = !busy && counters.errors > 0 ? "error" : state.badge;
|
|
497
|
-
const note = badge === "error" && state.badge === "ready" ? "an error was logged · press e to view it" : state.note;
|
|
1211
|
+
const note = status.prompt ? "commands updated · press y to refresh them, n to skip" : badge === "error" && state.badge === "ready" ? "an error was logged · press e to view it" : state.note;
|
|
498
1212
|
const filled = Math.round(Math.min(1, Math.max(0, status.progress.fraction)) * 20);
|
|
499
1213
|
let shown = [
|
|
500
1214
|
...(config.dev.banner === false ? [] : config.dev.banner?.flatMap((line) => line.split("\n")) ?? [null]).slice(0, 4).map((line, index) => ({
|
|
@@ -636,7 +1350,7 @@ function Panel({ status, config, counters, frame, elapsedMs, width, height, conf
|
|
|
636
1350
|
" ",
|
|
637
1351
|
/* @__PURE__ */ jsx(Text, {
|
|
638
1352
|
bold: true,
|
|
639
|
-
backgroundColor: paint(confirmQuit || busy ? "busy" : badge === "error" ? "error" : "success"),
|
|
1353
|
+
backgroundColor: paint(confirmQuit || busy || badge === "stopped" ? "busy" : badge === "error" ? "error" : "success"),
|
|
640
1354
|
color: paint(badge === "error" && !confirmQuit ? "onError" : "onBright"),
|
|
641
1355
|
children: ` ${confirmQuit ? "QUIT?" : badge === "restarting" ? "RESTART" : badge.toUpperCase()} `
|
|
642
1356
|
}),
|
|
@@ -855,44 +1569,79 @@ function ThemeOverlay({ current, height, width, onPreview, onSelect, onClose })
|
|
|
855
1569
|
|
|
856
1570
|
//#endregion
|
|
857
1571
|
//#region src/dev/tui/ink/DevApp.tsx
|
|
1572
|
+
/** The dashboard needs room for its sidebar next to a readable log line, and for more than a handful of entries. */
|
|
1573
|
+
const DASHBOARD_MIN_COLUMNS = 90;
|
|
1574
|
+
const DASHBOARD_MIN_ROWS = 20;
|
|
1575
|
+
/** Below this even an explicit `dashboard` falls back to the panel: the sidebar alone would fill the terminal. */
|
|
1576
|
+
const DASHBOARD_HARD_MIN_COLUMNS = 60;
|
|
1577
|
+
const DASHBOARD_HARD_MIN_ROWS = 10;
|
|
1578
|
+
/** The layout to paint for a terminal size: what was asked for when it fits, the panel otherwise. */
|
|
1579
|
+
function resolveLayout(layout, columns, rows) {
|
|
1580
|
+
if (layout === "panel") return "panel";
|
|
1581
|
+
if (columns < DASHBOARD_HARD_MIN_COLUMNS || rows < DASHBOARD_HARD_MIN_ROWS) return "panel";
|
|
1582
|
+
if (layout === "dashboard") return "dashboard";
|
|
1583
|
+
return columns >= 90 && rows >= 20 ? "dashboard" : "panel";
|
|
1584
|
+
}
|
|
858
1585
|
/**
|
|
859
|
-
*
|
|
860
|
-
*
|
|
1586
|
+
* Two layouts over one service. The dashboard is a full-screen view in the alternate buffer; the panel is pinned to
|
|
1587
|
+
* the bottom of the normal buffer with the logs folded away, and is what a small terminal gets. The browsable
|
|
1588
|
+
* overlays use the alternate buffer from either, preserving the terminal's history and the panel when they close.
|
|
861
1589
|
*/
|
|
862
|
-
function DevApp({ service, color, theme, reducedMotion, onQuit, onViewChange, onCopy, onThemeSave }) {
|
|
1590
|
+
function DevApp({ service, color, theme, reducedMotion, layout = "auto", filter, onQuit, onViewChange, onCopy, onThemeSave }) {
|
|
863
1591
|
const { columns, rows } = useWindowSize();
|
|
864
|
-
const [
|
|
1592
|
+
const [overlay, setOverlay] = useState(null);
|
|
1593
|
+
const [chosen, setChosen] = useState(layout);
|
|
865
1594
|
const [confirmQuit, setConfirmQuit] = useState(false);
|
|
1595
|
+
const [captured, setCaptured] = useState(false);
|
|
1596
|
+
const [, repaint] = useState(0);
|
|
866
1597
|
const [saved, setSaved] = useState(theme);
|
|
867
1598
|
const [preview, setPreview] = useState(theme);
|
|
1599
|
+
const [initialFilter] = useState(() => filter ?? initialLogView(service.config));
|
|
868
1600
|
const status = useDevStatus(service);
|
|
869
1601
|
const counters = useLogCounters(service);
|
|
870
1602
|
const badge = describeBadge(status).badge;
|
|
871
|
-
const busy = badge
|
|
872
|
-
const
|
|
1603
|
+
const busy = isBusy(badge);
|
|
1604
|
+
const base = resolveLayout(chosen, columns, rows);
|
|
1605
|
+
const clock = useBuildClock(status.progress.startedAt, busy && overlay === null, reducedMotion);
|
|
873
1606
|
const width = Math.max(1, columns);
|
|
874
1607
|
const height = Math.max(2, rows - 1);
|
|
875
|
-
const
|
|
876
|
-
|
|
877
|
-
|
|
1608
|
+
const fullScreen = overlay !== null || base === "dashboard";
|
|
1609
|
+
const shown = useRef(fullScreen);
|
|
1610
|
+
const show = (nextOverlay, nextChosen = chosen) => {
|
|
1611
|
+
const next = nextOverlay !== null || resolveLayout(nextChosen, columns, rows) === "dashboard";
|
|
1612
|
+
shown.current = next;
|
|
1613
|
+
onViewChange(next);
|
|
1614
|
+
setOverlay(nextOverlay);
|
|
1615
|
+
setChosen(nextChosen);
|
|
878
1616
|
};
|
|
1617
|
+
useEffect(() => {
|
|
1618
|
+
if (shown.current === fullScreen) return;
|
|
1619
|
+
shown.current = fullScreen;
|
|
1620
|
+
onViewChange(fullScreen);
|
|
1621
|
+
repaint((count) => count + 1);
|
|
1622
|
+
}, [fullScreen, onViewChange]);
|
|
1623
|
+
const onCapture = useCallback((capturing) => setCaptured(capturing), []);
|
|
879
1624
|
useInput((input, key) => {
|
|
880
1625
|
if (key.ctrl && input === "c") return onQuit();
|
|
881
1626
|
if (key.ctrl && input === "l") return service.clearLogs();
|
|
882
|
-
if (
|
|
1627
|
+
if (overlay !== null || captured) return;
|
|
883
1628
|
if (confirmQuit) {
|
|
884
1629
|
if (input === "y") return onQuit();
|
|
885
1630
|
return setConfirmQuit(false);
|
|
886
1631
|
}
|
|
887
1632
|
if (key.ctrl && input === "r") return void service.restart("manual");
|
|
888
1633
|
if (key.ctrl && input === "d") return busy ? setConfirmQuit(true) : onQuit();
|
|
1634
|
+
if (status.prompt && (input === "y" || input === "n")) return service.answerPrompt(input === "y");
|
|
889
1635
|
switch (input) {
|
|
890
1636
|
case "q": return busy ? setConfirmQuit(true) : onQuit();
|
|
891
1637
|
case "r":
|
|
892
1638
|
service.restart("manual");
|
|
893
1639
|
return;
|
|
894
|
-
case "
|
|
895
|
-
|
|
1640
|
+
case "d":
|
|
1641
|
+
service.disconnect();
|
|
1642
|
+
return;
|
|
1643
|
+
case "l": return show("logs");
|
|
1644
|
+
case "e": return base === "panel" ? show("errors") : void 0;
|
|
896
1645
|
case "c": return service.clearLogs();
|
|
897
1646
|
case "o":
|
|
898
1647
|
openDevUrl(status.url).catch((error) => service.log("stars", "warn", error.message));
|
|
@@ -900,10 +1649,11 @@ function DevApp({ service, color, theme, reducedMotion, onQuit, onViewChange, on
|
|
|
900
1649
|
case "t":
|
|
901
1650
|
service.toggleTunnel();
|
|
902
1651
|
return;
|
|
903
|
-
case "i": return
|
|
904
|
-
case "T": return
|
|
1652
|
+
case "i": return show("info");
|
|
1653
|
+
case "T": return show("theme");
|
|
1654
|
+
case "v": return show(null, base === "dashboard" ? "panel" : "dashboard");
|
|
905
1655
|
case "h":
|
|
906
|
-
case "?": return
|
|
1656
|
+
case "?": return show("help");
|
|
907
1657
|
}
|
|
908
1658
|
});
|
|
909
1659
|
return /* @__PURE__ */ jsx(ThemeProvider, {
|
|
@@ -913,28 +1663,28 @@ function DevApp({ service, color, theme, reducedMotion, onQuit, onViewChange, on
|
|
|
913
1663
|
width,
|
|
914
1664
|
height,
|
|
915
1665
|
flexDirection: "column",
|
|
916
|
-
justifyContent:
|
|
1666
|
+
justifyContent: fullScreen ? "flex-start" : "flex-end",
|
|
917
1667
|
children: [
|
|
918
|
-
(
|
|
1668
|
+
(overlay === "logs" || overlay === "errors") && /* @__PURE__ */ jsx(LogBrowser, {
|
|
919
1669
|
service,
|
|
920
1670
|
height,
|
|
921
1671
|
width,
|
|
922
|
-
lastError:
|
|
1672
|
+
lastError: overlay === "errors",
|
|
923
1673
|
onCopy,
|
|
924
|
-
onClose: () =>
|
|
1674
|
+
onClose: () => show(null)
|
|
925
1675
|
}),
|
|
926
|
-
|
|
1676
|
+
overlay === "help" && /* @__PURE__ */ jsx(HelpOverlay, {
|
|
927
1677
|
height,
|
|
928
1678
|
width,
|
|
929
|
-
onClose: () =>
|
|
1679
|
+
onClose: () => show(null)
|
|
930
1680
|
}),
|
|
931
|
-
|
|
1681
|
+
overlay === "info" && /* @__PURE__ */ jsx(InfoOverlay, {
|
|
932
1682
|
service,
|
|
933
1683
|
height,
|
|
934
1684
|
width,
|
|
935
|
-
onClose: () =>
|
|
1685
|
+
onClose: () => show(null)
|
|
936
1686
|
}),
|
|
937
|
-
|
|
1687
|
+
overlay === "theme" && /* @__PURE__ */ jsx(ThemeOverlay, {
|
|
938
1688
|
current: saved,
|
|
939
1689
|
height,
|
|
940
1690
|
width,
|
|
@@ -943,14 +1693,29 @@ function DevApp({ service, color, theme, reducedMotion, onQuit, onViewChange, on
|
|
|
943
1693
|
setSaved(setting);
|
|
944
1694
|
setPreview(setting);
|
|
945
1695
|
onThemeSave(setting);
|
|
946
|
-
|
|
1696
|
+
show(null);
|
|
947
1697
|
},
|
|
948
1698
|
onClose: () => {
|
|
949
1699
|
setPreview(saved);
|
|
950
|
-
|
|
1700
|
+
show(null);
|
|
951
1701
|
}
|
|
952
1702
|
}),
|
|
953
|
-
|
|
1703
|
+
base === "dashboard" && /* @__PURE__ */ jsx(Box, {
|
|
1704
|
+
display: overlay === null ? "flex" : "none",
|
|
1705
|
+
children: /* @__PURE__ */ jsx(Dashboard, {
|
|
1706
|
+
service,
|
|
1707
|
+
status,
|
|
1708
|
+
counters,
|
|
1709
|
+
...clock,
|
|
1710
|
+
width,
|
|
1711
|
+
height,
|
|
1712
|
+
confirmQuit,
|
|
1713
|
+
initialFilter,
|
|
1714
|
+
active: overlay === null,
|
|
1715
|
+
onCapture
|
|
1716
|
+
})
|
|
1717
|
+
}),
|
|
1718
|
+
base === "panel" && overlay === null && /* @__PURE__ */ jsx(Panel, {
|
|
954
1719
|
status,
|
|
955
1720
|
config: service.config,
|
|
956
1721
|
counters,
|
|
@@ -998,8 +1763,9 @@ function captureOutput(service, stream, fallback) {
|
|
|
998
1763
|
//#endregion
|
|
999
1764
|
//#region src/dev/tui/tui.tsx
|
|
1000
1765
|
/**
|
|
1001
|
-
*
|
|
1002
|
-
*
|
|
1766
|
+
* The full-screen dashboard and the overlays in the alternate buffer, the bottom-aligned panel in the normal one.
|
|
1767
|
+
* External output is captured for the log views; the renderer alone writes to the terminal. Switching views and
|
|
1768
|
+
* teardown restore the original buffer.
|
|
1003
1769
|
*
|
|
1004
1770
|
* `start()` resolves when the user quits, which is what `stars dev` waits on before shutting the bot down.
|
|
1005
1771
|
*/
|
|
@@ -1013,14 +1779,17 @@ function createTuiRenderer(service, options = {}) {
|
|
|
1013
1779
|
return typeof value === "function" ? value.bind(target) : value;
|
|
1014
1780
|
} });
|
|
1015
1781
|
const restoreOutput = stdout === process.stdout ? [captureOutput(service, process.stdout, "info"), captureOutput(service, process.stderr, "warn")] : [];
|
|
1016
|
-
|
|
1782
|
+
const layout = options.layout ?? service.config.dev.layout ?? "auto";
|
|
1783
|
+
let alternate = resolveLayout(layout, stdout.columns ?? 80, stdout.rows ?? 24) === "dashboard";
|
|
1784
|
+
if (alternate) write("\x1B[?1049h\x1B[H");
|
|
1017
1785
|
let stopped = false;
|
|
1018
|
-
const switchView = (
|
|
1019
|
-
if (
|
|
1786
|
+
const switchView = (fullScreen) => {
|
|
1787
|
+
if (fullScreen === alternate || stopped) return;
|
|
1020
1788
|
instance.clear();
|
|
1021
|
-
write(
|
|
1022
|
-
alternate =
|
|
1789
|
+
write(fullScreen ? "\x1B[?1049h\x1B[H" : "\x1B[?1049l");
|
|
1790
|
+
alternate = fullScreen;
|
|
1023
1791
|
};
|
|
1792
|
+
service.promptable = true;
|
|
1024
1793
|
let quit;
|
|
1025
1794
|
const quitting = new Promise((resolve) => {
|
|
1026
1795
|
quit = resolve;
|
|
@@ -1031,6 +1800,8 @@ function createTuiRenderer(service, options = {}) {
|
|
|
1031
1800
|
theme: options.theme ?? "auto",
|
|
1032
1801
|
onThemeSave: options.onThemeSave ?? (() => {}),
|
|
1033
1802
|
reducedMotion: options.reducedMotion ?? false,
|
|
1803
|
+
layout,
|
|
1804
|
+
filter: options.filter,
|
|
1034
1805
|
onQuit: quit,
|
|
1035
1806
|
onViewChange: switchView,
|
|
1036
1807
|
onCopy: (text) => write(`\u001B]52;c;${Buffer.from(text.slice(0, 65536)).toString("base64")}\u0007`)
|
|
@@ -1049,6 +1820,7 @@ function createTuiRenderer(service, options = {}) {
|
|
|
1049
1820
|
stop() {
|
|
1050
1821
|
if (stopped) return;
|
|
1051
1822
|
stopped = true;
|
|
1823
|
+
service.promptable = false;
|
|
1052
1824
|
if (alternate) instance.clear();
|
|
1053
1825
|
instance.unmount();
|
|
1054
1826
|
if (alternate) write("\x1B[?1049l");
|
|
@@ -1065,4 +1837,4 @@ function createTuiRenderer(service, options = {}) {
|
|
|
1065
1837
|
|
|
1066
1838
|
//#endregion
|
|
1067
1839
|
export { createTuiRenderer };
|
|
1068
|
-
//# sourceMappingURL=tui-
|
|
1840
|
+
//# sourceMappingURL=tui-nldGSD4c.js.map
|