@f5-sales-demo/xcsh 19.90.0 → 19.91.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@f5-sales-demo/xcsh",
4
- "version": "19.90.0",
4
+ "version": "19.91.0",
5
5
  "description": "Coding agent CLI with read, bash, edit, write tools and session management",
6
6
  "homepage": "https://github.com/f5-sales-demo/xcsh",
7
7
  "author": "Can Boluk",
@@ -54,28 +54,28 @@
54
54
  "check-workflow-field-coverage": "bun scripts/check-workflow-field-coverage.ts"
55
55
  },
56
56
  "dependencies": {
57
- "@agentclientprotocol/sdk": "0.16.1",
57
+ "@agentclientprotocol/sdk": "1.3.0",
58
58
  "@mozilla/readability": "^0.6",
59
- "@f5-sales-demo/xcsh-stats": "19.90.0",
60
- "@f5-sales-demo/pi-agent-core": "19.90.0",
61
- "@f5-sales-demo/pi-ai": "19.90.0",
62
- "@f5-sales-demo/pi-natives": "19.90.0",
63
- "@f5-sales-demo/pi-resource-management": "19.90.0",
64
- "@f5-sales-demo/pi-tui": "19.90.0",
65
- "@f5-sales-demo/pi-utils": "19.90.0",
59
+ "@f5-sales-demo/xcsh-stats": "19.91.0",
60
+ "@f5-sales-demo/pi-agent-core": "19.91.0",
61
+ "@f5-sales-demo/pi-ai": "19.91.0",
62
+ "@f5-sales-demo/pi-natives": "19.91.0",
63
+ "@f5-sales-demo/pi-resource-management": "19.91.0",
64
+ "@f5-sales-demo/pi-tui": "19.91.0",
65
+ "@f5-sales-demo/pi-utils": "19.91.0",
66
66
  "@sinclair/typebox": "^0.34",
67
67
  "@xterm/headless": "^6.0",
68
68
  "ajv": "^8.20",
69
69
  "chalk": "^5.6",
70
- "diff": "^8.0",
71
- "fflate": "0.8.2",
70
+ "diff": "^9.0",
71
+ "fflate": "0.8.3",
72
72
  "handlebars": "^4.7",
73
73
  "linkedom": "^0.18",
74
- "lru-cache": "11.3.1",
75
- "markit-ai": "0.5.0",
74
+ "lru-cache": "11.5.2",
75
+ "markit-ai": "0.5.3",
76
76
  "puppeteer": "^24.37",
77
77
  "yaml": "2.9.0",
78
- "zod": "4.3.6"
78
+ "zod": "4.4.3"
79
79
  },
80
80
  "devDependencies": {
81
81
  "@types/bun": "^1.3"
@@ -275,6 +275,32 @@ export class ChatHandler {
275
275
  chat.lastKeepaliveAt = nowMs;
276
276
  this.#server.send({ type: "chat_keepalive", id: chat.id } satisfies ChatKeepalive);
277
277
  }
278
+ } else if (ame.type === "server_tool_start") {
279
+ // PROVIDER-SIDE ACTIVITY (#2340): the provider runs web search itself, which takes
280
+ // seconds before any token streams. This notice is the only feedback in that window —
281
+ // without it the panel shows a bare "Thinking…" and looks hung. It doubles as a
282
+ // liveness signal, since the panel re-arms its first-token timer on a tool notice.
283
+ const label = serverToolLabels(ame.toolName);
284
+ this.#server.send({
285
+ type: "chat_tool_notice",
286
+ id: chat.id,
287
+ tool: ame.toolName,
288
+ ok: true,
289
+ detail: ame.query ? `${label.running}: ${ame.query}` : `${label.running}…`,
290
+ });
291
+ } else if (ame.type === "server_tool_end") {
292
+ const label = serverToolLabels(ame.toolName);
293
+ const failed = ame.errorCode !== undefined;
294
+ const count = ame.resultCount ?? 0;
295
+ this.#server.send({
296
+ type: "chat_tool_notice",
297
+ id: chat.id,
298
+ tool: ame.toolName,
299
+ ok: !failed,
300
+ detail: failed
301
+ ? `${label.done} failed: ${ame.errorCode}`
302
+ : `${label.done}: ${count} result${count === 1 ? "" : "s"}`,
303
+ });
278
304
  }
