@f5xc-salesdemos/xcsh 18.83.1 → 18.83.2
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/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@f5xc-salesdemos/xcsh",
|
|
4
|
-
"version": "18.83.
|
|
4
|
+
"version": "18.83.2",
|
|
5
5
|
"description": "Coding agent CLI with read, bash, edit, write tools and session management",
|
|
6
6
|
"homepage": "https://github.com/f5xc-salesdemos/xcsh",
|
|
7
7
|
"author": "Can Boluk",
|
|
@@ -49,12 +49,12 @@
|
|
|
49
49
|
"dependencies": {
|
|
50
50
|
"@agentclientprotocol/sdk": "0.16.1",
|
|
51
51
|
"@mozilla/readability": "^0.6",
|
|
52
|
-
"@f5xc-salesdemos/xcsh-stats": "18.83.
|
|
53
|
-
"@f5xc-salesdemos/pi-agent-core": "18.83.
|
|
54
|
-
"@f5xc-salesdemos/pi-ai": "18.83.
|
|
55
|
-
"@f5xc-salesdemos/pi-natives": "18.83.
|
|
56
|
-
"@f5xc-salesdemos/pi-tui": "18.83.
|
|
57
|
-
"@f5xc-salesdemos/pi-utils": "18.83.
|
|
52
|
+
"@f5xc-salesdemos/xcsh-stats": "18.83.2",
|
|
53
|
+
"@f5xc-salesdemos/pi-agent-core": "18.83.2",
|
|
54
|
+
"@f5xc-salesdemos/pi-ai": "18.83.2",
|
|
55
|
+
"@f5xc-salesdemos/pi-natives": "18.83.2",
|
|
56
|
+
"@f5xc-salesdemos/pi-tui": "18.83.2",
|
|
57
|
+
"@f5xc-salesdemos/pi-utils": "18.83.2",
|
|
58
58
|
"@sinclair/typebox": "^0.34",
|
|
59
59
|
"@xterm/headless": "^6.0",
|
|
60
60
|
"ajv": "^8.18",
|
|
@@ -17,17 +17,17 @@ export interface BuildInfo {
|
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
export const BUILD_INFO: BuildInfo = {
|
|
20
|
-
"version": "18.83.
|
|
21
|
-
"commit": "
|
|
22
|
-
"shortCommit": "
|
|
20
|
+
"version": "18.83.2",
|
|
21
|
+
"commit": "eacd7c8423fea2f500040cc43d684efdbeb2c8b1",
|
|
22
|
+
"shortCommit": "eacd7c8",
|
|
23
23
|
"branch": "main",
|
|
24
|
-
"tag": "v18.83.
|
|
25
|
-
"commitDate": "2026-05-
|
|
26
|
-
"buildDate": "2026-05-
|
|
24
|
+
"tag": "v18.83.2",
|
|
25
|
+
"commitDate": "2026-05-27T04:27:56Z",
|
|
26
|
+
"buildDate": "2026-05-27T04:46:58.363Z",
|
|
27
27
|
"dirty": true,
|
|
28
28
|
"prNumber": "",
|
|
29
29
|
"repoUrl": "https://github.com/f5xc-salesdemos/xcsh",
|
|
30
30
|
"repoSlug": "f5xc-salesdemos/xcsh",
|
|
31
|
-
"commitUrl": "https://github.com/f5xc-salesdemos/xcsh/commit/
|
|
32
|
-
"releaseUrl": "https://github.com/f5xc-salesdemos/xcsh/releases/tag/v18.83.
|
|
31
|
+
"commitUrl": "https://github.com/f5xc-salesdemos/xcsh/commit/eacd7c8423fea2f500040cc43d684efdbeb2c8b1",
|
|
32
|
+
"releaseUrl": "https://github.com/f5xc-salesdemos/xcsh/releases/tag/v18.83.2"
|
|
33
33
|
};
|
package/src/tools/gh-renderer.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
|
|
1
|
+
/** TUI renderer for gh_run_watch — bordered output with live-updating job trees. */
|
|
2
|
+
import { type Component, padding, Text, visibleWidth } from "@f5xc-salesdemos/pi-tui";
|
|
2
3
|
import type { RenderResultOptions } from "../extensibility/custom-tools/types";
|
|
3
4
|
import type { Theme, ThemeColor } from "../modes/theme/theme";
|
|
4
|
-
import { renderStatusLine } from "../tui";
|
|
5
|
+
import { CachedOutputBlock, F5_TOOL_BORDER_COLOR, renderStatusLine } from "../tui";
|
|
5
6
|
import type {
|
|
6
7
|
GhRunWatchFailedLogDetails,
|
|
7
8
|
GhRunWatchJobDetails,
|
|
@@ -10,8 +11,9 @@ import type {
|
|
|
10
11
|
GhToolDetails,
|
|
11
12
|
} from "./gh";
|
|
12
13
|
import {
|
|
14
|
+
addSection,
|
|
15
|
+
formatErrorMessage,
|
|
13
16
|
formatExpandHint,
|
|
14
|
-
formatStatusIcon,
|
|
15
17
|
PREVIEW_LIMITS,
|
|
16
18
|
replaceTabs,
|
|
17
19
|
type ToolUIColor,
|
|
@@ -23,6 +25,8 @@ type GhRunWatchRenderArgs = {
|
|
|
23
25
|
branch?: string;
|
|
24
26
|
};
|
|
25
27
|
|
|
28
|
+
const TOOL_TITLE = "GitHub Run Watch";
|
|
29
|
+
|
|
26
30
|
const SUCCESS_CONCLUSIONS = new Set(["success", "neutral", "skipped"]);
|
|
27
31
|
const FAILURE_CONCLUSIONS = new Set(["failure", "timed_out", "cancelled", "action_required", "startup_failure"]);
|
|
28
32
|
const RUNNING_STATUSES = new Set(["in_progress"]);
|
|
@@ -30,95 +34,31 @@ const PENDING_STATUSES = new Set(["queued", "requested", "waiting", "pending"]);
|
|
|
30
34
|
const FALLBACK_WIDTH = 80;
|
|
31
35
|
|
|
32
36
|
function formatShortSha(value: string | undefined): string | undefined {
|
|
33
|
-
if (!value)
|
|
34
|
-
return undefined;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
+
if (!value) return undefined;
|
|
37
38
|
return value.slice(0, 12);
|
|
38
39
|
}
|
|
39
40
|
|
|
40
|
-
function getWatchHeader(watch: GhRunWatchViewDetails): string {
|
|
41
|
-
if (watch.mode === "run" && watch.run) {
|
|
42
|
-
if (watch.state === "watching") {
|
|
43
|
-
return `watching run #${watch.run.id} on ${watch.repo}`;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
return `run #${watch.run.id} on ${watch.repo}`;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
const shortSha = formatShortSha(watch.headSha) ?? "this commit";
|
|
50
|
-
if (watch.state === "watching") {
|
|
51
|
-
return `watching ${shortSha} on ${watch.repo}`;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
return `workflow runs for ${shortSha} on ${watch.repo}`;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
41
|
function getRunLabel(run: GhRunWatchRunDetails): string {
|
|
58
42
|
return replaceTabs(run.workflowName ?? run.displayTitle ?? "GitHub Actions");
|
|
59
43
|
}
|
|
60
44
|
|
|
61
|
-
function getRunMeta(run: GhRunWatchRunDetails): string[] {
|
|
62
|
-
const parts: string[] = [];
|
|
63
|
-
if (run.branch) {
|
|
64
|
-
parts.push(replaceTabs(run.branch));
|
|
65
|
-
} else if (run.headSha) {
|
|
66
|
-
parts.push(formatShortSha(run.headSha) ?? run.headSha);
|
|
67
|
-
}
|
|
68
|
-
parts.push(`#${run.id}`);
|
|
69
|
-
return parts;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
function formatRunLine(run: GhRunWatchRunDetails, theme: Theme): string {
|
|
73
|
-
const title = theme.fg("contentAccent", getRunLabel(run));
|
|
74
|
-
const metaParts = getRunMeta(run);
|
|
75
|
-
const meta = metaParts.map((part, index) =>
|
|
76
|
-
index === metaParts.length - 1 ? theme.fg("muted", part) : theme.fg("text", part),
|
|
77
|
-
);
|
|
78
|
-
return [title, ...meta].join(" ");
|
|
79
|
-
}
|
|
80
|
-
|
|
81
45
|
function getJobStateVisual(
|
|
82
46
|
job: GhRunWatchJobDetails,
|
|
83
47
|
theme: Theme,
|
|
84
48
|
): { iconRaw: string; iconColor: ToolUIColor; textColor: ThemeColor } {
|
|
85
49
|
if (job.conclusion && SUCCESS_CONCLUSIONS.has(job.conclusion)) {
|
|
86
|
-
return {
|
|
87
|
-
iconRaw: theme.status.success,
|
|
88
|
-
iconColor: "success",
|
|
89
|
-
textColor: "success",
|
|
90
|
-
};
|
|
50
|
+
return { iconRaw: theme.status.success, iconColor: "success", textColor: "success" };
|
|
91
51
|
}
|
|
92
|
-
|
|
93
52
|
if (job.conclusion && FAILURE_CONCLUSIONS.has(job.conclusion)) {
|
|
94
|
-
return {
|
|
95
|
-
iconRaw: theme.status.error,
|
|
96
|
-
iconColor: "error",
|
|
97
|
-
textColor: "error",
|
|
98
|
-
};
|
|
53
|
+
return { iconRaw: theme.status.error, iconColor: "error", textColor: "error" };
|
|
99
54
|
}
|
|
100
|
-
|
|
101
55
|
if (job.status && RUNNING_STATUSES.has(job.status)) {
|
|
102
|
-
return {
|
|
103
|
-
iconRaw: theme.status.enabled,
|
|
104
|
-
iconColor: "warning",
|
|
105
|
-
textColor: "warning",
|
|
106
|
-
};
|
|
56
|
+
return { iconRaw: theme.status.enabled, iconColor: "warning", textColor: "warning" };
|
|
107
57
|
}
|
|
108
|
-
|
|
109
58
|
if (job.status && PENDING_STATUSES.has(job.status)) {
|
|
110
|
-
return {
|
|
111
|
-
iconRaw: theme.status.shadowed,
|
|
112
|
-
iconColor: "muted",
|
|
113
|
-
textColor: "muted",
|
|
114
|
-
};
|
|
59
|
+
return { iconRaw: theme.status.shadowed, iconColor: "muted", textColor: "muted" };
|
|
115
60
|
}
|
|
116
|
-
|
|
117
|
-
return {
|
|
118
|
-
iconRaw: theme.status.shadowed,
|
|
119
|
-
iconColor: "muted",
|
|
120
|
-
textColor: "muted",
|
|
121
|
-
};
|
|
61
|
+
return { iconRaw: theme.status.shadowed, iconColor: "muted", textColor: "muted" };
|
|
122
62
|
}
|
|
123
63
|
|
|
124
64
|
function renderJobLine(job: GhRunWatchJobDetails, width: number, theme: Theme): string {
|
|
@@ -137,30 +77,27 @@ function renderJobLine(job: GhRunWatchJobDetails, width: number, theme: Theme):
|
|
|
137
77
|
return line;
|
|
138
78
|
}
|
|
139
79
|
|
|
140
|
-
function
|
|
141
|
-
const
|
|
142
|
-
if (run.
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
80
|
+
function buildRunSectionLabel(run: GhRunWatchRunDetails, theme: Theme): string {
|
|
81
|
+
const parts = [getRunLabel(run)];
|
|
82
|
+
if (run.branch) parts.push(theme.fg("muted", run.branch));
|
|
83
|
+
parts.push(theme.fg("dim", `#${run.id}`));
|
|
84
|
+
return parts.join(" ");
|
|
85
|
+
}
|
|
146
86
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
return lines;
|
|
87
|
+
function buildRunJobLines(run: GhRunWatchRunDetails, width: number, theme: Theme): string[] {
|
|
88
|
+
if (run.jobs.length === 0) return [theme.fg("dim", " waiting for workflow jobs...")];
|
|
89
|
+
return run.jobs.map(job => renderJobLine(job, width, theme));
|
|
151
90
|
}
|
|
152
91
|
|
|
153
|
-
function
|
|
92
|
+
function buildFailedLogLines(
|
|
154
93
|
failedLogs: GhRunWatchFailedLogDetails[],
|
|
155
94
|
width: number,
|
|
156
95
|
theme: Theme,
|
|
157
96
|
expanded: boolean,
|
|
158
97
|
): string[] {
|
|
159
|
-
if (failedLogs.length === 0)
|
|
160
|
-
return [];
|
|
161
|
-
}
|
|
98
|
+
if (failedLogs.length === 0) return [];
|
|
162
99
|
|
|
163
|
-
const lines = [
|
|
100
|
+
const lines: string[] = [];
|
|
164
101
|
for (const entry of failedLogs) {
|
|
165
102
|
const context = entry.workflowName ? `${entry.workflowName} #${entry.runId}` : `run #${entry.runId}`;
|
|
166
103
|
lines.push(
|
|
@@ -185,99 +122,38 @@ function renderFailedLogs(
|
|
|
185
122
|
lines.push(theme.fg("dim", ` … ${remaining} more log lines ${formatExpandHint(theme, false, true)}`));
|
|
186
123
|
}
|
|
187
124
|
}
|
|
188
|
-
|
|
189
125
|
return lines;
|
|
190
126
|
}
|
|
191
127
|
|
|
192
|
-
function
|
|
193
|
-
watch
|
|
194
|
-
theme: Theme,
|
|
195
|
-
options: RenderResultOptions,
|
|
196
|
-
width: number,
|
|
197
|
-
): string[] {
|
|
198
|
-
const lines = [theme.fg("muted", getWatchHeader(watch))];
|
|
199
|
-
|
|
200
|
-
if (watch.note) {
|
|
201
|
-
lines.push(theme.fg("dim", replaceTabs(watch.note)));
|
|
202
|
-
}
|
|
128
|
+
function deriveWatchState(watch: GhRunWatchViewDetails): "pending" | "success" | "error" {
|
|
129
|
+
if (watch.state === "watching") return "pending";
|
|
203
130
|
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
const runs = watch.runs ?? [];
|
|
208
|
-
if (runs.length === 0) {
|
|
209
|
-
lines.push(theme.fg("dim", "waiting for workflow runs..."));
|
|
210
|
-
} else {
|
|
211
|
-
runs.forEach((run, index) => {
|
|
212
|
-
if (index > 0) {
|
|
213
|
-
lines.push("");
|
|
214
|
-
}
|
|
215
|
-
lines.push(...renderRunBlock(run, width, theme));
|
|
216
|
-
});
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
lines.push(...renderFailedLogs(watch.failedLogs ?? [], width, theme, options.expanded));
|
|
221
|
-
return lines;
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
function renderFallbackText(
|
|
225
|
-
result: { content: Array<{ type: string; text?: string }>; isError?: boolean },
|
|
226
|
-
theme: Theme,
|
|
227
|
-
): Component {
|
|
228
|
-
const text = result.content
|
|
229
|
-
.filter(part => part.type === "text")
|
|
230
|
-
.map(part => part.text)
|
|
231
|
-
.filter((value): value is string => typeof value === "string" && value.length > 0)
|
|
232
|
-
.join("\n");
|
|
233
|
-
if (text) {
|
|
234
|
-
return new Text(replaceTabs(text), 0, 0);
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
const header = renderStatusLine(
|
|
238
|
-
{
|
|
239
|
-
title: "GitHub Run Watch",
|
|
240
|
-
description: result.isError ? "failed" : "no output",
|
|
241
|
-
},
|
|
242
|
-
theme,
|
|
131
|
+
const allRuns = watch.mode === "run" && watch.run ? [watch.run] : (watch.runs ?? []);
|
|
132
|
+
const hasFailure = allRuns.some(run =>
|
|
133
|
+
run.jobs.some(job => job.conclusion && FAILURE_CONCLUSIONS.has(job.conclusion)),
|
|
243
134
|
);
|
|
244
|
-
|
|
135
|
+
if (hasFailure) return "error";
|
|
136
|
+
|
|
137
|
+
const allDone = allRuns.every(run => run.jobs.length > 0 && run.jobs.every(job => job.conclusion));
|
|
138
|
+
return allDone ? "success" : "pending";
|
|
245
139
|
}
|
|
246
140
|
|
|
247
141
|
export const ghRunWatchToolRenderer = {
|
|
248
|
-
renderCall(args: GhRunWatchRenderArgs,
|
|
249
|
-
const lines: string[] = [];
|
|
250
|
-
|
|
251
|
-
// Header with spinner: "⠋ GitHub Run Watch"
|
|
252
|
-
const icon =
|
|
253
|
-
options.spinnerFrame !== undefined
|
|
254
|
-
? formatStatusIcon("running", uiTheme, options.spinnerFrame)
|
|
255
|
-
: formatStatusIcon("pending", uiTheme);
|
|
256
|
-
|
|
257
|
-
// Build a target description that mirrors the result view style
|
|
142
|
+
renderCall(args: GhRunWatchRenderArgs, _options: RenderResultOptions, uiTheme: Theme): Component {
|
|
258
143
|
const runId = typeof args.run === "string" && args.run.trim().length > 0 ? args.run.trim() : undefined;
|
|
259
144
|
const branch = typeof args.branch === "string" && args.branch.trim().length > 0 ? args.branch.trim() : undefined;
|
|
260
145
|
|
|
146
|
+
let description: string;
|
|
261
147
|
if (runId) {
|
|
262
|
-
|
|
263
|
-
const title = uiTheme.fg("contentAccent", "GitHub Run Watch");
|
|
264
|
-
const meta = uiTheme.fg("muted", `#${runId}`);
|
|
265
|
-
lines.push(`${icon} ${title} ${meta}`);
|
|
148
|
+
description = uiTheme.fg("muted", `run #${runId}`);
|
|
266
149
|
} else if (branch) {
|
|
267
|
-
|
|
268
|
-
const title = uiTheme.fg("contentAccent", "GitHub Run Watch");
|
|
269
|
-
const meta = uiTheme.fg("text", branch);
|
|
270
|
-
lines.push(`${icon} ${title} ${meta}`);
|
|
150
|
+
description = uiTheme.fg("muted", branch);
|
|
271
151
|
} else {
|
|
272
|
-
|
|
273
|
-
const title = uiTheme.fg("contentAccent", "GitHub Run Watch");
|
|
274
|
-
const meta = uiTheme.fg("muted", "current HEAD");
|
|
275
|
-
lines.push(`${icon} ${title} ${meta}`);
|
|
152
|
+
description = uiTheme.fg("muted", "current HEAD");
|
|
276
153
|
}
|
|
277
154
|
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
return new Text(lines.join("\n"), 0, 0);
|
|
155
|
+
const text = renderStatusLine({ icon: "pending", title: TOOL_TITLE, description }, uiTheme);
|
|
156
|
+
return new Text(text, 0, 0);
|
|
281
157
|
},
|
|
282
158
|
|
|
283
159
|
renderResult(
|
|
@@ -286,16 +162,102 @@ export const ghRunWatchToolRenderer = {
|
|
|
286
162
|
uiTheme: Theme,
|
|
287
163
|
): Component {
|
|
288
164
|
const watch = result.details?.watch;
|
|
165
|
+
const isError = result.isError === true;
|
|
166
|
+
|
|
167
|
+
if (!watch && isError) {
|
|
168
|
+
const errorText = result.content?.find(c => c.type === "text")?.text;
|
|
169
|
+
return new Text(formatErrorMessage(errorText, uiTheme), 0, 0);
|
|
170
|
+
}
|
|
171
|
+
|
|
289
172
|
if (!watch) {
|
|
290
|
-
|
|
173
|
+
const text = result.content
|
|
174
|
+
.filter(part => part.type === "text")
|
|
175
|
+
.map(part => part.text)
|
|
176
|
+
.filter((value): value is string => typeof value === "string" && value.length > 0)
|
|
177
|
+
.join("\n");
|
|
178
|
+
if (text) return new Text(replaceTabs(text), 0, 0);
|
|
179
|
+
|
|
180
|
+
const header = renderStatusLine({ title: TOOL_TITLE, description: "no output" }, uiTheme);
|
|
181
|
+
return new Text(header, 0, 0);
|
|
291
182
|
}
|
|
292
183
|
|
|
184
|
+
const outputBlock = new CachedOutputBlock();
|
|
185
|
+
|
|
293
186
|
return {
|
|
294
187
|
render(width: number): string[] {
|
|
295
188
|
const lineWidth = Math.max(24, width || FALLBACK_WIDTH);
|
|
296
|
-
|
|
189
|
+
const sections: Array<{ label?: string; lines: string[] }> = [];
|
|
190
|
+
const meta: string[] = [];
|
|
191
|
+
|
|
192
|
+
if (watch.note) {
|
|
193
|
+
meta.push(uiTheme.fg("dim", replaceTabs(watch.note)));
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Build run sections
|
|
197
|
+
if (watch.mode === "run" && watch.run) {
|
|
198
|
+
const sectionLabel = buildRunSectionLabel(watch.run, uiTheme);
|
|
199
|
+
addSection(sections, sectionLabel, buildRunJobLines(watch.run, lineWidth - 4, uiTheme), uiTheme);
|
|
200
|
+
} else if (watch.mode === "commit") {
|
|
201
|
+
const runs = watch.runs ?? [];
|
|
202
|
+
if (runs.length === 0) {
|
|
203
|
+
addSection(sections, "Workflows", [uiTheme.fg("dim", " waiting for workflow runs...")], uiTheme);
|
|
204
|
+
} else {
|
|
205
|
+
for (const run of runs) {
|
|
206
|
+
addSection(
|
|
207
|
+
sections,
|
|
208
|
+
buildRunSectionLabel(run, uiTheme),
|
|
209
|
+
buildRunJobLines(run, lineWidth - 4, uiTheme),
|
|
210
|
+
uiTheme,
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Failed logs section
|
|
217
|
+
const failedLogLines = buildFailedLogLines(
|
|
218
|
+
watch.failedLogs ?? [],
|
|
219
|
+
lineWidth - 4,
|
|
220
|
+
uiTheme,
|
|
221
|
+
options.expanded,
|
|
222
|
+
);
|
|
223
|
+
if (failedLogLines.length > 0) {
|
|
224
|
+
addSection(sections, "Failed Logs", failedLogLines, uiTheme);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Build header
|
|
228
|
+
let description: string;
|
|
229
|
+
if (watch.mode === "run" && watch.run) {
|
|
230
|
+
description =
|
|
231
|
+
watch.state === "watching"
|
|
232
|
+
? `watching run #${watch.run.id} on ${watch.repo}`
|
|
233
|
+
: `run #${watch.run.id} on ${watch.repo}`;
|
|
234
|
+
} else {
|
|
235
|
+
const shortSha = formatShortSha(watch.headSha) ?? "this commit";
|
|
236
|
+
description =
|
|
237
|
+
watch.state === "watching"
|
|
238
|
+
? `watching ${shortSha} on ${watch.repo}`
|
|
239
|
+
: `runs for ${shortSha} on ${watch.repo}`;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const header = renderStatusLine(
|
|
243
|
+
{
|
|
244
|
+
title: TOOL_TITLE,
|
|
245
|
+
titleColor: "contentAccent",
|
|
246
|
+
description,
|
|
247
|
+
meta: meta.length > 0 ? meta : undefined,
|
|
248
|
+
},
|
|
249
|
+
uiTheme,
|
|
250
|
+
);
|
|
251
|
+
|
|
252
|
+
const state = options.isPartial ? "pending" : deriveWatchState(watch);
|
|
253
|
+
return outputBlock.render(
|
|
254
|
+
{ header, state, sections, width: lineWidth, borderColor: F5_TOOL_BORDER_COLOR },
|
|
255
|
+
uiTheme,
|
|
256
|
+
);
|
|
257
|
+
},
|
|
258
|
+
invalidate() {
|
|
259
|
+
outputBlock.invalidate();
|
|
297
260
|
},
|
|
298
|
-
invalidate() {},
|
|
299
261
|
};
|
|
300
262
|
},
|
|
301
263
|
|
|
@@ -1,8 +1,13 @@
|
|
|
1
|
+
/** TUI renderer for inspect_image — bordered output with themed sections. */
|
|
1
2
|
import type { Component } from "@f5xc-salesdemos/pi-tui";
|
|
3
|
+
import { Text } from "@f5xc-salesdemos/pi-tui";
|
|
2
4
|
import type { RenderResultOptions } from "../extensibility/custom-tools/types";
|
|
3
5
|
import type { Theme } from "../modes/theme/theme";
|
|
4
|
-
import { renderStatusLine } from "../tui";
|
|
5
|
-
import {
|
|
6
|
+
import { CachedOutputBlock, F5_TOOL_BORDER_COLOR, renderStatusLine } from "../tui";
|
|
7
|
+
import { addSection, formatErrorMessage, replaceTabs, shortenPath } from "./render-utils";
|
|
8
|
+
|
|
9
|
+
const TOOL_TITLE = "Inspect Image";
|
|
10
|
+
const MAX_OUTPUT_LINES = 30;
|
|
6
11
|
|
|
7
12
|
interface InspectImageRenderArgs {
|
|
8
13
|
path?: string;
|
|
@@ -21,28 +26,13 @@ interface InspectImageRendererResult {
|
|
|
21
26
|
isError?: boolean;
|
|
22
27
|
}
|
|
23
28
|
|
|
24
|
-
const INSPECT_OUTPUT_COLLAPSED_LINES = 4;
|
|
25
|
-
const INSPECT_OUTPUT_EXPANDED_LINES = 16;
|
|
26
|
-
|
|
27
29
|
export const inspectImageToolRenderer = {
|
|
28
30
|
renderCall(args: InspectImageRenderArgs, _options: RenderResultOptions, uiTheme: Theme): Component {
|
|
29
31
|
const rawPath = args.path ?? "";
|
|
30
32
|
const pathDisplay = rawPath ? shortenPath(rawPath) : "…";
|
|
31
|
-
const
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
const header = truncateToWidth(
|
|
35
|
-
renderStatusLine({ icon: "pending", title: "Inspect Image", description: pathDisplay }, uiTheme),
|
|
36
|
-
width,
|
|
37
|
-
);
|
|
38
|
-
if (!question) {
|
|
39
|
-
return [header];
|
|
40
|
-
}
|
|
41
|
-
const questionLine = ` ${uiTheme.fg("dim", uiTheme.tree.last)} ${uiTheme.fg("dim", "Question:")} ${uiTheme.fg("contentAccent", truncateToWidth(replaceTabs(question), Math.max(20, width - 25)))}`;
|
|
42
|
-
return [header, questionLine];
|
|
43
|
-
},
|
|
44
|
-
invalidate() {},
|
|
45
|
-
};
|
|
33
|
+
const description = uiTheme.fg("muted", pathDisplay);
|
|
34
|
+
const text = renderStatusLine({ icon: "pending", title: TOOL_TITLE, description }, uiTheme);
|
|
35
|
+
return new Text(text, 0, 0);
|
|
46
36
|
},
|
|
47
37
|
|
|
48
38
|
renderResult(
|
|
@@ -52,61 +42,60 @@ export const inspectImageToolRenderer = {
|
|
|
52
42
|
args?: InspectImageRenderArgs,
|
|
53
43
|
): Component {
|
|
54
44
|
const details = result.details;
|
|
45
|
+
const isError = result.isError === true;
|
|
46
|
+
|
|
47
|
+
if (isError && !details) {
|
|
48
|
+
const errorText = result.content?.find(c => c.type === "text")?.text;
|
|
49
|
+
return new Text(formatErrorMessage(errorText, uiTheme), 0, 0);
|
|
50
|
+
}
|
|
51
|
+
|
|
55
52
|
const rawPath = details?.imagePath ?? args?.path ?? "";
|
|
56
53
|
const pathDisplay = rawPath ? shortenPath(rawPath) : "image";
|
|
57
|
-
const
|
|
58
|
-
|
|
59
|
-
|
|
54
|
+
const sections: Array<{ label?: string; lines: string[] }> = [];
|
|
55
|
+
const meta: string[] = [];
|
|
56
|
+
|
|
57
|
+
if (details?.model) meta.push(uiTheme.fg("dim", details.model));
|
|
58
|
+
if (details?.mimeType) meta.push(uiTheme.fg("dim", details.mimeType));
|
|
59
|
+
|
|
60
|
+
if (isError) {
|
|
61
|
+
const errorText = result.content?.find(c => c.type === "text")?.text ?? "Unknown error";
|
|
62
|
+
addSection(sections, "Error", [uiTheme.fg("error", errorText)], uiTheme);
|
|
63
|
+
} else {
|
|
64
|
+
if (args?.question) {
|
|
65
|
+
addSection(sections, "Question", [uiTheme.fg("chromeAccent", ` ${args.question}`)], uiTheme);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const outputText = result.content.find(c => c.type === "text")?.text?.trimEnd() ?? "";
|
|
69
|
+
if (outputText) {
|
|
70
|
+
const outputLines = outputText.split("\n").map(line => replaceTabs(uiTheme.fg("toolOutput", line)));
|
|
71
|
+
addSection(sections, "Analysis", outputLines, uiTheme, MAX_OUTPUT_LINES);
|
|
72
|
+
} else {
|
|
73
|
+
addSection(sections, "Analysis", [uiTheme.fg("dim", " (no output)")], uiTheme);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
60
77
|
const header = renderStatusLine(
|
|
61
78
|
{
|
|
62
|
-
title:
|
|
79
|
+
title: TOOL_TITLE,
|
|
80
|
+
titleColor: "contentAccent",
|
|
63
81
|
description: pathDisplay,
|
|
82
|
+
meta: meta.length > 0 ? meta : undefined,
|
|
64
83
|
},
|
|
65
84
|
uiTheme,
|
|
66
85
|
);
|
|
67
|
-
const question = args?.question?.trim();
|
|
68
|
-
const outputText = result.content.find(content => content.type === "text")?.text?.trimEnd() ?? "";
|
|
69
86
|
|
|
87
|
+
const outputBlock = new CachedOutputBlock();
|
|
70
88
|
return {
|
|
71
89
|
render(width: number): string[] {
|
|
72
|
-
const
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
if (!outputText) {
|
|
80
|
-
lines.push(uiTheme.fg("dim", "(no output)"));
|
|
81
|
-
if (metaParts.length > 0) {
|
|
82
|
-
lines.push("");
|
|
83
|
-
lines.push(uiTheme.fg("dim", metaParts.join(" · ")));
|
|
84
|
-
}
|
|
85
|
-
return lines;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
lines.push("");
|
|
89
|
-
const outputLines = replaceTabs(outputText).split("\n");
|
|
90
|
-
const maxLines = options.expanded ? INSPECT_OUTPUT_EXPANDED_LINES : INSPECT_OUTPUT_COLLAPSED_LINES;
|
|
91
|
-
for (const line of outputLines.slice(0, maxLines)) {
|
|
92
|
-
lines.push(uiTheme.fg("toolOutput", truncateToWidth(line, width)));
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
if (outputLines.length > maxLines) {
|
|
96
|
-
const remaining = outputLines.length - maxLines;
|
|
97
|
-
const hint = formatExpandHint(uiTheme, options.expanded, true);
|
|
98
|
-
lines.push(`${uiTheme.fg("dim", `… ${remaining} more lines`)}${hint ? ` ${hint}` : ""}`);
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
if (metaParts.length > 0) {
|
|
102
|
-
lines.push("");
|
|
103
|
-
lines.push(uiTheme.fg("dim", metaParts.join(" · ")));
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
return lines;
|
|
90
|
+
const state = options.isPartial ? "pending" : isError ? "error" : "success";
|
|
91
|
+
return outputBlock.render({ header, state, sections, width, borderColor: F5_TOOL_BORDER_COLOR }, uiTheme);
|
|
92
|
+
},
|
|
93
|
+
invalidate() {
|
|
94
|
+
outputBlock.invalidate();
|
|
107
95
|
},
|
|
108
|
-
invalidate() {},
|
|
109
96
|
};
|
|
110
97
|
},
|
|
98
|
+
|
|
111
99
|
mergeCallAndResult: true,
|
|
100
|
+
inline: true,
|
|
112
101
|
};
|