@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.1",
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.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.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.1",
21
- "commit": "255a105f19a8485d74487ef9914154fdd6ad5bfd",
22
- "shortCommit": "255a105",
20
+ "version": "18.83.2",
21
+ "commit": "eacd7c8423fea2f500040cc43d684efdbeb2c8b1",
22
+ "shortCommit": "eacd7c8",
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.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/255a105f19a8485d74487ef9914154fdd6ad5bfd",
32
- "releaseUrl": "https://github.com/f5xc-salesdemos/xcsh/releases/tag/v18.83.1"
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
  };
@@ -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
  };