279
305
  } else if (event.type === "message_end" && event.message.role === "assistant") {
280
306
  const msg = event.message as AssistantMessage;
@@ -686,10 +712,40 @@ function trimTrailingMarkup(url: string): string {
686
712
  return url.replace(/[*_~`,.;:!?'")\]}>]+$/, "");
687
713
  }
688
714
 
715
+ /** Panel-facing labels for provider-side tools, used for the activity rows (#2340). */
716
+ function serverToolLabels(toolName: string): { running: string; done: string } {
717
+ switch (toolName) {
718
+ case "web_search":
719
+ return { running: "Searching the web", done: "Web search" };
720
+ case "web_fetch":
721
+ return { running: "Fetching a page", done: "Page fetch" };
722
+ default:
723
+ return { running: `${toolName}: running`, done: toolName };
724
+ }
725
+ }
726
+
689
727
  export function extractReferences(msg: AssistantMessage): ChatReference[] {
690
728
  const refs: ChatReference[] = [];
691
729
  const seen = new Set<string>();
692
730
 
731
+ // STRUCTURED CITATIONS FIRST (#2340): a provider-side search reports the real page title, so
732
+ // it beats the regex scrape below — which can only guess a title from the URL path, and finds
733
+ // nothing at all when the model cites a source without printing its URL in the prose. Done as
734
+ // its own pass so a citation in a later block still wins over a scrape in an earlier one.
735
+ for (const block of msg.content) {
736
+ if (block.type !== "text" || !block.citations) continue;
737
+ for (const citation of block.citations) {
738
+ const url = citation.url;
739
+ if (!url || seen.has(url)) continue;
740
+ seen.add(url);
741
+ refs.push({
742
+ kind: classifyReferenceKind(url),
743
+ title: citation.title?.trim() || titleFromUrl(url),
744
+ url,
745
+ });
746
+ }
747
+ }
748
+
693
749
  for (const block of msg.content) {
694
750
  if (block.type !== "text") continue;
695
751
 
@@ -17,17 +17,17 @@ export interface BuildInfo {
17
17
  }
18
18
 
19
19
  export const BUILD_INFO: BuildInfo = {
20
- "version": "19.90.0",
21
- "commit": "9aab47a02ab0c46d3a98d15cf4f08ba02487b93c",
22
- "shortCommit": "9aab47a",
20
+ "version": "19.91.0",
21
+ "commit": "37a9e074a2203b5faeee3e41f7a2d7704f120c4f",
22
+ "shortCommit": "37a9e07",
23
23
  "branch": "main",
24
- "tag": "v19.90.0",
25
- "commitDate": "2026-07-25T01:23:55Z",
26
- "buildDate": "2026-07-25T01:59:21.915Z",
24
+ "tag": "v19.91.0",
25
+ "commitDate": "2026-07-25T15:33:10Z",
26
+ "buildDate": "2026-07-25T15:59:23.847Z",
27
27
  "dirty": true,
28
28
  "prNumber": "",
29
29
  "repoUrl": "https://github.com/f5-sales-demo/xcsh",
30
30
  "repoSlug": "f5-sales-demo/xcsh",
31
- "commitUrl": "https://github.com/f5-sales-demo/xcsh/commit/9aab47a02ab0c46d3a98d15cf4f08ba02487b93c",
32
- "releaseUrl": "https://github.com/f5-sales-demo/xcsh/releases/tag/v19.90.0"
31
+ "commitUrl": "https://github.com/f5-sales-demo/xcsh/commit/37a9e074a2203b5faeee3e41f7a2d7704f120c4f",
32
+ "releaseUrl": "https://github.com/f5-sales-demo/xcsh/releases/tag/v19.91.0"
33
33
  };
@@ -2,10 +2,10 @@
2
2
 
3
3
  import type { ConsoleCatalogData } from "./console-catalog-types";
4
4
 
5
- export const CONSOLE_CATALOG_VERSION = "f80a8344bc9184cd84f26957168025cef5d9ad07";
5
+ export const CONSOLE_CATALOG_VERSION = "e58701e87983ff5967d0d4f68e74a78c64fdfbf1";
6
6
 
7
7
  export const CONSOLE_CATALOG_DATA: ConsoleCatalogData = {
8
- version: "f80a8344bc9184cd84f26957168025cef5d9ad07",
8
+ version: "e58701e87983ff5967d0d4f68e74a78c64fdfbf1",
9
9
  workflows: {
10
10
  "address-allocator/create":
11
11
  'schema: urn:xcsh:console:workflow:v1\nid: address-allocator-create\nlabel: Create IP Address Allocators\nresource: address-allocator\noperation: create\npreconditions:\n - user_logged_in\n - namespace_selected\n - "role_minimum: admin"\nparams:\n namespace:\n required: true\n description: Target namespace\n example: demo\n name:\n required: true\n description: IP Address Allocators name (lowercase alphanumeric and hyphens)\n example: example-address-allocator\n address_allocator_mode:\n required: true\n description: Address Allocator Mode\n allocation_unit:\n required: false\n description: Allocation Unit\n default: 0\n address_pool:\n required: false\n description: Address Pool\n default: value\n address_allocation_scheme:\n required: false\n description: "Server-required: Field should be not nil"\n default: value\nsteps:\n - id: navigate-to-list\n action: navigate\n url: /web/workspaces/multi-cloud-network-connect/manage/networking/legacy_network_configuration/address_allocators\n wait_for: text(\'IP Address Allocators\')\n description: Navigate to IP Address Allocators list page\n - id: click-add-tab\n action: click\n selector: text(\'Add IP Address Allocator\')\n wait_for: textbox[name=\'Name\']\n description: Click Add IP Address Allocator to open the create form\n - id: fill-name\n action: fill\n selector: textbox[name=\'Name\']\n value: "{name}"\n description: Enter Name\n - id: select-address_allocator_mode\n action: select\n selector: listbox\n context: Address Allocator Mode section\n value: "{address_allocator_mode}"\n description: Select Address Allocator Mode\n - id: fill-allocation_unit\n action: fill\n selector: spinbutton[name=\'Allocation Unit\']\n value: "{allocation_unit}"\n description: Set Allocation Unit\n - id: fill-address_pool\n action: fill\n selector: ngx-datatable input.form-control\n context: Address Pool table\n value: "{address_pool}"\n description: Enter Address Pool in the existing table row (no Add Item needed — the table ships one empty row)\n - id: select-address_allocation_scheme\n action: select\n selector: listbox\n context: Address Allocation Scheme section\n value: "{address_allocation_scheme}"\n description: Select Address Allocation Scheme\n - id: save\n action: click\n selector: "[class*=\'save-bt\'],[class*=\'submit-button\']"\n context: footer\n wait_for: text(\'{name}\')\n wait_timeout_ms: 30000\n description: Save/submit the form (union selector matches save-bt OR submit-button)\npostconditions:\n - resource_list_page_visible\n - "resource_name_in_list: {name}"\nmetadata:\n confidence: inferred\n discovered_at: 2026-06-24\n console_version: "2025.06"\n notes: Auto-generated by scripts/generate-workflows.ts from api-specs-enriched field metadata.\n',
@@ -25,14 +25,11 @@ import {
25
25
  type ResumeSessionResponse,
26
26
  type SessionConfigOption,
27
27
  type SessionInfo,
28
- type SessionModelState,
29
28
  type SessionModeState,
30
29
  type SessionNotification,
31
30
  type SessionUpdate,
32
31
  type SetSessionConfigOptionRequest,
33
32
  type SetSessionConfigOptionResponse,
34
- type SetSessionModelRequest,
35
- type SetSessionModelResponse,
36
33
  type SetSessionModeRequest,
37
34
  type SetSessionModeResponse,
38
35
  type Usage,
@@ -201,7 +198,6 @@ export class AcpAgent implements Agent {
201
198
  const response: NewSessionResponse = {
202
199
  sessionId: record.session.sessionId,
203
200
  configOptions: this.#buildConfigOptions(record.session),
204
- models: this.#buildModelState(record.session),
205
201
  modes: this.#buildModeState(),
206
202
  };
207
203
  this.#scheduleBootstrapUpdates(record.session.sessionId);
@@ -214,7 +210,6 @@ export class AcpAgent implements Agent {
214
210
  await this.#replaySessionHistory(record);
215
211
  const response: LoadSessionResponse = {
216
212
  configOptions: this.#buildConfigOptions(record.session),
217
- models: this.#buildModelState(record.session),
218
213
  modes: this.#buildModeState(),
219
214
  };
220
215
  this.#scheduleBootstrapUpdates(record.session.sessionId);
@@ -238,12 +233,11 @@ export class AcpAgent implements Agent {
238
233
  };
239
234
  }
240
235
 
241
- async unstable_resumeSession(params: ResumeSessionRequest): Promise<ResumeSessionResponse> {
236
+ async resumeSession(params: ResumeSessionRequest): Promise<ResumeSessionResponse> {
242
237
  this.#assertAbsoluteCwd(params.cwd);
243
238
  const record = await this.#resumeManagedSession(params.sessionId, params.cwd, params.mcpServers ?? []);
244
239
  const response: ResumeSessionResponse = {
245
240
  configOptions: this.#buildConfigOptions(record.session),
246
- models: this.#buildModelState(record.session),
247
241
  modes: this.#buildModeState(),
248
242
  };
249
243
  this.#scheduleBootstrapUpdates(record.session.sessionId);
@@ -256,14 +250,13 @@ export class AcpAgent implements Agent {
256
250
  const response: ForkSessionResponse = {
257
251
  sessionId: record.session.sessionId,
258
252
  configOptions: this.#buildConfigOptions(record.session),
259
- models: this.#buildModelState(record.session),
260
253
  modes: this.#buildModeState(),
261
254
  };
262
255
  this.#scheduleBootstrapUpdates(record.session.sessionId);
263
256
  return response;
264
257
  }
265
258
 
266
- async unstable_closeSession(params: CloseSessionRequest): Promise<CloseSessionResponse> {
259
+ async closeSession(params: CloseSessionRequest): Promise<CloseSessionResponse> {
267
260
  const record = this.#sessions.get(params.sessionId);
268
261
  if (!record) {
269
262
  return {};
@@ -317,19 +310,6 @@ export class AcpAgent implements Agent {
317
310
  return { configOptions };
318
311
  }
319
312
 
320
- async unstable_setSessionModel(params: SetSessionModelRequest): Promise<SetSessionModelResponse> {
321
- const record = this.#getSessionRecord(params.sessionId);
322
- await this.#setModelById(record.session, params.modelId);
323
- await this.#connection.sessionUpdate({
324
- sessionId: record.session.sessionId,
325
- update: {
326
- sessionUpdate: "config_option_update",
327
- configOptions: this.#buildConfigOptions(record.session),
328
- },
329
- });
330
- return {};
331
- }
332
-
333
313
  async prompt(params: PromptRequest): Promise<PromptResponse> {
334
314
  const record = this.#getSessionRecord(params.sessionId);
335
315
  if (record.promptTurn && !record.promptTurn.settled) {
@@ -339,7 +319,7 @@ export class AcpAgent implements Agent {
339
319
  const converted = this.#convertPromptBlocks(params.prompt);
340
320
  const pendingPrompt = Promise.withResolvers<PromptResponse>();
341
321
  record.promptTurn = {
342
- userMessageId: params.messageId ?? crypto.randomUUID(),
322
+ userMessageId: crypto.randomUUID(),
343
323
  cancelRequested: false,
344
324
  settled: false,
345
325
  usageBaseline: this.#cloneUsageStatistics(record.session.sessionManager.getUsageStatistics()),
@@ -371,7 +351,6 @@ export class AcpAgent implements Agent {
371
351
  this.#finishPrompt(record, {
372
352
  stopReason: "cancelled",
373
353
  usage: this.#buildTurnUsage(promptTurn.usageBaseline, record.session.sessionManager.getUsageStatistics()),
374
- userMessageId: promptTurn.userMessageId,
375
354
  });
376
355
  } catch (error: unknown) {
377
356
  this.#finishPrompt(record, undefined, error);
@@ -563,7 +542,6 @@ export class AcpAgent implements Agent {
563
542
  this.#finishPrompt(record, {
564
543
  stopReason: promptTurn.cancelRequested ? "cancelled" : "end_turn",
565
544
  usage: this.#buildTurnUsage(promptTurn.usageBaseline, record.session.sessionManager.getUsageStatistics()),
566
- userMessageId: promptTurn.userMessageId,
567
545
  });
568
546
  }
569
547
  }
@@ -674,28 +652,6 @@ export class AcpAgent implements Agent {
674
652
  return configOptions;
675
653
  }
676
654
 
677
- #buildModelState(session: AgentSession): SessionModelState | undefined {
678
- const models = session.getAvailableModels();
679
- if (models.length === 0) {
680
- return undefined;
681
- }
682
-
683
- const availableModels = models.map(model => ({
684
- modelId: this.#toModelId(model),
685
- name: model.name,
686
- description: `${model.provider}/${model.id}`,
687
- }));
688
- const currentModelId = session.model ? this.#toModelId(session.model) : availableModels[0]?.modelId;
689
- if (!currentModelId) {
690
- return undefined;
691
- }
692
-
693
- return {
694
- availableModels,
695
- currentModelId,
696
- };
697
- }
698
-
699
655
  #buildThinkingOptions(session: AgentSession): Array<{ value: string; name: string; description?: string }> {
700
656
  return [
701
657
  { value: THINKING_OFF, name: "Off" },
@@ -1264,6 +1220,15 @@ export class AcpAgent implements Agent {
1264
1220
  headers: this.#toNameValueMap(server.headers),
1265
1221
  };
1266
1222
  }
1223
+ // ACP 1.x added an `acp` transport, whose descriptor carries a serverId
1224
+ // instead of a url/headers pair. We never opt into it -- `initialize`
1225
+ // advertises only `mcpCapabilities.http` and `.sse` -- so reject it
1226
+ // explicitly rather than letting it fall through to the sse branch.
1227
+ if (server.type === "acp") {
1228
+ throw new Error(
1229
+ `Unsupported MCP transport "acp" for server "${server.name}": xcsh advertises only http and sse`,
1230
+ );
1231
+ }
1267
1232
  return {
1268
1233
  type: "sse",
1269
1234
  url: server.url,
@@ -1301,7 +1266,6 @@ export class AcpAgent implements Agent {
1301
1266
  this.#finishPrompt(record, {
1302
1267
  stopReason: "cancelled",
1303
1268
  usage: this.#buildTurnUsage(promptTurn.usageBaseline, record.session.sessionManager.getUsageStatistics()),
1304
- userMessageId: promptTurn.userMessageId,
1305
1269
  });
1306
1270
  }
1307
1271
 
package/src/thinking.ts CHANGED
@@ -32,7 +32,12 @@ const THINKING_LEVEL_METADATA: Record<ThinkingLevel, ThinkingLevelMetadata> = {
32
32
  [ThinkingLevel.XHigh]: {
33
33
  value: ThinkingLevel.XHigh,
34
34
  label: "xhigh",
35
- description: "Maximum reasoning (~32k tokens)",
35
+ description: "Very deep reasoning (~32k tokens)",
36
+ },
37
+ [ThinkingLevel.Max]: {
38
+ value: ThinkingLevel.Max,
39
+ label: "max",
40
+ description: "Maximum reasoning (~64k tokens)",
36
41
  },
37
42
  };
38
43