@f5xc-salesdemos/xcsh 14.0.3 → 14.1.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/CHANGELOG.md +37 -2
- package/package.json +7 -7
- package/src/cli/read-cli.ts +28 -3
- package/src/config/settings-schema.ts +2 -2
- package/src/edit/modes/chunk.ts +1 -1
- package/src/extensibility/custom-tools/loader.ts +23 -14
- package/src/modes/components/welcome.ts +59 -59
- package/src/modes/controllers/input-controller.ts +9 -6
- package/src/modes/theme/defaults/index.ts +4 -0
- package/src/modes/theme/defaults/xcsh-dark.json +89 -0
- package/src/modes/theme/defaults/xcsh-light.json +91 -0
- package/src/prompts/tools/chunk-edit.md +54 -54
- package/src/prompts/tools/read-chunk.md +1 -1
- package/src/sdk.ts +21 -11
- package/src/session/agent-session.ts +71 -35
- package/src/session/tool-choice-queue.ts +213 -0
- package/src/slash-commands/builtin-registry.ts +56 -8
- package/src/tools/ast-edit.ts +2 -1
- package/src/tools/fetch.ts +65 -1
- package/src/tools/index.ts +11 -3
- package/src/tools/read.ts +21 -14
- package/src/tools/resolve.ts +68 -36
- package/src/utils/edit-mode.ts +11 -1
- package/src/tools/pending-action.ts +0 -49
package/CHANGELOG.md
CHANGED
|
@@ -2,14 +2,37 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
## [14.0.2] - 2026-04-09
|
|
6
|
+
### Added
|
|
7
|
+
|
|
8
|
+
- Added `/force` slash command to force the next agent turn to use a specific tool
|
|
9
|
+
- Added `ToolChoiceQueue` for managing tool-choice directives with lifecycle callbacks and requeue semantics
|
|
10
|
+
- Added `setForcedToolChoice()` method to AgentSession to programmatically force tool invocations
|
|
11
|
+
- Added `toolChoiceQueue` property to AgentSession for direct queue access
|
|
12
|
+
- Added `peekQueueInvoker()` method to AgentSession to retrieve in-flight tool invocation handlers
|
|
13
|
+
- Added `queueResolveHandler()` function as the canonical entry point for preview/apply workflows
|
|
14
|
+
- Added `buildToolChoice()` and `steer()` methods to ToolSession for tool-choice queue integration
|
|
15
|
+
- Added `getToolChoiceQueue()` method to ToolSession for accessing the tool-choice queue
|
|
16
|
+
- Added support for embedded URL selectors (`:raw` and `:L#-L#` line ranges) in read command paths
|
|
17
|
+
- Added `parseReadUrlTarget` function to parse and validate URL read targets with line range support
|
|
7
18
|
- Added `decl` region to chunk selector for targeting declarations without leading trivia
|
|
8
19
|
- Exported `hooks` subpath for extensibility API access
|
|
9
20
|
- Added `build` script for compiling binary artifacts
|
|
10
21
|
|
|
11
22
|
### Changed
|
|
12
23
|
|
|
24
|
+
- Refactored pending action handling from `PendingActionStore` to `ToolChoiceQueue` with generator-based directives
|
|
25
|
+
- Changed tool-choice override mechanism from simple override to a queue-based system with callbacks
|
|
26
|
+
- Updated `ResolveTool` to dispatch to in-flight queue invokers instead of popping from a pending action store
|
|
27
|
+
- Updated custom tool loader to accept `pushPendingAction` callback instead of `PendingActionStore` instance
|
|
28
|
+
- Updated `AstEditTool` to use `queueResolveHandler()` for preview/apply semantics
|
|
29
|
+
- Changed eager-todo prelude to use the tool-choice queue instead of simple override
|
|
30
|
+
- Updated todo reminder suppression to check for user-forced directives via `consumeLastServedLabel()`
|
|
31
|
+
- Made model-specific edit mode defaults conditional on `PI_STRICT_EDIT_MODE` environment variable for greater flexibility in edit mode selection
|
|
32
|
+
- Updated slash command handlers to support returning remaining text as prompt input instead of consuming input entirely
|
|
33
|
+
- Enhanced slash command parser to recognize both whitespace and colon (`:`) as command argument separators
|
|
34
|
+
- Updated indentation guidance for chunk edit content to use single leading spaces per indent level instead of tabs
|
|
35
|
+
- Updated read CLI to delegate URL inputs through the read tool pipeline instead of treating them as local file paths
|
|
13
36
|
- Updated chunk edit documentation to clarify region semantics and emphasize using the narrowest region for edits
|
|
14
37
|
- Improved chunk selector guidance with visual diagram showing region boundaries
|
|
15
38
|
- Renamed `build:binary` script to `build`
|
|
@@ -17,6 +40,18 @@
|
|
|
17
40
|
- Added `check:types`, `lint`, `fmt`, and `fix` scripts for improved developer workflow
|
|
18
41
|
- Simplified TypeScript configuration by extending workspace-level config
|
|
19
42
|
|
|
43
|
+
### Removed
|
|
44
|
+
|
|
45
|
+
- Removed `PendingActionStore` class and related pending-action module
|
|
46
|
+
- Removed `pendingActionStore` parameter from AgentSession config
|
|
47
|
+
- Removed `pendingActionStore` from ToolSession interface
|
|
48
|
+
- Removed `consumeNextToolChoiceOverride()` method from AgentSession (replaced by `nextToolChoice()`)
|
|
49
|
+
|
|
50
|
+
### Fixed
|
|
51
|
+
|
|
52
|
+
- Fixed tool-choice queue cleanup on agent loop abort to prevent orphaned in-flight directives
|
|
53
|
+
- Fixed requeue semantics to preserve `onInvoked` and `onRejected` callbacks across multiple abort cycles
|
|
54
|
+
|
|
20
55
|
## [14.0.1] - 2026-04-08
|
|
21
56
|
|
|
22
57
|
### Changed
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@f5xc-salesdemos/xcsh",
|
|
4
|
-
"version": "14.0
|
|
4
|
+
"version": "14.1.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": "14.0
|
|
50
|
-
"@f5xc-salesdemos/pi-agent-core": "14.0
|
|
51
|
-
"@f5xc-salesdemos/pi-ai": "14.0
|
|
52
|
-
"@f5xc-salesdemos/pi-natives": "14.0
|
|
53
|
-
"@f5xc-salesdemos/pi-tui": "14.0
|
|
54
|
-
"@f5xc-salesdemos/pi-utils": "14.0
|
|
49
|
+
"@f5xc-salesdemos/xcsh-stats": "14.1.0",
|
|
50
|
+
"@f5xc-salesdemos/pi-agent-core": "14.1.0",
|
|
51
|
+
"@f5xc-salesdemos/pi-ai": "14.1.0",
|
|
52
|
+
"@f5xc-salesdemos/pi-natives": "14.1.0",
|
|
53
|
+
"@f5xc-salesdemos/pi-tui": "14.1.0",
|
|
54
|
+
"@f5xc-salesdemos/pi-utils": "14.1.0",
|
|
55
55
|
"@sinclair/typebox": "^0.34",
|
|
56
56
|
"@xterm/headless": "^6.0",
|
|
57
57
|
"ajv": "^8.18",
|
package/src/cli/read-cli.ts
CHANGED
|
@@ -1,21 +1,47 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Read CLI command handler.
|
|
3
3
|
*
|
|
4
|
-
* Handles `xcsh read` subcommand — emits chunk-mode read output for
|
|
4
|
+
* Handles `xcsh read` subcommand — emits chunk-mode read output for files,
|
|
5
|
+
* and delegates URL reads through the read tool pipeline.
|
|
5
6
|
*/
|
|
6
7
|
import * as path from "node:path";
|
|
7
8
|
import chalk from "chalk";
|
|
9
|
+
import { Settings } from "../config/settings";
|
|
8
10
|
import { formatChunkedRead, resolveAnchorStyle } from "../edit/modes/chunk";
|
|
9
11
|
import { getLanguageFromPath } from "../modes/theme/theme";
|
|
12
|
+
import type { ToolSession } from "../tools";
|
|
13
|
+
import { parseReadUrlTarget } from "../tools/fetch";
|
|
14
|
+
import { ReadTool } from "../tools/read";
|
|
10
15
|
|
|
11
16
|
export interface ReadCommandArgs {
|
|
12
17
|
path: string;
|
|
13
18
|
sel?: string;
|
|
14
19
|
}
|
|
15
20
|
|
|
21
|
+
function createCliReadSession(cwd: string, settings: Settings): ToolSession {
|
|
22
|
+
return {
|
|
23
|
+
cwd,
|
|
24
|
+
hasUI: false,
|
|
25
|
+
hasEditTool: true,
|
|
26
|
+
getSessionFile: () => null,
|
|
27
|
+
getSessionSpawns: () => null,
|
|
28
|
+
settings,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
16
32
|
export async function runReadCommand(cmd: ReadCommandArgs): Promise<void> {
|
|
17
|
-
const
|
|
33
|
+
const cwd = process.cwd();
|
|
34
|
+
const parsedUrlTarget = parseReadUrlTarget(cmd.path, cmd.sel);
|
|
35
|
+
if (parsedUrlTarget) {
|
|
36
|
+
const settings = await Settings.init({ cwd });
|
|
37
|
+
const tool = new ReadTool(createCliReadSession(cwd, settings));
|
|
38
|
+
const result = await tool.execute("cli-read", { path: cmd.path, sel: cmd.sel });
|
|
39
|
+
const text = result.content.find((content): content is { type: "text"; text: string } => content.type === "text");
|
|
40
|
+
console.log(text?.text ?? "");
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
18
43
|
|
|
44
|
+
const filePath = path.resolve(cmd.path);
|
|
19
45
|
const file = Bun.file(filePath);
|
|
20
46
|
if (!(await file.exists())) {
|
|
21
47
|
console.error(chalk.red(`Error: File not found: ${cmd.path}`));
|
|
@@ -24,7 +50,6 @@ export async function runReadCommand(cmd: ReadCommandArgs): Promise<void> {
|
|
|
24
50
|
|
|
25
51
|
const readPath = cmd.sel ? `${filePath}:${cmd.sel}` : filePath;
|
|
26
52
|
const language = getLanguageFromPath(filePath);
|
|
27
|
-
const cwd = process.cwd();
|
|
28
53
|
|
|
29
54
|
try {
|
|
30
55
|
const result = await formatChunkedRead({
|
|
@@ -238,7 +238,7 @@ export const SETTINGS_SCHEMA = {
|
|
|
238
238
|
// Theme
|
|
239
239
|
"theme.dark": {
|
|
240
240
|
type: "string",
|
|
241
|
-
default: "
|
|
241
|
+
default: "xcsh-dark",
|
|
242
242
|
ui: {
|
|
243
243
|
tab: "appearance",
|
|
244
244
|
label: "Dark Theme",
|
|
@@ -249,7 +249,7 @@ export const SETTINGS_SCHEMA = {
|
|
|
249
249
|
|
|
250
250
|
"theme.light": {
|
|
251
251
|
type: "string",
|
|
252
|
-
default: "light",
|
|
252
|
+
default: "xcsh-light",
|
|
253
253
|
ui: {
|
|
254
254
|
tab: "appearance",
|
|
255
255
|
label: "Light Theme",
|
package/src/edit/modes/chunk.ts
CHANGED
|
@@ -312,7 +312,7 @@ export const chunkToolEditSchema = Type.Object({
|
|
|
312
312
|
"Chunk selector. Format: 'path@region' for insertions, 'path#CRC@region' for replace. Omit @region to target the full chunk. Valid regions: head, body, tail, decl.",
|
|
313
313
|
}),
|
|
314
314
|
content: Type.String({
|
|
315
|
-
description: "New content. Use
|
|
315
|
+
description: "New content. Use one leading space per indent level; do not include the chunk's base padding.",
|
|
316
316
|
}),
|
|
317
317
|
});
|
|
318
318
|
export const chunkEditParamsSchema = Type.Object(
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* to avoid import resolution issues with custom tools loaded from user directories.
|
|
6
6
|
*/
|
|
7
7
|
import * as path from "node:path";
|
|
8
|
+
import type { AgentToolResult } from "@f5xc-salesdemos/pi-agent-core";
|
|
8
9
|
import { logger } from "@f5xc-salesdemos/pi-utils";
|
|
9
10
|
import * as typebox from "@sinclair/typebox";
|
|
10
11
|
import { toolCapability } from "../../capability/tool";
|
|
@@ -13,7 +14,6 @@ import type { ExecOptions } from "../../exec/exec";
|
|
|
13
14
|
import { execCommand } from "../../exec/exec";
|
|
14
15
|
import type { HookUIContext } from "../../extensibility/hooks/types";
|
|
15
16
|
import { getAllPluginToolPaths } from "../../extensibility/plugins/loader";
|
|
16
|
-
import type { PendingActionStore } from "../../tools/pending-action";
|
|
17
17
|
import { createNoOpUIContext, resolvePath } from "../utils";
|
|
18
18
|
import type { CustomToolAPI, CustomToolFactory, LoadedCustomTool, ToolLoadError } from "./types";
|
|
19
19
|
|
|
@@ -88,7 +88,12 @@ export class CustomToolLoader {
|
|
|
88
88
|
pi: typeof import("@f5xc-salesdemos/xcsh"),
|
|
89
89
|
cwd: string,
|
|
90
90
|
builtInToolNames: string[],
|
|
91
|
-
|
|
91
|
+
pushPendingAction?: (action: {
|
|
92
|
+
label: string;
|
|
93
|
+
sourceToolName: string;
|
|
94
|
+
apply(reason: string): Promise<AgentToolResult<unknown>>;
|
|
95
|
+
reject?(reason: string): Promise<AgentToolResult<unknown> | undefined>;
|
|
96
|
+
}) => void,
|
|
92
97
|
) {
|
|
93
98
|
this.#sharedApi = {
|
|
94
99
|
cwd,
|
|
@@ -100,15 +105,14 @@ export class CustomToolLoader {
|
|
|
100
105
|
typebox,
|
|
101
106
|
pi,
|
|
102
107
|
pushPendingAction: action => {
|
|
103
|
-
if (!
|
|
108
|
+
if (!pushPendingAction) {
|
|
104
109
|
throw new Error("Pending action store unavailable for custom tools in this runtime.");
|
|
105
110
|
}
|
|
106
|
-
|
|
111
|
+
pushPendingAction({
|
|
107
112
|
label: action.label,
|
|
108
113
|
sourceToolName: action.sourceToolName ?? "custom_tool",
|
|
109
114
|
apply: action.apply,
|
|
110
115
|
reject: action.reject,
|
|
111
|
-
details: action.details,
|
|
112
116
|
});
|
|
113
117
|
},
|
|
114
118
|
};
|
|
@@ -159,14 +163,14 @@ export async function loadCustomTools(
|
|
|
159
163
|
pathsWithSources: ToolPathWithSource[],
|
|
160
164
|
cwd: string,
|
|
161
165
|
builtInToolNames: string[],
|
|
162
|
-
|
|
166
|
+
pushPendingAction?: (action: {
|
|
167
|
+
label: string;
|
|
168
|
+
sourceToolName: string;
|
|
169
|
+
apply(reason: string): Promise<AgentToolResult<unknown>>;
|
|
170
|
+
reject?(reason: string): Promise<AgentToolResult<unknown> | undefined>;
|
|
171
|
+
}) => void,
|
|
163
172
|
) {
|
|
164
|
-
const loader = new CustomToolLoader(
|
|
165
|
-
await import("@f5xc-salesdemos/xcsh"),
|
|
166
|
-
cwd,
|
|
167
|
-
builtInToolNames,
|
|
168
|
-
pendingActionStore,
|
|
169
|
-
);
|
|
173
|
+
const loader = new CustomToolLoader(await import("@f5xc-salesdemos/xcsh"), cwd, builtInToolNames, pushPendingAction);
|
|
170
174
|
await loader.load(pathsWithSources);
|
|
171
175
|
return {
|
|
172
176
|
tools: loader.tools,
|
|
@@ -191,7 +195,12 @@ export async function discoverAndLoadCustomTools(
|
|
|
191
195
|
configuredPaths: string[],
|
|
192
196
|
cwd: string,
|
|
193
197
|
builtInToolNames: string[],
|
|
194
|
-
|
|
198
|
+
pushPendingAction?: (action: {
|
|
199
|
+
label: string;
|
|
200
|
+
sourceToolName: string;
|
|
201
|
+
apply(reason: string): Promise<AgentToolResult<unknown>>;
|
|
202
|
+
reject?(reason: string): Promise<AgentToolResult<unknown> | undefined>;
|
|
203
|
+
}) => void,
|
|
195
204
|
) {
|
|
196
205
|
const allPathsWithSources: ToolPathWithSource[] = [];
|
|
197
206
|
const seen = new Set<string>();
|
|
@@ -225,5 +234,5 @@ export async function discoverAndLoadCustomTools(
|
|
|
225
234
|
addPath(resolvePath(configPath, cwd), { provider: "config", providerName: "Config", level: "project" });
|
|
226
235
|
}
|
|
227
236
|
|
|
228
|
-
return loadCustomTools(allPathsWithSources, cwd, builtInToolNames,
|
|
237
|
+
return loadCustomTools(allPathsWithSources, cwd, builtInToolNames, pushPendingAction);
|
|
229
238
|
}
|
|
@@ -14,7 +14,7 @@ export interface LspServerInfo {
|
|
|
14
14
|
}
|
|
15
15
|
|
|
16
16
|
/**
|
|
17
|
-
* Premium welcome screen with
|
|
17
|
+
* Premium welcome screen with F5 XCSH logo and two-column layout.
|
|
18
18
|
*/
|
|
19
19
|
export class WelcomeComponent implements Component {
|
|
20
20
|
constructor(
|
|
@@ -42,14 +42,14 @@ export class WelcomeComponent implements Component {
|
|
|
42
42
|
|
|
43
43
|
render(termWidth: number): string[] {
|
|
44
44
|
// Box dimensions - responsive with max width and small-terminal support
|
|
45
|
-
const maxWidth =
|
|
45
|
+
const maxWidth = 120;
|
|
46
46
|
const boxWidth = Math.min(maxWidth, Math.max(0, termWidth - 2));
|
|
47
47
|
if (boxWidth < 4) {
|
|
48
48
|
return [];
|
|
49
49
|
}
|
|
50
50
|
const dualContentWidth = boxWidth - 3; // 3 = │ + │ + │
|
|
51
|
-
const preferredLeftCol =
|
|
52
|
-
const minLeftCol =
|
|
51
|
+
const preferredLeftCol = 50;
|
|
52
|
+
const minLeftCol = 48; // F5 logo width (46 chars + padding)
|
|
53
53
|
const minRightCol = 20;
|
|
54
54
|
const leftMinContentWidth = Math.max(
|
|
55
55
|
minLeftCol,
|
|
@@ -67,23 +67,40 @@ export class WelcomeComponent implements Component {
|
|
|
67
67
|
const leftCol = showRightColumn ? dualLeftCol : boxWidth - 2;
|
|
68
68
|
const rightCol = showRightColumn ? dualRightCol : 0;
|
|
69
69
|
|
|
70
|
-
//
|
|
70
|
+
// F5 XCSH globe logo
|
|
71
71
|
// biome-ignore format: preserve ASCII art layout
|
|
72
|
-
const
|
|
72
|
+
const f5Logo = [
|
|
73
|
+
" ________",
|
|
74
|
+
" (▒▒▒▒▓▓▓▓▓▓▓▓▒▒▒▒)",
|
|
75
|
+
" (▒▒▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▒▒▒)",
|
|
76
|
+
" (▒▒▓▓▓▓██████████▓▓▓▓█████████████)",
|
|
77
|
+
" (▒▓▓▓▓██████▒▒▒▒▒███▓▓██████████████▒)",
|
|
78
|
+
" (▒▓▓▓▓██████▒▓▓▓▓▓▒▒▒▓██▒▒▒▒▒▒▒▒▒▒▒▒▒▓▒)",
|
|
79
|
+
" (▒▓▓▓▓▓██████▓▓▓▓▓▓▓▓▓██▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▒)",
|
|
80
|
+
" (▒▓▓███████████████▓▓▓▓█████████████▓▓▓▓▓▓▒)",
|
|
81
|
+
"(▒▓▓▓▒▒▒███████▒▒▒▒▒▓▓▓████████████████▓▓▓▓▓▒)",
|
|
82
|
+
"|▒▓▓▓▓▓▓▒██████▓▓▓▓▓▓▓████████████████████▓▓▒|",
|
|
83
|
+
"|▒▓▓▓▓▓▓▓██████▓▓▓▓▓▓▓▒▒▒▒▒▒▒▒▒▒▒██████████▓▒|",
|
|
84
|
+
"(▒▓▓▓▓▓▓▓██████▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▒▒████████▒▒)",
|
|
85
|
+
" (▒▓▓▓▓▓▓██████▓▓▓▓▓▓▓███▓▓▓▓▓▓▓▓▓▓▒▒▒████▒▒)",
|
|
86
|
+
" (▒▓▓▓▓▓██████▓▓▓▓▓▓█████▓▓▓▓▓▓▓▓▓▓▓▓███▒▒)",
|
|
87
|
+
" (▒▒██████████▓▓▓▓▓▒██████▓▓▓▓▓▓▓▓███▒▒▒)",
|
|
88
|
+
" (▒▒▒▒▒██████████▓▓▒▒█████████████▒▒▓▒)",
|
|
89
|
+
" (▒▓▓▒▒▒▒▒▒▒▒▒▒▓▓▓▓▒▒▒▒▒▒▒▒▒▒▒▒▒▓▒)",
|
|
90
|
+
" (▒▒▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▒▒▒)",
|
|
91
|
+
" (▒▒▒▒▓▓▓▓▓▓▓▓▒▒▒▒)",
|
|
92
|
+
];
|
|
73
93
|
|
|
74
|
-
// Apply
|
|
75
|
-
const logoColored =
|
|
94
|
+
// Apply F5 branding colors to logo
|
|
95
|
+
const logoColored = f5Logo.map(line => this.#f5ColorLine(line));
|
|
76
96
|
|
|
77
|
-
//
|
|
78
|
-
const
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
this.#centerText(theme.fg("muted", this.modelName), leftCol),
|
|
85
|
-
this.#centerText(theme.fg("borderMuted", this.providerName), leftCol),
|
|
86
|
-
];
|
|
97
|
+
// Center the logo as a block (widest line = 46 chars), preserving internal alignment
|
|
98
|
+
const logoMaxWidth = 46;
|
|
99
|
+
const logoBlockPad = Math.max(0, Math.floor((leftCol - logoMaxWidth) / 2));
|
|
100
|
+
const logoPadStr = padding(logoBlockPad);
|
|
101
|
+
|
|
102
|
+
// Left column - logo only
|
|
103
|
+
const leftLines = [...logoColored.map(l => logoPadStr + l), ""];
|
|
87
104
|
|
|
88
105
|
// Right column separator
|
|
89
106
|
const separatorWidth = Math.max(0, rightCol - 2); // padding on each side
|
|
@@ -135,28 +152,29 @@ export class WelcomeComponent implements Component {
|
|
|
135
152
|
"",
|
|
136
153
|
];
|
|
137
154
|
|
|
138
|
-
// Border characters (
|
|
155
|
+
// Border characters (themed)
|
|
156
|
+
const border = (s: string) => theme.fg("borderMuted", s);
|
|
139
157
|
const hChar = theme.boxRound.horizontal;
|
|
140
|
-
const h =
|
|
141
|
-
const v =
|
|
142
|
-
const tl =
|
|
143
|
-
const tr =
|
|
144
|
-
const bl =
|
|
145
|
-
const br =
|
|
158
|
+
const h = border(hChar);
|
|
159
|
+
const v = border(theme.boxRound.vertical);
|
|
160
|
+
const tl = border(theme.boxRound.topLeft);
|
|
161
|
+
const tr = border(theme.boxRound.topRight);
|
|
162
|
+
const bl = border(theme.boxRound.bottomLeft);
|
|
163
|
+
const br = border(theme.boxRound.bottomRight);
|
|
146
164
|
|
|
147
165
|
const lines: string[] = [];
|
|
148
166
|
|
|
149
167
|
// Top border with embedded title
|
|
150
168
|
const title = ` ${APP_NAME} v${this.version} `;
|
|
151
169
|
const titlePrefixRaw = hChar.repeat(3);
|
|
152
|
-
const titleStyled =
|
|
170
|
+
const titleStyled = border(titlePrefixRaw) + theme.bold(theme.fg("text", title));
|
|
153
171
|
const titleVisLen = visibleWidth(titlePrefixRaw) + visibleWidth(title);
|
|
154
172
|
const titleSpace = boxWidth - 2;
|
|
155
173
|
if (titleVisLen >= titleSpace) {
|
|
156
174
|
lines.push(tl + truncateToWidth(titleStyled, titleSpace) + tr);
|
|
157
175
|
} else {
|
|
158
176
|
const afterTitle = titleSpace - titleVisLen;
|
|
159
|
-
lines.push(tl + titleStyled +
|
|
177
|
+
lines.push(tl + titleStyled + border(hChar.repeat(afterTitle)) + tr);
|
|
160
178
|
}
|
|
161
179
|
|
|
162
180
|
// Content rows
|
|
@@ -172,7 +190,7 @@ export class WelcomeComponent implements Component {
|
|
|
172
190
|
}
|
|
173
191
|
// Bottom border
|
|
174
192
|
if (showRightColumn) {
|
|
175
|
-
lines.push(bl + h.repeat(leftCol) +
|
|
193
|
+
lines.push(bl + h.repeat(leftCol) + border(theme.boxSharp.teeUp) + h.repeat(rightCol) + br);
|
|
176
194
|
} else {
|
|
177
195
|
lines.push(bl + h.repeat(leftCol) + br);
|
|
178
196
|
}
|
|
@@ -180,40 +198,22 @@ export class WelcomeComponent implements Component {
|
|
|
180
198
|
return lines;
|
|
181
199
|
}
|
|
182
200
|
|
|
183
|
-
/**
|
|
184
|
-
#
|
|
185
|
-
const
|
|
186
|
-
|
|
187
|
-
return truncateToWidth(text, width);
|
|
188
|
-
}
|
|
189
|
-
const leftPad = Math.floor((width - visLen) / 2);
|
|
190
|
-
const rightPad = width - visLen - leftPad;
|
|
191
|
-
return padding(leftPad) + text + padding(rightPad);
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
/** Apply magenta→cyan gradient to a string */
|
|
195
|
-
#gradientLine(line: string): string {
|
|
196
|
-
const colors = [
|
|
197
|
-
"\x1b[38;5;199m", // bright magenta
|
|
198
|
-
"\x1b[38;5;171m", // magenta-purple
|
|
199
|
-
"\x1b[38;5;135m", // purple
|
|
200
|
-
"\x1b[38;5;99m", // purple-blue
|
|
201
|
-
"\x1b[38;5;75m", // cyan-blue
|
|
202
|
-
"\x1b[38;5;51m", // bright cyan
|
|
203
|
-
];
|
|
201
|
+
/** Apply F5 branding colors: ▓→red solid, █→bold white, ▒→red, outlines→red */
|
|
202
|
+
#f5ColorLine(line: string): string {
|
|
203
|
+
const red = "\x1b[38;5;160m"; // F5 red (#ca260a)
|
|
204
|
+
const white = "\x1b[1;37m"; // bold white
|
|
204
205
|
const reset = "\x1b[0m";
|
|
205
206
|
|
|
206
207
|
let result = "";
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
result += colors[colorIdx] + char + reset;
|
|
208
|
+
for (const char of line) {
|
|
209
|
+
if (char === "▓") {
|
|
210
|
+
result += `${red}\u2588${reset}`; // render as solid block in red
|
|
211
|
+
} else if (char === "█") {
|
|
212
|
+
result += `${white}\u2588${reset}`; // solid block in bold white
|
|
213
|
+
} else if (char === "▒") {
|
|
214
|
+
result += `${red}\u2592${reset}`; // medium shade in red
|
|
215
|
+
} else if ("()|_".includes(char)) {
|
|
216
|
+
result += `${red}${char}${reset}`; // outlines in red
|
|
217
217
|
} else {
|
|
218
218
|
result += char;
|
|
219
219
|
}
|
|
@@ -219,14 +219,17 @@ export class InputController {
|
|
|
219
219
|
if (!text) return;
|
|
220
220
|
|
|
221
221
|
// Handle built-in slash commands
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
) {
|
|
222
|
+
const slashResult = await executeBuiltinSlashCommand(text, {
|
|
223
|
+
ctx: this.ctx,
|
|
224
|
+
handleBackgroundCommand: () => this.handleBackgroundCommand(),
|
|
225
|
+
});
|
|
226
|
+
if (slashResult === true) {
|
|
228
227
|
return;
|
|
229
228
|
}
|
|
229
|
+
if (typeof slashResult === "string") {
|
|
230
|
+
// Command handled but returned remaining text to use as prompt
|
|
231
|
+
text = slashResult;
|
|
232
|
+
}
|
|
230
233
|
|
|
231
234
|
// Handle skill commands (/skill:name [args])
|
|
232
235
|
if (text.startsWith("/skill:")) {
|
|
@@ -96,6 +96,8 @@ import porcelain from "./porcelain.json" with { type: "json" };
|
|
|
96
96
|
import quartz from "./quartz.json" with { type: "json" };
|
|
97
97
|
import sandstone from "./sandstone.json" with { type: "json" };
|
|
98
98
|
import titanium from "./titanium.json" with { type: "json" };
|
|
99
|
+
import xcshDark from "./xcsh-dark.json" with { type: "json" };
|
|
100
|
+
import xcshLight from "./xcsh-light.json" with { type: "json" };
|
|
99
101
|
|
|
100
102
|
export const defaultThemes = {
|
|
101
103
|
alabaster: alabaster,
|
|
@@ -196,4 +198,6 @@ export const defaultThemes = {
|
|
|
196
198
|
quartz: quartz,
|
|
197
199
|
sandstone: sandstone,
|
|
198
200
|
titanium: titanium,
|
|
201
|
+
"xcsh-dark": xcshDark,
|
|
202
|
+
"xcsh-light": xcshLight,
|
|
199
203
|
};
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://raw.githubusercontent.com/f5xc-salesdemos/xcsh/main/packages/coding-agent/theme-schema.json",
|
|
3
|
+
"name": "xcsh-dark",
|
|
4
|
+
"vars": {
|
|
5
|
+
"f5Red": "#ca260a",
|
|
6
|
+
"f5DarkRed": "#8a1a07",
|
|
7
|
+
"charcoal": "#151820",
|
|
8
|
+
"deepCharcoal": "#0f1216",
|
|
9
|
+
"brightWhite": "#e8ecf4",
|
|
10
|
+
"coolGray": "#9ca3b0",
|
|
11
|
+
"warmAmber": "#ffb347",
|
|
12
|
+
"signalGreen": "#00ff88",
|
|
13
|
+
"alertRed": "#ff4757",
|
|
14
|
+
"subtleGray": "#2a3038"
|
|
15
|
+
},
|
|
16
|
+
"colors": {
|
|
17
|
+
"accent": "f5Red",
|
|
18
|
+
"border": "subtleGray",
|
|
19
|
+
"borderAccent": "f5Red",
|
|
20
|
+
"borderMuted": "f5Red",
|
|
21
|
+
"success": "signalGreen",
|
|
22
|
+
"error": "alertRed",
|
|
23
|
+
"warning": "warmAmber",
|
|
24
|
+
"muted": "coolGray",
|
|
25
|
+
"dim": "#6b7280",
|
|
26
|
+
"text": "",
|
|
27
|
+
"thinkingText": "coolGray",
|
|
28
|
+
"selectedBg": "f5DarkRed",
|
|
29
|
+
"userMessageBg": "deepCharcoal",
|
|
30
|
+
"userMessageText": "",
|
|
31
|
+
"customMessageBg": "subtleGray",
|
|
32
|
+
"customMessageText": "",
|
|
33
|
+
"customMessageLabel": "warmAmber",
|
|
34
|
+
"toolPendingBg": "deepCharcoal",
|
|
35
|
+
"toolSuccessBg": "deepCharcoal",
|
|
36
|
+
"toolErrorBg": "#1a0f10",
|
|
37
|
+
"toolTitle": "",
|
|
38
|
+
"toolOutput": "coolGray",
|
|
39
|
+
"mdHeading": "f5Red",
|
|
40
|
+
"mdLink": "f5Red",
|
|
41
|
+
"mdLinkUrl": "f5DarkRed",
|
|
42
|
+
"mdCode": "signalGreen",
|
|
43
|
+
"mdCodeBlock": "coolGray",
|
|
44
|
+
"mdCodeBlockBorder": "subtleGray",
|
|
45
|
+
"mdQuote": "coolGray",
|
|
46
|
+
"mdQuoteBorder": "subtleGray",
|
|
47
|
+
"mdHr": "subtleGray",
|
|
48
|
+
"mdListBullet": "f5Red",
|
|
49
|
+
"toolDiffAdded": "signalGreen",
|
|
50
|
+
"toolDiffRemoved": "alertRed",
|
|
51
|
+
"toolDiffContext": "coolGray",
|
|
52
|
+
"syntaxComment": "#6b7280",
|
|
53
|
+
"syntaxKeyword": "f5Red",
|
|
54
|
+
"syntaxFunction": "signalGreen",
|
|
55
|
+
"syntaxVariable": "brightWhite",
|
|
56
|
+
"syntaxString": "warmAmber",
|
|
57
|
+
"syntaxNumber": "warmAmber",
|
|
58
|
+
"syntaxType": "f5Red",
|
|
59
|
+
"syntaxOperator": "f5Red",
|
|
60
|
+
"syntaxPunctuation": "coolGray",
|
|
61
|
+
"thinkingOff": "#4a5058",
|
|
62
|
+
"thinkingMinimal": "#5a6068",
|
|
63
|
+
"thinkingLow": "#6a7078",
|
|
64
|
+
"thinkingMedium": "coolGray",
|
|
65
|
+
"thinkingHigh": "f5Red",
|
|
66
|
+
"thinkingXhigh": "warmAmber",
|
|
67
|
+
"bashMode": "signalGreen",
|
|
68
|
+
"statusLineBg": "deepCharcoal",
|
|
69
|
+
"statusLineSep": "subtleGray",
|
|
70
|
+
"statusLineModel": "f5Red",
|
|
71
|
+
"statusLinePath": "brightWhite",
|
|
72
|
+
"statusLineGitClean": "signalGreen",
|
|
73
|
+
"statusLineGitDirty": "warmAmber",
|
|
74
|
+
"statusLineContext": "coolGray",
|
|
75
|
+
"statusLineSpend": "warmAmber",
|
|
76
|
+
"statusLineStaged": "signalGreen",
|
|
77
|
+
"statusLineDirty": "warmAmber",
|
|
78
|
+
"statusLineUntracked": "coolGray",
|
|
79
|
+
"statusLineOutput": "f5DarkRed",
|
|
80
|
+
"statusLineCost": "warmAmber",
|
|
81
|
+
"statusLineSubagents": "f5Red",
|
|
82
|
+
"pythonMode": "#f0c040"
|
|
83
|
+
},
|
|
84
|
+
"export": {
|
|
85
|
+
"pageBg": "charcoal",
|
|
86
|
+
"cardBg": "deepCharcoal",
|
|
87
|
+
"infoBg": "subtleGray"
|
|
88
|
+
}
|
|
89
|
+
}
|