jules-orchestrator-kit 0.52.8 → 0.53.0
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/README.md +66 -7
- package/bin/agentctl.mjs +172 -1
- package/bin/init.js +87 -74
- package/index.mjs +10 -0
- package/package.json +5 -2
- package/scripts/jules-create.mjs +5 -16
- package/scripts/jules-patch.mjs +18 -10
- package/scripts/stale-base-check.mjs +1 -1
- package/src/ci-templates.mjs +199 -0
- package/src/config.mjs +39 -11
- package/src/engine.mjs +2 -21
- package/src/env-aliases.mjs +80 -0
- package/src/mcp.mjs +19 -1
- package/src/ops/doctor-registry.mjs +53 -32
- package/src/ops/next-step.mjs +38 -7
- package/src/profiles-io.mjs +77 -0
- package/src/profiles.mjs +188 -0
- package/src/provider-readiness.mjs +215 -0
- package/src/security.mjs +42 -1
- package/src/tui.mjs +14 -3
- package/src/wizard-init.mjs +32 -4
- package/src/ops/doctor-planner.mjs +0 -220
- package/src/ops/receipts.mjs +0 -171
- package/src/ops/swarm-actions.mjs +0 -156
- package/src/ops/task-actions.mjs +0 -214
- package/src/ops/transaction.mjs +0 -278
- package/src/ux/capabilities.mjs +0 -180
- package/src/ux/diff-viewer.mjs +0 -289
- package/src/ux/layout.mjs +0 -195
- package/src/ux/log-viewer.mjs +0 -110
- package/src/ux/palette.mjs +0 -179
- package/src/ux/queue-model.mjs +0 -206
- package/src/ux/renderer.mjs +0 -188
- package/src/ux/swarm-model.mjs +0 -146
- package/src/ux/terminal-session.mjs +0 -215
- package/src/ux/widgets.mjs +0 -323
- /package/src/{ux/key-decoder.mjs → key-decoder.mjs} +0 -0
package/src/ux/diff-viewer.mjs
DELETED
|
@@ -1,289 +0,0 @@
|
|
|
1
|
-
import { sanitizeControlChars } from "./renderer.mjs";
|
|
2
|
-
import { clipText } from "./layout.mjs";
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* @typedef {Object} DiffLine
|
|
6
|
-
* @property {"context" | "add" | "delete" | "header" | "no-newline"} kind
|
|
7
|
-
* @property {number} [oldLine]
|
|
8
|
-
* @property {number} [newLine]
|
|
9
|
-
* @property {string} text
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* @typedef {Object} DiffHunk
|
|
14
|
-
* @property {string} header
|
|
15
|
-
* @property {number} oldStart
|
|
16
|
-
* @property {number} oldCount
|
|
17
|
-
* @property {number} newStart
|
|
18
|
-
* @property {number} newCount
|
|
19
|
-
* @property {DiffLine[]} lines
|
|
20
|
-
*/
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* @typedef {Object} DiffFile
|
|
24
|
-
* @property {string} oldPath
|
|
25
|
-
* @property {string} newPath
|
|
26
|
-
* @property {"added" | "modified" | "deleted" | "renamed" | "binary"} status
|
|
27
|
-
* @property {DiffHunk[]} hunks
|
|
28
|
-
* @property {number} additions
|
|
29
|
-
* @property {number} deletions
|
|
30
|
-
*/
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
* @typedef {Object} DiffDocument
|
|
34
|
-
* @property {DiffFile[]} files
|
|
35
|
-
* @property {boolean} truncated
|
|
36
|
-
* @property {number} totalBytes
|
|
37
|
-
*/
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
* Parse unified diff text into structured DiffDocument.
|
|
41
|
-
* @param {string} text
|
|
42
|
-
* @param {Object} [options]
|
|
43
|
-
* @param {number} [options.maxBytes=1000000]
|
|
44
|
-
* @param {number} [options.maxFiles=100]
|
|
45
|
-
* @param {number} [options.maxLines=5000]
|
|
46
|
-
* @returns {DiffDocument}
|
|
47
|
-
*/
|
|
48
|
-
export function parseUnifiedDiff(text, options = {}) {
|
|
49
|
-
const maxBytes = options.maxBytes ?? 1_000_000;
|
|
50
|
-
const maxFiles = options.maxFiles ?? 100;
|
|
51
|
-
const maxLines = options.maxLines ?? 5000;
|
|
52
|
-
|
|
53
|
-
let truncated = false;
|
|
54
|
-
text = typeof text === "string" ? text : String(text || "");
|
|
55
|
-
const totalBytes = Buffer.byteLength(text, "utf-8");
|
|
56
|
-
const byteLimit = Number.isFinite(maxBytes) ? Math.max(0, Math.floor(maxBytes)) : 1_000_000;
|
|
57
|
-
|
|
58
|
-
if (totalBytes > byteLimit) {
|
|
59
|
-
text = Buffer.from(text, "utf-8").subarray(0, byteLimit).toString("utf-8");
|
|
60
|
-
truncated = true;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
const rawLines = text.split(/\r?\n/);
|
|
64
|
-
const files = [];
|
|
65
|
-
|
|
66
|
-
let currentFile = null;
|
|
67
|
-
let currentHunk = null;
|
|
68
|
-
let oldLineNum = 0;
|
|
69
|
-
let newLineNum = 0;
|
|
70
|
-
let lineCount = 0;
|
|
71
|
-
|
|
72
|
-
for (let rawIndex = 0; rawIndex < rawLines.length; rawIndex++) {
|
|
73
|
-
const rawLine = rawLines[rawIndex];
|
|
74
|
-
if (lineCount >= maxLines) {
|
|
75
|
-
truncated = true;
|
|
76
|
-
break;
|
|
77
|
-
}
|
|
78
|
-
lineCount++;
|
|
79
|
-
|
|
80
|
-
const line = sanitizeControlChars(rawLine);
|
|
81
|
-
if (line === "" && rawLine === "" && rawIndex === rawLines.length - 1) {
|
|
82
|
-
continue;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
if (line.startsWith("diff --git ")) {
|
|
86
|
-
if (files.length >= maxFiles) {
|
|
87
|
-
truncated = true;
|
|
88
|
-
break;
|
|
89
|
-
}
|
|
90
|
-
const parts = line.split(" ");
|
|
91
|
-
const oldPath = parts[2] ? parts[2].replace(/^a\//, "") : "";
|
|
92
|
-
const newPath = parts[3] ? parts[3].replace(/^b\//, "") : "";
|
|
93
|
-
|
|
94
|
-
currentFile = {
|
|
95
|
-
oldPath,
|
|
96
|
-
newPath,
|
|
97
|
-
status: "modified",
|
|
98
|
-
hunks: [],
|
|
99
|
-
additions: 0,
|
|
100
|
-
deletions: 0,
|
|
101
|
-
};
|
|
102
|
-
files.push(currentFile);
|
|
103
|
-
currentHunk = null;
|
|
104
|
-
continue;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
if (!currentFile && (line.startsWith("--- ") || line.startsWith("+++ "))) {
|
|
108
|
-
currentFile = {
|
|
109
|
-
oldPath: "",
|
|
110
|
-
newPath: "",
|
|
111
|
-
status: "modified",
|
|
112
|
-
hunks: [],
|
|
113
|
-
additions: 0,
|
|
114
|
-
deletions: 0,
|
|
115
|
-
};
|
|
116
|
-
files.push(currentFile);
|
|
117
|
-
}
|
|
118
|
-
if (!currentFile) continue;
|
|
119
|
-
|
|
120
|
-
if (!currentHunk && line.startsWith("--- ")) {
|
|
121
|
-
currentFile.oldPath = line.slice(4).replace(/^a\//, "");
|
|
122
|
-
continue;
|
|
123
|
-
}
|
|
124
|
-
if (!currentHunk && line.startsWith("+++ ")) {
|
|
125
|
-
currentFile.newPath = line.slice(4).replace(/^b\//, "");
|
|
126
|
-
continue;
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
if (line.startsWith("new file mode ")) {
|
|
130
|
-
currentFile.status = "added";
|
|
131
|
-
continue;
|
|
132
|
-
}
|
|
133
|
-
if (line.startsWith("deleted file mode ")) {
|
|
134
|
-
currentFile.status = "deleted";
|
|
135
|
-
continue;
|
|
136
|
-
}
|
|
137
|
-
if (line.startsWith("similarity index ") || line.startsWith("rename from ") || line.startsWith("rename to ")) {
|
|
138
|
-
currentFile.status = "renamed";
|
|
139
|
-
if (line.startsWith("rename from ")) currentFile.oldPath = line.slice("rename from ".length);
|
|
140
|
-
if (line.startsWith("rename to ")) currentFile.newPath = line.slice("rename to ".length);
|
|
141
|
-
continue;
|
|
142
|
-
}
|
|
143
|
-
if (line.startsWith("Binary files ")) {
|
|
144
|
-
currentFile.status = "binary";
|
|
145
|
-
continue;
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
// Hunk header: @@ -oldStart,oldCount +newStart,newCount @@
|
|
149
|
-
const hunkMatch = line.match(/^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@/);
|
|
150
|
-
if (hunkMatch) {
|
|
151
|
-
oldLineNum = parseInt(hunkMatch[1], 10);
|
|
152
|
-
const oldCount = hunkMatch[2] !== undefined ? parseInt(hunkMatch[2], 10) : 1;
|
|
153
|
-
newLineNum = parseInt(hunkMatch[3], 10);
|
|
154
|
-
const newCount = hunkMatch[4] !== undefined ? parseInt(hunkMatch[4], 10) : 1;
|
|
155
|
-
|
|
156
|
-
currentHunk = {
|
|
157
|
-
header: line,
|
|
158
|
-
oldStart: oldLineNum,
|
|
159
|
-
oldCount,
|
|
160
|
-
newStart: newLineNum,
|
|
161
|
-
newCount,
|
|
162
|
-
lines: [],
|
|
163
|
-
};
|
|
164
|
-
currentFile.hunks.push(currentHunk);
|
|
165
|
-
continue;
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
if (!currentHunk) continue;
|
|
169
|
-
|
|
170
|
-
if (line.startsWith("+")) {
|
|
171
|
-
currentFile.additions++;
|
|
172
|
-
currentHunk.lines.push({
|
|
173
|
-
kind: "add",
|
|
174
|
-
newLine: newLineNum++,
|
|
175
|
-
text: line.slice(1),
|
|
176
|
-
});
|
|
177
|
-
} else if (line.startsWith("-")) {
|
|
178
|
-
currentFile.deletions++;
|
|
179
|
-
currentHunk.lines.push({
|
|
180
|
-
kind: "delete",
|
|
181
|
-
oldLine: oldLineNum++,
|
|
182
|
-
text: line.slice(1),
|
|
183
|
-
});
|
|
184
|
-
} else if (line.startsWith("\\ No newline")) {
|
|
185
|
-
currentHunk.lines.push({
|
|
186
|
-
kind: "no-newline",
|
|
187
|
-
text: line,
|
|
188
|
-
});
|
|
189
|
-
} else {
|
|
190
|
-
currentHunk.lines.push({
|
|
191
|
-
kind: "context",
|
|
192
|
-
oldLine: oldLineNum++,
|
|
193
|
-
newLine: newLineNum++,
|
|
194
|
-
text: line.startsWith(" ") ? line.slice(1) : line,
|
|
195
|
-
});
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
return { files, truncated, totalBytes };
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
/**
|
|
203
|
-
* Render unified diff view frame.
|
|
204
|
-
* @param {DiffDocument} document
|
|
205
|
-
* @param {Record<string, any>} state
|
|
206
|
-
* @param {import("./capabilities.mjs").TerminalCapabilities} caps
|
|
207
|
-
* @returns {import("./renderer.mjs").RenderFrame}
|
|
208
|
-
*/
|
|
209
|
-
export function renderDiffView(document, state, caps) {
|
|
210
|
-
const width = caps.columns || 80;
|
|
211
|
-
const height = caps.rows || 24;
|
|
212
|
-
|
|
213
|
-
const fileIdx = Math.max(0, Math.min((document.files.length || 1) - 1, state.fileIdx || 0));
|
|
214
|
-
const currentFile = document.files[fileIdx];
|
|
215
|
-
|
|
216
|
-
const lines = [];
|
|
217
|
-
|
|
218
|
-
// Header line
|
|
219
|
-
const title = ` Diff · File [${fileIdx + 1}/${document.files.length || 0}] ${currentFile ? currentFile.newPath || currentFile.oldPath : ""} `;
|
|
220
|
-
lines.push({
|
|
221
|
-
runs: [
|
|
222
|
-
{ text: clipText(title, width), style: { fg: "cyan", bold: true, inverse: true } },
|
|
223
|
-
],
|
|
224
|
-
});
|
|
225
|
-
|
|
226
|
-
if (!currentFile) {
|
|
227
|
-
lines.push({ runs: [{ text: "No files changed.", style: { dim: true } }] });
|
|
228
|
-
return { width, height, lines };
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
// File summary line
|
|
232
|
-
const metaStr = ` Status: ${currentFile.status.toUpperCase()} +${currentFile.additions} -${currentFile.deletions} `;
|
|
233
|
-
lines.push({ runs: [{ text: metaStr, style: { fg: "yellow" } }] });
|
|
234
|
-
|
|
235
|
-
let lineOffset = state.lineOffset || 0;
|
|
236
|
-
let renderCount = 0;
|
|
237
|
-
const maxBodyLines = height - 3;
|
|
238
|
-
|
|
239
|
-
for (const hunk of currentFile.hunks) {
|
|
240
|
-
if (renderCount >= maxBodyLines) break;
|
|
241
|
-
|
|
242
|
-
// Hunk header line
|
|
243
|
-
lines.push({
|
|
244
|
-
runs: [{ text: clipText(hunk.header, width), style: { fg: "cyan", dim: true } }],
|
|
245
|
-
});
|
|
246
|
-
renderCount++;
|
|
247
|
-
|
|
248
|
-
for (const line of hunk.lines) {
|
|
249
|
-
if (renderCount >= maxBodyLines) break;
|
|
250
|
-
|
|
251
|
-
if (lineOffset > 0) {
|
|
252
|
-
lineOffset--;
|
|
253
|
-
continue;
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
const oldNumStr = line.oldLine !== undefined ? String(line.oldLine).padStart(5) : " ";
|
|
257
|
-
const newNumStr = line.newLine !== undefined ? String(line.newLine).padStart(5) : " ";
|
|
258
|
-
const numCol = `${oldNumStr} | ${newNumStr} | `;
|
|
259
|
-
|
|
260
|
-
/** @type {import("./renderer.mjs").TextStyle} */
|
|
261
|
-
let style = { fg: "default" };
|
|
262
|
-
let prefix = " ";
|
|
263
|
-
|
|
264
|
-
if (line.kind === "add") {
|
|
265
|
-
style = { fg: "green" };
|
|
266
|
-
prefix = "+";
|
|
267
|
-
} else if (line.kind === "delete") {
|
|
268
|
-
style = { fg: "red" };
|
|
269
|
-
prefix = "-";
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
const lineText = clipText(`${numCol}${prefix}${line.text}`, width);
|
|
273
|
-
lines.push({ runs: [{ text: lineText, style }] });
|
|
274
|
-
renderCount++;
|
|
275
|
-
}
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
// Footer keybindings line
|
|
279
|
-
lines.push({
|
|
280
|
-
runs: [
|
|
281
|
-
{
|
|
282
|
-
text: clipText("[/]: switch file n/N: hunk Up/Down: scroll q: exit", width),
|
|
283
|
-
style: { fg: "cyan", dim: true },
|
|
284
|
-
},
|
|
285
|
-
],
|
|
286
|
-
});
|
|
287
|
-
|
|
288
|
-
return { width, height, lines };
|
|
289
|
-
}
|
package/src/ux/layout.mjs
DELETED
|
@@ -1,195 +0,0 @@
|
|
|
1
|
-
const ANSI_ESCAPE_REGEX = /(?:\u001b\][\s\S]*?(?:\u0007|\u001b\\)|\u001b\[[0-?]*[ -/]*[@-~]|\u001b[@-Z\\-_]|\u009b[0-?]*[ -/]*[@-~])/g;
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Remove terminal control sequences before measuring or rendering text.
|
|
5
|
-
* @param {string} value
|
|
6
|
-
* @returns {string}
|
|
7
|
-
*/
|
|
8
|
-
export function stripAnsi(value) {
|
|
9
|
-
if (value === null || value === undefined) return "";
|
|
10
|
-
return String(value).replace(ANSI_ESCAPE_REGEX, "").replace(/\u001b\][\s\S]*$/g, "");
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
function isCombining(code) {
|
|
14
|
-
return (
|
|
15
|
-
(code >= 0x0300 && code <= 0x036f) ||
|
|
16
|
-
(code >= 0x1ab0 && code <= 0x1aff) ||
|
|
17
|
-
(code >= 0x1dc0 && code <= 0x1dff) ||
|
|
18
|
-
(code >= 0x20d0 && code <= 0x20ff) ||
|
|
19
|
-
(code >= 0xfe00 && code <= 0xfe0f) ||
|
|
20
|
-
(code >= 0xe0100 && code <= 0xe01ef) ||
|
|
21
|
-
(code >= 0x1f3fb && code <= 0x1f3ff) ||
|
|
22
|
-
code === 0x200d
|
|
23
|
-
);
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
function isEmoji(code) {
|
|
27
|
-
return (
|
|
28
|
-
(code >= 0x1f000 && code <= 0x1faff) ||
|
|
29
|
-
(code >= 0x2600 && code <= 0x27ff)
|
|
30
|
-
);
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
function graphemeClusters(value) {
|
|
34
|
-
if (typeof Intl?.Segmenter === "function") {
|
|
35
|
-
const segmenter = new Intl.Segmenter(undefined, { granularity: "grapheme" });
|
|
36
|
-
return Array.from(segmenter.segment(value), ({ segment }) => segment);
|
|
37
|
-
}
|
|
38
|
-
return Array.from(value);
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* Calculate display width of string without external dependencies.
|
|
43
|
-
* Handles wide characters, emoji ranges, and combining characters.
|
|
44
|
-
* @param {string} str
|
|
45
|
-
* @returns {number}
|
|
46
|
-
*/
|
|
47
|
-
export function getStringWidth(str) {
|
|
48
|
-
const plain = stripAnsi(str);
|
|
49
|
-
if (!plain) return 0;
|
|
50
|
-
|
|
51
|
-
let width = 0;
|
|
52
|
-
let joinNext = false;
|
|
53
|
-
let regionalIndicators = 0;
|
|
54
|
-
for (const char of plain) {
|
|
55
|
-
const code = char.codePointAt(0) || 0;
|
|
56
|
-
|
|
57
|
-
if (isCombining(code)) {
|
|
58
|
-
if (code === 0x200d) joinNext = true;
|
|
59
|
-
continue;
|
|
60
|
-
}
|
|
61
|
-
if (code < 32 || (code >= 0x7f && code <= 0x9f)) {
|
|
62
|
-
continue;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
if (code >= 0x1f1e6 && code <= 0x1f1ff) {
|
|
66
|
-
if (regionalIndicators % 2 === 0) width += 2;
|
|
67
|
-
regionalIndicators++;
|
|
68
|
-
continue;
|
|
69
|
-
}
|
|
70
|
-
regionalIndicators = 0;
|
|
71
|
-
|
|
72
|
-
if (joinNext) {
|
|
73
|
-
joinNext = false;
|
|
74
|
-
continue;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
if (
|
|
78
|
-
(code >= 0x1100 && code <= 0x115f) ||
|
|
79
|
-
(code >= 0x2e80 && code <= 0xa4cf) ||
|
|
80
|
-
(code >= 0xac00 && code <= 0xd7a3) ||
|
|
81
|
-
(code >= 0xf900 && code <= 0xfaff) ||
|
|
82
|
-
(code >= 0xfe10 && code <= 0xfe19) ||
|
|
83
|
-
(code >= 0xfe30 && code <= 0xfe6f) ||
|
|
84
|
-
(code >= 0xff00 && code <= 0xff60) ||
|
|
85
|
-
(code >= 0xffe0 && code <= 0xffe6) ||
|
|
86
|
-
isEmoji(code)
|
|
87
|
-
) {
|
|
88
|
-
width += 2;
|
|
89
|
-
} else {
|
|
90
|
-
width += 1;
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
return width;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
/**
|
|
97
|
-
* Truncate or clip text to fit maximum display width.
|
|
98
|
-
* @param {string} text
|
|
99
|
-
* @param {number} maxWidth
|
|
100
|
-
* @param {string} [ellipsis="…"]
|
|
101
|
-
* @returns {string}
|
|
102
|
-
*/
|
|
103
|
-
export function clipText(text, maxWidth, ellipsis = "…") {
|
|
104
|
-
const widthLimit = Number.isFinite(maxWidth) ? Math.max(0, Math.floor(maxWidth)) : 0;
|
|
105
|
-
if (widthLimit === 0) return "";
|
|
106
|
-
|
|
107
|
-
const plain = stripAnsi(text);
|
|
108
|
-
if (getStringWidth(plain) <= widthLimit) return plain;
|
|
109
|
-
|
|
110
|
-
const cleanEllipsis = stripAnsi(ellipsis);
|
|
111
|
-
const ellipsisWidth = getStringWidth(cleanEllipsis);
|
|
112
|
-
if (ellipsisWidth >= widthLimit) {
|
|
113
|
-
let fitted = "";
|
|
114
|
-
for (const cluster of graphemeClusters(cleanEllipsis)) {
|
|
115
|
-
if (getStringWidth(fitted + cluster) > widthLimit) break;
|
|
116
|
-
fitted += cluster;
|
|
117
|
-
}
|
|
118
|
-
return fitted;
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
const targetWidth = widthLimit - ellipsisWidth;
|
|
122
|
-
let truncated = "";
|
|
123
|
-
let accumulatedWidth = 0;
|
|
124
|
-
for (const cluster of graphemeClusters(plain)) {
|
|
125
|
-
const clusterWidth = getStringWidth(cluster);
|
|
126
|
-
if (accumulatedWidth + clusterWidth > targetWidth) break;
|
|
127
|
-
accumulatedWidth += clusterWidth;
|
|
128
|
-
truncated += cluster;
|
|
129
|
-
}
|
|
130
|
-
return truncated + cleanEllipsis;
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
/**
|
|
134
|
-
* Pad text to a fixed display width.
|
|
135
|
-
* @param {string} text
|
|
136
|
-
* @param {number} width
|
|
137
|
-
* @param {"left" | "right" | "center"} [align="left"]
|
|
138
|
-
* @returns {string}
|
|
139
|
-
*/
|
|
140
|
-
export function padText(text, width, align = "left") {
|
|
141
|
-
const textWidth = getStringWidth(text);
|
|
142
|
-
if (textWidth >= width) return clipText(text, width);
|
|
143
|
-
|
|
144
|
-
const missing = width - textWidth;
|
|
145
|
-
if (align === "right") {
|
|
146
|
-
return " ".repeat(missing) + text;
|
|
147
|
-
}
|
|
148
|
-
if (align === "center") {
|
|
149
|
-
const leftPad = Math.floor(missing / 2);
|
|
150
|
-
const rightPad = missing - leftPad;
|
|
151
|
-
return " ".repeat(leftPad) + text + " ".repeat(rightPad);
|
|
152
|
-
}
|
|
153
|
-
return text + " ".repeat(missing);
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
/**
|
|
157
|
-
* Compute responsive layout panes based on terminal dimensions.
|
|
158
|
-
* @param {number} width
|
|
159
|
-
* @param {number} height
|
|
160
|
-
*/
|
|
161
|
-
export function computeResponsiveLayout(width, height) {
|
|
162
|
-
const headerHeight = Math.min(4, Math.max(2, Math.floor(height * 0.1)));
|
|
163
|
-
const footerHeight = 2;
|
|
164
|
-
const bodyHeight = Math.max(4, height - headerHeight - footerHeight);
|
|
165
|
-
|
|
166
|
-
/** @type {"single" | "tabs" | "split-55" | "split-50"} */
|
|
167
|
-
let layoutMode = "single";
|
|
168
|
-
let mainWidth = width;
|
|
169
|
-
let detailWidth = 0;
|
|
170
|
-
|
|
171
|
-
if (width >= 140) {
|
|
172
|
-
layoutMode = "split-50";
|
|
173
|
-
mainWidth = Math.floor(width * 0.5);
|
|
174
|
-
detailWidth = width - mainWidth - 1;
|
|
175
|
-
} else if (width >= 100) {
|
|
176
|
-
layoutMode = "split-55";
|
|
177
|
-
mainWidth = Math.floor(width * 0.55);
|
|
178
|
-
detailWidth = width - mainWidth - 1;
|
|
179
|
-
} else if (width >= 60) {
|
|
180
|
-
layoutMode = "tabs";
|
|
181
|
-
} else {
|
|
182
|
-
layoutMode = "single";
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
return {
|
|
186
|
-
width,
|
|
187
|
-
height,
|
|
188
|
-
headerHeight,
|
|
189
|
-
footerHeight,
|
|
190
|
-
bodyHeight,
|
|
191
|
-
layoutMode,
|
|
192
|
-
mainWidth,
|
|
193
|
-
detailWidth,
|
|
194
|
-
};
|
|
195
|
-
}
|
package/src/ux/log-viewer.mjs
DELETED
|
@@ -1,110 +0,0 @@
|
|
|
1
|
-
import { sanitizeControlChars } from "./renderer.mjs";
|
|
2
|
-
import { clipText } from "./layout.mjs";
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* @typedef {Object} LogLine
|
|
6
|
-
* @property {number} number
|
|
7
|
-
* @property {string} [timestamp]
|
|
8
|
-
* @property {"stdout" | "stderr" | "system"} [stream]
|
|
9
|
-
* @property {"debug" | "info" | "warn" | "error"} [level]
|
|
10
|
-
* @property {string} text
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* @typedef {Object} LogDocument
|
|
15
|
-
* @property {LogLine[]} lines
|
|
16
|
-
* @property {string} source
|
|
17
|
-
* @property {number} truncatedBefore
|
|
18
|
-
* @property {number} truncatedAfter
|
|
19
|
-
* @property {boolean} followable
|
|
20
|
-
*/
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* Render bounded log viewer frame.
|
|
24
|
-
* @param {LogDocument} document
|
|
25
|
-
* @param {Record<string, any>} state
|
|
26
|
-
* @param {import("./capabilities.mjs").TerminalCapabilities} caps
|
|
27
|
-
* @returns {import("./renderer.mjs").RenderFrame}
|
|
28
|
-
*/
|
|
29
|
-
export function renderLogView(document, state, caps) {
|
|
30
|
-
const width = caps.columns || 80;
|
|
31
|
-
const height = caps.rows || 24;
|
|
32
|
-
|
|
33
|
-
const rawLines = document.lines || [];
|
|
34
|
-
const filter = (state.filter || "").toLowerCase();
|
|
35
|
-
const errorsOnly = Boolean(state.errorsOnly);
|
|
36
|
-
const follow = Boolean(state.follow);
|
|
37
|
-
|
|
38
|
-
// Filter lines
|
|
39
|
-
const filtered = rawLines.filter((l) => {
|
|
40
|
-
if (errorsOnly && l.level !== "error" && l.stream !== "stderr") {
|
|
41
|
-
return false;
|
|
42
|
-
}
|
|
43
|
-
if (filter) {
|
|
44
|
-
const clean = (l.text || "").toLowerCase();
|
|
45
|
-
if (!clean.includes(filter)) return false;
|
|
46
|
-
}
|
|
47
|
-
return true;
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
const maxBodyLines = Math.max(2, height - 3);
|
|
51
|
-
let offset = state.offset || 0;
|
|
52
|
-
|
|
53
|
-
if (follow) {
|
|
54
|
-
offset = Math.max(0, filtered.length - maxBodyLines);
|
|
55
|
-
} else {
|
|
56
|
-
offset = Math.max(0, Math.min(filtered.length - 1, offset));
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
const lines = [];
|
|
60
|
-
|
|
61
|
-
// Header line
|
|
62
|
-
const headerStr = ` Log Viewer · ${document.source || "Output"} · Showing ${filtered.length} lines ${errorsOnly ? "(Errors Only)" : ""} `;
|
|
63
|
-
lines.push({
|
|
64
|
-
runs: [{ text: clipText(headerStr, width), style: { fg: "cyan", bold: true, inverse: true } }],
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
// Status sub-header line
|
|
68
|
-
const subHeader = ` Filter: ${filter || "none"} Follow: ${follow ? "ON" : "OFF"} Showing ${offset + 1}-${Math.min(filtered.length, offset + maxBodyLines)} of ${filtered.length} `;
|
|
69
|
-
lines.push({
|
|
70
|
-
runs: [{ text: clipText(subHeader, width), style: { fg: "yellow", dim: true } }],
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
// Body lines
|
|
74
|
-
const slice = filtered.slice(offset, offset + maxBodyLines);
|
|
75
|
-
for (const item of slice) {
|
|
76
|
-
const numStr = String(item.number).padStart(5);
|
|
77
|
-
const streamStr = item.stream ? item.stream.padStart(6) : " ";
|
|
78
|
-
const cleanText = sanitizeControlChars(item.text);
|
|
79
|
-
|
|
80
|
-
/** @type {import("./renderer.mjs").TextStyle} */
|
|
81
|
-
let style = { fg: "default" };
|
|
82
|
-
if (item.level === "error" || item.stream === "stderr") {
|
|
83
|
-
style = { fg: "red" };
|
|
84
|
-
} else if (item.level === "warn") {
|
|
85
|
-
style = { fg: "yellow" };
|
|
86
|
-
} else if (item.stream === "system") {
|
|
87
|
-
style = { fg: "cyan", dim: true };
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
const lineText = clipText(`${numStr} | ${streamStr} | ${cleanText}`, width);
|
|
91
|
-
lines.push({ runs: [{ text: lineText, style }] });
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
// Pad remaining height
|
|
95
|
-
while (lines.length < height - 1) {
|
|
96
|
-
lines.push({ runs: [{ text: " ".repeat(width), style: { fg: "default" } }] });
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
// Footer status bar
|
|
100
|
-
lines.push({
|
|
101
|
-
runs: [
|
|
102
|
-
{
|
|
103
|
-
text: clipText("Up/Down: scroll PgUp/PgDn / search e: errors only f: follow q: back", width),
|
|
104
|
-
style: { fg: "cyan", dim: true },
|
|
105
|
-
},
|
|
106
|
-
],
|
|
107
|
-
});
|
|
108
|
-
|
|
109
|
-
return { width, height, lines };
|
|
110
|
-
}
|