pi-better-harness 0.3.15 → 0.3.17
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/node_modules/pi-better-background-tasks/package.json +1 -1
- package/node_modules/pi-better-background-tasks/src/shared-navigator.ts +27 -58
- package/node_modules/pi-better-plan/package.json +1 -1
- package/node_modules/pi-better-plan/src/index.ts +10 -6
- package/node_modules/pi-better-plan/src/workflow-plan.ts +14 -4
- package/node_modules/pi-better-subagents/package.json +1 -1
- package/node_modules/pi-better-subagents/shared-navigator.ts +27 -58
- package/package.json +4 -4
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { truncateToWidth as piTruncateToWidth, visibleWidth as piVisibleWidth } from "@earendil-works/pi-tui";
|
|
2
3
|
import type { Component } from "@earendil-works/pi-tui";
|
|
3
4
|
import { createRenderScheduler, type RenderScheduler } from "./shared-render-scheduler.ts";
|
|
4
5
|
|
|
@@ -280,10 +281,10 @@ function refreshMainListWidget(): void {
|
|
|
280
281
|
}) : undefined;
|
|
281
282
|
const changed = nextSignature !== s.lastMainListSignature;
|
|
282
283
|
s.lastMainListSignature = nextSignature;
|
|
283
|
-
s.lastMainListLines = rows.length ? buildMainListLines(rows, MAIN_LIST_FALLBACK_WIDTH, deps.truncate, themeFg(ctx), {
|
|
284
|
+
s.lastMainListLines = rows.length ? ["", ...buildMainListLines(rows, MAIN_LIST_FALLBACK_WIDTH, deps.truncate, themeFg(ctx), {
|
|
284
285
|
selectedId: s.mainListFocused ? s.mainListSelectedId : undefined,
|
|
285
286
|
focused: s.mainListFocused === true,
|
|
286
|
-
}) : undefined;
|
|
287
|
+
})] : undefined;
|
|
287
288
|
if (!rows.length) {
|
|
288
289
|
if (!s.mainListWidgetInstalled) return;
|
|
289
290
|
try { (ctx.ui as any).setWidget?.(MAIN_LIST_WIDGET_KEY, undefined); } catch { /* ignore */ }
|
|
@@ -323,10 +324,10 @@ function createMainListWidget(tui: { requestRender?(): void }, theme: unknown, d
|
|
|
323
324
|
state().lastMainListLines = undefined;
|
|
324
325
|
return [];
|
|
325
326
|
}
|
|
326
|
-
const lines = buildMainListLines(rows, renderWidth(width), deps.truncate, themeFgFromTheme(theme), {
|
|
327
|
+
const lines = ["", ...buildMainListLines(rows, renderWidth(width), deps.truncate, themeFgFromTheme(theme), {
|
|
327
328
|
selectedId: state().mainListFocused ? state().mainListSelectedId : undefined,
|
|
328
329
|
focused: state().mainListFocused === true,
|
|
329
|
-
});
|
|
330
|
+
})];
|
|
330
331
|
state().lastMainListLines = lines;
|
|
331
332
|
return lines;
|
|
332
333
|
},
|
|
@@ -1269,7 +1270,8 @@ export function wrapLogText(text: string, width: number): string[] {
|
|
|
1269
1270
|
continue;
|
|
1270
1271
|
}
|
|
1271
1272
|
const sourceIndent = source.match(/^[ \t]*/)?.[0] ?? "";
|
|
1272
|
-
|
|
1273
|
+
// Leave room for a full-width grapheme even on deeply indented log lines.
|
|
1274
|
+
const indent = sourceIndent.replace(/\t/g, " ").slice(0, max - 2);
|
|
1273
1275
|
let remaining = source.slice(sourceIndent.length);
|
|
1274
1276
|
const contentWidth = Math.max(1, max - visibleWidth(indent));
|
|
1275
1277
|
if (!remaining) {
|
|
@@ -1302,33 +1304,25 @@ function lastDelimiterBreak(value: string): number {
|
|
|
1302
1304
|
return last;
|
|
1303
1305
|
}
|
|
1304
1306
|
|
|
1307
|
+
const GRAPHEME_SEGMENTER = new Intl.Segmenter(undefined, { granularity: "grapheme" });
|
|
1308
|
+
|
|
1305
1309
|
function splitVisiblePrefix(value: string, width: number): [string, string] {
|
|
1306
1310
|
const max = Math.max(0, Math.floor(width));
|
|
1307
1311
|
let visible = 0;
|
|
1308
|
-
let
|
|
1309
|
-
|
|
1310
|
-
if (
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
}
|
|
1316
|
-
}
|
|
1317
|
-
if (value[index] === "<") {
|
|
1318
|
-
const close = value.indexOf(">", index);
|
|
1319
|
-
if (close !== -1) {
|
|
1320
|
-
const tag = value.slice(index, close + 1);
|
|
1321
|
-
if (/^<\/?[a-zA-Z][\w-]*>$/.test(tag) || tag === "</>") {
|
|
1322
|
-
index = close + 1;
|
|
1323
|
-
continue;
|
|
1324
|
-
}
|
|
1325
|
-
}
|
|
1312
|
+
let end = 0;
|
|
1313
|
+
for (const { segment, index } of GRAPHEME_SEGMENTER.segment(value)) {
|
|
1314
|
+
if (index < end) continue;
|
|
1315
|
+
const formatting = value.slice(index).match(FORMATTING_PREFIX_RE);
|
|
1316
|
+
if (formatting) {
|
|
1317
|
+
end = index + formatting[0].length;
|
|
1318
|
+
continue;
|
|
1326
1319
|
}
|
|
1327
|
-
const
|
|
1328
|
-
|
|
1329
|
-
visible +=
|
|
1320
|
+
const cells = piVisibleWidth(segment);
|
|
1321
|
+
if (visible + cells > max) break;
|
|
1322
|
+
visible += cells;
|
|
1323
|
+
end = index + segment.length;
|
|
1330
1324
|
}
|
|
1331
|
-
return [value.slice(0,
|
|
1325
|
+
return [value.slice(0, end), value.slice(end)];
|
|
1332
1326
|
}
|
|
1333
1327
|
|
|
1334
1328
|
function cycleLogTailRows(current: number): number {
|
|
@@ -1387,44 +1381,19 @@ function safeTruncate(line: string, width: number, truncate: (s: string, width:
|
|
|
1387
1381
|
|
|
1388
1382
|
const ANSI_RE = new RegExp("[\\u001B\\u009B][[\\]()#;?]*(?:(?:(?:[a-zA-Z\\d]*(?:;[a-zA-Z\\d]*)*)?\\u0007)|(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PR-TZcf-ntqry=><~]))", "g");
|
|
1389
1383
|
|
|
1384
|
+
const FORMATTING_PREFIX_RE = new RegExp(`^(?:${ANSI_RE.source}|<\\/?[a-zA-Z][\\w-]*>|</>)`);
|
|
1385
|
+
|
|
1390
1386
|
function visibleWidth(value: string): number {
|
|
1391
|
-
|
|
1387
|
+
const cleaned = String(value ?? "")
|
|
1392
1388
|
.replace(ANSI_RE, "")
|
|
1393
1389
|
.replace(/<\/?[a-zA-Z][\w-]*>/g, "")
|
|
1394
|
-
.replace(/<\/>/g, "")
|
|
1395
|
-
|
|
1390
|
+
.replace(/<\/>/g, "");
|
|
1391
|
+
return piVisibleWidth(cleaned);
|
|
1396
1392
|
}
|
|
1397
1393
|
|
|
1398
1394
|
function truncateVisible(value: string, width: number): string {
|
|
1399
1395
|
const str = String(value ?? "");
|
|
1400
1396
|
const max = Math.max(0, Math.floor(width || 0));
|
|
1401
1397
|
if (visibleWidth(str) <= max) return str;
|
|
1402
|
-
|
|
1403
|
-
let vis = 0;
|
|
1404
|
-
let i = 0;
|
|
1405
|
-
while (i < str.length && vis < max) {
|
|
1406
|
-
if (str[i] === "\u001b" || str[i] === "\u009b") {
|
|
1407
|
-
const match = str.slice(i).match(ANSI_RE);
|
|
1408
|
-
if (match && match.index === 0) {
|
|
1409
|
-
out += match[0];
|
|
1410
|
-
i += match[0].length;
|
|
1411
|
-
continue;
|
|
1412
|
-
}
|
|
1413
|
-
}
|
|
1414
|
-
if (str[i] === "<") {
|
|
1415
|
-
const close = str.indexOf(">", i);
|
|
1416
|
-
if (close !== -1) {
|
|
1417
|
-
const tag = str.slice(i, close + 1);
|
|
1418
|
-
if (/^<\/?[a-zA-Z][\w-]*>$/.test(tag) || tag === "</>") {
|
|
1419
|
-
out += tag;
|
|
1420
|
-
i = close + 1;
|
|
1421
|
-
continue;
|
|
1422
|
-
}
|
|
1423
|
-
}
|
|
1424
|
-
}
|
|
1425
|
-
out += str[i];
|
|
1426
|
-
vis += 1;
|
|
1427
|
-
i += 1;
|
|
1428
|
-
}
|
|
1429
|
-
return out;
|
|
1398
|
+
return piTruncateToWidth(str, max);
|
|
1430
1399
|
}
|
|
@@ -162,12 +162,13 @@ export default function planExtension(pi: ExtensionAPI): void {
|
|
|
162
162
|
if (displayMode === "hidden") return [];
|
|
163
163
|
if (displayedWorkflowOwner) {
|
|
164
164
|
if (displayedWorkflowOwner !== "rush-issues") return [];
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
165
|
+
const fg = typeof theme?.fg === "function"
|
|
166
|
+
? (color: string, value: string) => theme.fg(color as never, value)
|
|
167
|
+
: undefined;
|
|
168
|
+
return rushPlan ? ["", ...renderRushPlan(rushPlan, width, false, fg)] : [];
|
|
168
169
|
}
|
|
169
170
|
if (!currentPlan) return [];
|
|
170
|
-
return renderCompactPlan(currentPlan, width, theme as never);
|
|
171
|
+
return ["", ...renderCompactPlan(currentPlan, width, theme as never)];
|
|
171
172
|
},
|
|
172
173
|
invalidate() {},
|
|
173
174
|
dispose() {
|
|
@@ -191,8 +192,11 @@ export default function planExtension(pi: ExtensionAPI): void {
|
|
|
191
192
|
ctx.ui.notify(renderRushPlan(plan, 160, true).join("\n"), "info");
|
|
192
193
|
return;
|
|
193
194
|
}
|
|
194
|
-
await ctx.ui.custom<void>((tui,
|
|
195
|
-
const
|
|
195
|
+
await ctx.ui.custom<void>((tui, theme, _keys, done) => {
|
|
196
|
+
const fg = typeof theme?.fg === "function"
|
|
197
|
+
? (color: string, value: string) => theme.fg(color as never, value)
|
|
198
|
+
: undefined;
|
|
199
|
+
const component = createRushPlanComponent(plan, () => done(), fg);
|
|
196
200
|
return { render: (width) => component.render(width), handleInput(data) {
|
|
197
201
|
component.handleInput?.(data);
|
|
198
202
|
tui.requestRender();
|
|
@@ -98,9 +98,15 @@ export function workflowBinding(entries: Iterable<{ type: string; customType?: s
|
|
|
98
98
|
return binding;
|
|
99
99
|
}
|
|
100
100
|
|
|
101
|
-
export function renderRushPlan(
|
|
101
|
+
export function renderRushPlan(
|
|
102
|
+
plan: RushPlan,
|
|
103
|
+
width: number,
|
|
104
|
+
full = false,
|
|
105
|
+
fg?: (color: string, value: string) => string,
|
|
106
|
+
): string[] {
|
|
102
107
|
const completed = plan.issues.filter((unit) => unit.status === "succeeded").length;
|
|
103
|
-
const
|
|
108
|
+
const summary = `rev ${plan.planRevision} ${completed}/${plan.issues.length} units${plan.spec?.title ? ` ${plan.spec.title}` : ""}`;
|
|
109
|
+
const lines = [fg ? `${fg("warning", "rush-issues")} ${fg("dim", summary)}` : `rush-issues ${summary}`];
|
|
104
110
|
const fleet = ["explore", "combine", "canary", "review", "cicd"]
|
|
105
111
|
.map((stage) => `${stage}: ${stage === "canary" && !plan.warehouseCanaryRequired ? "n/a" : plan.fleet[stage]?.status ?? "pending"}`);
|
|
106
112
|
lines.push(`fleet ${fleet.join(" ")}`);
|
|
@@ -115,9 +121,13 @@ export function renderRushPlan(plan: RushPlan, width: number, full = false): str
|
|
|
115
121
|
return lines.map((line) => truncateToWidth(line, Math.max(1, width)));
|
|
116
122
|
}
|
|
117
123
|
|
|
118
|
-
export function createRushPlanComponent(
|
|
124
|
+
export function createRushPlanComponent(
|
|
125
|
+
plan: RushPlan,
|
|
126
|
+
onClose: () => void,
|
|
127
|
+
fg?: (color: string, value: string) => string,
|
|
128
|
+
): Component {
|
|
119
129
|
return {
|
|
120
|
-
render: (width) => renderRushPlan(plan, width, true),
|
|
130
|
+
render: (width) => renderRushPlan(plan, width, true, fg),
|
|
121
131
|
handleInput(data) {
|
|
122
132
|
if (matchesKey(data, "escape") || matchesKey(data, "left") || matchesKey(data, "ctrl+c")) onClose();
|
|
123
133
|
},
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { truncateToWidth as piTruncateToWidth, visibleWidth as piVisibleWidth } from "@earendil-works/pi-tui";
|
|
2
3
|
import type { Component } from "@earendil-works/pi-tui";
|
|
3
4
|
import { createRenderScheduler, type RenderScheduler } from "./shared-render-scheduler.ts";
|
|
4
5
|
|
|
@@ -280,10 +281,10 @@ function refreshMainListWidget(): void {
|
|
|
280
281
|
}) : undefined;
|
|
281
282
|
const changed = nextSignature !== s.lastMainListSignature;
|
|
282
283
|
s.lastMainListSignature = nextSignature;
|
|
283
|
-
s.lastMainListLines = rows.length ? buildMainListLines(rows, MAIN_LIST_FALLBACK_WIDTH, deps.truncate, themeFg(ctx), {
|
|
284
|
+
s.lastMainListLines = rows.length ? ["", ...buildMainListLines(rows, MAIN_LIST_FALLBACK_WIDTH, deps.truncate, themeFg(ctx), {
|
|
284
285
|
selectedId: s.mainListFocused ? s.mainListSelectedId : undefined,
|
|
285
286
|
focused: s.mainListFocused === true,
|
|
286
|
-
}) : undefined;
|
|
287
|
+
})] : undefined;
|
|
287
288
|
if (!rows.length) {
|
|
288
289
|
if (!s.mainListWidgetInstalled) return;
|
|
289
290
|
try { (ctx.ui as any).setWidget?.(MAIN_LIST_WIDGET_KEY, undefined); } catch { /* ignore */ }
|
|
@@ -323,10 +324,10 @@ function createMainListWidget(tui: { requestRender?(): void }, theme: unknown, d
|
|
|
323
324
|
state().lastMainListLines = undefined;
|
|
324
325
|
return [];
|
|
325
326
|
}
|
|
326
|
-
const lines = buildMainListLines(rows, renderWidth(width), deps.truncate, themeFgFromTheme(theme), {
|
|
327
|
+
const lines = ["", ...buildMainListLines(rows, renderWidth(width), deps.truncate, themeFgFromTheme(theme), {
|
|
327
328
|
selectedId: state().mainListFocused ? state().mainListSelectedId : undefined,
|
|
328
329
|
focused: state().mainListFocused === true,
|
|
329
|
-
});
|
|
330
|
+
})];
|
|
330
331
|
state().lastMainListLines = lines;
|
|
331
332
|
return lines;
|
|
332
333
|
},
|
|
@@ -1269,7 +1270,8 @@ export function wrapLogText(text: string, width: number): string[] {
|
|
|
1269
1270
|
continue;
|
|
1270
1271
|
}
|
|
1271
1272
|
const sourceIndent = source.match(/^[ \t]*/)?.[0] ?? "";
|
|
1272
|
-
|
|
1273
|
+
// Leave room for a full-width grapheme even on deeply indented log lines.
|
|
1274
|
+
const indent = sourceIndent.replace(/\t/g, " ").slice(0, max - 2);
|
|
1273
1275
|
let remaining = source.slice(sourceIndent.length);
|
|
1274
1276
|
const contentWidth = Math.max(1, max - visibleWidth(indent));
|
|
1275
1277
|
if (!remaining) {
|
|
@@ -1302,33 +1304,25 @@ function lastDelimiterBreak(value: string): number {
|
|
|
1302
1304
|
return last;
|
|
1303
1305
|
}
|
|
1304
1306
|
|
|
1307
|
+
const GRAPHEME_SEGMENTER = new Intl.Segmenter(undefined, { granularity: "grapheme" });
|
|
1308
|
+
|
|
1305
1309
|
function splitVisiblePrefix(value: string, width: number): [string, string] {
|
|
1306
1310
|
const max = Math.max(0, Math.floor(width));
|
|
1307
1311
|
let visible = 0;
|
|
1308
|
-
let
|
|
1309
|
-
|
|
1310
|
-
if (
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
}
|
|
1316
|
-
}
|
|
1317
|
-
if (value[index] === "<") {
|
|
1318
|
-
const close = value.indexOf(">", index);
|
|
1319
|
-
if (close !== -1) {
|
|
1320
|
-
const tag = value.slice(index, close + 1);
|
|
1321
|
-
if (/^<\/?[a-zA-Z][\w-]*>$/.test(tag) || tag === "</>") {
|
|
1322
|
-
index = close + 1;
|
|
1323
|
-
continue;
|
|
1324
|
-
}
|
|
1325
|
-
}
|
|
1312
|
+
let end = 0;
|
|
1313
|
+
for (const { segment, index } of GRAPHEME_SEGMENTER.segment(value)) {
|
|
1314
|
+
if (index < end) continue;
|
|
1315
|
+
const formatting = value.slice(index).match(FORMATTING_PREFIX_RE);
|
|
1316
|
+
if (formatting) {
|
|
1317
|
+
end = index + formatting[0].length;
|
|
1318
|
+
continue;
|
|
1326
1319
|
}
|
|
1327
|
-
const
|
|
1328
|
-
|
|
1329
|
-
visible +=
|
|
1320
|
+
const cells = piVisibleWidth(segment);
|
|
1321
|
+
if (visible + cells > max) break;
|
|
1322
|
+
visible += cells;
|
|
1323
|
+
end = index + segment.length;
|
|
1330
1324
|
}
|
|
1331
|
-
return [value.slice(0,
|
|
1325
|
+
return [value.slice(0, end), value.slice(end)];
|
|
1332
1326
|
}
|
|
1333
1327
|
|
|
1334
1328
|
function cycleLogTailRows(current: number): number {
|
|
@@ -1387,44 +1381,19 @@ function safeTruncate(line: string, width: number, truncate: (s: string, width:
|
|
|
1387
1381
|
|
|
1388
1382
|
const ANSI_RE = new RegExp("[\\u001B\\u009B][[\\]()#;?]*(?:(?:(?:[a-zA-Z\\d]*(?:;[a-zA-Z\\d]*)*)?\\u0007)|(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PR-TZcf-ntqry=><~]))", "g");
|
|
1389
1383
|
|
|
1384
|
+
const FORMATTING_PREFIX_RE = new RegExp(`^(?:${ANSI_RE.source}|<\\/?[a-zA-Z][\\w-]*>|</>)`);
|
|
1385
|
+
|
|
1390
1386
|
function visibleWidth(value: string): number {
|
|
1391
|
-
|
|
1387
|
+
const cleaned = String(value ?? "")
|
|
1392
1388
|
.replace(ANSI_RE, "")
|
|
1393
1389
|
.replace(/<\/?[a-zA-Z][\w-]*>/g, "")
|
|
1394
|
-
.replace(/<\/>/g, "")
|
|
1395
|
-
|
|
1390
|
+
.replace(/<\/>/g, "");
|
|
1391
|
+
return piVisibleWidth(cleaned);
|
|
1396
1392
|
}
|
|
1397
1393
|
|
|
1398
1394
|
function truncateVisible(value: string, width: number): string {
|
|
1399
1395
|
const str = String(value ?? "");
|
|
1400
1396
|
const max = Math.max(0, Math.floor(width || 0));
|
|
1401
1397
|
if (visibleWidth(str) <= max) return str;
|
|
1402
|
-
|
|
1403
|
-
let vis = 0;
|
|
1404
|
-
let i = 0;
|
|
1405
|
-
while (i < str.length && vis < max) {
|
|
1406
|
-
if (str[i] === "\u001b" || str[i] === "\u009b") {
|
|
1407
|
-
const match = str.slice(i).match(ANSI_RE);
|
|
1408
|
-
if (match && match.index === 0) {
|
|
1409
|
-
out += match[0];
|
|
1410
|
-
i += match[0].length;
|
|
1411
|
-
continue;
|
|
1412
|
-
}
|
|
1413
|
-
}
|
|
1414
|
-
if (str[i] === "<") {
|
|
1415
|
-
const close = str.indexOf(">", i);
|
|
1416
|
-
if (close !== -1) {
|
|
1417
|
-
const tag = str.slice(i, close + 1);
|
|
1418
|
-
if (/^<\/?[a-zA-Z][\w-]*>$/.test(tag) || tag === "</>") {
|
|
1419
|
-
out += tag;
|
|
1420
|
-
i = close + 1;
|
|
1421
|
-
continue;
|
|
1422
|
-
}
|
|
1423
|
-
}
|
|
1424
|
-
}
|
|
1425
|
-
out += str[i];
|
|
1426
|
-
vis += 1;
|
|
1427
|
-
i += 1;
|
|
1428
|
-
}
|
|
1429
|
-
return out;
|
|
1398
|
+
return piTruncateToWidth(str, max);
|
|
1430
1399
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-better-harness",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.17",
|
|
4
4
|
"description": "Pi extension bundle for a write sandbox, subagents, background tasks, SSH, goals, and structured plans.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -50,12 +50,12 @@
|
|
|
50
50
|
"test": "node --test test/*.test.mjs"
|
|
51
51
|
},
|
|
52
52
|
"dependencies": {
|
|
53
|
-
"pi-better-background-tasks": "0.2.
|
|
53
|
+
"pi-better-background-tasks": "0.2.15",
|
|
54
54
|
"pi-better-goal": "0.4.0",
|
|
55
|
-
"pi-better-plan": "0.3.
|
|
55
|
+
"pi-better-plan": "0.3.3",
|
|
56
56
|
"pi-better-sandbox": "0.3.0",
|
|
57
57
|
"pi-better-ssh": "0.1.1",
|
|
58
|
-
"pi-better-subagents": "0.1.
|
|
58
|
+
"pi-better-subagents": "0.1.31",
|
|
59
59
|
"yaml": "^2.9.1"
|
|
60
60
|
},
|
|
61
61
|
"bundledDependencies": [
|