@f5xc-salesdemos/xcsh 14.4.0 → 14.6.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/autoresearch/dashboard.ts +9 -6
- package/src/autoresearch/tools/init-experiment.ts +1 -1
- package/src/autoresearch/tools/log-experiment.ts +1 -1
- package/src/config/model-registry.ts +1 -1
- package/src/config/settings-schema.ts +12 -1
- package/src/debug/index.ts +8 -6
- package/src/debug/log-viewer.ts +4 -4
- package/src/edit/renderer.ts +2 -2
- package/src/exa/render.ts +3 -3
- package/src/exec/bash-executor.ts +37 -2
- package/src/extensibility/hooks/types.ts +1 -1
- package/src/lsp/render.ts +8 -8
- package/src/modes/components/agent-dashboard.ts +11 -7
- package/src/modes/components/bordered-loader.ts +1 -1
- package/src/modes/components/btw-panel.ts +1 -1
- package/src/modes/components/extensions/extension-dashboard.ts +1 -1
- package/src/modes/components/extensions/extension-list.ts +4 -4
- package/src/modes/components/extensions/inspector-panel.ts +11 -11
- package/src/modes/components/history-search.ts +1 -1
- package/src/modes/components/hook-editor.ts +1 -1
- package/src/modes/components/hook-input.ts +3 -1
- package/src/modes/components/hook-selector.ts +5 -3
- package/src/modes/components/login-dialog.ts +1 -1
- package/src/modes/components/mcp-add-wizard.ts +32 -32
- package/src/modes/components/model-selector.ts +4 -4
- package/src/modes/components/oauth-selector.ts +2 -2
- package/src/modes/components/plugin-settings.ts +4 -4
- package/src/modes/components/read-tool-group.ts +1 -1
- package/src/modes/components/session-observer-overlay.ts +5 -3
- package/src/modes/components/session-selector.ts +1 -1
- package/src/modes/components/settings-selector.ts +2 -2
- package/src/modes/components/status-line/presets.ts +11 -0
- package/src/modes/components/status-line/segments.ts +55 -26
- package/src/modes/components/status-line/types.ts +12 -1
- package/src/modes/components/status-line-segment-editor.ts +5 -4
- package/src/modes/components/status-line.ts +135 -51
- package/src/modes/components/tree-selector.ts +4 -4
- package/src/modes/components/user-message-selector.ts +1 -1
- package/src/modes/components/welcome.ts +3 -3
- package/src/modes/controllers/command-controller.ts +23 -10
- package/src/modes/controllers/event-controller.ts +1 -1
- package/src/modes/controllers/extension-ui-controller.ts +3 -3
- package/src/modes/controllers/input-controller.ts +2 -2
- package/src/modes/controllers/mcp-command-controller.ts +26 -24
- package/src/modes/controllers/selector-controller.ts +2 -2
- package/src/modes/controllers/ssh-command-controller.ts +10 -10
- package/src/modes/interactive-mode.ts +28 -42
- package/src/modes/shared.ts +1 -1
- package/src/modes/theme/defaults/xcsh-dark.json +35 -19
- package/src/modes/theme/defaults/xcsh-light.json +3 -0
- package/src/modes/theme/theme.ts +80 -7
- package/src/modes/utils/ui-helpers.ts +3 -3
- package/src/sdk.ts +2 -3
- package/src/task/render.ts +3 -3
- package/src/tools/ask.ts +2 -2
- package/src/tools/ast-edit.ts +1 -1
- package/src/tools/ast-grep.ts +1 -1
- package/src/tools/bash-interactive.ts +1 -1
- package/src/tools/bash.ts +8 -1
- package/src/tools/find.ts +1 -1
- package/src/tools/gh-renderer.ts +4 -4
- package/src/tools/grep.ts +1 -1
- package/src/tools/inspect-image-renderer.ts +2 -2
- package/src/tools/python.ts +1 -1
- package/src/tools/render-utils.ts +12 -4
- package/src/tools/renderers.ts +5 -2
- package/src/tools/review.ts +1 -1
- package/src/tools/search-tool-bm25.ts +1 -1
- package/src/tools/todo-write.ts +2 -2
- package/src/tools/write.ts +2 -2
- package/src/tui/code-cell.ts +1 -1
- package/src/tui/file-list.ts +2 -2
- package/src/tui/output-block.ts +2 -2
- package/src/tui/status-line.ts +1 -1
- package/src/utils/gitstatus.ts +140 -0
- package/src/web/search/render.ts +1 -1
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@f5xc-salesdemos/xcsh",
|
|
4
|
-
"version": "14.
|
|
4
|
+
"version": "14.6.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.
|
|
50
|
-
"@f5xc-salesdemos/pi-agent-core": "14.
|
|
51
|
-
"@f5xc-salesdemos/pi-ai": "14.
|
|
52
|
-
"@f5xc-salesdemos/pi-natives": "14.
|
|
53
|
-
"@f5xc-salesdemos/pi-tui": "14.
|
|
54
|
-
"@f5xc-salesdemos/pi-utils": "14.
|
|
49
|
+
"@f5xc-salesdemos/xcsh-stats": "14.6.0",
|
|
50
|
+
"@f5xc-salesdemos/pi-agent-core": "14.6.0",
|
|
51
|
+
"@f5xc-salesdemos/pi-ai": "14.6.0",
|
|
52
|
+
"@f5xc-salesdemos/pi-natives": "14.6.0",
|
|
53
|
+
"@f5xc-salesdemos/pi-tui": "14.6.0",
|
|
54
|
+
"@f5xc-salesdemos/pi-utils": "14.6.0",
|
|
55
55
|
"@sinclair/typebox": "^0.34",
|
|
56
56
|
"@xterm/headless": "^6.0",
|
|
57
57
|
"ajv": "^8.18",
|
|
@@ -123,7 +123,7 @@ export function createDashboardController(): DashboardController {
|
|
|
123
123
|
}
|
|
124
124
|
|
|
125
125
|
function renderRunningOnly(runtime: AutoresearchRuntime, state: ExperimentState, theme: Theme): string {
|
|
126
|
-
const parts = [theme.fg("
|
|
126
|
+
const parts = [theme.fg("contentAccent", "autoresearch"), theme.fg("warning", " running...")];
|
|
127
127
|
if (state.name) {
|
|
128
128
|
parts.push(theme.fg("dim", ` | ${replaceTabs(state.name)}`));
|
|
129
129
|
}
|
|
@@ -148,13 +148,16 @@ function renderExpandedHeader(runtime: AutoresearchRuntime, width: number, theme
|
|
|
148
148
|
const label = state.name ? ` autoresearch: ${replaceTabs(state.name)} ` : " autoresearch ";
|
|
149
149
|
const hint = theme.fg("dim", ` ctrl+x collapse ctrl+shift+x overlay${status ? ` ${status}` : ""} `);
|
|
150
150
|
const fillWidth = Math.max(0, width - visibleWidth(label) - visibleWidth(hint));
|
|
151
|
-
return truncateToWidth(
|
|
151
|
+
return truncateToWidth(
|
|
152
|
+
theme.fg("contentAccent", label) + theme.fg("borderMuted", "-".repeat(fillWidth)) + hint,
|
|
153
|
+
width,
|
|
154
|
+
);
|
|
152
155
|
}
|
|
153
156
|
|
|
154
157
|
function renderCollapsedLine(runtime: AutoresearchRuntime, state: ExperimentState, theme: Theme): string {
|
|
155
158
|
if (runtime.lastRunSummary) {
|
|
156
159
|
const parts = [
|
|
157
|
-
theme.fg("
|
|
160
|
+
theme.fg("contentAccent", "autoresearch"),
|
|
158
161
|
theme.fg("warning", ` pending run #${runtime.lastRunSummary.runNumber}`),
|
|
159
162
|
theme.fg("dim", runtime.lastRunSummary.passed ? " pass" : " fail"),
|
|
160
163
|
];
|
|
@@ -174,7 +177,7 @@ function renderCollapsedLine(runtime: AutoresearchRuntime, state: ExperimentStat
|
|
|
174
177
|
}
|
|
175
178
|
if (state.results.length === 0) {
|
|
176
179
|
const modeStatus = runtime.autoresearchMode ? "baseline pending" : "mode off";
|
|
177
|
-
const parts = [theme.fg("
|
|
180
|
+
const parts = [theme.fg("contentAccent", "autoresearch"), theme.fg("warning", ` ${modeStatus}`)];
|
|
178
181
|
if (state.name) {
|
|
179
182
|
parts.push(theme.fg("dim", ` | ${replaceTabs(state.name)}`));
|
|
180
183
|
}
|
|
@@ -190,7 +193,7 @@ function renderCollapsedLine(runtime: AutoresearchRuntime, state: ExperimentStat
|
|
|
190
193
|
const best = findBestResult(state);
|
|
191
194
|
const archivedRuns = Math.max(0, state.results.length - current.length);
|
|
192
195
|
const parts = [
|
|
193
|
-
theme.fg("
|
|
196
|
+
theme.fg("contentAccent", "autoresearch"),
|
|
194
197
|
theme.fg("muted", ` ${current.length} runs`),
|
|
195
198
|
theme.fg("success", ` ${kept} kept`),
|
|
196
199
|
];
|
|
@@ -356,7 +359,7 @@ function renderResultRow(
|
|
|
356
359
|
const statusColor = result.status === "keep" ? "success" : result.status === "discard" ? "warning" : "error";
|
|
357
360
|
const line =
|
|
358
361
|
`${theme.fg("dim", String(runNumber).padEnd(4))}` +
|
|
359
|
-
`${theme.fg("
|
|
362
|
+
`${theme.fg("contentAccent", (result.commit || "-").padEnd(10))}` +
|
|
360
363
|
`${theme.fg(statusColor, formatNum(result.metric, state.metricUnit).padEnd(12))}` +
|
|
361
364
|
`${secondary}` +
|
|
362
365
|
`${theme.fg(statusColor, result.status.padEnd(14))}` +
|
|
@@ -381,7 +381,7 @@ export function createInitExperimentTool(
|
|
|
381
381
|
}
|
|
382
382
|
|
|
383
383
|
function renderInitCall(name: string, theme: Theme): string {
|
|
384
|
-
return `${theme.fg("toolTitle", theme.bold("init_experiment"))} ${theme.fg("
|
|
384
|
+
return `${theme.fg("toolTitle", theme.bold("init_experiment"))} ${theme.fg("contentAccent", truncateToWidth(replaceTabs(name), 100))}`;
|
|
385
385
|
}
|
|
386
386
|
|
|
387
387
|
function collectLoggedRunNumbers(results: ExperimentState["results"]): Set<number> {
|
|
@@ -767,7 +767,7 @@ function renderSummary(details: LogDetails, theme: Theme): string {
|
|
|
767
767
|
const { experiment, state } = details;
|
|
768
768
|
const color = experiment.status === "keep" ? "success" : experiment.status === "discard" ? "warning" : "error";
|
|
769
769
|
let summary = `${theme.fg(color, experiment.status.toUpperCase())} ${theme.fg("muted", truncateToWidth(replaceTabs(experiment.description), 100))}`;
|
|
770
|
-
summary += ` ${theme.fg("
|
|
770
|
+
summary += ` ${theme.fg("contentAccent", `${state.metricName}=${formatNum(experiment.metric, state.metricUnit)}`)}`;
|
|
771
771
|
if (state.bestMetric !== null) {
|
|
772
772
|
summary += ` ${theme.fg("dim", `baseline ${formatNum(state.bestMetric, state.metricUnit)}`)}`;
|
|
773
773
|
}
|
|
@@ -50,7 +50,7 @@ export interface ModelRoleInfo {
|
|
|
50
50
|
export const MODEL_ROLES: Record<ModelRole, ModelRoleInfo> = {
|
|
51
51
|
default: { tag: "DEFAULT", name: "Default", color: "success" },
|
|
52
52
|
smol: { tag: "SMOL", name: "Fast", color: "warning" },
|
|
53
|
-
slow: { tag: "SLOW", name: "Thinking", color: "
|
|
53
|
+
slow: { tag: "SLOW", name: "Thinking", color: "contentAccent" },
|
|
54
54
|
vision: { tag: "VISION", name: "Vision", color: "error" },
|
|
55
55
|
plan: { tag: "PLAN", name: "Architect", color: "muted" },
|
|
56
56
|
commit: { tag: "COMMIT", name: "Commit", color: "dim" },
|
|
@@ -55,6 +55,7 @@ export const TAB_METADATA: Record<SettingTab, { label: string; icon: `tab.${stri
|
|
|
55
55
|
|
|
56
56
|
/** Status line segment identifiers */
|
|
57
57
|
export type StatusLineSegmentId =
|
|
58
|
+
| "os_icon"
|
|
58
59
|
| "pi"
|
|
59
60
|
| "model"
|
|
60
61
|
| "plan_mode"
|
|
@@ -278,7 +279,7 @@ export const SETTINGS_SCHEMA = {
|
|
|
278
279
|
// Status line
|
|
279
280
|
"statusLine.preset": {
|
|
280
281
|
type: "enum",
|
|
281
|
-
values: ["default", "minimal", "compact", "full", "nerd", "ascii", "custom"] as const,
|
|
282
|
+
values: ["default", "minimal", "compact", "full", "nerd", "ascii", "xcsh", "custom"] as const,
|
|
282
283
|
default: "default",
|
|
283
284
|
ui: {
|
|
284
285
|
tab: "appearance",
|
|
@@ -647,6 +648,16 @@ export const SETTINGS_SCHEMA = {
|
|
|
647
648
|
},
|
|
648
649
|
},
|
|
649
650
|
|
|
651
|
+
"startup.clearScreen": {
|
|
652
|
+
type: "boolean",
|
|
653
|
+
default: false,
|
|
654
|
+
ui: {
|
|
655
|
+
tab: "interaction",
|
|
656
|
+
label: "Clear Screen on Startup",
|
|
657
|
+
description: "Clear the terminal screen when xcsh starts",
|
|
658
|
+
},
|
|
659
|
+
},
|
|
660
|
+
|
|
650
661
|
"startup.checkUpdate": {
|
|
651
662
|
type: "boolean",
|
|
652
663
|
default: true,
|
package/src/debug/index.ts
CHANGED
|
@@ -54,7 +54,7 @@ export class DebugSelectorComponent extends Container {
|
|
|
54
54
|
|
|
55
55
|
// Title
|
|
56
56
|
this.addChild(new DynamicBorder());
|
|
57
|
-
this.addChild(new Text(theme.bold(theme.fg("
|
|
57
|
+
this.addChild(new Text(theme.bold(theme.fg("contentAccent", "Debug Tools")), 1, 0));
|
|
58
58
|
this.addChild(new Spacer(1));
|
|
59
59
|
|
|
60
60
|
// Select list
|
|
@@ -121,7 +121,9 @@ export class DebugSelectorComponent extends Container {
|
|
|
121
121
|
|
|
122
122
|
// Show message and wait for keypress
|
|
123
123
|
this.ctx.chatContainer.addChild(new Spacer(1));
|
|
124
|
-
this.ctx.chatContainer.addChild(
|
|
124
|
+
this.ctx.chatContainer.addChild(
|
|
125
|
+
new Text(theme.fg("contentAccent", `${theme.status.info} CPU profiling started`), 1, 0),
|
|
126
|
+
);
|
|
125
127
|
this.ctx.chatContainer.addChild(new Spacer(1));
|
|
126
128
|
this.ctx.chatContainer.addChild(
|
|
127
129
|
new Text(theme.fg("muted", "Reproduce the performance issue, then press Enter to stop profiling."), 1, 0),
|
|
@@ -150,7 +152,7 @@ export class DebugSelectorComponent extends Container {
|
|
|
150
152
|
// Stop profiling and create report
|
|
151
153
|
const loader = new Loader(
|
|
152
154
|
this.ctx.ui,
|
|
153
|
-
spinner => theme.fg("
|
|
155
|
+
spinner => theme.fg("spinnerAccent", spinner),
|
|
154
156
|
text => theme.fg("muted", text),
|
|
155
157
|
"Generating report...",
|
|
156
158
|
getSymbolTheme().spinnerFrames,
|
|
@@ -215,7 +217,7 @@ export class DebugSelectorComponent extends Container {
|
|
|
215
217
|
async #handleDumpReport(): Promise<void> {
|
|
216
218
|
const loader = new Loader(
|
|
217
219
|
this.ctx.ui,
|
|
218
|
-
spinner => theme.fg("
|
|
220
|
+
spinner => theme.fg("spinnerAccent", spinner),
|
|
219
221
|
text => theme.fg("muted", text),
|
|
220
222
|
"Creating report bundle...",
|
|
221
223
|
getSymbolTheme().spinnerFrames,
|
|
@@ -250,7 +252,7 @@ export class DebugSelectorComponent extends Container {
|
|
|
250
252
|
async #handleMemoryReport(): Promise<void> {
|
|
251
253
|
const loader = new Loader(
|
|
252
254
|
this.ctx.ui,
|
|
253
|
-
spinner => theme.fg("
|
|
255
|
+
spinner => theme.fg("spinnerAccent", spinner),
|
|
254
256
|
text => theme.fg("muted", text),
|
|
255
257
|
"Generating heap snapshot...",
|
|
256
258
|
getSymbolTheme().spinnerFrames,
|
|
@@ -386,7 +388,7 @@ export class DebugSelectorComponent extends Container {
|
|
|
386
388
|
// Clear cache
|
|
387
389
|
const loader = new Loader(
|
|
388
390
|
this.ctx.ui,
|
|
389
|
-
spinner => theme.fg("
|
|
391
|
+
spinner => theme.fg("spinnerAccent", spinner),
|
|
390
392
|
text => theme.fg("muted", text),
|
|
391
393
|
"Clearing artifact cache...",
|
|
392
394
|
getSymbolTheme().spinnerFrames,
|
package/src/debug/log-viewer.ts
CHANGED
|
@@ -634,7 +634,7 @@ export class DebugLogViewerComponent implements Component {
|
|
|
634
634
|
|
|
635
635
|
#filterText(): string {
|
|
636
636
|
const sanitized = replaceTabs(sanitizeText(this.#model.filterQuery));
|
|
637
|
-
const query = sanitized.length === 0 ? "" : theme.fg("
|
|
637
|
+
const query = sanitized.length === 0 ? "" : theme.fg("contentAccent", sanitized);
|
|
638
638
|
const pidStatus = this.#model.isProcessFilterEnabled()
|
|
639
639
|
? theme.fg("success", "pid:on")
|
|
640
640
|
: theme.fg("muted", "pid:off");
|
|
@@ -740,7 +740,7 @@ export class DebugLogViewerComponent implements Component {
|
|
|
740
740
|
|
|
741
741
|
if (row.kind === "load-older") {
|
|
742
742
|
const active = this.#model.cursorRowIndex === rowIndex;
|
|
743
|
-
const marker = active ? theme.fg("
|
|
743
|
+
const marker = active ? theme.fg("chromeAccent", "❯") : " ";
|
|
744
744
|
const prefix = `${marker} `;
|
|
745
745
|
const contentWidth = Math.max(1, innerWidth - visibleWidth(prefix));
|
|
746
746
|
const label = truncateToWidth(LOAD_OLDER_LABEL, contentWidth);
|
|
@@ -756,8 +756,8 @@ export class DebugLogViewerComponent implements Component {
|
|
|
756
756
|
const cursorLogIndex = this.#model.cursorLogIndex;
|
|
757
757
|
const active = cursorLogIndex !== undefined && cursorLogIndex === logIndex;
|
|
758
758
|
const expanded = this.#model.isExpanded(logIndex);
|
|
759
|
-
const marker = active ? theme.fg("
|
|
760
|
-
const fold = expanded ? theme.fg("
|
|
759
|
+
const marker = active ? theme.fg("chromeAccent", "❯") : selected ? theme.fg("chromeAccent", "•") : " ";
|
|
760
|
+
const fold = expanded ? theme.fg("contentAccent", "▾") : theme.fg("muted", "▸");
|
|
761
761
|
const prefix = `${marker}${fold} `;
|
|
762
762
|
const contentWidth = Math.max(1, innerWidth - visibleWidth(prefix));
|
|
763
763
|
|
package/src/edit/renderer.ts
CHANGED
|
@@ -128,14 +128,14 @@ function formatEditPathDisplay(
|
|
|
128
128
|
uiTheme: Theme,
|
|
129
129
|
options?: { rename?: string; firstChangedLine?: number },
|
|
130
130
|
): string {
|
|
131
|
-
let pathDisplay = rawPath ? uiTheme.fg("
|
|
131
|
+
let pathDisplay = rawPath ? uiTheme.fg("contentAccent", shortenPath(rawPath)) : uiTheme.fg("toolOutput", "…");
|
|
132
132
|
|
|
133
133
|
if (options?.firstChangedLine) {
|
|
134
134
|
pathDisplay += uiTheme.fg("warning", `:${options.firstChangedLine}`);
|
|
135
135
|
}
|
|
136
136
|
|
|
137
137
|
if (options?.rename) {
|
|
138
|
-
pathDisplay += ` ${uiTheme.fg("dim", "→")} ${uiTheme.fg("
|
|
138
|
+
pathDisplay += ` ${uiTheme.fg("dim", "→")} ${uiTheme.fg("contentAccent", shortenPath(options.rename))}`;
|
|
139
139
|
}
|
|
140
140
|
|
|
141
141
|
return pathDisplay;
|
package/src/exa/render.ts
CHANGED
|
@@ -164,7 +164,7 @@ export function renderExaResult(
|
|
|
164
164
|
const domain = res.url ? getDomain(res.url) : "";
|
|
165
165
|
const domainPart = domain ? uiTheme.fg("dim", ` (${domain})`) : "";
|
|
166
166
|
|
|
167
|
-
text += `\n ${uiTheme.fg("dim", branch)} ${uiTheme.fg("
|
|
167
|
+
text += `\n ${uiTheme.fg("dim", branch)} ${uiTheme.fg("contentAccent", title)}${domainPart}`;
|
|
168
168
|
|
|
169
169
|
if (res.url) {
|
|
170
170
|
text += `\n ${uiTheme.fg("dim", cont)} ${uiTheme.fg("dim", uiTheme.tree.hook)} ${uiTheme.fg(
|
|
@@ -206,7 +206,7 @@ export function renderExaResult(
|
|
|
206
206
|
|
|
207
207
|
if (res.highlights?.length) {
|
|
208
208
|
text += `\n ${uiTheme.fg("dim", cont)} ${uiTheme.fg("dim", uiTheme.tree.hook)} ${uiTheme.fg(
|
|
209
|
-
"
|
|
209
|
+
"contentAccent",
|
|
210
210
|
"Highlights",
|
|
211
211
|
)}`;
|
|
212
212
|
const maxHighlights = Math.min(res.highlights.length, 3);
|
|
@@ -235,7 +235,7 @@ export function renderExaCall(args: Record<string, unknown>, toolName: string, u
|
|
|
235
235
|
const query = typeof args.query === "string" ? truncateToWidth(args.query, 80) : "?";
|
|
236
236
|
const numResults = typeof args.num_results === "number" ? args.num_results : undefined;
|
|
237
237
|
|
|
238
|
-
let text = `${uiTheme.fg("toolTitle", toolLabel)} ${uiTheme.fg("
|
|
238
|
+
let text = `${uiTheme.fg("toolTitle", toolLabel)} ${uiTheme.fg("contentAccent", query)}`;
|
|
239
239
|
if (numResults !== undefined) {
|
|
240
240
|
text += ` ${uiTheme.fg("muted", `results:${numResults}`)}`;
|
|
241
241
|
}
|
|
@@ -34,6 +34,8 @@ export interface BashResult {
|
|
|
34
34
|
outputLines: number;
|
|
35
35
|
outputBytes: number;
|
|
36
36
|
artifactId?: string;
|
|
37
|
+
/** Actual working directory after the command ran (persistent shell only). */
|
|
38
|
+
newCwd?: string;
|
|
37
39
|
}
|
|
38
40
|
|
|
39
41
|
const HARD_TIMEOUT_GRACE_MS = 5_000;
|
|
@@ -62,7 +64,11 @@ export async function executeBash(command: string, options?: BashExecutorOptions
|
|
|
62
64
|
|
|
63
65
|
// Apply command prefix if configured
|
|
64
66
|
const prefixedCommand = prefix ? `${prefix} ${command}` : command;
|
|
65
|
-
|
|
67
|
+
|
|
68
|
+
// CWD capture sentinels — used to detect directory changes from cd commands.
|
|
69
|
+
// Only appended for persistent shell sessions (one-shot shells don't persist CWD).
|
|
70
|
+
const CWD_SENTINEL_START = "__XCSH_CWD__:";
|
|
71
|
+
const CWD_SENTINEL_END = ":__XCSH_CWD_END__";
|
|
66
72
|
|
|
67
73
|
// Create output sink for truncation and artifact handling
|
|
68
74
|
const sink = new OutputSink({
|
|
@@ -100,6 +106,11 @@ export async function executeBash(command: string, options?: BashExecutorOptions
|
|
|
100
106
|
shellSession = new Shell({ sessionEnv: shellEnv, snapshotPath: snapshotPath ?? undefined });
|
|
101
107
|
shellSessions.set(sessionKey, shellSession);
|
|
102
108
|
}
|
|
109
|
+
|
|
110
|
+
// Append CWD sentinel only for persistent shell sessions
|
|
111
|
+
const finalCommand = shellSession
|
|
112
|
+
? `${prefixedCommand}\nprintf '${CWD_SENTINEL_START}%s${CWD_SENTINEL_END}\\n' "$PWD"`
|
|
113
|
+
: prefixedCommand;
|
|
103
114
|
const userSignal = options?.signal;
|
|
104
115
|
const runAbortController = new AbortController();
|
|
105
116
|
const abortCurrentExecution = () => {
|
|
@@ -204,11 +215,35 @@ export async function executeBash(command: string, options?: BashExecutorOptions
|
|
|
204
215
|
};
|
|
205
216
|
}
|
|
206
217
|
|
|
218
|
+
// Parse CWD sentinel from output, strip it from the displayed result.
|
|
219
|
+
const dumpResult = await sink.dump();
|
|
220
|
+
let newCwd: string | undefined;
|
|
221
|
+
if (shellSession) {
|
|
222
|
+
const sentinelIdx = dumpResult.output.lastIndexOf(CWD_SENTINEL_START);
|
|
223
|
+
if (sentinelIdx !== -1) {
|
|
224
|
+
const endIdx = dumpResult.output.indexOf(CWD_SENTINEL_END, sentinelIdx);
|
|
225
|
+
if (endIdx !== -1) {
|
|
226
|
+
const captured = dumpResult.output.slice(sentinelIdx + CWD_SENTINEL_START.length, endIdx).trim();
|
|
227
|
+
if (captured) newCwd = captured;
|
|
228
|
+
// Strip the sentinel from displayed output (from sentinel start to end of its line)
|
|
229
|
+
const afterLine = dumpResult.output.indexOf("\n", endIdx + CWD_SENTINEL_END.length);
|
|
230
|
+
const stripEnd = afterLine === -1 ? dumpResult.output.length : afterLine + 1;
|
|
231
|
+
const strippedBytes = stripEnd - sentinelIdx;
|
|
232
|
+
dumpResult.output = dumpResult.output.slice(0, sentinelIdx) + dumpResult.output.slice(stripEnd);
|
|
233
|
+
dumpResult.totalBytes = Math.max(0, dumpResult.totalBytes - strippedBytes);
|
|
234
|
+
dumpResult.totalLines = Math.max(0, dumpResult.totalLines - 1);
|
|
235
|
+
dumpResult.outputBytes = dumpResult.output.length;
|
|
236
|
+
dumpResult.outputLines = Math.max(0, dumpResult.outputLines - 1);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
207
241
|
// Normal completion
|
|
208
242
|
return {
|
|
209
243
|
exitCode: winner.result.exitCode,
|
|
210
244
|
cancelled: false,
|
|
211
|
-
|
|
245
|
+
newCwd,
|
|
246
|
+
...dumpResult,
|
|
212
247
|
};
|
|
213
248
|
} catch (err) {
|
|
214
249
|
resetSession = true;
|
|
@@ -86,7 +86,7 @@ export interface HookUIContext {
|
|
|
86
86
|
*
|
|
87
87
|
* // Async factory with fire-and-forget work
|
|
88
88
|
* const result = await ctx.ui.custom(async (tui, theme, done) => {
|
|
89
|
-
* const loader = new CancellableLoader(tui, theme.fg("
|
|
89
|
+
* const loader = new CancellableLoader(tui, theme.fg("contentAccent"), theme.fg("muted"), "Working...");
|
|
90
90
|
* loader.onAbort = () => done(null);
|
|
91
91
|
* doWork(loader.signal).then(done); // Don't await - fire and forget
|
|
92
92
|
* return loader;
|
package/src/lsp/render.ts
CHANGED
|
@@ -221,7 +221,7 @@ function renderHover(
|
|
|
221
221
|
const afterCode = fullText.slice(fullText.indexOf("```", 3) + 3).trim();
|
|
222
222
|
|
|
223
223
|
const codeLines = highlightCode(code, lang, theme);
|
|
224
|
-
const icon = theme.styledSymbol("status.info", "
|
|
224
|
+
const icon = theme.styledSymbol("status.info", "contentAccent");
|
|
225
225
|
const langLabel = lang ? theme.fg("mdCodeBlockBorder", ` ${lang}`) : "";
|
|
226
226
|
|
|
227
227
|
if (expanded) {
|
|
@@ -438,7 +438,7 @@ function renderReferences(refMatch: RegExpMatchArray, lines: string[], expanded:
|
|
|
438
438
|
const fileCont = isLastFile ? " " : `${theme.tree.vertical} `;
|
|
439
439
|
|
|
440
440
|
const fileMeta = `${locs.length} reference${locs.length !== 1 ? "s" : ""}`;
|
|
441
|
-
output += `\n ${theme.fg("dim", fileBranch)} ${theme.fg("
|
|
441
|
+
output += `\n ${theme.fg("dim", fileBranch)} ${theme.fg("contentAccent", file)} ${theme.fg("dim", fileMeta)}`;
|
|
442
442
|
|
|
443
443
|
if (maxLocsPerFile > 0) {
|
|
444
444
|
const locsToShow = locs.slice(0, maxLocsPerFile);
|
|
@@ -494,7 +494,7 @@ function renderReferences(refMatch: RegExpMatchArray, lines: string[], expanded:
|
|
|
494
494
|
*/
|
|
495
495
|
function renderSymbols(symbolsMatch: RegExpMatchArray, lines: string[], expanded: boolean, theme: Theme): string[] {
|
|
496
496
|
const fileName = symbolsMatch[1];
|
|
497
|
-
const icon = theme.styledSymbol("status.info", "
|
|
497
|
+
const icon = theme.styledSymbol("status.info", "contentAccent");
|
|
498
498
|
|
|
499
499
|
interface SymbolInfo {
|
|
500
500
|
name: string;
|
|
@@ -557,7 +557,7 @@ function renderSymbols(symbolsMatch: RegExpMatchArray, lines: string[], expanded
|
|
|
557
557
|
const isLast = isLastSibling(i);
|
|
558
558
|
const branch = isLast ? theme.tree.last : theme.tree.branch;
|
|
559
559
|
const detailPrefix = isLast ? " " : `${theme.tree.vertical} `;
|
|
560
|
-
output += `\n${prefix}${theme.fg("dim", branch)} ${theme.fg("
|
|
560
|
+
output += `\n${prefix}${theme.fg("dim", branch)} ${theme.fg("contentAccent", sym.icon)} ${theme.fg("contentAccent", sym.name)}`;
|
|
561
561
|
output += `\n${prefix}${theme.fg("dim", detailPrefix)}${theme.fg("muted", `line ${sym.line}`)}`;
|
|
562
562
|
}
|
|
563
563
|
return output.split("\n");
|
|
@@ -572,7 +572,7 @@ function renderSymbols(symbolsMatch: RegExpMatchArray, lines: string[], expanded
|
|
|
572
572
|
const sym = topLevel[i];
|
|
573
573
|
const isLast = i === topLevel.length - 1 && topLevelCount <= 3;
|
|
574
574
|
const branch = isLast ? theme.tree.last : theme.tree.branch;
|
|
575
|
-
output += `\n ${theme.fg("dim", branch)} ${theme.fg("
|
|
575
|
+
output += `\n ${theme.fg("dim", branch)} ${theme.fg("contentAccent", sym.icon)} ${theme.fg("contentAccent", sym.name)} ${theme.fg(
|
|
576
576
|
"muted",
|
|
577
577
|
`line ${sym.line}`,
|
|
578
578
|
)}`;
|
|
@@ -600,7 +600,7 @@ function renderGeneric(text: string, lines: string[], expanded: boolean, theme:
|
|
|
600
600
|
? theme.styledSymbol("status.error", "error")
|
|
601
601
|
: hasSuccess && !hasError
|
|
602
602
|
? theme.styledSymbol("status.success", "success")
|
|
603
|
-
: theme.styledSymbol("status.info", "
|
|
603
|
+
: theme.styledSymbol("status.info", "contentAccent");
|
|
604
604
|
|
|
605
605
|
if (expanded) {
|
|
606
606
|
let output = `${icon} ${theme.fg("dim", "Output")}`;
|
|
@@ -662,14 +662,14 @@ function parseDiagnosticLine(line: string): ParsedDiagnostic | null {
|
|
|
662
662
|
return { file, line: lineNum, col: colNum, severity: severity.toLowerCase(), message };
|
|
663
663
|
}
|
|
664
664
|
|
|
665
|
-
function severityToColor(severity: string): "error" | "warning" | "
|
|
665
|
+
function severityToColor(severity: string): "error" | "warning" | "contentAccent" | "dim" {
|
|
666
666
|
switch (severity) {
|
|
667
667
|
case "error":
|
|
668
668
|
return "error";
|
|
669
669
|
case "warning":
|
|
670
670
|
return "warning";
|
|
671
671
|
case "info":
|
|
672
|
-
return "
|
|
672
|
+
return "contentAccent";
|
|
673
673
|
default:
|
|
674
674
|
return "dim";
|
|
675
675
|
}
|
|
@@ -216,7 +216,7 @@ class AgentListPane implements Component {
|
|
|
216
216
|
let line = ` ${status} ${replaceTabs(agent.name)} ${source}${override}`;
|
|
217
217
|
|
|
218
218
|
if (selected) {
|
|
219
|
-
line = theme.bg("selectedBg", theme.bold(theme.fg("
|
|
219
|
+
line = theme.bg("selectedBg", theme.bold(theme.fg("chromeAccent", line)));
|
|
220
220
|
} else if (agent.disabled) {
|
|
221
221
|
line = theme.fg("dim", line);
|
|
222
222
|
}
|
|
@@ -253,7 +253,7 @@ class AgentInspectorPane implements Component {
|
|
|
253
253
|
? theme.fg("dim", `${theme.status.disabled} Disabled`)
|
|
254
254
|
: theme.fg("success", `${theme.status.enabled} Enabled`);
|
|
255
255
|
|
|
256
|
-
lines.push(theme.bold(theme.fg("
|
|
256
|
+
lines.push(theme.bold(theme.fg("contentAccent", replaceTabs(this.agent.name))));
|
|
257
257
|
lines.push("");
|
|
258
258
|
lines.push(`${theme.fg("muted", "Status:")} ${state}`);
|
|
259
259
|
lines.push(`${theme.fg("muted", "Source:")} ${SOURCE_LABEL[this.agent.source]}`);
|
|
@@ -791,7 +791,7 @@ export class AgentDashboard extends Container {
|
|
|
791
791
|
}
|
|
792
792
|
|
|
793
793
|
#renderCreateInput(): void {
|
|
794
|
-
this.addChild(new Text(theme.bold(theme.fg("
|
|
794
|
+
this.addChild(new Text(theme.bold(theme.fg("contentAccent", " Create New Agent")), 0, 0));
|
|
795
795
|
this.addChild(new Spacer(1));
|
|
796
796
|
this.addChild(new Text(theme.fg("muted", "Describe what the new agent should do:"), 0, 0));
|
|
797
797
|
this.addChild(new Spacer(1));
|
|
@@ -802,7 +802,7 @@ export class AgentDashboard extends Container {
|
|
|
802
802
|
this.addChild(new Text(theme.fg("muted", `Scope: ${this.#createScope}`), 0, 0));
|
|
803
803
|
if (this.#createGenerating) {
|
|
804
804
|
this.addChild(new Spacer(1));
|
|
805
|
-
this.addChild(new Text(theme.fg("
|
|
805
|
+
this.addChild(new Text(theme.fg("contentAccent", "Generating agent specification..."), 0, 0));
|
|
806
806
|
if (this.#createStreamingText) {
|
|
807
807
|
this.addChild(new Spacer(1));
|
|
808
808
|
const maxPreview = Math.max(3, this.terminalHeight - 18);
|
|
@@ -834,7 +834,7 @@ export class AgentDashboard extends Container {
|
|
|
834
834
|
const spec = this.#createSpec;
|
|
835
835
|
if (!spec) return;
|
|
836
836
|
|
|
837
|
-
this.addChild(new Text(theme.bold(theme.fg("
|
|
837
|
+
this.addChild(new Text(theme.bold(theme.fg("contentAccent", " Review Generated Agent")), 0, 0));
|
|
838
838
|
this.addChild(new Spacer(1));
|
|
839
839
|
this.addChild(new Text(theme.fg("muted", `Identifier: ${spec.identifier}`), 0, 0));
|
|
840
840
|
this.addChild(new Text(theme.fg("muted", `Scope: ${this.#createScope}`), 0, 0));
|
|
@@ -882,7 +882,7 @@ export class AgentDashboard extends Container {
|
|
|
882
882
|
#buildLayout(): void {
|
|
883
883
|
this.clear();
|
|
884
884
|
this.addChild(new DynamicBorder());
|
|
885
|
-
this.addChild(new Text(theme.bold(theme.fg("
|
|
885
|
+
this.addChild(new Text(theme.bold(theme.fg("contentAccent", " Agent Control Center")), 0, 0));
|
|
886
886
|
this.addChild(new Text(this.#renderTabBar(), 0, 0));
|
|
887
887
|
this.addChild(new Spacer(1));
|
|
888
888
|
|
|
@@ -911,7 +911,11 @@ export class AgentDashboard extends Container {
|
|
|
911
911
|
const suggestions = this.#getModelSuggestions(draft);
|
|
912
912
|
|
|
913
913
|
this.addChild(
|
|
914
|
-
new Text(
|
|
914
|
+
new Text(
|
|
915
|
+
theme.bold(theme.fg("contentAccent", `Model override: ${replaceTabs(this.#editingAgentName)}`)),
|
|
916
|
+
0,
|
|
917
|
+
0,
|
|
918
|
+
),
|
|
915
919
|
);
|
|
916
920
|
this.addChild(new Spacer(1));
|
|
917
921
|
this.addChild(new Text(theme.fg("muted", "Enter model pattern (empty clears override)"), 0, 0));
|
|
@@ -12,7 +12,7 @@ export class BorderedLoader extends Container {
|
|
|
12
12
|
this.addChild(new DynamicBorder(borderColor));
|
|
13
13
|
this.#loader = new CancellableLoader(
|
|
14
14
|
tui,
|
|
15
|
-
s => theme.fg("
|
|
15
|
+
s => theme.fg("spinnerAccent", s),
|
|
16
16
|
s => theme.fg("muted", s),
|
|
17
17
|
message,
|
|
18
18
|
);
|
|
@@ -66,7 +66,7 @@ export class BtwPanelComponent extends Container {
|
|
|
66
66
|
this.clear();
|
|
67
67
|
this.addChild(new DynamicBorder(str => theme.fg("dim", str)));
|
|
68
68
|
this.addChild(new Spacer(1));
|
|
69
|
-
this.addChild(new Text(theme.fg("
|
|
69
|
+
this.addChild(new Text(theme.fg("contentAccent", replaceTabs(this.#question)), 1, 0));
|
|
70
70
|
this.addChild(new Spacer(1));
|
|
71
71
|
this.addChild(this.#contentComponent());
|
|
72
72
|
this.addChild(new Spacer(1));
|
|
@@ -105,7 +105,7 @@ export class ExtensionDashboard extends Container {
|
|
|
105
105
|
this.addChild(new DynamicBorder());
|
|
106
106
|
|
|
107
107
|
// Title
|
|
108
|
-
this.addChild(new Text(theme.bold(theme.fg("
|
|
108
|
+
this.addChild(new Text(theme.bold(theme.fg("contentAccent", " Extension Control Center")), 0, 0));
|
|
109
109
|
|
|
110
110
|
// Tab bar
|
|
111
111
|
this.addChild(new Text(this.#renderTabBar(), 0, 0));
|
|
@@ -117,7 +117,7 @@ export class ExtensionList implements Component {
|
|
|
117
117
|
// Search bar
|
|
118
118
|
const searchPrefix = theme.fg("muted", "Search: ");
|
|
119
119
|
const searchText = this.#searchQuery || (this.#focused ? "" : theme.fg("dim", "type to filter"));
|
|
120
|
-
const cursor = this.#focused ? theme.fg("
|
|
120
|
+
const cursor = this.#focused ? theme.fg("chromeAccent", "_") : "";
|
|
121
121
|
lines.push(searchPrefix + searchText + cursor);
|
|
122
122
|
lines.push("");
|
|
123
123
|
|
|
@@ -167,7 +167,7 @@ export class ExtensionList implements Component {
|
|
|
167
167
|
let line = `${checkbox} ${icon} ${label} ${badge}`;
|
|
168
168
|
|
|
169
169
|
if (isSelected) {
|
|
170
|
-
line = theme.bold(theme.fg("
|
|
170
|
+
line = theme.bold(theme.fg("contentAccent", line));
|
|
171
171
|
line = theme.bg("selectedBg", line);
|
|
172
172
|
} else if (!item.enabled) {
|
|
173
173
|
line = theme.fg("dim", line);
|
|
@@ -181,7 +181,7 @@ export class ExtensionList implements Component {
|
|
|
181
181
|
let line = `${item.icon} ${item.label} ${countBadge}`;
|
|
182
182
|
|
|
183
183
|
if (isSelected) {
|
|
184
|
-
line = theme.bold(theme.fg("
|
|
184
|
+
line = theme.bold(theme.fg("contentAccent", line));
|
|
185
185
|
line = theme.bg("selectedBg", line);
|
|
186
186
|
} else {
|
|
187
187
|
line = theme.fg("muted", line);
|
|
@@ -205,7 +205,7 @@ export class ExtensionList implements Component {
|
|
|
205
205
|
let line = ` ${stateIcon} `;
|
|
206
206
|
|
|
207
207
|
if (isSelected && !masterDisabled) {
|
|
208
|
-
name = theme.bold(theme.fg("
|
|
208
|
+
name = theme.bold(theme.fg("contentAccent", name));
|
|
209
209
|
} else if (effectivelyDisabled) {
|
|
210
210
|
name = theme.fg("dim", name);
|
|
211
211
|
} else if (ext.state === "shadowed") {
|
|
@@ -27,7 +27,7 @@ export class InspectorPanel implements Component {
|
|
|
27
27
|
const lines: string[] = [];
|
|
28
28
|
|
|
29
29
|
// Name header
|
|
30
|
-
lines.push(theme.bold(theme.fg("
|
|
30
|
+
lines.push(theme.bold(theme.fg("contentAccent", ext.displayName)));
|
|
31
31
|
lines.push("");
|
|
32
32
|
|
|
33
33
|
// Kind badge
|
|
@@ -143,7 +143,7 @@ export class InspectorPanel implements Component {
|
|
|
143
143
|
|
|
144
144
|
// Headers
|
|
145
145
|
if (/^#{1,6}\s/.test(highlighted)) {
|
|
146
|
-
highlighted = theme.bold(theme.fg("
|
|
146
|
+
highlighted = theme.bold(theme.fg("contentAccent", highlighted));
|
|
147
147
|
}
|
|
148
148
|
// Code blocks
|
|
149
149
|
else if (/^```/.test(highlighted)) {
|
|
@@ -151,11 +151,11 @@ export class InspectorPanel implements Component {
|
|
|
151
151
|
}
|
|
152
152
|
// Lists
|
|
153
153
|
else if (/^[\s]*[-*+]\s/.test(highlighted)) {
|
|
154
|
-
highlighted = highlighted.replace(/^([\s]*[-*+]\s)/, theme.fg("
|
|
154
|
+
highlighted = highlighted.replace(/^([\s]*[-*+]\s)/, theme.fg("contentAccent", "$1"));
|
|
155
155
|
}
|
|
156
156
|
// Numbered lists
|
|
157
157
|
else if (/^[\s]*\d+\.\s/.test(highlighted)) {
|
|
158
|
-
highlighted = highlighted.replace(/^([\s]*\d+\.\s)/, theme.fg("
|
|
158
|
+
highlighted = highlighted.replace(/^([\s]*\d+\.\s)/, theme.fg("contentAccent", "$1"));
|
|
159
159
|
}
|
|
160
160
|
|
|
161
161
|
return highlighted;
|
|
@@ -181,7 +181,7 @@ export class InspectorPanel implements Component {
|
|
|
181
181
|
const isRequired = required.has(name);
|
|
182
182
|
const defaultVal = param.default !== undefined ? `Default: ${param.default}` : null;
|
|
183
183
|
|
|
184
|
-
const nameCol = theme.fg("
|
|
184
|
+
const nameCol = theme.fg("contentAccent", name.padEnd(12));
|
|
185
185
|
const typeCol = theme.fg("muted", type.padEnd(10));
|
|
186
186
|
const reqCol = isRequired
|
|
187
187
|
? theme.fg("warning", "Required")
|
|
@@ -240,7 +240,7 @@ export class InspectorPanel implements Component {
|
|
|
240
240
|
const command = mcp?.command || mcp?.cmd || "";
|
|
241
241
|
const args = mcp?.args || mcp?.arguments || [];
|
|
242
242
|
|
|
243
|
-
lines.push(` ${theme.fg("muted", "Transport:")} ${theme.fg("
|
|
243
|
+
lines.push(` ${theme.fg("muted", "Transport:")} ${theme.fg("contentAccent", transport)}`);
|
|
244
244
|
|
|
245
245
|
if (command) {
|
|
246
246
|
lines.push(` ${theme.fg("muted", "Command:")} ${theme.fg("success", command)}`);
|
|
@@ -272,7 +272,7 @@ export class InspectorPanel implements Component {
|
|
|
272
272
|
if (ext.trigger) {
|
|
273
273
|
lines.push(theme.fg("muted", "Trigger:"));
|
|
274
274
|
lines.push(theme.fg("dim", theme.boxSharp.horizontal.repeat(Math.min(width - 2, 40))));
|
|
275
|
-
lines.push(` ${theme.fg("
|
|
275
|
+
lines.push(` ${theme.fg("contentAccent", ext.trigger)}`);
|
|
276
276
|
lines.push("");
|
|
277
277
|
}
|
|
278
278
|
|
|
@@ -281,16 +281,16 @@ export class InspectorPanel implements Component {
|
|
|
281
281
|
|
|
282
282
|
#getKindBadge(kind: string): string {
|
|
283
283
|
const kindColors: Record<string, string> = {
|
|
284
|
-
"extension-module": "
|
|
285
|
-
skill: "
|
|
284
|
+
"extension-module": "contentAccent",
|
|
285
|
+
skill: "contentAccent",
|
|
286
286
|
rule: "success",
|
|
287
287
|
tool: "warning",
|
|
288
|
-
mcp: "
|
|
288
|
+
mcp: "contentAccent",
|
|
289
289
|
prompt: "muted",
|
|
290
290
|
hook: "warning",
|
|
291
291
|
"context-file": "dim",
|
|
292
292
|
instruction: "muted",
|
|
293
|
-
"slash-command": "
|
|
293
|
+
"slash-command": "contentAccent",
|
|
294
294
|
};
|
|
295
295
|
|
|
296
296
|
const color = kindColors[kind] || "muted";
|
|
@@ -53,7 +53,7 @@ class HistoryResultsList implements Component {
|
|
|
53
53
|
|
|
54
54
|
const cursorSymbol = `${theme.nav.cursor} `;
|
|
55
55
|
const cursorWidth = visibleWidth(cursorSymbol);
|
|
56
|
-
const cursor = isSelected ? theme.fg("
|
|
56
|
+
const cursor = isSelected ? theme.fg("chromeAccent", cursorSymbol) : padding(cursorWidth);
|
|
57
57
|
const maxWidth = width - cursorWidth;
|
|
58
58
|
|
|
59
59
|
const normalized = entry.prompt.replace(/\s+/g, " ").trim();
|