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.
@@ -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
- }
@@ -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
- }