@f5xc-salesdemos/xcsh 14.4.0 → 14.5.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.
Files changed (77) hide show
  1. package/package.json +7 -7
  2. package/src/autoresearch/dashboard.ts +9 -6
  3. package/src/autoresearch/tools/init-experiment.ts +1 -1
  4. package/src/autoresearch/tools/log-experiment.ts +1 -1
  5. package/src/config/model-registry.ts +1 -1
  6. package/src/config/settings-schema.ts +12 -1
  7. package/src/debug/index.ts +8 -6
  8. package/src/debug/log-viewer.ts +4 -4
  9. package/src/edit/renderer.ts +2 -2
  10. package/src/exa/render.ts +3 -3
  11. package/src/exec/bash-executor.ts +37 -2
  12. package/src/extensibility/hooks/types.ts +1 -1
  13. package/src/lsp/render.ts +8 -8
  14. package/src/modes/components/agent-dashboard.ts +11 -7
  15. package/src/modes/components/bordered-loader.ts +1 -1
  16. package/src/modes/components/btw-panel.ts +1 -1
  17. package/src/modes/components/extensions/extension-dashboard.ts +1 -1
  18. package/src/modes/components/extensions/extension-list.ts +4 -4
  19. package/src/modes/components/extensions/inspector-panel.ts +11 -11
  20. package/src/modes/components/history-search.ts +1 -1
  21. package/src/modes/components/hook-editor.ts +1 -1
  22. package/src/modes/components/hook-input.ts +3 -1
  23. package/src/modes/components/hook-selector.ts +5 -3
  24. package/src/modes/components/login-dialog.ts +1 -1
  25. package/src/modes/components/mcp-add-wizard.ts +32 -32
  26. package/src/modes/components/model-selector.ts +4 -4
  27. package/src/modes/components/oauth-selector.ts +2 -2
  28. package/src/modes/components/plugin-settings.ts +4 -4
  29. package/src/modes/components/read-tool-group.ts +1 -1
  30. package/src/modes/components/session-observer-overlay.ts +5 -3
  31. package/src/modes/components/session-selector.ts +1 -1
  32. package/src/modes/components/settings-selector.ts +2 -2
  33. package/src/modes/components/status-line/presets.ts +11 -0
  34. package/src/modes/components/status-line/segments.ts +55 -26
  35. package/src/modes/components/status-line/types.ts +12 -1
  36. package/src/modes/components/status-line-segment-editor.ts +5 -4
  37. package/src/modes/components/status-line.ts +135 -51
  38. package/src/modes/components/tree-selector.ts +4 -4
  39. package/src/modes/components/user-message-selector.ts +1 -1
  40. package/src/modes/components/welcome.ts +3 -3
  41. package/src/modes/controllers/command-controller.ts +23 -10
  42. package/src/modes/controllers/event-controller.ts +1 -1
  43. package/src/modes/controllers/extension-ui-controller.ts +3 -3
  44. package/src/modes/controllers/input-controller.ts +2 -2
  45. package/src/modes/controllers/mcp-command-controller.ts +26 -24
  46. package/src/modes/controllers/selector-controller.ts +2 -2
  47. package/src/modes/controllers/ssh-command-controller.ts +10 -10
  48. package/src/modes/interactive-mode.ts +28 -42
  49. package/src/modes/shared.ts +1 -1
  50. package/src/modes/theme/defaults/xcsh-dark.json +35 -19
  51. package/src/modes/theme/defaults/xcsh-light.json +3 -0
  52. package/src/modes/theme/theme.ts +80 -7
  53. package/src/modes/utils/ui-helpers.ts +3 -3
  54. package/src/sdk.ts +2 -3
  55. package/src/task/render.ts +3 -3
  56. package/src/tools/ask.ts +2 -2
  57. package/src/tools/ast-edit.ts +1 -1
  58. package/src/tools/ast-grep.ts +1 -1
  59. package/src/tools/bash-interactive.ts +1 -1
  60. package/src/tools/bash.ts +8 -1
  61. package/src/tools/find.ts +1 -1
  62. package/src/tools/gh-renderer.ts +4 -4
  63. package/src/tools/grep.ts +1 -1
  64. package/src/tools/inspect-image-renderer.ts +2 -2
  65. package/src/tools/python.ts +1 -1
  66. package/src/tools/render-utils.ts +12 -4
  67. package/src/tools/renderers.ts +5 -2
  68. package/src/tools/review.ts +1 -1
  69. package/src/tools/search-tool-bm25.ts +1 -1
  70. package/src/tools/todo-write.ts +2 -2
  71. package/src/tools/write.ts +2 -2
  72. package/src/tui/code-cell.ts +1 -1
  73. package/src/tui/file-list.ts +2 -2
  74. package/src/tui/output-block.ts +2 -2
  75. package/src/tui/status-line.ts +1 -1
  76. package/src/utils/gitstatus.ts +140 -0
  77. 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.0",
