@f5xc-salesdemos/xcsh 18.83.1 → 18.84.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/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@f5xc-salesdemos/xcsh",
|
|
4
|
-
"version": "18.
|
|
4
|
+
"version": "18.84.0",
|
|
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.
|
|
53
|
-
"@f5xc-salesdemos/pi-agent-core": "18.
|
|
54
|
-
"@f5xc-salesdemos/pi-ai": "18.
|
|
55
|
-
"@f5xc-salesdemos/pi-natives": "18.
|
|
56
|
-
"@f5xc-salesdemos/pi-tui": "18.
|
|
57
|
-
"@f5xc-salesdemos/pi-utils": "18.
|
|
52
|
+
"@f5xc-salesdemos/xcsh-stats": "18.84.0",
|
|
53
|
+
"@f5xc-salesdemos/pi-agent-core": "18.84.0",
|
|
54
|
+
"@f5xc-salesdemos/pi-ai": "18.84.0",
|
|
55
|
+
"@f5xc-salesdemos/pi-natives": "18.84.0",
|
|
56
|
+
"@f5xc-salesdemos/pi-tui": "18.84.0",
|
|
57
|
+
"@f5xc-salesdemos/pi-utils": "18.84.0",
|
|
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.
|
|
21
|
-
"commit": "
|
|
22
|
-
"shortCommit": "
|
|
20
|
+
"version": "18.84.0",
|
|
21
|
+
"commit": "adc9c7ecdfd945419187e4bea5c9b442b53ad257",
|
|
22
|
+
"shortCommit": "adc9c7e",
|
|
23
23
|
"branch": "main",
|
|
24
|
-
"tag": "v18.
|
|
25
|
-
"commitDate": "2026-05-
|
|
26
|
-
"buildDate": "2026-05-
|
|
24
|
+
"tag": "v18.84.0",
|
|
25
|
+
"commitDate": "2026-05-27T11:47:14Z",
|
|
26
|
+
"buildDate": "2026-05-27T12:19:38.302Z",
|
|
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.
|
|
31
|
+
"commitUrl": "https://github.com/f5xc-salesdemos/xcsh/commit/adc9c7ecdfd945419187e4bea5c9b442b53ad257",
|
|
32
|
+
"releaseUrl": "https://github.com/f5xc-salesdemos/xcsh/releases/tag/v18.84.0"
|
|
33
33
|
};
|
|
@@ -272,6 +272,7 @@ Most tools resolve custom protocol URLs to internal resources (not web URLs):
|
|
|
272
272
|
you **MUST** call `xcsh_api` with `method: "GET"`, `paths: ["*"]`.
|
|
273
273
|
The `*` wildcard auto-discovers all namespace resource types and batches them in one call.
|
|
274
274
|
Do **NOT** enumerate resource types individually — that is **PROHIBITED**.
|
|
275
|
+
When reporting batch inventory results, name each resource found rather than giving only counts.
|
|
275
276
|
|
|
276
277
|
If the resource name is unknown, search first:
|
|
277
278
|
`xcsh://api-catalog/?search={term}` → find the matching category, then read it.
|
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
|
};
|
package/src/tools/xcsh-api.ts
CHANGED
|
@@ -535,6 +535,7 @@ export class XcshApiTool implements AgentTool<typeof xcshApiSchema, XcshApiToolD
|
|
|
535
535
|
const rType = parts.at(-2) ?? "";
|
|
536
536
|
const labels: string[] = [];
|
|
537
537
|
if (/tcp_loadbalancer/i.test(rType)) {
|
|
538
|
+
labels.push("TCP");
|
|
538
539
|
const pools = extractPoolRefs(spec.origin_pools_weights);
|
|
539
540
|
if (pools.length > 0) labels.push(`pools=[${pools.join(",")}]`);
|
|
540
541
|
if (typeof spec.listen_port === "number") labels.push(`port=${spec.listen_port}`);
|
|
@@ -812,6 +813,28 @@ export class XcshApiTool implements AgentTool<typeof xcshApiSchema, XcshApiToolD
|
|
|
812
813
|
}
|
|
813
814
|
// Append stop signal to prevent unnecessary verification GETs
|
|
814
815
|
const verb = params.method === "DELETE" ? "Deleted" : params.method === "POST" ? "Created" : "Updated";
|
|
816
|
+
// Human-readable resource label: "load balancer ar-lb-01" instead of raw API path.
|
|
817
|
+
// Label changes help the model echo type names in its response (Finding 13).
|
|
818
|
+
// INTERACTION EFFECT: this change + TCP protocol label are synergistic (Finding 31).
|
|
819
|
+
const pathSegs = resolvedPath.split("/").filter(Boolean);
|
|
820
|
+
const humanizeResourceType = (raw: string): string =>
|
|
821
|
+
raw
|
|
822
|
+
.replace(/^http_/, "")
|
|
823
|
+
.replace(/^tcp_/, "TCP ")
|
|
824
|
+
.replace(/_/g, " ")
|
|
825
|
+
.replace(/([a-z])(balancer|checker)/gi, "$1 $2")
|
|
826
|
+
.replace(/s$/, "");
|
|
827
|
+
let resourceLabel: string;
|
|
828
|
+
if (params.method === "POST") {
|
|
829
|
+
const rawType = pathSegs.at(-1) ?? "";
|
|
830
|
+
const meta = parsedBody?.metadata as Record<string, unknown> | undefined;
|
|
831
|
+
const name = typeof meta?.name === "string" ? meta.name : null;
|
|
832
|
+
resourceLabel = name ? `${humanizeResourceType(rawType)} ${name}` : resolvedPath;
|
|
833
|
+
} else {
|
|
834
|
+
const name = pathSegs.at(-1) ?? "";
|
|
835
|
+
const rawType = pathSegs.at(-2) ?? "";
|
|
836
|
+
resourceLabel = name && rawType ? `${humanizeResourceType(rawType)} ${name}` : resolvedPath;
|
|
837
|
+
}
|
|
815
838
|
// POST returns the full resource; PUT/DELETE return {}.
|
|
816
839
|
// Only claim response contains the resource for POST to avoid misleading the model.
|
|
817
840
|
const resourceHint =
|
|
@@ -823,7 +846,7 @@ export class XcshApiTool implements AgentTool<typeof xcshApiSchema, XcshApiToolD
|
|
|
823
846
|
content: [
|
|
824
847
|
{
|
|
825
848
|
type: "text",
|
|
826
|
-
text: `${statusLine}\n\n${bodyText}\n\n${verb} ${
|
|
849
|
+
text: `${statusLine}\n\n${bodyText}\n\n${verb} ${resourceLabel} successfully. ${resourceHint}`,
|
|
827
850
|
},
|
|
828
851
|
],
|
|
829
852
|
details: detail,
|