@aliou/pi-processes 0.10.3 → 0.10.5
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/extensions/processes/message-renderer.ts +109 -14
- package/extensions/processes/notifications/log-matchers.ts +7 -2
- package/extensions/processes/tools/components/index.ts +5 -1
- package/extensions/processes/tools/components/watch.ts +12 -1
- package/extensions/processes/tools/output/render.ts +17 -3
- package/extensions/processes-dock/components/log-dock-component.ts +1 -1
- package/extensions/processes-logs/components/log-file-viewer.ts +14 -5
- package/extensions/processes-logs/components/log-overlay-component.ts +1 -1
- package/extensions/shared/display-text.ts +83 -18
- package/extensions/shared/log-line.ts +7 -9
- package/extensions/{process-tabs.ts → shared/process-tabs.ts} +3 -3
- package/package.json +11 -6
|
@@ -2,11 +2,17 @@ import type {
|
|
|
2
2
|
ExtensionAPI,
|
|
3
3
|
MessageRenderOptions,
|
|
4
4
|
Theme,
|
|
5
|
+
ThemeColor,
|
|
5
6
|
} from "@earendil-works/pi-coding-agent";
|
|
6
7
|
import { type Component, Container, Text } from "@earendil-works/pi-tui";
|
|
7
8
|
import { truncateForDisplay } from "../shared/display-text";
|
|
8
9
|
import { MESSAGE_TYPE_PROCESS_NOTIFICATION } from "./constants";
|
|
9
|
-
import type {
|
|
10
|
+
import type {
|
|
11
|
+
Attention,
|
|
12
|
+
ProcessNotificationDetails,
|
|
13
|
+
ProcessNotificationKind,
|
|
14
|
+
} from "./notifications/types";
|
|
15
|
+
import { formatPatternForDisplay, quoteFilter } from "./tools/components";
|
|
10
16
|
|
|
11
17
|
export function registerProcessNotificationRenderer(pi: ExtensionAPI): void {
|
|
12
18
|
pi.registerMessageRenderer<ProcessNotificationDetails>(
|
|
@@ -25,11 +31,17 @@ export function renderProcessNotificationMessage(
|
|
|
25
31
|
return undefined;
|
|
26
32
|
}
|
|
27
33
|
|
|
34
|
+
// outputPad is only populated on Pi 0.82.1+ (see MessageRenderOptions);
|
|
35
|
+
// fall back to 0 so this renderer doesn't break on older Pi builds.
|
|
36
|
+
const outputPad = options.outputPad ?? 0;
|
|
37
|
+
|
|
28
38
|
const container = new Container();
|
|
29
|
-
container.addChild(new Text(formatSummary(details, theme),
|
|
39
|
+
container.addChild(new Text(formatSummary(details, theme), outputPad, 0));
|
|
30
40
|
|
|
31
41
|
if (options.expanded) {
|
|
32
|
-
container.addChild(
|
|
42
|
+
container.addChild(
|
|
43
|
+
new Text(formatDetailLine(details, theme), outputPad, 0),
|
|
44
|
+
);
|
|
33
45
|
}
|
|
34
46
|
|
|
35
47
|
return container;
|
|
@@ -66,17 +78,100 @@ function formatDetailLine(
|
|
|
66
78
|
details: ProcessNotificationDetails,
|
|
67
79
|
theme: Theme,
|
|
68
80
|
): string {
|
|
69
|
-
const parts = [
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
81
|
+
const parts: string[] = [
|
|
82
|
+
field("id", details.processId, theme),
|
|
83
|
+
field("notify", formatAttention(details.attention, theme), theme),
|
|
84
|
+
];
|
|
85
|
+
|
|
86
|
+
if (details.logMatch) {
|
|
87
|
+
const pattern = quoteFilter(
|
|
88
|
+
formatPatternForDisplay(details.logMatch.pattern),
|
|
89
|
+
details.logMatch.mode,
|
|
90
|
+
);
|
|
91
|
+
parts.push(
|
|
92
|
+
field("pattern", theme.fg("accent", pattern), theme),
|
|
93
|
+
field("stream", theme.fg("muted", `[${details.logMatch.stream}]`), theme),
|
|
94
|
+
);
|
|
95
|
+
} else {
|
|
96
|
+
if (details.status) {
|
|
97
|
+
parts.push(
|
|
98
|
+
field(
|
|
99
|
+
"status",
|
|
100
|
+
theme.fg(statusTone(details.kind), details.status),
|
|
101
|
+
theme,
|
|
102
|
+
),
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (details.exitCode !== undefined && details.exitCode !== null) {
|
|
107
|
+
parts.push(
|
|
108
|
+
field(
|
|
109
|
+
"exit",
|
|
110
|
+
theme.fg(statusTone(details.kind), String(details.exitCode)),
|
|
111
|
+
theme,
|
|
112
|
+
),
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (details.endReason) {
|
|
117
|
+
parts.push(field("reason", theme.fg("muted", details.endReason), theme));
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (details.signal) {
|
|
121
|
+
const number =
|
|
122
|
+
details.signal.number === null
|
|
123
|
+
? ""
|
|
124
|
+
: ` (${details.signal.number}, ${details.signal.description})`;
|
|
125
|
+
parts.push(
|
|
126
|
+
field(
|
|
127
|
+
"signal",
|
|
128
|
+
theme.fg("muted", `${details.signal.name}${number}`),
|
|
129
|
+
theme,
|
|
130
|
+
),
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return parts.join(" ");
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** `label:` in muted, matching `buildField` in tools/utils.ts. */
|
|
139
|
+
function field(label: string, value: string, theme: Theme): string {
|
|
140
|
+
return `${theme.fg("muted", `${label}:`)} ${value}`;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Plain-language attention level, reusing the tone mapping from
|
|
145
|
+
* `attentionTone` in tools/components/watch.ts (turn/context keep their
|
|
146
|
+
* color; ignore switches from muted to dim to read as "quieter").
|
|
147
|
+
*/
|
|
148
|
+
function formatAttention(attention: Attention, theme: Theme): string {
|
|
149
|
+
switch (attention) {
|
|
150
|
+
case "turn":
|
|
151
|
+
return theme.bold(theme.fg("warning", "now"));
|
|
152
|
+
case "context":
|
|
153
|
+
return theme.bold(theme.fg("success", "next turn"));
|
|
154
|
+
default:
|
|
155
|
+
return theme.bold(theme.fg("dim", "silent"));
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Same tone family as `statusColor` in shared/ui.ts: success for a clean
|
|
161
|
+
* exit, error for failure/crash, dim for killed.
|
|
162
|
+
*/
|
|
163
|
+
function statusTone(kind: ProcessNotificationKind): ThemeColor {
|
|
164
|
+
switch (kind) {
|
|
165
|
+
case "success":
|
|
166
|
+
return "success";
|
|
167
|
+
case "failure":
|
|
168
|
+
case "crash":
|
|
169
|
+
return "error";
|
|
170
|
+
case "killed":
|
|
171
|
+
return "dim";
|
|
172
|
+
default:
|
|
173
|
+
return "muted";
|
|
174
|
+
}
|
|
80
175
|
}
|
|
81
176
|
|
|
82
177
|
function summaryVerb(details: ProcessNotificationDetails): string {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { compileLineMatcher } from "../../../src/utils/match-line";
|
|
2
|
+
import { plainTextForDisplay } from "../../shared/display-text";
|
|
2
3
|
import type { NotifyConfig } from "./registry";
|
|
3
4
|
import type { Attention } from "./types";
|
|
4
5
|
|
|
@@ -130,8 +131,12 @@ function splitAppendedIntoLines(
|
|
|
130
131
|
for (const entry of appended) {
|
|
131
132
|
const split = entry.text.split("\n");
|
|
132
133
|
for (const line of split) {
|
|
133
|
-
|
|
134
|
-
|
|
134
|
+
// Match the same per-line visible text the log views render. This keeps
|
|
135
|
+
// CR-overwritten progress and invisible escape bytes from firing
|
|
136
|
+
// watches, without making watches depend on viewport width.
|
|
137
|
+
const displayLine = plainTextForDisplay(line);
|
|
138
|
+
if (displayLine.length === 0) continue;
|
|
139
|
+
lines.push({ type: entry.type, text: displayLine });
|
|
135
140
|
}
|
|
136
141
|
}
|
|
137
142
|
|
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
export { ProcessActionHeader } from "./process-action-header";
|
|
2
2
|
export { ProcessActionTitle } from "./process-action-title";
|
|
3
3
|
export { ToolLayout } from "./tool-layout";
|
|
4
|
-
export {
|
|
4
|
+
export {
|
|
5
|
+
buildMatcherLine,
|
|
6
|
+
formatPatternForDisplay,
|
|
7
|
+
quoteFilter,
|
|
8
|
+
} from "./watch";
|
|
@@ -15,6 +15,8 @@ export function buildMatcherLine(
|
|
|
15
15
|
const attentionTone =
|
|
16
16
|
on === "turn" ? "warning" : on === "context" ? "success" : "muted";
|
|
17
17
|
|
|
18
|
+
const pattern = quoteFilter(formatPatternForDisplay(matcher.pattern), mode);
|
|
19
|
+
|
|
18
20
|
return new Text(
|
|
19
21
|
[
|
|
20
22
|
prefix,
|
|
@@ -22,13 +24,22 @@ export function buildMatcherLine(
|
|
|
22
24
|
" ",
|
|
23
25
|
theme.bold(theme.fg(attentionTone, `${on}${repeat}`)),
|
|
24
26
|
" ",
|
|
25
|
-
theme.fg("accent",
|
|
27
|
+
theme.fg("accent", pattern),
|
|
26
28
|
].join(""),
|
|
27
29
|
0,
|
|
28
30
|
0,
|
|
29
31
|
);
|
|
30
32
|
}
|
|
31
33
|
|
|
34
|
+
export function quoteFilter(
|
|
35
|
+
pattern: string,
|
|
36
|
+
mode: "literal" | "regex",
|
|
37
|
+
): string {
|
|
38
|
+
if (mode === "regex") return `/${pattern}/`;
|
|
39
|
+
if (pattern.includes('"')) return `'${pattern}'`;
|
|
40
|
+
return `"${pattern}"`;
|
|
41
|
+
}
|
|
42
|
+
|
|
32
43
|
export function formatPatternForDisplay(pattern: string): string {
|
|
33
44
|
let formatted = "";
|
|
34
45
|
|
|
@@ -2,7 +2,8 @@ import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
|
2
2
|
import { Container, Spacer, Text } from "@earendil-works/pi-tui";
|
|
3
3
|
|
|
4
4
|
import { sanitizeForDisplay } from "../../../shared/display-text";
|
|
5
|
-
import {
|
|
5
|
+
import { truncateToWidth } from "../../../shared/truncate";
|
|
6
|
+
import { ProcessActionHeader, quoteFilter } from "../components";
|
|
6
7
|
import type { ProcessesParamsType } from "../schema";
|
|
7
8
|
import { buildField } from "../utils";
|
|
8
9
|
import type { OutputDetails } from ".";
|
|
@@ -73,9 +74,23 @@ export function buildCollapsed(
|
|
|
73
74
|
),
|
|
74
75
|
);
|
|
75
76
|
|
|
77
|
+
if (details.pattern) {
|
|
78
|
+
const quoted = quoteFilter(details.pattern, details.mode);
|
|
79
|
+
const value = truncateToWidth(sanitizeForDisplay(quoted), 80);
|
|
80
|
+
container.addChild(
|
|
81
|
+
new Text(
|
|
82
|
+
`${theme.fg("muted", "filter:")} ${theme.fg("accent", value)}`,
|
|
83
|
+
0,
|
|
84
|
+
0,
|
|
85
|
+
),
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
|
|
76
89
|
const bodyLines = extractOutputBody(contentText, details);
|
|
77
90
|
const preview = bodyLines.slice(-2).map(sanitizeForDisplay).join("\n");
|
|
78
91
|
|
|
92
|
+
container.addChild(new Spacer(1));
|
|
93
|
+
|
|
79
94
|
if (preview) {
|
|
80
95
|
container.addChild(new Text(theme.fg("muted", preview), 0, 0));
|
|
81
96
|
} else {
|
|
@@ -119,9 +134,8 @@ function buildOutputMeta(details: OutputDetails, theme: Theme): Container {
|
|
|
119
134
|
container.addChild(buildField("stream", details.stream, theme));
|
|
120
135
|
|
|
121
136
|
if (details.pattern) {
|
|
122
|
-
const modeTag = details.mode === "regex" ? " (regex)" : "";
|
|
123
137
|
container.addChild(
|
|
124
|
-
buildField("filter",
|
|
138
|
+
buildField("filter", quoteFilter(details.pattern, details.mode), theme),
|
|
125
139
|
);
|
|
126
140
|
}
|
|
127
141
|
|
|
@@ -3,9 +3,9 @@ import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
|
3
3
|
import { type Component, visibleWidth } from "@earendil-works/pi-tui";
|
|
4
4
|
import type { ProcessInfo } from "../../../src/types";
|
|
5
5
|
import { LIVE_STATUSES } from "../../../src/types";
|
|
6
|
-
import { renderProcessTab } from "../../process-tabs";
|
|
7
6
|
import { sanitizeForDisplay } from "../../shared/display-text";
|
|
8
7
|
import { displayTextOf, renderLogLine } from "../../shared/log-line";
|
|
8
|
+
import { renderProcessTab } from "../../shared/process-tabs";
|
|
9
9
|
import { truncateToWidth } from "../../shared/truncate";
|
|
10
10
|
import {
|
|
11
11
|
clampNameColumn,
|
|
@@ -1,8 +1,15 @@
|
|
|
1
1
|
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import { visibleWidth } from "@earendil-works/pi-tui";
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
plainTextForDisplay,
|
|
5
|
+
sanitizeForDisplay,
|
|
6
|
+
} from "../../shared/display-text";
|
|
4
7
|
import { trimToBudget } from "../../shared/line-buffer";
|
|
5
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
displayTextOf,
|
|
10
|
+
type LogLineEmphasis,
|
|
11
|
+
renderLogLine,
|
|
12
|
+
} from "../../shared/log-line";
|
|
6
13
|
import { truncateToWidth } from "../../shared/truncate";
|
|
7
14
|
import type { ProcessLogLine } from "../logs-client";
|
|
8
15
|
|
|
@@ -122,8 +129,9 @@ export class LogFileViewer {
|
|
|
122
129
|
* priority (search current match > search match > notify match > stream).
|
|
123
130
|
*/
|
|
124
131
|
addNotifyMatch(match: { line: string }): void {
|
|
125
|
-
// Stored
|
|
126
|
-
|
|
132
|
+
// Stored as plain visible text so invisible escape bytes cannot be what
|
|
133
|
+
// makes a notification marker match.
|
|
134
|
+
const line = plainTextForDisplay(match.line);
|
|
127
135
|
if (line) this.notifyLines.add(line);
|
|
128
136
|
}
|
|
129
137
|
|
|
@@ -192,7 +200,8 @@ export class LogFileViewer {
|
|
|
192
200
|
? "search-current"
|
|
193
201
|
: matchSet.has(visibleIndex)
|
|
194
202
|
? "search"
|
|
195
|
-
: this.notifyLines.has(line.text)
|
|
203
|
+
: this.notifyLines.has(line.text) ||
|
|
204
|
+
this.notifyLines.has(displayTextOf(line))
|
|
196
205
|
? "notify"
|
|
197
206
|
: "none";
|
|
198
207
|
return renderLogLine(line, { theme: this.theme, width, emphasis });
|
|
@@ -17,8 +17,8 @@ import {
|
|
|
17
17
|
import { LIVE_STATUSES, type ProcessInfo } from "../../../src/types";
|
|
18
18
|
import { formatRuntime } from "../../../src/utils/format";
|
|
19
19
|
import { isRecord } from "../../../src/utils/is-record";
|
|
20
|
-
import { renderProcessTab } from "../../process-tabs";
|
|
21
20
|
import { truncateForDisplay } from "../../shared/display-text";
|
|
21
|
+
import { renderProcessTab } from "../../shared/process-tabs";
|
|
22
22
|
import { truncateToWidth } from "../../shared/truncate";
|
|
23
23
|
import { LineComponent, LinesComponent, RuleComponent } from "../../shared/ui";
|
|
24
24
|
import { requestProcessList } from "../client";
|
|
@@ -13,10 +13,17 @@ const ESC = String.fromCodePoint(0x001b);
|
|
|
13
13
|
const BEL = String.fromCodePoint(0x0007);
|
|
14
14
|
const ST = String.fromCodePoint(0x009c);
|
|
15
15
|
const RESET = `${ESC}[0m`;
|
|
16
|
+
const C1_DCS = String.fromCodePoint(0x0090);
|
|
17
|
+
const C1_SOS = String.fromCodePoint(0x0098);
|
|
18
|
+
const C1_OSC = String.fromCodePoint(0x009d);
|
|
19
|
+
const C1_PM = String.fromCodePoint(0x009e);
|
|
20
|
+
const C1_APC = String.fromCodePoint(0x009f);
|
|
21
|
+
const C1_STRING_INTRODUCERS = [C1_DCS, C1_SOS, C1_OSC, C1_PM, C1_APC];
|
|
16
22
|
|
|
17
|
-
// Control characters a single display row must never contain. Tabs
|
|
18
|
-
// separately; newlines are dropped because they
|
|
19
|
-
// C1 controls are included: a raw \u009b is an
|
|
23
|
+
// Control characters a single display row must never contain. Tabs and
|
|
24
|
+
// carriage returns are handled separately; newlines are dropped because they
|
|
25
|
+
// would shift the whole frame. C1 controls are included: a raw \u009b is an
|
|
26
|
+
// alias for CSI on some terminals.
|
|
20
27
|
// biome-ignore lint/suspicious/noControlCharactersInRegex: this regex intentionally targets terminal control characters.
|
|
21
28
|
const DISPLAY_CONTROL_CHARS = /[\u0000-\u0008\u000a-\u001f\u007f-\u009f]/gu;
|
|
22
29
|
|
|
@@ -41,7 +48,9 @@ const TAB_WIDTH = 8;
|
|
|
41
48
|
* into the rest of the frame.
|
|
42
49
|
*/
|
|
43
50
|
export function sanitizeForDisplay(text: string): string {
|
|
44
|
-
if (!text.includes(ESC)
|
|
51
|
+
if (!text.includes(ESC) && !hasC1StringIntroducer(text)) {
|
|
52
|
+
return cleanPlainText(text, 0).text;
|
|
53
|
+
}
|
|
45
54
|
|
|
46
55
|
let out = "";
|
|
47
56
|
let cursor = 0;
|
|
@@ -49,21 +58,34 @@ export function sanitizeForDisplay(text: string): string {
|
|
|
49
58
|
let keptSgr = false;
|
|
50
59
|
|
|
51
60
|
while (cursor < text.length) {
|
|
52
|
-
const
|
|
53
|
-
if (
|
|
54
|
-
|
|
61
|
+
const sequenceAt = findNextSequenceStart(text, cursor);
|
|
62
|
+
if (sequenceAt === -1) {
|
|
63
|
+
const chunk = cleanPlainText(text.slice(cursor), column);
|
|
64
|
+
if (chunk.resetsRow) {
|
|
65
|
+
out = "";
|
|
66
|
+
keptSgr = false;
|
|
67
|
+
}
|
|
68
|
+
out += chunk.text;
|
|
55
69
|
break;
|
|
56
70
|
}
|
|
57
|
-
const chunk = cleanPlainText(text.slice(cursor,
|
|
71
|
+
const chunk = cleanPlainText(text.slice(cursor, sequenceAt), column);
|
|
72
|
+
if (chunk.resetsRow) {
|
|
73
|
+
out = "";
|
|
74
|
+
keptSgr = false;
|
|
75
|
+
}
|
|
58
76
|
out += chunk.text;
|
|
59
77
|
column = chunk.column;
|
|
60
78
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
79
|
+
if (text[sequenceAt] === ESC) {
|
|
80
|
+
const sequence = readEscapeSequence(text, sequenceAt);
|
|
81
|
+
if (sequence.isSgr) {
|
|
82
|
+
out += text.slice(sequenceAt, sequence.end);
|
|
83
|
+
keptSgr = true;
|
|
84
|
+
}
|
|
85
|
+
cursor = sequence.end;
|
|
86
|
+
} else {
|
|
87
|
+
cursor = readC1StringSequence(text, sequenceAt).end;
|
|
65
88
|
}
|
|
66
|
-
cursor = sequence.end;
|
|
67
89
|
}
|
|
68
90
|
|
|
69
91
|
if (!keptSgr || out.endsWith(RESET)) return out;
|
|
@@ -79,6 +101,22 @@ export function truncateForDisplay(text: string, width: number): string {
|
|
|
79
101
|
return closeSgr(truncateToWidth(sanitizeForDisplay(text), width, "…"));
|
|
80
102
|
}
|
|
81
103
|
|
|
104
|
+
/**
|
|
105
|
+
* Return the unstyled text a user can actually read after terminal controls
|
|
106
|
+
* have been interpreted/dropped. Use this for comparisons such as search,
|
|
107
|
+
* notify markers, and log watches so invisible escape bytes cannot match.
|
|
108
|
+
*/
|
|
109
|
+
export function plainTextForDisplay(text: string): string {
|
|
110
|
+
return stripSgr(sanitizeForDisplay(text));
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const SGR = new RegExp(`${ESC}\\[[0-9;:]*m`, "gu");
|
|
114
|
+
|
|
115
|
+
/** Drop the SGR sequences `sanitizeForDisplay` kept for rendering colors. */
|
|
116
|
+
export function stripSgr(text: string): string {
|
|
117
|
+
return text.replace(SGR, "");
|
|
118
|
+
}
|
|
119
|
+
|
|
82
120
|
/**
|
|
83
121
|
* Re-close colors after truncation. `sanitizeForDisplay` ends a colored string
|
|
84
122
|
* with a reset, but truncating can cut that reset off and let the color bleed
|
|
@@ -97,21 +135,42 @@ export function closeSgr(text: string): string {
|
|
|
97
135
|
function cleanPlainText(
|
|
98
136
|
text: string,
|
|
99
137
|
column: number,
|
|
100
|
-
): { text: string; column: number } {
|
|
101
|
-
const
|
|
138
|
+
): { text: string; column: number; resetsRow: boolean } {
|
|
139
|
+
const carriageReturn = text.lastIndexOf("\r");
|
|
140
|
+
const resetsRow = carriageReturn !== -1;
|
|
141
|
+
const visibleText = resetsRow ? text.slice(carriageReturn + 1) : text;
|
|
142
|
+
const startColumn = resetsRow ? 0 : column;
|
|
143
|
+
const clean = visibleText.replace(DISPLAY_CONTROL_CHARS, "");
|
|
102
144
|
if (!clean.includes("\t")) {
|
|
103
|
-
return {
|
|
145
|
+
return {
|
|
146
|
+
text: clean,
|
|
147
|
+
column: startColumn + visibleWidth(clean),
|
|
148
|
+
resetsRow,
|
|
149
|
+
};
|
|
104
150
|
}
|
|
105
151
|
|
|
106
152
|
const parts = clean.split("\t");
|
|
107
153
|
let out = parts[0] ?? "";
|
|
108
|
-
let col =
|
|
154
|
+
let col = startColumn + visibleWidth(out);
|
|
109
155
|
for (const part of parts.slice(1)) {
|
|
110
156
|
const spaces = TAB_WIDTH - (col % TAB_WIDTH);
|
|
111
157
|
out += " ".repeat(spaces) + part;
|
|
112
158
|
col += spaces + visibleWidth(part);
|
|
113
159
|
}
|
|
114
|
-
return { text: out, column: col };
|
|
160
|
+
return { text: out, column: col, resetsRow };
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function findNextSequenceStart(text: string, cursor: number): number {
|
|
164
|
+
let next = text.indexOf(ESC, cursor);
|
|
165
|
+
for (const introducer of C1_STRING_INTRODUCERS) {
|
|
166
|
+
const index = text.indexOf(introducer, cursor);
|
|
167
|
+
if (index !== -1 && (next === -1 || index < next)) next = index;
|
|
168
|
+
}
|
|
169
|
+
return next;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function hasC1StringIntroducer(text: string): boolean {
|
|
173
|
+
return C1_STRING_INTRODUCERS.some((introducer) => text.includes(introducer));
|
|
115
174
|
}
|
|
116
175
|
|
|
117
176
|
/**
|
|
@@ -151,6 +210,12 @@ function readEscapeSequence(
|
|
|
151
210
|
return { end: start + 2, isSgr: false };
|
|
152
211
|
}
|
|
153
212
|
|
|
213
|
+
function readC1StringSequence(text: string, start: number): { end: number } {
|
|
214
|
+
const introducer = text[start];
|
|
215
|
+
const allowBel = introducer === C1_OSC || introducer === C1_APC;
|
|
216
|
+
return { end: findStringTerminator(text, start + 1, allowBel) };
|
|
217
|
+
}
|
|
218
|
+
|
|
154
219
|
/** Index just past the terminator of a string sequence, else end of input. */
|
|
155
220
|
function findStringTerminator(
|
|
156
221
|
text: string,
|
|
@@ -10,7 +10,12 @@
|
|
|
10
10
|
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
11
11
|
import { visibleWidth } from "@earendil-works/pi-tui";
|
|
12
12
|
|
|
13
|
-
import {
|
|
13
|
+
import {
|
|
14
|
+
closeSgr,
|
|
15
|
+
plainTextForDisplay,
|
|
16
|
+
sanitizeForDisplay,
|
|
17
|
+
stripSgr,
|
|
18
|
+
} from "./display-text";
|
|
14
19
|
import { truncateToWidth } from "./truncate";
|
|
15
20
|
|
|
16
21
|
export interface DisplayLogLine {
|
|
@@ -60,7 +65,7 @@ export function renderLogLine(
|
|
|
60
65
|
|
|
61
66
|
/** Text of a log line as the views display it, for match comparisons. */
|
|
62
67
|
export function displayTextOf(line: DisplayLogLine): string {
|
|
63
|
-
return
|
|
68
|
+
return plainTextForDisplay(line.text);
|
|
64
69
|
}
|
|
65
70
|
|
|
66
71
|
function toneLogText(
|
|
@@ -75,10 +80,3 @@ function toneLogText(
|
|
|
75
80
|
if (type === "stderr") return theme.fg("warning", text);
|
|
76
81
|
return text;
|
|
77
82
|
}
|
|
78
|
-
|
|
79
|
-
const SGR = new RegExp(`${String.fromCodePoint(0x001b)}\\[[0-9;:]*m`, "gu");
|
|
80
|
-
|
|
81
|
-
/** Drop the SGR sequences `sanitizeForDisplay` kept. */
|
|
82
|
-
function stripSgr(text: string): string {
|
|
83
|
-
return text.replace(SGR, "");
|
|
84
|
-
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
|
|
3
|
-
import type { ProcessInfo } from "
|
|
4
|
-
import { truncateForDisplay } from "./
|
|
5
|
-
import { MAX_TAB_NAME, statusDot } from "./
|
|
3
|
+
import type { ProcessInfo } from "../../src/types";
|
|
4
|
+
import { truncateForDisplay } from "./display-text";
|
|
5
|
+
import { MAX_TAB_NAME, statusDot } from "./ui";
|
|
6
6
|
|
|
7
7
|
export { MAX_TAB_NAME, statusDot as renderProcessTabDot };
|
|
8
8
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aliou/pi-processes",
|
|
3
|
-
"version": "0.10.
|
|
3
|
+
"version": "0.10.5",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"private": false,
|
|
@@ -23,7 +23,8 @@
|
|
|
23
23
|
"skills": [
|
|
24
24
|
"./skills/pi-processes"
|
|
25
25
|
],
|
|
26
|
-
"
|
|
26
|
+
"image": "https://assets.aliou.me/github/aliou/pi-processes/banner-v0.10.x.png",
|
|
27
|
+
"video": "https://assets.aliou.me/pi-extensions/demos/processes/v0.10.0/agent-starts-processes.mp4"
|
|
27
28
|
},
|
|
28
29
|
"publishConfig": {
|
|
29
30
|
"access": "public"
|
|
@@ -56,9 +57,9 @@
|
|
|
56
57
|
"@aliou/biome-plugins": "^0.8.1",
|
|
57
58
|
"@biomejs/biome": "^2.4.15",
|
|
58
59
|
"@changesets/cli": "^2.27.11",
|
|
59
|
-
"@earendil-works/pi-ai": "0.
|
|
60
|
-
"@earendil-works/pi-coding-agent": "0.
|
|
61
|
-
"@earendil-works/pi-tui": "0.
|
|
60
|
+
"@earendil-works/pi-ai": "0.84.1",
|
|
61
|
+
"@earendil-works/pi-coding-agent": "0.84.1",
|
|
62
|
+
"@earendil-works/pi-tui": "0.84.1",
|
|
62
63
|
"@golevelup/ts-vitest": "^4.0.0",
|
|
63
64
|
"@types/node": "^25.0.10",
|
|
64
65
|
"husky": "^9.1.7",
|
|
@@ -66,20 +67,24 @@
|
|
|
66
67
|
"ts-json-schema-generator": "^2.9.0",
|
|
67
68
|
"typebox": "1.1.38",
|
|
68
69
|
"typescript": "^5.9.3",
|
|
69
|
-
"vitest": "^4.1.5"
|
|
70
|
+
"vitest": "^4.1.5",
|
|
71
|
+
"vitest-evals": "0.15.0"
|
|
70
72
|
},
|
|
71
73
|
"scripts": {
|
|
72
74
|
"typecheck": "tsc --noEmit",
|
|
75
|
+
"typecheck:evals": "tsc --noEmit -p tests/evals/tsconfig.json",
|
|
73
76
|
"lint": "biome check",
|
|
74
77
|
"format": "biome check --write",
|
|
75
78
|
"gen:schema": "ts-json-schema-generator --path extensions/processes/config/types.ts --type ProcessConfig --no-type-check -o schema.json",
|
|
76
79
|
"check:schema": "ts-json-schema-generator --path extensions/processes/config/types.ts --type ProcessConfig --no-type-check -o /tmp/pi-processes-schema-check.json && diff -q schema.json /tmp/pi-processes-schema-check.json",
|
|
80
|
+
"check:changesets": "node scripts/check-changesets.ts",
|
|
77
81
|
"check:lockfile": "pnpm install --frozen-lockfile --ignore-scripts",
|
|
78
82
|
"prepare": "[ -d .git ] && husky || true",
|
|
79
83
|
"changeset": "changeset",
|
|
80
84
|
"version": "changeset version",
|
|
81
85
|
"test": "vitest run",
|
|
82
86
|
"test:e2e": "vitest run --config vitest.e2e.config.ts",
|
|
87
|
+
"test:evals": "vitest run --config tests/evals/vitest.config.ts",
|
|
83
88
|
"release": "pnpm changeset publish"
|
|
84
89
|
},
|
|
85
90
|
"packageManager": "pnpm@11.5.1",
|