@f5-sales-demo/xcsh 19.52.0 → 19.53.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.52.0",
4
+ "version": "19.53.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",
@@ -55,13 +55,13 @@
55
55
  "dependencies": {
56
56
  "@agentclientprotocol/sdk": "0.16.1",
57
57
  "@mozilla/readability": "^0.6",
58
- "@f5-sales-demo/xcsh-stats": "19.52.0",
59
- "@f5-sales-demo/pi-agent-core": "19.52.0",
60
- "@f5-sales-demo/pi-ai": "19.52.0",
61
- "@f5-sales-demo/pi-natives": "19.52.0",
62
- "@f5-sales-demo/pi-resource-management": "19.52.0",
63
- "@f5-sales-demo/pi-tui": "19.52.0",
64
- "@f5-sales-demo/pi-utils": "19.52.0",
58
+ "@f5-sales-demo/xcsh-stats": "19.53.0",
59
+ "@f5-sales-demo/pi-agent-core": "19.53.0",
60
+ "@f5-sales-demo/pi-ai": "19.53.0",
61
+ "@f5-sales-demo/pi-natives": "19.53.0",
62
+ "@f5-sales-demo/pi-resource-management": "19.53.0",
63
+ "@f5-sales-demo/pi-tui": "19.53.0",
64
+ "@f5-sales-demo/pi-utils": "19.53.0",
65
65
  "@sinclair/typebox": "^0.34",
66
66
  "@xterm/headless": "^6.0",
67
67
  "ajv": "^8.20",
@@ -25,7 +25,7 @@ export interface ExtensionCapabilities {
25
25
 
26
26
  export const EXTENSION_CAPABILITIES: ExtensionCapabilities = {
27
27
  "version": "0.1.0",
28
- "contractVersion": "1.0.0",
28
+ "contractVersion": "1.2.0",
29
29
  "protocol": "tool_request/result",
30
30
  "tools": [
31
31
  {
@@ -584,6 +584,18 @@ export const EXTENSION_CAPABILITIES: ExtensionCapabilities = {
584
584
  "readOnly": true
585
585
  }
586
586
  },
587
+ {
588
+ "name": "get_page_context",
589
+ "summary": "Return a snapshot of the active console page (url, AX tree, captured XC API body) for chat grounding.",
590
+ "category": "read",
591
+ "params": {
592
+ "type": "object",
593
+ "properties": {}
594
+ },
595
+ "flags": {
596
+ "readOnly": true
597
+ }
598
+ },
587
599
  {
588
600
  "name": "javascript_tool",
589
601
  "summary": "Evaluate arbitrary JS in the page (length-capped).",
@@ -706,10 +718,30 @@ export const EXTENSION_CAPABILITIES: ExtensionCapabilities = {
706
718
  "viewport": {
707
719
  "tool": "resize_window",
708
720
  "description": "Control the browser window size."
721
+ },
722
+ "chat": {
723
+ "contextTool": "get_page_context",
724
+ "transport": "websocket-bridge",
725
+ "modes": [
726
+ "educational",
727
+ "presentation",
728
+ "configuration",
729
+ "screenshot",
730
+ "annotation"
731
+ ],
732
+ "messages": [
733
+ "chat_request",
734
+ "chat_delta",
735
+ "chat_done",
736
+ "chat_error",
737
+ "chat_stop",
738
+ "chat_tool_notice"
739
+ ],
740
+ "description": "User ↔ xcsh chat over the bridge. The extension side panel sends chat_request (with mode and page-context snapshot); xcsh streams chat_delta tokens then a terminal chat_done (with reference links) or chat_error. Chat ids are prefixed \"c-\". Tool calls during a turn use the normal tool_request flow. chat_stop halts a streaming response. chat_tool_notice is emitted by the EXTENSION (the service worker) to the panel as a best-effort UI signal when a tool runs during a turn — it is NOT sent by xcsh; xcsh must not produce it to avoid double-rendering in the panel."
709
741
  }
710
742
  }
711
743
  };
712
744
 
713
- export const EXTENSION_CONTRACT_VERSION = "1.0.0";
745
+ export const EXTENSION_CONTRACT_VERSION = "1.2.0";
714
746
 
715
- export const EXTENSION_TOOL_NAMES: readonly string[] = ["ping","capabilities","reload","debug_exec","detach","navigate","login","scroll_to","resize_window","tabs_list","tabs_create","tabs_close","click","click_element","click_xy","type_text","form_input","key_press","select_option","label_select","file_upload","read_ax","get_page_text","find","wait_for","assert_text","screenshot","read_console","read_network","wait_for_api_response","javascript_tool","browser_batch","set_explain_mode","annotate"];
747
+ export const EXTENSION_TOOL_NAMES: readonly string[] = ["ping","capabilities","reload","debug_exec","detach","navigate","login","scroll_to","resize_window","tabs_list","tabs_create","tabs_close","click","click_element","click_xy","type_text","form_input","key_press","select_option","label_select","file_upload","read_ax","get_page_text","find","wait_for","assert_text","screenshot","read_console","read_network","wait_for_api_response","get_page_context","javascript_tool","browser_batch","set_explain_mode","annotate"];
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "version": "0.1.0",
3
- "contractVersion": "1.0.0",
3
+ "contractVersion": "1.2.0",
4
4
  "protocol": "tool_request/result",
5
5
  "tools": [
6
6
  {
@@ -69,9 +69,7 @@
69
69
  "category": "navigation",
70
70
  "params": {
71
71
  "type": "object",
72
- "required": [
73
- "url"
74
- ],
72
+ "required": ["url"],
75
73
  "properties": {
76
74
  "url": {
77
75
  "type": "string"
@@ -88,11 +86,7 @@
88
86
  "category": "navigation",
89
87
  "params": {
90
88
  "type": "object",
91
- "required": [
92
- "email",
93
- "password",
94
- "consoleUrl"
95
- ],
89
+ "required": ["email", "password", "consoleUrl"],
96
90
  "properties": {
97
91
  "email": {
98
92
  "type": "string"
@@ -115,9 +109,7 @@
115
109
  "category": "navigation",
116
110
  "params": {
117
111
  "type": "object",
118
- "required": [
119
- "ref"
120
- ],
112
+ "required": ["ref"],
121
113
  "properties": {
122
114
  "ref": {
123
115
  "type": "string"
@@ -134,10 +126,7 @@
134
126
  "category": "navigation",
135
127
  "params": {
136
128
  "type": "object",
137
- "required": [
138
- "width",
139
- "height"
140
- ],
129
+ "required": ["width", "height"],
141
130
  "properties": {
142
131
  "width": {
143
132
  "type": "number"
@@ -167,9 +156,7 @@
167
156
  "category": "navigation",
168
157
  "params": {
169
158
  "type": "object",
170
- "required": [
171
- "url"
172
- ],
159
+ "required": ["url"],
173
160
  "properties": {
174
161
  "url": {
175
162
  "type": "string"
@@ -186,9 +173,7 @@
186
173
  "category": "navigation",
187
174
  "params": {
188
175
  "type": "object",
189
- "required": [
190
- "tabId"
191
- ],
176
+ "required": ["tabId"],
192
177
  "properties": {
193
178
  "tabId": {
194
179
  "type": "number"
@@ -205,9 +190,7 @@
205
190
  "category": "interaction",
206
191
  "params": {
207
192
  "type": "object",
208
- "required": [
209
- "ref"
210
- ],
193
+ "required": ["ref"],
211
194
  "properties": {
212
195
  "ref": {
213
196
  "type": "string"
@@ -224,9 +207,7 @@
224
207
  "category": "interaction",
225
208
  "params": {
226
209
  "type": "object",
227
- "required": [
228
- "js"
229
- ],
210
+ "required": ["js"],
230
211
  "properties": {
231
212
  "js": {
232
213
  "type": "string"
@@ -246,10 +227,7 @@
246
227
  "category": "interaction",
247
228
  "params": {
248
229
  "type": "object",
249
- "required": [
250
- "x",
251
- "y"
252
- ],
230
+ "required": ["x", "y"],
253
231
  "properties": {
254
232
  "x": {
255
233
  "type": "number"
@@ -269,9 +247,7 @@
269
247
  "category": "interaction",
270
248
  "params": {
271
249
  "type": "object",
272
- "required": [
273
- "text"
274
- ],
250
+ "required": ["text"],
275
251
  "properties": {
276
252
  "text": {
277
253
  "type": "string"
@@ -288,10 +264,7 @@
288
264
  "category": "interaction",
289
265
  "params": {
290
266
  "type": "object",
291
- "required": [
292
- "ref",
293
- "value"
294
- ],
267
+ "required": ["ref", "value"],
295
268
  "properties": {
296
269
  "ref": {
297
270
  "type": "string"
@@ -311,9 +284,7 @@
311
284
  "category": "interaction",
312
285
  "params": {
313
286
  "type": "object",
314
- "required": [
315
- "key"
316
- ],
287
+ "required": ["key"],
317
288
  "properties": {
318
289
  "key": {
319
290
  "type": "string"
@@ -330,10 +301,7 @@
330
301
  "category": "interaction",
331
302
  "params": {
332
303
  "type": "object",
333
- "required": [
334
- "ref",
335
- "value"
336
- ],
304
+ "required": ["ref", "value"],
337
305
  "properties": {
338
306
  "ref": {
339
307
  "type": "string"
@@ -353,10 +321,7 @@
353
321
  "category": "interaction",
354
322
  "params": {
355
323
  "type": "object",
356
- "required": [
357
- "selector",
358
- "value"
359
- ],
324
+ "required": ["selector", "value"],
360
325
  "properties": {
361
326
  "selector": {
362
327
  "type": "string"
@@ -382,10 +347,7 @@
382
347
  "category": "interaction",
383
348
  "params": {
384
349
  "type": "object",
385
- "required": [
386
- "ref",
387
- "files"
388
- ],
350
+ "required": ["ref", "files"],
389
351
  "properties": {
390
352
  "ref": {
391
353
  "type": "string"
@@ -432,9 +394,7 @@
432
394
  "category": "read",
433
395
  "params": {
434
396
  "type": "object",
435
- "required": [
436
- "selector"
437
- ],
397
+ "required": ["selector"],
438
398
  "properties": {
439
399
  "selector": {
440
400
  "type": "string"
@@ -451,9 +411,7 @@
451
411
  "category": "read",
452
412
  "params": {
453
413
  "type": "object",
454
- "required": [
455
- "selector"
456
- ],
414
+ "required": ["selector"],
457
415
  "properties": {
458
416
  "selector": {
459
417
  "type": "string"
@@ -476,10 +434,7 @@
476
434
  "category": "read",
477
435
  "params": {
478
436
  "type": "object",
479
- "required": [
480
- "selector",
481
- "expected"
482
- ],
437
+ "required": ["selector", "expected"],
483
438
  "properties": {
484
439
  "selector": {
485
440
  "type": "string"
@@ -559,15 +514,25 @@
559
514
  "readOnly": true
560
515
  }
561
516
  },
517
+ {
518
+ "name": "get_page_context",
519
+ "summary": "Return a snapshot of the active console page (url, AX tree, captured XC API body) for chat grounding.",
520
+ "category": "read",
521
+ "params": {
522
+ "type": "object",
523
+ "properties": {}
524
+ },
525
+ "flags": {
526
+ "readOnly": true
527
+ }
528
+ },
562
529
  {
563
530
  "name": "javascript_tool",
564
531
  "summary": "Evaluate arbitrary JS in the page (length-capped).",
565
532
  "category": "script",
566
533
  "params": {
567
534
  "type": "object",
568
- "required": [
569
- "code"
570
- ],
535
+ "required": ["code"],
571
536
  "properties": {
572
537
  "code": {
573
538
  "type": "string"
@@ -584,17 +549,13 @@
584
549
  "category": "script",
585
550
  "params": {
586
551
  "type": "object",
587
- "required": [
588
- "actions"
589
- ],
552
+ "required": ["actions"],
590
553
  "properties": {
591
554
  "actions": {
592
555
  "type": "array",
593
556
  "items": {
594
557
  "type": "object",
595
- "required": [
596
- "tool"
597
- ],
558
+ "required": ["tool"],
598
559
  "properties": {
599
560
  "tool": {
600
561
  "type": "string"
@@ -629,9 +590,7 @@
629
590
  "category": "annotation",
630
591
  "params": {
631
592
  "type": "object",
632
- "required": [
633
- "kind"
634
- ],
593
+ "required": ["kind"],
635
594
  "properties": {
636
595
  "kind": {
637
596
  "type": "string"
@@ -668,10 +627,7 @@
668
627
  "tool": "annotate",
669
628
  "requiresExplainMode": true,
670
629
  "autoFingerprintOnClick": true,
671
- "kinds": [
672
- "fingerprint",
673
- "highlight"
674
- ],
630
+ "kinds": ["fingerprint", "highlight"],
675
631
  "description": "Transient annotation overlays. In explain mode, clicks auto-draw a fingerprint; `annotate` draws a named overlay by ref or coordinates."
676
632
  },
677
633
  "capture": {
@@ -681,6 +637,13 @@
681
637
  "viewport": {
682
638
  "tool": "resize_window",
683
639
  "description": "Control the browser window size."
640
+ },
641
+ "chat": {
642
+ "contextTool": "get_page_context",
643
+ "transport": "websocket-bridge",
644
+ "modes": ["educational", "presentation", "configuration", "screenshot", "annotation"],
645
+ "messages": ["chat_request", "chat_delta", "chat_done", "chat_error", "chat_stop", "chat_tool_notice"],
646
+ "description": "User ↔ xcsh chat over the bridge. The extension side panel sends chat_request (with mode and page-context snapshot); xcsh streams chat_delta tokens then a terminal chat_done (with reference links) or chat_error. Chat ids are prefixed \"c-\". Tool calls during a turn use the normal tool_request flow. chat_stop halts a streaming response. chat_tool_notice is emitted by the EXTENSION (the service worker) to the panel as a best-effort UI signal when a tool runs during a turn — it is NOT sent by xcsh; xcsh must not produce it to avoid double-rendering in the panel."
684
647
  }
685
648
  }
686
649
  }
@@ -0,0 +1,196 @@
1
+ import type { AssistantMessage } from "@f5-sales-demo/pi-ai";
2
+ import type { AgentSession, AgentSessionEvent } from "../session/agent-session";
3
+ import {
4
+ type ChatDelta,
5
+ type ChatDone,
6
+ type ChatError,
7
+ type ChatReference,
8
+ type ChatRequest,
9
+ type InteractionMode,
10
+ isChatRequest,
11
+ isChatStop,
12
+ type PageContextSnapshot,
13
+ } from "./chat-protocol";
14
+ import type { BridgeServer } from "./extension-bridge";
15
+
16
+ interface ActiveChat {
17
+ id: string;
18
+ seq: number;
19
+ terminalSent: boolean;
20
+ unsubscribe: () => void;
21
+ }
22
+
23
+ export class ChatHandler {
24
+ #server: BridgeServer;
25
+ #session: AgentSession;
26
+ #activeChats = new Map<string, ActiveChat>();
27
+
28
+ constructor(server: BridgeServer, session: AgentSession) {
29
+ this.#server = server;
30
+ this.#session = session;
31
+ }
32
+
33
+ attach(): void {
34
+ this.#server.onMessage(msg => {
35
+ if (isChatRequest(msg)) this.#handleChatRequest(msg as unknown as ChatRequest);
36
+ else if (isChatStop(msg)) this.#handleChatStop(msg as unknown as { id: string });
37
+ });
38
+
39
+ this.#server.onDisconnected(() => {
40
+ for (const chat of this.#activeChats.values()) {
41
+ this.#sendTerminal(chat, { type: "chat_error", id: chat.id, error: "bridge disconnected" });
42
+ chat.unsubscribe();
43
+ }
44
+ this.#activeChats.clear();
45
+ });
46
+ }
47
+
48
+ async #handleChatRequest(req: ChatRequest): Promise<void> {
49
+ const { id } = req;
50
+
51
+ if (this.#session.isStreaming || this.#activeChats.size > 0) {
52
+ this.#server.send({ type: "chat_error", id, error: "session busy" } satisfies ChatError);
53
+ return;
54
+ }
55
+
56
+ const chat: ActiveChat = { id, seq: 0, terminalSent: false, unsubscribe: () => {} };
57
+ this.#activeChats.set(id, chat);
58
+
59
+ const unsubscribe = this.#session.subscribe((event: AgentSessionEvent) => {
60
+ this.#handleSessionEvent(chat, event);
61
+ });
62
+ chat.unsubscribe = unsubscribe;
63
+
64
+ const prompt = composeChatPrompt(req.text, req.context, req.mode);
65
+
66
+ try {
67
+ await this.#session.prompt(prompt, { expandPromptTemplates: false, synthetic: false });
68
+ } catch (err: unknown) {
69
+ const message = err instanceof Error ? err.message : "unknown error";
70
+ this.#sendTerminal(chat, { type: "chat_error", id, error: message });
71
+ } finally {
72
+ if (!chat.terminalSent) {
73
+ this.#sendTerminal(chat, { type: "chat_done", id });
74
+ }
75
+ chat.unsubscribe();
76
+ this.#activeChats.delete(id);
77
+ }
78
+ }
79
+
80
+ #handleSessionEvent(chat: ActiveChat, event: AgentSessionEvent): void {
81
+ if (chat.terminalSent) return;
82
+
83
+ if (event.type === "message_update" && "assistantMessageEvent" in event) {
84
+ const ame = event.assistantMessageEvent;
85
+ if (ame.type === "text_delta") {
86
+ this.#server.send({
87
+ type: "chat_delta",
88
+ id: chat.id,
89
+ seq: chat.seq++,
90
+ delta: ame.delta,
91
+ } satisfies ChatDelta);
92
+ } else if (ame.type === "error") {
93
+ const errorMsg = ame.error?.errorMessage ?? "LLM error";
94
+ this.#sendTerminal(chat, { type: "chat_error", id: chat.id, error: errorMsg });
95
+ }
96
+ } else if (event.type === "message_end" && event.message.role === "assistant") {
97
+ const msg = event.message as AssistantMessage;
98
+ if (msg.stopReason === "error") {
99
+ this.#sendTerminal(chat, {
100
+ type: "chat_error",
101
+ id: chat.id,
102
+ error: msg.errorMessage ?? "assistant error",
103
+ });
104
+ } else {
105
+ const references = extractReferences(msg);
106
+ this.#sendTerminal(chat, {
107
+ type: "chat_done",
108
+ id: chat.id,
109
+ ...(references.length > 0 ? { references } : {}),
110
+ });
111
+ }
112
+ }
113
+ }
114
+
115
+ #handleChatStop(stop: { id: string }): void {
116
+ const chat = this.#activeChats.get(stop.id);
117
+ if (!chat) return;
118
+ this.#session.agent.abort();
119
+ }
120
+
121
+ #sendTerminal(chat: ActiveChat, frame: ChatDone | ChatError): void {
122
+ if (chat.terminalSent) return;
123
+ chat.terminalSent = true;
124
+ this.#server.send(frame);
125
+ }
126
+
127
+ dispose(): void {
128
+ for (const chat of this.#activeChats.values()) {
129
+ this.#sendTerminal(chat, { type: "chat_error", id: chat.id, error: "session disposed" });
130
+ chat.unsubscribe();
131
+ }
132
+ this.#activeChats.clear();
133
+ }
134
+ }
135
+
136
+ const MODE_INSTRUCTIONS: Record<InteractionMode, string> = {
137
+ educational: "Explain concepts and settings in depth. Help the user understand what they're looking at and why.",
138
+ presentation: "Guide a structured walkthrough. Narrate each step clearly for a live audience.",
139
+ configuration: "Help the user build or modify F5 XC configuration. Be precise and action-oriented.",
140
+ screenshot: "Focus on capturing annotated screenshots that document the current state.",
141
+ annotation: "Create on-page teaching annotations that highlight key elements and explain their purpose.",
142
+ };
143
+
144
+ export function composeChatPrompt(text: string, context: PageContextSnapshot | null, mode: InteractionMode): string {
145
+ const parts: string[] = [];
146
+
147
+ parts.push(`[Chat mode: ${mode}] ${MODE_INSTRUCTIONS[mode]}`);
148
+
149
+ if (context) {
150
+ parts.push("");
151
+ parts.push(`[Page context — captured at ${new Date(context.capturedAt).toISOString()}]`);
152
+ parts.push(`URL: ${context.url}`);
153
+ parts.push(`Title: ${context.title}`);
154
+
155
+ if (context.api) {
156
+ parts.push(`API resource (${context.api.resourceType}, status ${context.api.status}): ${context.api.url}`);
157
+ if (context.api.body) {
158
+ const body =
159
+ typeof context.api.body === "string" ? context.api.body : JSON.stringify(context.api.body, null, 2);
160
+ parts.push(body);
161
+ }
162
+ if (context.api.truncated) {
163
+ parts.push("[API body was truncated]");
164
+ }
165
+ }
166
+
167
+ if (context.ax) {
168
+ const ax = typeof context.ax === "string" ? context.ax : JSON.stringify(context.ax);
169
+ parts.push(`Accessibility tree: ${ax}`);
170
+ }
171
+
172
+ if (context.truncated) {
173
+ parts.push("[Page context was truncated]");
174
+ }
175
+
176
+ parts.push("---");
177
+ }
178
+
179
+ parts.push("");
180
+ parts.push(text);
181
+ return parts.join("\n");
182
+ }
183
+
184
+ function extractReferences(msg: AssistantMessage): ChatReference[] {
185
+ const refs: ChatReference[] = [];
186
+ for (const block of msg.content) {
187
+ if (block.type !== "text") continue;
188
+ const urlRegex = /\[([^\]]+)\]\((https?:\/\/[^\s)]+)\)/g;
189
+ for (let match = urlRegex.exec(block.text); match !== null; match = urlRegex.exec(block.text)) {
190
+ const [, title, url] = match;
191
+ const kind = url.includes("docs.cloud.f5.com") || url.includes("docs.") ? "doc" : "console";
192
+ refs.push({ kind, title, url });
193
+ }
194
+ }
195
+ return refs;
196
+ }
@@ -0,0 +1,109 @@
1
+ /**
2
+ * Wire protocol types for the Chrome extension chat side window.
3
+ * Contract source of truth: capabilities.json v1.2.0.
4
+ */
5
+
6
+ // ---------------------------------------------------------------------------
7
+ // Page context snapshot (auto-attached by extension to every chat_request)
8
+ // ---------------------------------------------------------------------------
9
+
10
+ export interface PageContextApi {
11
+ url: string;
12
+ status: number;
13
+ resourceType: string;
14
+ body: unknown;
15
+ truncated: boolean;
16
+ }
17
+
18
+ export interface PageContextSnapshot {
19
+ v: 1;
20
+ capturedAt: number;
21
+ tabId: number;
22
+ url: string;
23
+ path: string;
24
+ title: string;
25
+ ax: unknown | null;
26
+ api: PageContextApi | null;
27
+ truncated: boolean;
28
+ }
29
+
30
+ // ---------------------------------------------------------------------------
31
+ // Interaction modes
32
+ // ---------------------------------------------------------------------------
33
+
34
+ export type InteractionMode = "educational" | "presentation" | "configuration" | "screenshot" | "annotation";
35
+
36
+ const VALID_MODES = new Set<string>(["educational", "presentation", "configuration", "screenshot", "annotation"]);
37
+
38
+ // ---------------------------------------------------------------------------
39
+ // References (attached to chat_done)
40
+ // ---------------------------------------------------------------------------
41
+
42
+ export interface ChatReference {
43
+ kind: "doc" | "console";
44
+ title: string;
45
+ url: string;
46
+ }
47
+
48
+ // ---------------------------------------------------------------------------
49
+ // Inbound messages (extension → xcsh)
50
+ // ---------------------------------------------------------------------------
51
+
52
+ export interface ChatRequest {
53
+ type: "chat_request";
54
+ id: string;
55
+ text: string;
56
+ context: PageContextSnapshot | null;
57
+ mode: InteractionMode;
58
+ history_hint: string;
59
+ }
60
+
61
+ export interface ChatStop {
62
+ type: "chat_stop";
63
+ id: string;
64
+ }
65
+
66
+ // ---------------------------------------------------------------------------
67
+ // Outbound messages (xcsh → extension)
68
+ // ---------------------------------------------------------------------------
69
+
70
+ export interface ChatDelta {
71
+ type: "chat_delta";
72
+ id: string;
73
+ seq: number;
74
+ delta: string;
75
+ }
76
+
77
+ export interface ChatDone {
78
+ type: "chat_done";
79
+ id: string;
80
+ references?: ChatReference[];
81
+ }
82
+
83
+ export interface ChatError {
84
+ type: "chat_error";
85
+ id: string;
86
+ error: string;
87
+ }
88
+
89
+ // ---------------------------------------------------------------------------
90
+ // Validators
91
+ // ---------------------------------------------------------------------------
92
+
93
+ function hasChatIdPrefix(id: unknown): id is string {
94
+ return typeof id === "string" && id.startsWith("c-");
95
+ }
96
+
97
+ export function isChatRequest(msg: Record<string, unknown>): boolean {
98
+ return (
99
+ msg.type === "chat_request" &&
100
+ hasChatIdPrefix(msg.id) &&
101
+ typeof msg.text === "string" &&
102
+ typeof msg.mode === "string" &&
103
+ VALID_MODES.has(msg.mode)
104
+ );
105
+ }
106
+
107
+ export function isChatStop(msg: Record<string, unknown>): boolean {
108
+ return msg.type === "chat_stop" && hasChatIdPrefix(msg.id);
109
+ }
@@ -80,6 +80,7 @@ export class BridgeServer {
80
80
  #client: ServerWebSocket<undefined> | null = null;
81
81
  #onConnected: Array<() => void> = [];
82
82
  #onDisconnected: Array<() => void> = [];
83
+ #onMessage: Array<(msg: Record<string, unknown>) => void> = [];
83
84
 
84
85
  /** The port the WebSocket server is listening on (0 = not bound). */
85
86
  get port(): number {
@@ -98,6 +99,16 @@ export class BridgeServer {
98
99
  this.#onDisconnected.push(cb);
99
100
  }
100
101
 
102
+ /** Register a listener for messages not handled by the built-in router (tool_result, ping). */
103
+ onMessage(cb: (msg: Record<string, unknown>) => void): void {
104
+ this.#onMessage.push(cb);
105
+ }
106
+
107
+ /** Send a fire-and-forget JSON frame to the connected client. */
108
+ send(payload: unknown): void {
109
+ this.#client?.send(JSON.stringify(payload));
110
+ }
111
+
101
112
  /** Bind the WebSocket server to a loopback port. Called by {@link startBridgeServer}. */
102
113
  listen(port: number, opts?: { skipOriginCheck?: boolean }): void {
103
114
  this.#server = Bun.serve({
@@ -149,6 +160,8 @@ export class BridgeServer {
149
160
  });
150
161
  } else if (msg.type === "ping") {
151
162
  ws.send(JSON.stringify({ type: "pong" }));
163
+ } else {
164
+ for (const cb of this.#onMessage) cb(msg as Record<string, unknown>);
152
165
  }
153
166
  }
154
167
 
@@ -1,5 +1,6 @@
1
1
  import { type AxNode, matchNode } from "./ax";
2
2
  import { EXTENSION_CONTRACT_VERSION } from "./capabilities.generated";
3
+ import type { PageContextSnapshot } from "./chat-protocol";
3
4
  import { type BridgeServer, startBridgeServer, type ToolResult } from "./extension-bridge";
4
5
  import { checkContractVersion } from "./extension-contract";
5
6
  import { ExtensionPageActions } from "./extension-page-actions";
@@ -75,6 +76,8 @@ export interface ExtensionPage {
75
76
  value: string;
76
77
  optionCount: number;
77
78
  }>;
79
+ /** Read the current page-context snapshot (URL, title, AX tree, live API body). */
80
+ getPageContext(): Promise<PageContextSnapshot>;
78
81
  /** Toggle the extension's "explain mode" — the gate for on-page annotation
79
82
  * overlays (fingerprints/highlights), used during human-paced walkthroughs. */
80
83
  setExplainMode(enabled: boolean): Promise<void>;
@@ -165,6 +168,10 @@ class BridgeExtensionPage implements ExtensionPage {
165
168
  return unwrap(await this.#server.request("screenshot", {}), "screenshot") as string;
166
169
  }
167
170
 
171
+ async getPageContext(): Promise<PageContextSnapshot> {
172
+ return unwrap(await this.#server.request("get_page_context", {}), "get_page_context") as PageContextSnapshot;
173
+ }
174
+
168
175
  async setExplainMode(enabled: boolean): Promise<void> {
169
176
  unwrap(await this.#server.request("set_explain_mode", { enabled }), "set_explain_mode");
170
177
  }
@@ -4,6 +4,8 @@ export * from "./auth";
4
4
  export * from "./ax";
5
5
  export * from "./cdp-core";
6
6
  export * from "./cdp-page-actions";
7
+ export * from "./chat-handler";
8
+ export * from "./chat-protocol";
7
9
  export * from "./chrome-locate";
8
10
  export * from "./dom-context";
9
11
  export * from "./extension-bridge";
@@ -17,17 +17,17 @@ export interface BuildInfo {
17
17
  }
18
18
 
19
19
  export const BUILD_INFO: BuildInfo = {
20
- "version": "19.52.0",
21
- "commit": "a54c0128815fd00752c40aba2a1650817ca9874f",
22
- "shortCommit": "a54c012",
20
+ "version": "19.53.0",
21
+ "commit": "bd260c74c8205835f4934fc2251fc66d5ef95335",
22
+ "shortCommit": "bd260c7",
23
23
  "branch": "main",
24
- "tag": "v19.52.0",
25
- "commitDate": "2026-06-29T19:21:11Z",
26
- "buildDate": "2026-06-29T19:48:42.397Z",
24
+ "tag": "v19.53.0",
25
+ "commitDate": "2026-06-30T11:39:51Z",
26
+ "buildDate": "2026-06-30T12:06:01.035Z",
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/a54c0128815fd00752c40aba2a1650817ca9874f",
32
- "releaseUrl": "https://github.com/f5-sales-demo/xcsh/releases/tag/v19.52.0"
31
+ "commitUrl": "https://github.com/f5-sales-demo/xcsh/commit/bd260c74c8205835f4934fc2251fc66d5ef95335",
32
+ "releaseUrl": "https://github.com/f5-sales-demo/xcsh/releases/tag/v19.53.0"
33
33
  };
package/src/main.ts CHANGED
@@ -21,6 +21,8 @@ import {
21
21
  VERSION,
22
22
  } from "@f5-sales-demo/pi-utils";
23
23
  import chalk from "chalk";
24
+ import { ChatHandler } from "./browser/chat-handler";
25
+ import { startBridgeServer } from "./browser/extension-bridge";
24
26
  import { invalidate as invalidateFsCache } from "./capability/fs";
25
27
  import type { Args } from "./cli/args";
26
28
  import { processFileArguments } from "./cli/file-processor";
@@ -883,6 +885,18 @@ export async function runRootCommand(parsed: Args, rawArgs: string[]): Promise<v
883
885
  return nextSession;
884
886
  };
885
887
 
888
+ // Start the extension bridge chat handler when the extension provider is active.
889
+ // The bridge server starts eagerly so chat_request messages are accepted immediately.
890
+ if (process.env.XCSH_BROWSER_PROVIDER?.toLowerCase() === "extension") {
891
+ const bridgeServer = await startBridgeServer();
892
+ const chatHandler = new ChatHandler(bridgeServer, session);
893
+ chatHandler.attach();
894
+ session.addDisposeHook(() => {
895
+ chatHandler.dispose();
896
+ return bridgeServer.close();
897
+ });
898
+ }
899
+
886
900
  if (mode === "rpc") {
887
901
  await runRpcMode(session);
888
902
  } else if (mode === "acp") {
@@ -0,0 +1,56 @@
1
+ import type { AgentTool, AgentToolResult } from "@f5-sales-demo/pi-agent-core";
2
+ import { Type } from "@sinclair/typebox";
3
+ import type { PageContextSnapshot } from "../browser/chat-protocol";
4
+ import type { ToolSession } from ".";
5
+ import { ToolError } from "./tool-errors";
6
+
7
+ const schema = Type.Object({}, { additionalProperties: false });
8
+
9
+ export class GetPageContextTool implements AgentTool<typeof schema> {
10
+ readonly name = "get_page_context";
11
+ readonly label = "GetPageContext";
12
+ readonly description =
13
+ "Read the current page context snapshot from the Chrome extension (URL, title, accessibility tree, live API response). Use this to refresh page state mid-conversation if the user may have navigated.";
14
+ readonly parameters = schema;
15
+ readonly strict = true;
16
+
17
+ #session: ToolSession;
18
+
19
+ constructor(session: ToolSession) {
20
+ this.#session = session;
21
+ }
22
+
23
+ static createIf(session: ToolSession): GetPageContextTool | null {
24
+ if (!session.bridgeServer) return null;
25
+ return new GetPageContextTool(session);
26
+ }
27
+
28
+ async execute(_toolCallId: string, _params: Record<string, never>): Promise<AgentToolResult> {
29
+ const server = this.#session.bridgeServer;
30
+ if (!server?.connected) {
31
+ throw new ToolError("Chrome extension is not connected.");
32
+ }
33
+
34
+ const result = await server.request("get_page_context", {});
35
+ if (result.is_error) {
36
+ throw new ToolError(`Failed to get page context: ${JSON.stringify(result.content)}`);
37
+ }
38
+
39
+ const ctx = result.content as PageContextSnapshot;
40
+ const parts: string[] = [];
41
+ parts.push(`URL: ${ctx.url}`);
42
+ parts.push(`Title: ${ctx.title}`);
43
+ parts.push(`Path: ${ctx.path}`);
44
+ if (ctx.api) {
45
+ parts.push(`API (${ctx.api.resourceType}, ${ctx.api.status}): ${ctx.api.url}`);
46
+ if (ctx.api.body) {
47
+ parts.push(typeof ctx.api.body === "string" ? ctx.api.body : JSON.stringify(ctx.api.body, null, 2));
48
+ }
49
+ }
50
+ if (ctx.ax) {
51
+ parts.push(`AX: ${typeof ctx.ax === "string" ? ctx.ax : JSON.stringify(ctx.ax)}`);
52
+ }
53
+
54
+ return { content: [{ type: "text", text: parts.join("\n") }] };
55
+ }
56
+ }
@@ -31,6 +31,7 @@ import { DebugTool } from "./debug";
31
31
  import { DisplayImageTool } from "./display-image";
32
32
  import { ExitPlanModeTool } from "./exit-plan-mode";
33
33
  import { FindTool } from "./find";
34
+ import { GetPageContextTool } from "./get-page-context";
34
35
  import { GrepTool } from "./grep";
35
36
  import { InspectImageTool } from "./inspect-image";
36
37
  import { NotebookTool } from "./notebook";
@@ -198,6 +199,9 @@ export interface ToolSession {
198
199
 
199
200
  /** Queue a hidden message to be injected at the next agent turn. */
200
201
  queueDeferredMessage?(message: CustomMessage): void;
202
+
203
+ /** Extension bridge server (available when Chrome extension is connected). */
204
+ bridgeServer?: import("../browser/extension-bridge").BridgeServer;
201
205
  }
202
206
 
203
207
  type ToolFactory = (session: ToolSession) => Tool | null | Promise<Tool | null>;
@@ -233,6 +237,7 @@ export const BUILTIN_TOOLS: Record<string, ToolFactory> = {
233
237
  write: s => new WriteTool(s),
234
238
  xcsh_api: s => new XcshApiTool(s),
235
239
  set_presentation_profile: s => new SetPresentationProfileTool(s),
240
+ get_page_context: GetPageContextTool.createIf,
236
241
  };
237
242
 
238
243
  export const HIDDEN_TOOLS: Record<string, ToolFactory> = {