@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.83.1",
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.83.1",
53
- "@f5xc-salesdemos/pi-agent-core": "18.83.1",
54
- "@f5xc-salesdemos/pi-ai": "18.83.1",
55
- "@f5xc-salesdemos/pi-natives": "18.83.1",
56
- "@f5xc-salesdemos/pi-tui": "18.83.1",
57
- "@f5xc-salesdemos/pi-utils": "18.83.1",
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.83.1",
21
- "commit": "255a105f19a8485d74487ef9914154fdd6ad5bfd",
22
- "shortCommit": "255a105",
20
+ "version": "18.84.0",
21
+ "commit": "adc9c7ecdfd945419187e4bea5c9b442b53ad257",
22
+ "shortCommit": "adc9c7e",
23
23
  "branch": "main",
24
- "tag": "v18.83.1",
25
- "commitDate": "2026-05-27T01:34:26Z",
26
- "buildDate": "2026-05-27T02:02:08.778Z",
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/255a105f19a8485d74487ef9914154fdd6ad5bfd",
32
- "releaseUrl": "https://github.com/f5xc-salesdemos/xcsh/releases/tag/v18.83.1"
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.
@@ -1,7 +1,8 @@
1
- import { type Component, padding, Text, truncateToWidth, visibleWidth } from "@f5xc-salesdemos/pi-tui";
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 renderRunBlock(run: GhRunWatchRunDetails, width: number, theme: Theme): string[] {
141
- const lines = [formatRunLine(run, theme)];
142
- if (run.jobs.length === 0) {
143
- lines.push(theme.fg("dim", "waiting for workflow jobs..."));
144
- return lines;
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
- for (const job of run.jobs) {
148
- lines.push(renderJobLine(job, width, theme));
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 renderFailedLogs(
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 = ["", theme.fg("error", "failed logs")];
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 buildRenderedLines(
193
- watch: GhRunWatchViewDetails,
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
- if (watch.mode === "run" && watch.run) {
205
- lines.push(...renderRunBlock(watch.run, width, theme));
206
- } else if (watch.mode === "commit") {
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
- return new Text(header, 0, 0);
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, options: RenderResultOptions, uiTheme: Theme): Component {
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
- // " GitHub Run Watch run #12345"
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
- // " GitHub Run Watch feature-branch"
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
- // " GitHub Run Watch current HEAD"
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
- lines.push(uiTheme.fg("dim", " waiting for workflow data..."));
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
- return renderFallbackText(result, uiTheme);
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
- return buildRenderedLines(watch, uiTheme, options, lineWidth).map(line => truncateToWidth(line, lineWidth));
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 { formatExpandHint, replaceTabs, shortenPath, truncateToWidth } from "./render-utils";
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 question = args.question?.trim();
32
- return {
33
- render(width: number): string[] {
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 metaParts: string[] = [];
58
- if (details?.model) metaParts.push(details.model);
59
- if (details?.mimeType) metaParts.push(details.mimeType);
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: "Inspect Image",
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 lines: string[] = [header];
73
- if (question) {
74
- lines.push(
75
- ` ${uiTheme.fg("dim", uiTheme.tree.branch)} ${uiTheme.fg("dim", "Question:")} ${uiTheme.fg("contentAccent", truncateToWidth(replaceTabs(question), Math.max(20, width - 25)))}`,
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
  };
@@ -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} ${resolvedPath} successfully. ${resourceHint}`,
849
+ text: `${statusLine}\n\n${bodyText}\n\n${verb} ${resourceLabel} successfully. ${resourceHint}`,
827
850
  },
828
851
  ],
829
852
  details: detail,