4
+ "version": "14.5.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.4.0",
50
- "@f5xc-salesdemos/pi-agent-core": "14.4.0",
51
- "@f5xc-salesdemos/pi-ai": "14.4.0",
52
- "@f5xc-salesdemos/pi-natives": "14.4.0",
53
- "@f5xc-salesdemos/pi-tui": "14.4.0",
54
- "@f5xc-salesdemos/pi-utils": "14.4.0",
49
+ "@f5xc-salesdemos/xcsh-stats": "14.5.0",
50
+ "@f5xc-salesdemos/pi-agent-core": "14.5.0",
51
+ "@f5xc-salesdemos/pi-ai": "14.5.0",
52
+ "@f5xc-salesdemos/pi-natives": "14.5.0",
53
+ "@f5xc-salesdemos/pi-tui": "14.5.0",
54
+ "@f5xc-salesdemos/pi-utils": "14.5.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("accent", "autoresearch"), theme.fg("warning", " running...")];
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(theme.fg("accent", label) + theme.fg("borderMuted", "-".repeat(fillWidth)) + hint, width);
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("accent", "autoresearch"),
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("accent", "autoresearch"), theme.fg("warning", ` ${modeStatus}`)];
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("accent", "autoresearch"),
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("accent", (result.commit || "-").padEnd(10))}` +
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("accent", truncateToWidth(replaceTabs(name), 100))}`;
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("accent", `${state.metricName}=${formatNum(experiment.metric, state.metricUnit)}`)}`;
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: "accent" },
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,
@@ -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("accent", "Debug Tools")), 1, 0));
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(new Text(theme.fg("accent", `${theme.status.info} CPU profiling started`), 1, 0));
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("accent", spinner),
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("accent", spinner),
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("accent", spinner),
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("accent", spinner),
391
+ spinner => theme.fg("spinnerAccent", spinner),
390
392
  text => theme.fg("muted", text),
391
393
  "Clearing artifact cache...",
392
394
  getSymbolTheme().spinnerFrames,
@@ -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("accent", sanitized);
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("accent", "❯") : " ";
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("accent", "❯") : selected ? theme.fg("accent", "•") : " ";
760
- const fold = expanded ? theme.fg("accent", "▾") : theme.fg("muted", "▸");
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
 
@@ -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("accent", shortenPath(rawPath)) : uiTheme.fg("toolOutput", "…");
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("accent", shortenPath(options.rename))}`;
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("accent", title)}${domainPart}`;
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
- "accent",
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("accent", query)}`;
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
- const finalCommand = prefixedCommand;
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
- ...(await sink.dump()),
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("accent"), theme.fg("muted"), "Working...");
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", "accent");
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("accent", file)} ${theme.fg("dim", fileMeta)}`;
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", "accent");
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("accent", sym.icon)} ${theme.fg("accent", sym.name)}`;
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("accent", sym.icon)} ${theme.fg("accent", sym.name)} ${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", "accent");
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" | "accent" | "dim" {
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 "accent";
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("accent", line)));
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("accent", replaceTabs(this.agent.name))));
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("accent", " Create New Agent")), 0, 0));
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("accent", "Generating agent specification..."), 0, 0));
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("accent", " Review Generated Agent")), 0, 0));
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("accent", " Agent Control Center")), 0, 0));
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(theme.bold(theme.fg("accent", `Model override: ${replaceTabs(this.#editingAgentName)}`)), 0, 0),
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("accent", s),
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("accent", replaceTabs(this.#question)), 1, 0));
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("accent", " Extension Control Center")), 0, 0));
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("accent", "_") : "";
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("accent", line));
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("accent", line));
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("accent", name));
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("accent", ext.displayName)));
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("accent", highlighted));
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("accent", "$1"));
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("accent", "$1"));
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("accent", name.padEnd(12));
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("accent", transport)}`);
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("accent", ext.trigger)}`);
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": "accent",
285
- skill: "accent",
284
+ "extension-module": "contentAccent",
285
+ skill: "contentAccent",
286
286
  rule: "success",
287
287
  tool: "warning",
288
- mcp: "accent",
288
+ mcp: "contentAccent",
289
289
  prompt: "muted",
290
290
  hook: "warning",
291
291
  "context-file": "dim",
292
292
  instruction: "muted",
293
- "slash-command": "accent",
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("accent", cursorSymbol) : padding(cursorWidth);
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();