@f5xc-salesdemos/xcsh 15.6.2 → 15.8.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 +7 -7
- package/src/cli/update-cli.ts +118 -14
- package/src/config/settings-schema.ts +1 -1
- package/src/mcp/render.ts +24 -2
- package/src/modes/components/bash-execution.ts +36 -6
- package/src/modes/components/python-execution.ts +31 -3
- package/src/modes/components/status-line/presets.ts +3 -3
- package/src/modes/controllers/command-controller.ts +2 -2
- package/src/modes/theme/dark.json +1 -0
- package/src/modes/theme/defaults/alabaster.json +2 -1
- package/src/modes/theme/defaults/amethyst.json +2 -1
- package/src/modes/theme/defaults/anthracite.json +2 -1
- package/src/modes/theme/defaults/basalt.json +2 -1
- package/src/modes/theme/defaults/birch.json +2 -1
- package/src/modes/theme/defaults/dark-abyss.json +2 -1
- package/src/modes/theme/defaults/dark-arctic.json +2 -1
- package/src/modes/theme/defaults/dark-aurora.json +2 -1
- package/src/modes/theme/defaults/dark-catppuccin.json +2 -1
- package/src/modes/theme/defaults/dark-cavern.json +2 -1
- package/src/modes/theme/defaults/dark-copper.json +2 -1
- package/src/modes/theme/defaults/dark-cosmos.json +2 -1
- package/src/modes/theme/defaults/dark-cyberpunk.json +2 -1
- package/src/modes/theme/defaults/dark-dracula.json +2 -1
- package/src/modes/theme/defaults/dark-eclipse.json +2 -1
- package/src/modes/theme/defaults/dark-ember.json +2 -1
- package/src/modes/theme/defaults/dark-equinox.json +2 -1
- package/src/modes/theme/defaults/dark-forest.json +2 -1
- package/src/modes/theme/defaults/dark-github.json +2 -1
- package/src/modes/theme/defaults/dark-gruvbox.json +2 -1
- package/src/modes/theme/defaults/dark-lavender.json +2 -1
- package/src/modes/theme/defaults/dark-lunar.json +2 -1
- package/src/modes/theme/defaults/dark-midnight.json +2 -1
- package/src/modes/theme/defaults/dark-monochrome.json +2 -1
- package/src/modes/theme/defaults/dark-monokai.json +2 -1
- package/src/modes/theme/defaults/dark-nebula.json +2 -1
- package/src/modes/theme/defaults/dark-nord.json +2 -1
- package/src/modes/theme/defaults/dark-ocean.json +2 -1
- package/src/modes/theme/defaults/dark-one.json +2 -1
- package/src/modes/theme/defaults/dark-poimandres.json +137 -136
- package/src/modes/theme/defaults/dark-rainforest.json +2 -1
- package/src/modes/theme/defaults/dark-reef.json +2 -1
- package/src/modes/theme/defaults/dark-retro.json +2 -1
- package/src/modes/theme/defaults/dark-rose-pine.json +2 -1
- package/src/modes/theme/defaults/dark-sakura.json +2 -1
- package/src/modes/theme/defaults/dark-slate.json +2 -1
- package/src/modes/theme/defaults/dark-solarized.json +2 -1
- package/src/modes/theme/defaults/dark-solstice.json +2 -1
- package/src/modes/theme/defaults/dark-starfall.json +2 -1
- package/src/modes/theme/defaults/dark-sunset.json +2 -1
- package/src/modes/theme/defaults/dark-swamp.json +2 -1
- package/src/modes/theme/defaults/dark-synthwave.json +2 -1
- package/src/modes/theme/defaults/dark-taiga.json +2 -1
- package/src/modes/theme/defaults/dark-terminal.json +2 -1
- package/src/modes/theme/defaults/dark-tokyo-night.json +2 -1
- package/src/modes/theme/defaults/dark-tundra.json +2 -1
- package/src/modes/theme/defaults/dark-twilight.json +2 -1
- package/src/modes/theme/defaults/dark-volcanic.json +2 -1
- package/src/modes/theme/defaults/graphite.json +2 -1
- package/src/modes/theme/defaults/light-arctic.json +2 -1
- package/src/modes/theme/defaults/light-aurora-day.json +2 -1
- package/src/modes/theme/defaults/light-canyon.json +2 -1
- package/src/modes/theme/defaults/light-catppuccin.json +2 -1
- package/src/modes/theme/defaults/light-cirrus.json +2 -1
- package/src/modes/theme/defaults/light-coral.json +2 -1
- package/src/modes/theme/defaults/light-cyberpunk.json +2 -1
- package/src/modes/theme/defaults/light-dawn.json +2 -1
- package/src/modes/theme/defaults/light-dunes.json +2 -1
- package/src/modes/theme/defaults/light-eucalyptus.json +2 -1
- package/src/modes/theme/defaults/light-forest.json +2 -1
- package/src/modes/theme/defaults/light-frost.json +2 -1
- package/src/modes/theme/defaults/light-github.json +2 -1
- package/src/modes/theme/defaults/light-glacier.json +2 -1
- package/src/modes/theme/defaults/light-gruvbox.json +2 -1
- package/src/modes/theme/defaults/light-haze.json +2 -1
- package/src/modes/theme/defaults/light-honeycomb.json +2 -1
- package/src/modes/theme/defaults/light-lagoon.json +2 -1
- package/src/modes/theme/defaults/light-lavender.json +2 -1
- package/src/modes/theme/defaults/light-meadow.json +2 -1
- package/src/modes/theme/defaults/light-mint.json +2 -1
- package/src/modes/theme/defaults/light-monochrome.json +2 -1
- package/src/modes/theme/defaults/light-ocean.json +2 -1
- package/src/modes/theme/defaults/light-one.json +2 -1
- package/src/modes/theme/defaults/light-opal.json +2 -1
- package/src/modes/theme/defaults/light-orchard.json +2 -1
- package/src/modes/theme/defaults/light-paper.json +2 -1
- package/src/modes/theme/defaults/light-poimandres.json +137 -136
- package/src/modes/theme/defaults/light-prism.json +2 -1
- package/src/modes/theme/defaults/light-retro.json +2 -1
- package/src/modes/theme/defaults/light-sand.json +2 -1
- package/src/modes/theme/defaults/light-savanna.json +2 -1
- package/src/modes/theme/defaults/light-solarized.json +2 -1
- package/src/modes/theme/defaults/light-soleil.json +2 -1
- package/src/modes/theme/defaults/light-sunset.json +2 -1
- package/src/modes/theme/defaults/light-synthwave.json +2 -1
- package/src/modes/theme/defaults/light-tokyo-night.json +2 -1
- package/src/modes/theme/defaults/light-wetland.json +2 -1
- package/src/modes/theme/defaults/light-zenith.json +2 -1
- package/src/modes/theme/defaults/limestone.json +2 -1
- package/src/modes/theme/defaults/mahogany.json +2 -1
- package/src/modes/theme/defaults/marble.json +2 -1
- package/src/modes/theme/defaults/obsidian.json +2 -1
- package/src/modes/theme/defaults/onyx.json +2 -1
- package/src/modes/theme/defaults/pearl.json +2 -1
- package/src/modes/theme/defaults/porcelain.json +2 -1
- package/src/modes/theme/defaults/quartz.json +2 -1
- package/src/modes/theme/defaults/sandstone.json +2 -1
- package/src/modes/theme/defaults/titanium.json +2 -1
- package/src/modes/theme/defaults/xcsh-dark.json +2 -1
- package/src/modes/theme/defaults/xcsh-light.json +2 -1
- package/src/modes/theme/light.json +1 -0
- package/src/modes/theme/theme-schema.json +5 -0
- package/src/modes/theme/theme.ts +4 -0
- package/src/task/render.ts +21 -8
- package/src/tools/bash.ts +33 -2
- package/src/tools/grep.ts +22 -5
- package/src/tools/json-tree.ts +33 -10
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@f5xc-salesdemos/xcsh",
|
|
4
|
-
"version": "15.
|
|
4
|
+
"version": "15.8.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",
|
|
@@ -46,12 +46,12 @@
|
|
|
46
46
|
"dependencies": {
|
|
47
47
|
"@agentclientprotocol/sdk": "0.16.1",
|
|
48
48
|
"@mozilla/readability": "^0.6",
|
|
49
|
-
"@f5xc-salesdemos/xcsh-stats": "15.
|
|
50
|
-
"@f5xc-salesdemos/pi-agent-core": "15.
|
|
51
|
-
"@f5xc-salesdemos/pi-ai": "15.
|
|
52
|
-
"@f5xc-salesdemos/pi-natives": "15.
|
|
53
|
-
"@f5xc-salesdemos/pi-tui": "15.
|
|
54
|
-
"@f5xc-salesdemos/pi-utils": "15.
|
|
49
|
+
"@f5xc-salesdemos/xcsh-stats": "15.8.0",
|
|
50
|
+
"@f5xc-salesdemos/pi-agent-core": "15.8.0",
|
|
51
|
+
"@f5xc-salesdemos/pi-ai": "15.8.0",
|
|
52
|
+
"@f5xc-salesdemos/pi-natives": "15.8.0",
|
|
53
|
+
"@f5xc-salesdemos/pi-tui": "15.8.0",
|
|
54
|
+
"@f5xc-salesdemos/pi-utils": "15.8.0",
|
|
55
55
|
"@sinclair/typebox": "^0.34",
|
|
56
56
|
"@xterm/headless": "^6.0",
|
|
57
57
|
"ajv": "^8.18",
|
package/src/cli/update-cli.ts
CHANGED
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
* Update CLI command handler.
|
|
3
3
|
*
|
|
4
4
|
* Handles `xcsh update` to check for and install updates.
|
|
5
|
-
*
|
|
5
|
+
* Auto-detects the installation method (npm, brew, bun, or standalone binary)
|
|
6
|
+
* and updates through the appropriate channel.
|
|
6
7
|
*/
|
|
7
8
|
import * as fs from "node:fs";
|
|
8
9
|
import * as path from "node:path";
|
|
@@ -60,14 +61,74 @@ function isPathInDirectory(filePath: string, directoryPath: string): boolean {
|
|
|
60
61
|
return relativePath === "" || (!relativePath.startsWith("..") && !path.isAbsolute(relativePath));
|
|
61
62
|
}
|
|
62
63
|
|
|
63
|
-
type
|
|
64
|
+
export type InstallMethod = "npm" | "brew" | "bun" | "binary";
|
|
64
65
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
66
|
+
type UpdateTarget =
|
|
67
|
+
| { method: "npm"; path: string }
|
|
68
|
+
| { method: "brew"; path: string }
|
|
69
|
+
| { method: "bun" }
|
|
70
|
+
| { method: "binary"; path: string };
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Detect how xcsh was installed by examining the binary path.
|
|
74
|
+
*
|
|
75
|
+
* Detection order:
|
|
76
|
+
* 1. bun — binary is inside bun's global bin directory
|
|
77
|
+
* 2. npm — binary is a symlink whose resolution chain contains "node_modules"
|
|
78
|
+
* 3. brew — binary path or realpath contains "Cellar" or "homebrew"
|
|
79
|
+
* 4. binary — fallback for standalone installs
|
|
80
|
+
*/
|
|
81
|
+
function detectInstallMethod(binPath: string, bunBinDir: string | undefined): InstallMethod {
|
|
82
|
+
// 1. Bun: binary lives inside bun's global bin dir
|
|
83
|
+
if (bunBinDir && isPathInDirectory(binPath, bunBinDir)) {
|
|
84
|
+
return "bun";
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// 2. npm: binary is a symlink whose target chain contains node_modules
|
|
88
|
+
try {
|
|
89
|
+
const stats = fs.lstatSync(binPath);
|
|
90
|
+
if (stats.isSymbolicLink()) {
|
|
91
|
+
const linkTarget = fs.readlinkSync(binPath);
|
|
92
|
+
const resolvedTarget = path.resolve(path.dirname(binPath), linkTarget);
|
|
93
|
+
if (linkTarget.includes("node_modules") || resolvedTarget.includes("node_modules")) {
|
|
94
|
+
return "npm";
|
|
95
|
+
}
|
|
96
|
+
try {
|
|
97
|
+
const realPath = fs.realpathSync(binPath);
|
|
98
|
+
if (realPath.includes("node_modules")) {
|
|
99
|
+
return "npm";
|
|
100
|
+
}
|
|
101
|
+
} catch {
|
|
102
|
+
// realpath may fail if target doesn't exist
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
} catch {
|
|
106
|
+
// lstat/readlink may fail; fall through
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// 3. brew: path or realpath contains Cellar or homebrew
|
|
110
|
+
const lowerBinPath = binPath.toLowerCase();
|
|
111
|
+
if (lowerBinPath.includes("/cellar/") || lowerBinPath.includes("/homebrew/")) {
|
|
112
|
+
return "brew";
|
|
113
|
+
}
|
|
114
|
+
try {
|
|
115
|
+
const realPath = fs.realpathSync(binPath).toLowerCase();
|
|
116
|
+
if (realPath.includes("/cellar/") || realPath.includes("/homebrew/")) {
|
|
117
|
+
return "brew";
|
|
118
|
+
}
|
|
119
|
+
} catch {
|
|
120
|
+
// realpath may fail; fall through
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// 4. Standalone binary (fallback)
|
|
124
|
+
return "binary";
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function resolveUpdateMethod(ompPath: string, bunBinDir: string | undefined): InstallMethod {
|
|
128
|
+
return detectInstallMethod(ompPath, bunBinDir);
|
|
68
129
|
}
|
|
69
130
|
|
|
70
|
-
export function _resolveUpdateMethodForTest(ompPath: string, bunBinDir: string | undefined):
|
|
131
|
+
export function _resolveUpdateMethodForTest(ompPath: string, bunBinDir: string | undefined): InstallMethod {
|
|
71
132
|
return resolveUpdateMethod(ompPath, bunBinDir);
|
|
72
133
|
}
|
|
73
134
|
async function resolveUpdateTarget(): Promise<UpdateTarget> {
|
|
@@ -175,8 +236,9 @@ function resolveOmpPath(): string | undefined {
|
|
|
175
236
|
*/
|
|
176
237
|
async function verifyInstalledVersion(
|
|
177
238
|
expectedVersion: string,
|
|
239
|
+
explicitPath?: string,
|
|
178
240
|
): Promise<{ ok: boolean; actual?: string; path?: string }> {
|
|
179
|
-
const ompPath = resolveOmpPath();
|
|
241
|
+
const ompPath = explicitPath ?? resolveOmpPath();
|
|
180
242
|
if (!ompPath) return { ok: false };
|
|
181
243
|
try {
|
|
182
244
|
const result = await $`${ompPath} --version`.quiet().nothrow();
|
|
@@ -194,8 +256,8 @@ async function verifyInstalledVersion(
|
|
|
194
256
|
/**
|
|
195
257
|
* Print post-update verification result.
|
|
196
258
|
*/
|
|
197
|
-
async function printVerification(expectedVersion: string): Promise<void> {
|
|
198
|
-
const result = await verifyInstalledVersion(expectedVersion);
|
|
259
|
+
async function printVerification(expectedVersion: string, explicitPath?: string): Promise<void> {
|
|
260
|
+
const result = await verifyInstalledVersion(expectedVersion, explicitPath);
|
|
199
261
|
if (result.ok) {
|
|
200
262
|
console.log(chalk.green(`\n${theme.status.success} Updated to ${expectedVersion}`));
|
|
201
263
|
return;
|
|
@@ -231,6 +293,30 @@ async function updateViaBun(expectedVersion: string): Promise<void> {
|
|
|
231
293
|
await printVerification(expectedVersion);
|
|
232
294
|
}
|
|
233
295
|
|
|
296
|
+
/**
|
|
297
|
+
* Update via npm package manager.
|
|
298
|
+
*/
|
|
299
|
+
async function updateViaNpm(expectedVersion: string): Promise<void> {
|
|
300
|
+
console.log(chalk.dim("Updating via npm..."));
|
|
301
|
+
const result = await $`npm install -g ${PACKAGE}@${expectedVersion}`.nothrow();
|
|
302
|
+
if (result.exitCode !== 0) {
|
|
303
|
+
throw new Error(`npm install failed with exit code ${result.exitCode}`);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* Handle brew-installed xcsh.
|
|
309
|
+
*
|
|
310
|
+
* For corporate environments, brew-managed software must not be bypassed.
|
|
311
|
+
* Prints instructions instead of running brew upgrade automatically.
|
|
312
|
+
*/
|
|
313
|
+
function updateViaBrew(targetPath: string, expectedVersion: string): void {
|
|
314
|
+
console.log(chalk.yellow(`\n${APP_NAME} at ${targetPath} was installed via Homebrew.`));
|
|
315
|
+
console.log(chalk.yellow(`To update to ${expectedVersion}, run:`));
|
|
316
|
+
console.log(chalk.cyan(` brew upgrade ${APP_NAME}`));
|
|
317
|
+
console.log(chalk.dim("\nThis ensures the update goes through your organization's Homebrew tap."));
|
|
318
|
+
}
|
|
319
|
+
|
|
234
320
|
/**
|
|
235
321
|
* Download a release binary to a target path, replacing an existing file.
|
|
236
322
|
*/
|
|
@@ -261,7 +347,7 @@ async function updateViaBinaryAt(targetPath: string, expectedVersion: string): P
|
|
|
261
347
|
await fs.promises.rename(tempPath, targetPath);
|
|
262
348
|
await fs.promises.unlink(backupPath);
|
|
263
349
|
|
|
264
|
-
await printVerification(expectedVersion);
|
|
350
|
+
await printVerification(expectedVersion, targetPath);
|
|
265
351
|
console.log(chalk.dim(`Restart ${APP_NAME} to use the new version`));
|
|
266
352
|
} catch (err) {
|
|
267
353
|
if (fs.existsSync(backupPath) && !fs.existsSync(targetPath)) {
|
|
@@ -310,10 +396,22 @@ export async function runUpdateCommand(opts: { force: boolean; check: boolean })
|
|
|
310
396
|
// Choose update method based on the prioritized xcsh binary in PATH
|
|
311
397
|
try {
|
|
312
398
|
const target = await resolveUpdateTarget();
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
399
|
+
console.log(chalk.dim(`Install method: ${target.method}`));
|
|
400
|
+
|
|
401
|
+
switch (target.method) {
|
|
402
|
+
case "bun":
|
|
403
|
+
await updateViaBun(release.version);
|
|
404
|
+
break;
|
|
405
|
+
case "npm":
|
|
406
|
+
await updateViaNpm(release.version);
|
|
407
|
+
await printVerification(release.version);
|
|
408
|
+
break;
|
|
409
|
+
case "brew":
|
|
410
|
+
updateViaBrew(target.path, release.version);
|
|
411
|
+
return;
|
|
412
|
+
case "binary":
|
|
413
|
+
await updateViaBinaryAt(target.path, release.version);
|
|
414
|
+
break;
|
|
317
415
|
}
|
|
318
416
|
} catch (err) {
|
|
319
417
|
console.error(chalk.red(`Update failed: ${err}`));
|
|
@@ -334,6 +432,12 @@ ${chalk.bold("Options:")}
|
|
|
334
432
|
-c, --check Check for updates without installing
|
|
335
433
|
-f, --force Force reinstall even if up to date
|
|
336
434
|
|
|
435
|
+
${chalk.bold("Install methods (auto-detected):")}
|
|
436
|
+
npm Installed via npm install -g
|
|
437
|
+
brew Installed via Homebrew (prints upgrade instructions)
|
|
438
|
+
bun Installed via bun install -g
|
|
439
|
+
binary Standalone binary (direct download)
|
|
440
|
+
|
|
337
441
|
${chalk.bold("Examples:")}
|
|
338
442
|
${APP_NAME} update Update to latest version
|
|
339
443
|
${APP_NAME} update --check Check if updates are available
|
package/src/mcp/render.ts
CHANGED
|
@@ -8,6 +8,7 @@ import type { Component } from "@f5xc-salesdemos/pi-tui";
|
|
|
8
8
|
import { Text } from "@f5xc-salesdemos/pi-tui";
|
|
9
9
|
import type { RenderResultOptions } from "../extensibility/custom-tools/types";
|
|
10
10
|
import type { Theme } from "../modes/theme/theme";
|
|
11
|
+
import { highlightCode } from "../modes/theme/theme";
|
|
11
12
|
import {
|
|
12
13
|
formatArgsInline,
|
|
13
14
|
JSON_TREE_MAX_DEPTH_COLLAPSED,
|
|
@@ -114,8 +115,29 @@ export function renderMCPResult(
|
|
|
114
115
|
const maxOutputLines = expanded ? 12 : 4;
|
|
115
116
|
const displayLines = outputLines.slice(0, maxOutputLines);
|
|
116
117
|
|
|
117
|
-
|
|
118
|
-
|
|
118
|
+
// Try to syntax-highlight structured output (e.g. JSON)
|
|
119
|
+
const firstNonEmpty = trimmedOutput.trim();
|
|
120
|
+
const isJson =
|
|
121
|
+
firstNonEmpty.length <= 32_768 &&
|
|
122
|
+
(firstNonEmpty[0] === "{" || firstNonEmpty[0] === "[") &&
|
|
123
|
+
(() => {
|
|
124
|
+
try {
|
|
125
|
+
JSON.parse(firstNonEmpty);
|
|
126
|
+
return true;
|
|
127
|
+
} catch {
|
|
128
|
+
return false;
|
|
129
|
+
}
|
|
130
|
+
})();
|
|
131
|
+
|
|
132
|
+
if (isJson) {
|
|
133
|
+
const highlighted = highlightCode(displayLines.join("\n"), "json");
|
|
134
|
+
for (const line of highlighted) {
|
|
135
|
+
lines.push(truncateToWidth(line, 80));
|
|
136
|
+
}
|
|
137
|
+
} else {
|
|
138
|
+
for (const line of displayLines) {
|
|
139
|
+
lines.push(theme.fg("toolOutput", truncateToWidth(line, 80)));
|
|
140
|
+
}
|
|
119
141
|
}
|
|
120
142
|
|
|
121
143
|
if (outputLines.length > maxOutputLines) {
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
import { sanitizeText } from "@f5xc-salesdemos/pi-natives";
|
|
6
6
|
import { Container, ImageProtocol, Loader, Spacer, TERMINAL, Text, type TUI } from "@f5xc-salesdemos/pi-tui";
|
|
7
|
-
import { getSymbolTheme, theme } from "../../modes/theme/theme";
|
|
7
|
+
import { getSymbolTheme, highlightCode, theme } from "../../modes/theme/theme";
|
|
8
8
|
import { formatTruncationMetaNotice, type TruncationMeta } from "../../tools/output-meta";
|
|
9
9
|
import { getSixelLineMask, isSixelPassthroughEnabled, sanitizeWithOptionalSixelPassthrough } from "../../utils/sixel";
|
|
10
10
|
import { DynamicBorder } from "./dynamic-border";
|
|
@@ -13,6 +13,29 @@ import { truncateToVisualLines } from "./visual-truncate";
|
|
|
13
13
|
// Preview line limit when not expanded (matches tool execution behavior)
|
|
14
14
|
const PREVIEW_LINES = 20;
|
|
15
15
|
const STREAMING_LINE_CAP = PREVIEW_LINES * 5;
|
|
16
|
+
|
|
17
|
+
/** Max bytes to attempt JSON detection on (avoid parsing huge outputs) */
|
|
18
|
+
const JSON_DETECT_LIMIT = 32_768;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Detect if output looks like JSON and return syntax-highlighted lines.
|
|
22
|
+
* Returns undefined if not JSON or if detection should not be attempted.
|
|
23
|
+
*/
|
|
24
|
+
function highlightIfStructured(lines: string[]): string[] | undefined {
|
|
25
|
+
if (lines.length === 0) return undefined;
|
|
26
|
+
const firstNonEmpty = lines.find(l => l.trim().length > 0)?.trim();
|
|
27
|
+
if (!firstNonEmpty) return undefined;
|
|
28
|
+
// Only detect JSON (starts with { or [)
|
|
29
|
+
if (firstNonEmpty[0] !== "{" && firstNonEmpty[0] !== "[") return undefined;
|
|
30
|
+
const fullText = lines.join("\n");
|
|
31
|
+
if (fullText.length > JSON_DETECT_LIMIT) return undefined;
|
|
32
|
+
try {
|
|
33
|
+
JSON.parse(fullText);
|
|
34
|
+
return highlightCode(fullText, "json");
|
|
35
|
+
} catch {
|
|
36
|
+
return undefined;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
16
39
|
const MAX_DISPLAY_LINE_CHARS = 4000;
|
|
17
40
|
// Minimum interval between processing incoming chunks for display (ms).
|
|
18
41
|
// Chunks arriving faster than this are accumulated and processed in one batch.
|
|
@@ -163,15 +186,22 @@ export class BashExecutionComponent extends Container {
|
|
|
163
186
|
|
|
164
187
|
// Output
|
|
165
188
|
if (availableLines.length > 0) {
|
|
189
|
+
// Try to syntax-highlight structured output (e.g. JSON)
|
|
190
|
+
const highlightedLines = hasSixelOutput ? undefined : highlightIfStructured(availableLines);
|
|
191
|
+
|
|
166
192
|
if (this.#expanded || hasSixelOutput) {
|
|
167
|
-
const displayText =
|
|
168
|
-
|
|
169
|
-
|
|
193
|
+
const displayText = highlightedLines
|
|
194
|
+
? highlightedLines.join("\n")
|
|
195
|
+
: availableLines
|
|
196
|
+
.map((line, index) => (sixelLineMask?.[index] ? line : theme.fg("muted", line)))
|
|
197
|
+
.join("\n");
|
|
170
198
|
this.#contentContainer.addChild(new Text(`\n${displayText}`, 1, 0));
|
|
171
199
|
} else {
|
|
172
200
|
// Use shared visual truncation utility, recomputed per render width
|
|
173
|
-
const
|
|
174
|
-
|
|
201
|
+
const previewHighlighted = highlightedLines
|
|
202
|
+
? highlightedLines.slice(-previewLogicalLines.length).join("\n")
|
|
203
|
+
: previewLogicalLines.map(line => theme.fg("muted", line)).join("\n");
|
|
204
|
+
const previewText = `\n${previewHighlighted}`;
|
|
175
205
|
this.#contentContainer.addChild({
|
|
176
206
|
render: (width: number) => {
|
|
177
207
|
const { visualLines } = truncateToVisualLines(previewText, PREVIEW_LINES, width, 1);
|
|
@@ -13,6 +13,27 @@ import { truncateToVisualLines } from "./visual-truncate";
|
|
|
13
13
|
const PREVIEW_LINES = 20;
|
|
14
14
|
const MAX_DISPLAY_LINE_CHARS = 4000;
|
|
15
15
|
|
|
16
|
+
/** Max bytes to attempt JSON detection on */
|
|
17
|
+
const JSON_DETECT_LIMIT = 32_768;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Detect if output looks like JSON and return syntax-highlighted lines.
|
|
21
|
+
*/
|
|
22
|
+
function highlightIfStructured(lines: string[]): string[] | undefined {
|
|
23
|
+
if (lines.length === 0) return undefined;
|
|
24
|
+
const firstNonEmpty = lines.find(l => l.trim().length > 0)?.trim();
|
|
25
|
+
if (!firstNonEmpty) return undefined;
|
|
26
|
+
if (firstNonEmpty[0] !== "{" && firstNonEmpty[0] !== "[") return undefined;
|
|
27
|
+
const fullText = lines.join("\n");
|
|
28
|
+
if (fullText.length > JSON_DETECT_LIMIT) return undefined;
|
|
29
|
+
try {
|
|
30
|
+
JSON.parse(fullText);
|
|
31
|
+
return highlightCode(fullText, "json");
|
|
32
|
+
} catch {
|
|
33
|
+
return undefined;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
16
37
|
export class PythonExecutionComponent extends Container {
|
|
17
38
|
#outputLines: string[] = [];
|
|
18
39
|
#status: "running" | "complete" | "cancelled" | "error" = "running";
|
|
@@ -117,12 +138,19 @@ export class PythonExecutionComponent extends Container {
|
|
|
117
138
|
this.#contentContainer.addChild(this.#formatHeader(colorKey));
|
|
118
139
|
|
|
119
140
|
if (availableLines.length > 0) {
|
|
141
|
+
// Try to syntax-highlight structured output (e.g. JSON)
|
|
142
|
+
const highlightedLines = highlightIfStructured(availableLines);
|
|
143
|
+
|
|
120
144
|
if (this.#expanded) {
|
|
121
|
-
const displayText =
|
|
145
|
+
const displayText = highlightedLines
|
|
146
|
+
? highlightedLines.join("\n")
|
|
147
|
+
: availableLines.map(line => theme.fg("muted", line)).join("\n");
|
|
122
148
|
this.#contentContainer.addChild(new Text(`\n${displayText}`, 1, 0));
|
|
123
149
|
} else {
|
|
124
|
-
const
|
|
125
|
-
|
|
150
|
+
const previewHighlighted = highlightedLines
|
|
151
|
+
? highlightedLines.slice(-previewLogicalLines.length).join("\n")
|
|
152
|
+
: previewLogicalLines.map(line => theme.fg("muted", line)).join("\n");
|
|
153
|
+
const previewText = `\n${previewHighlighted}`;
|
|
126
154
|
this.#contentContainer.addChild({
|
|
127
155
|
render: (width: number) => {
|
|
128
156
|
const { visualLines } = truncateToVisualLines(previewText, PREVIEW_LINES, width, 1);
|
|
@@ -4,7 +4,7 @@ export const STATUS_LINE_PRESETS: Record<StatusLinePreset, PresetDef> = {
|
|
|
4
4
|
default: {
|
|
5
5
|
leftSegments: ["pi", "model", "plan_mode", "path", "git", "pr", "context_pct", "token_total", "cost"],
|
|
6
6
|
rightSegments: [],
|
|
7
|
-
separator: "powerline
|
|
7
|
+
separator: "powerline",
|
|
8
8
|
segmentOptions: {
|
|
9
9
|
model: { showThinkingLevel: true },
|
|
10
10
|
path: { abbreviate: true, maxLength: 40, stripWorkPrefix: true },
|
|
@@ -25,7 +25,7 @@ export const STATUS_LINE_PRESETS: Record<StatusLinePreset, PresetDef> = {
|
|
|
25
25
|
compact: {
|
|
26
26
|
leftSegments: ["model", "plan_mode", "git", "pr"],
|
|
27
27
|
rightSegments: ["cost", "context_pct"],
|
|
28
|
-
separator: "powerline
|
|
28
|
+
separator: "powerline",
|
|
29
29
|
segmentOptions: {
|
|
30
30
|
model: { showThinkingLevel: false },
|
|
31
31
|
git: { showBranch: true, showStaged: true, showUnstaged: true, showUntracked: false },
|
|
@@ -95,7 +95,7 @@ export const STATUS_LINE_PRESETS: Record<StatusLinePreset, PresetDef> = {
|
|
|
95
95
|
// User-defined - these are just defaults that get overridden
|
|
96
96
|
leftSegments: ["model", "plan_mode", "path", "git", "pr"],
|
|
97
97
|
rightSegments: ["token_total", "cost", "context_pct"],
|
|
98
|
-
separator: "powerline
|
|
98
|
+
separator: "powerline",
|
|
99
99
|
segmentOptions: {},
|
|
100
100
|
},
|
|
101
101
|
};
|
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
type UsageReport,
|
|
11
11
|
} from "@f5xc-salesdemos/pi-ai";
|
|
12
12
|
import { Loader, Markdown, padding, Spacer, Text, visibleWidth } from "@f5xc-salesdemos/pi-tui";
|
|
13
|
-
import { formatDuration, Snowflake, setProjectDir } from "@f5xc-salesdemos/pi-utils";
|
|
13
|
+
import { formatDuration, Snowflake, setProjectDir, setShellPwd } from "@f5xc-salesdemos/pi-utils";
|
|
14
14
|
import { $ } from "bun";
|
|
15
15
|
import { reset as resetCapabilities } from "../../capability";
|
|
16
16
|
import { clearClaudePluginRootsCache } from "../../discovery/helpers";
|
|
@@ -713,7 +713,7 @@ export class CommandController {
|
|
|
713
713
|
|
|
714
714
|
// Update CWD if the shell changed directory (e.g. via cd)
|
|
715
715
|
if (result.newCwd && result.newCwd !== this.ctx.sessionManager.getCwd()) {
|
|
716
|
-
|
|
716
|
+
setShellPwd(result.newCwd);
|
|
717
717
|
this.ctx.statusLine.invalidate();
|
|
718
718
|
this.ctx.ui.requestRender();
|
|
719
719
|
}
|