@f5-sales-demo/xcsh 21.5.6 → 21.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 +8 -8
- package/src/config/settings-schema.ts +12 -0
- package/src/context/profile.ts +285 -0
- package/src/debug/index.ts +34 -0
- package/src/internal-urls/build-info.generated.ts +8 -8
- package/src/mcp/discoverable-tool-metadata.ts +9 -0
- package/src/modes/components/auth-link-presenter.ts +36 -0
- package/src/modes/components/index.ts +1 -0
- package/src/modes/components/login-dialog.ts +9 -6
- package/src/modes/controllers/mcp-command-controller.ts +53 -71
- package/src/modes/controllers/selector-controller.ts +3 -4
- package/src/prompts/system/progressive-system-prompt.md +108 -0
- package/src/prompts/tools/search-tool-bm25-progressive.md +38 -0
- package/src/sdk.ts +53 -9
- package/src/session/agent-session.ts +77 -7
- package/src/session/session-manager.ts +34 -0
- package/src/system-prompt.ts +39 -2
- package/src/tools/index.ts +12 -1
- package/src/tools/search-tool-bm25.ts +47 -23
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@f5-sales-demo/xcsh",
|
|
4
|
-
"version": "21.
|
|
4
|
+
"version": "21.6.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",
|
|
@@ -61,13 +61,13 @@
|
|
|
61
61
|
},
|
|
62
62
|
"dependencies": {
|
|
63
63
|
"@agentclientprotocol/sdk": "1.3.0",
|
|
64
|
-
"@f5-sales-demo/pi-agent-core": "21.
|
|
65
|
-
"@f5-sales-demo/pi-ai": "21.
|
|
66
|
-
"@f5-sales-demo/pi-natives": "21.
|
|
67
|
-
"@f5-sales-demo/pi-resource-management": "21.
|
|
68
|
-
"@f5-sales-demo/pi-tui": "21.
|
|
69
|
-
"@f5-sales-demo/pi-utils": "21.
|
|
70
|
-
"@f5-sales-demo/xcsh-stats": "21.
|
|
64
|
+
"@f5-sales-demo/pi-agent-core": "21.6.0",
|
|
65
|
+
"@f5-sales-demo/pi-ai": "21.6.0",
|
|
66
|
+
"@f5-sales-demo/pi-natives": "21.6.0",
|
|
67
|
+
"@f5-sales-demo/pi-resource-management": "21.6.0",
|
|
68
|
+
"@f5-sales-demo/pi-tui": "21.6.0",
|
|
69
|
+
"@f5-sales-demo/pi-utils": "21.6.0",
|
|
70
|
+
"@f5-sales-demo/xcsh-stats": "21.6.0",
|
|
71
71
|
"@mozilla/readability": "^0.6",
|
|
72
72
|
"@sinclair/typebox": "^0.34",
|
|
73
73
|
"@xterm/headless": "^6.0",
|
|
@@ -549,6 +549,18 @@ export const SETTINGS_SCHEMA = {
|
|
|
549
549
|
},
|
|
550
550
|
},
|
|
551
551
|
|
|
552
|
+
"context.loadingMode": {
|
|
553
|
+
type: "enum",
|
|
554
|
+
values: ["eager", "progressive"] as const,
|
|
555
|
+
default: "eager",
|
|
556
|
+
ui: {
|
|
557
|
+
tab: "model",
|
|
558
|
+
label: "Context Loading",
|
|
559
|
+
description: "Load all capabilities eagerly or activate optional context progressively",
|
|
560
|
+
submenu: true,
|
|
561
|
+
},
|
|
562
|
+
},
|
|
563
|
+
|
|
552
564
|
// Dynamic routing
|
|
553
565
|
"routing.mode": {
|
|
554
566
|
type: "enum",
|
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
import type { Model, Usage } from "@f5-sales-demo/pi-ai";
|
|
2
|
+
|
|
3
|
+
export type ContextLoadingMode = "eager" | "progressive";
|
|
4
|
+
|
|
5
|
+
export type ContextComponentCategory =
|
|
6
|
+
| "system_prompt"
|
|
7
|
+
| "context_file"
|
|
8
|
+
| "skill"
|
|
9
|
+
| "plugin_catalog"
|
|
10
|
+
| "rule"
|
|
11
|
+
| "tool_description"
|
|
12
|
+
| "tool_schema"
|
|
13
|
+
| "message"
|
|
14
|
+
| "tool_result";
|
|
15
|
+
|
|
16
|
+
export interface ContextComponentProfile {
|
|
17
|
+
category: ContextComponentCategory;
|
|
18
|
+
label: string;
|
|
19
|
+
bytes: number;
|
|
20
|
+
estimatedTokens: number;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface ContextToolProfile {
|
|
24
|
+
name: string;
|
|
25
|
+
descriptionBytes: number;
|
|
26
|
+
schemaBytes: number;
|
|
27
|
+
estimatedTokens: number;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface ContextMessageProfile {
|
|
31
|
+
role: string;
|
|
32
|
+
bytes: number;
|
|
33
|
+
estimatedTokens: number;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface ProviderCallProfile {
|
|
37
|
+
call: number;
|
|
38
|
+
provider: string;
|
|
39
|
+
model: string;
|
|
40
|
+
api: string;
|
|
41
|
+
payloadBytes: number;
|
|
42
|
+
estimatedPayloadTokens: number;
|
|
43
|
+
categoryBytes: Record<"system_prompt" | "tools" | "messages" | "tool_results" | "other", number>;
|
|
44
|
+
toolCount: number;
|
|
45
|
+
messageCount: number;
|
|
46
|
+
tools: ContextToolProfile[];
|
|
47
|
+
messages: ContextMessageProfile[];
|
|
48
|
+
contextWindow: number;
|
|
49
|
+
providerInputTokens?: number;
|
|
50
|
+
providerCacheReadTokens?: number;
|
|
51
|
+
providerCacheWriteTokens?: number;
|
|
52
|
+
providerPromptTokens?: number;
|
|
53
|
+
providerOutputTokens?: number;
|
|
54
|
+
windowPercentage?: number;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface ContextProfile {
|
|
58
|
+
loadingMode: ContextLoadingMode;
|
|
59
|
+
systemPromptBytes: number;
|
|
60
|
+
estimatedSystemPromptTokens: number;
|
|
61
|
+
initialToolBytes: number;
|
|
62
|
+
deferredToolBytes: number;
|
|
63
|
+
components: ContextComponentProfile[];
|
|
64
|
+
tools: ContextToolProfile[];
|
|
65
|
+
providerCalls: ProviderCallProfile[];
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function estimateContextTokens(bytes: number): number {
|
|
69
|
+
return bytes <= 0 ? 0 : Math.ceil(bytes / 4);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function jsonBytes(value: unknown): number {
|
|
73
|
+
if (value === undefined) return 0;
|
|
74
|
+
try {
|
|
75
|
+
return Buffer.byteLength(JSON.stringify(value), "utf8");
|
|
76
|
+
} catch {
|
|
77
|
+
return 0;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
82
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function firstArray(record: Record<string, unknown>, keys: string[]): unknown[] {
|
|
86
|
+
for (const key of keys) {
|
|
87
|
+
const value = record[key];
|
|
88
|
+
if (Array.isArray(value)) return value;
|
|
89
|
+
}
|
|
90
|
+
return [];
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function firstDefined(record: Record<string, unknown>, keys: string[]): unknown {
|
|
94
|
+
for (const key of keys) {
|
|
95
|
+
if (record[key] !== undefined) return record[key];
|
|
96
|
+
}
|
|
97
|
+
return undefined;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function safeRole(value: unknown): string {
|
|
101
|
+
if (!isRecord(value) || typeof value.role !== "string") return "unknown";
|
|
102
|
+
return ["system", "developer", "user", "assistant", "tool", "toolResult", "model"].includes(value.role)
|
|
103
|
+
? value.role
|
|
104
|
+
: "other";
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function isToolResultMessage(value: unknown): boolean {
|
|
108
|
+
if (!isRecord(value)) return false;
|
|
109
|
+
if (value.role === "tool" || value.role === "toolResult" || value.type === "function_call_output") return true;
|
|
110
|
+
const content = Array.isArray(value.content) ? value.content : [];
|
|
111
|
+
if (content.some(block => isRecord(block) && block.type === "tool_result")) return true;
|
|
112
|
+
const parts = Array.isArray(value.parts) ? value.parts : [];
|
|
113
|
+
return parts.some(part => isRecord(part) && ("functionResponse" in part || "function_response" in part));
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function toolName(value: unknown): string | undefined {
|
|
117
|
+
if (!isRecord(value)) return undefined;
|
|
118
|
+
if (typeof value.name === "string") return value.name;
|
|
119
|
+
if (isRecord(value.function) && typeof value.function.name === "string") return value.function.name;
|
|
120
|
+
return undefined;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function flattenProviderTools(tools: unknown[]): unknown[] {
|
|
124
|
+
const flattened: unknown[] = [];
|
|
125
|
+
for (const tool of tools) {
|
|
126
|
+
if (isRecord(tool) && Array.isArray(tool.functionDeclarations)) {
|
|
127
|
+
flattened.push(...tool.functionDeclarations);
|
|
128
|
+
} else {
|
|
129
|
+
flattened.push(tool);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return flattened;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function profilePayloadTool(value: unknown, index: number): ContextToolProfile {
|
|
136
|
+
const record = isRecord(value) ? value : {};
|
|
137
|
+
const definition = isRecord(record.function) ? record.function : record;
|
|
138
|
+
const descriptionBytes = jsonBytes(definition.description);
|
|
139
|
+
const schemaBytes = jsonBytes(definition.parameters ?? definition.input_schema ?? definition.schema);
|
|
140
|
+
return {
|
|
141
|
+
name: toolName(value) ?? `tool_${index + 1}`,
|
|
142
|
+
descriptionBytes,
|
|
143
|
+
schemaBytes,
|
|
144
|
+
estimatedTokens: estimateContextTokens(descriptionBytes + schemaBytes),
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export function profileProviderPayload(payload: unknown, model: Model, call: number): ProviderCallProfile {
|
|
149
|
+
const record = isRecord(payload) ? payload : {};
|
|
150
|
+
const body = isRecord(record.request) ? record.request : record;
|
|
151
|
+
const system = firstDefined(body, ["systemInstruction", "system_instruction", "system", "instructions"]);
|
|
152
|
+
const rawTools = firstArray(body, ["tools", "functions"]);
|
|
153
|
+
const rawMessages = firstArray(body, ["contents", "messages", "input"]);
|
|
154
|
+
const tools = flattenProviderTools(rawTools);
|
|
155
|
+
const payloadBytes = jsonBytes(payload);
|
|
156
|
+
const systemBytes = jsonBytes(system);
|
|
157
|
+
const toolsBytes = jsonBytes(rawTools);
|
|
158
|
+
const toolResultBytes = rawMessages
|
|
159
|
+
.filter(isToolResultMessage)
|
|
160
|
+
.reduce<number>((sum, message) => sum + jsonBytes(message), 0);
|
|
161
|
+
const messagesBytes = rawMessages
|
|
162
|
+
.filter(message => !isToolResultMessage(message))
|
|
163
|
+
.reduce<number>((sum, message) => sum + jsonBytes(message), 0);
|
|
164
|
+
return {
|
|
165
|
+
call,
|
|
166
|
+
provider: model.provider,
|
|
167
|
+
model: model.id,
|
|
168
|
+
api: model.api,
|
|
169
|
+
payloadBytes,
|
|
170
|
+
estimatedPayloadTokens: estimateContextTokens(payloadBytes),
|
|
171
|
+
categoryBytes: {
|
|
172
|
+
system_prompt: systemBytes,
|
|
173
|
+
tools: toolsBytes,
|
|
174
|
+
messages: messagesBytes,
|
|
175
|
+
tool_results: toolResultBytes,
|
|
176
|
+
other: Math.max(0, payloadBytes - systemBytes - toolsBytes - messagesBytes - toolResultBytes),
|
|
177
|
+
},
|
|
178
|
+
toolCount: tools.length,
|
|
179
|
+
messageCount: rawMessages.length,
|
|
180
|
+
tools: tools.map(profilePayloadTool),
|
|
181
|
+
messages: rawMessages.map(message => {
|
|
182
|
+
const bytes = jsonBytes(message);
|
|
183
|
+
return { role: safeRole(message), bytes, estimatedTokens: estimateContextTokens(bytes) };
|
|
184
|
+
}),
|
|
185
|
+
contextWindow: model.contextWindow,
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export class ContextProfileCollector {
|
|
190
|
+
readonly #profile: ContextProfile;
|
|
191
|
+
#attributedComponents: ContextComponentProfile[] = [];
|
|
192
|
+
|
|
193
|
+
constructor(loadingMode: ContextLoadingMode) {
|
|
194
|
+
this.#profile = {
|
|
195
|
+
loadingMode,
|
|
196
|
+
systemPromptBytes: 0,
|
|
197
|
+
estimatedSystemPromptTokens: 0,
|
|
198
|
+
initialToolBytes: 0,
|
|
199
|
+
deferredToolBytes: 0,
|
|
200
|
+
components: [],
|
|
201
|
+
tools: [],
|
|
202
|
+
providerCalls: [],
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
setPrompt(
|
|
207
|
+
systemPrompt: string,
|
|
208
|
+
tools: Iterable<{ name: string; description?: string; parameters?: unknown }>,
|
|
209
|
+
): void {
|
|
210
|
+
const systemPromptBytes = Buffer.byteLength(systemPrompt, "utf8");
|
|
211
|
+
const toolProfiles = Array.from(tools, tool => {
|
|
212
|
+
const descriptionBytes = Buffer.byteLength(tool.description ?? "", "utf8");
|
|
213
|
+
const schemaBytes = jsonBytes(tool.parameters);
|
|
214
|
+
return {
|
|
215
|
+
name: tool.name,
|
|
216
|
+
descriptionBytes,
|
|
217
|
+
schemaBytes,
|
|
218
|
+
estimatedTokens: estimateContextTokens(descriptionBytes + schemaBytes),
|
|
219
|
+
};
|
|
220
|
+
});
|
|
221
|
+
this.#profile.systemPromptBytes = systemPromptBytes;
|
|
222
|
+
this.#profile.estimatedSystemPromptTokens = estimateContextTokens(systemPromptBytes);
|
|
223
|
+
this.#profile.tools = toolProfiles;
|
|
224
|
+
this.#profile.initialToolBytes = toolProfiles.reduce(
|
|
225
|
+
(sum, tool) => sum + tool.descriptionBytes + tool.schemaBytes,
|
|
226
|
+
0,
|
|
227
|
+
);
|
|
228
|
+
this.#profile.components = [
|
|
229
|
+
{
|
|
230
|
+
category: "system_prompt",
|
|
231
|
+
label: "rendered",
|
|
232
|
+
bytes: systemPromptBytes,
|
|
233
|
+
estimatedTokens: estimateContextTokens(systemPromptBytes),
|
|
234
|
+
},
|
|
235
|
+
...toolProfiles.flatMap(tool => [
|
|
236
|
+
{
|
|
237
|
+
category: "tool_description" as const,
|
|
238
|
+
label: tool.name,
|
|
239
|
+
bytes: tool.descriptionBytes,
|
|
240
|
+
estimatedTokens: estimateContextTokens(tool.descriptionBytes),
|
|
241
|
+
},
|
|
242
|
+
{
|
|
243
|
+
category: "tool_schema" as const,
|
|
244
|
+
label: tool.name,
|
|
245
|
+
bytes: tool.schemaBytes,
|
|
246
|
+
estimatedTokens: estimateContextTokens(tool.schemaBytes),
|
|
247
|
+
},
|
|
248
|
+
]),
|
|
249
|
+
...this.#attributedComponents,
|
|
250
|
+
];
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
setAttributedComponents(components: ContextComponentProfile[]): void {
|
|
254
|
+
this.#attributedComponents = components.map(component => ({ ...component }));
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
setDeferredTools(tools: Iterable<{ description?: string; parameters?: unknown }>): void {
|
|
258
|
+
this.#profile.deferredToolBytes = Array.from(tools).reduce(
|
|
259
|
+
(sum, tool) => sum + Buffer.byteLength(tool.description ?? "", "utf8") + jsonBytes(tool.parameters),
|
|
260
|
+
0,
|
|
261
|
+
);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
captureProviderPayload(payload: unknown, model: Model): void {
|
|
265
|
+
this.#profile.providerCalls.push(profileProviderPayload(payload, model, this.#profile.providerCalls.length + 1));
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
recordProviderUsage(model: Model, usage: Usage): void {
|
|
269
|
+
const call = [...this.#profile.providerCalls]
|
|
270
|
+
.reverse()
|
|
271
|
+
.find(candidate => candidate.model === model.id && candidate.providerPromptTokens === undefined);
|
|
272
|
+
if (!call) return;
|
|
273
|
+
const promptTokens = usage.input + usage.cacheRead + usage.cacheWrite;
|
|
274
|
+
call.providerInputTokens = usage.input;
|
|
275
|
+
call.providerCacheReadTokens = usage.cacheRead;
|
|
276
|
+
call.providerCacheWriteTokens = usage.cacheWrite;
|
|
277
|
+
call.providerPromptTokens = promptTokens;
|
|
278
|
+
call.providerOutputTokens = usage.output;
|
|
279
|
+
call.windowPercentage = call.contextWindow > 0 ? (promptTokens / call.contextWindow) * 100 : 0;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
snapshot(): ContextProfile {
|
|
283
|
+
return structuredClone(this.#profile);
|
|
284
|
+
}
|
|
285
|
+
}
|
package/src/debug/index.ts
CHANGED
|
@@ -8,6 +8,7 @@ import * as url from "node:url";
|
|
|
8
8
|
import { getWorkProfile } from "@f5-sales-demo/pi-natives";
|
|
9
9
|
import { Container, Loader, type SelectItem, SelectList, Spacer, Text } from "@f5-sales-demo/pi-tui";
|
|
10
10
|
import { getSessionsDir } from "@f5-sales-demo/pi-utils";
|
|
11
|
+
import type { ContextProfile } from "../context/profile";
|
|
11
12
|
import { DynamicBorder } from "../modes/components/dynamic-border";
|
|
12
13
|
import { getSelectListTheme, getSymbolTheme, theme } from "../modes/theme/theme";
|
|
13
14
|
import type { InteractiveModeContext } from "../modes/types";
|
|
@@ -27,6 +28,7 @@ const DEBUG_MENU_ITEMS: SelectItem[] = [
|
|
|
27
28
|
{ value: "memory", label: "Report: memory issue", description: "Heap snapshot + bundle" },
|
|
28
29
|
{ value: "logs", label: "View: recent logs", description: "Show last 50 log entries" },
|
|
29
30
|
{ value: "system", label: "View: system info", description: "Show environment details" },
|
|
31
|
+
{ value: "context-profile", label: "View: Context Profile", description: "Show privacy-safe context measurements" },
|
|
30
32
|
{
|
|
31
33
|
value: "transcript",
|
|
32
34
|
label: "Export: TUI transcript",
|
|
@@ -100,6 +102,9 @@ export class DebugSelectorComponent extends Container {
|
|
|
100
102
|
case "system":
|
|
101
103
|
await this.#handleViewSystemInfo();
|
|
102
104
|
break;
|
|
105
|
+
case "context-profile":
|
|
106
|
+
this.#handleContextProfile();
|
|
107
|
+
break;
|
|
103
108
|
case "transcript":
|
|
104
109
|
await this.#handleTranscriptExport();
|
|
105
110
|
break;
|
|
@@ -333,6 +338,14 @@ export class DebugSelectorComponent extends Container {
|
|
|
333
338
|
this.ctx.ui.requestRender();
|
|
334
339
|
}
|
|
335
340
|
|
|
341
|
+
#handleContextProfile(): void {
|
|
342
|
+
this.ctx.chatContainer.addChild(new Spacer(1));
|
|
343
|
+
this.ctx.chatContainer.addChild(new DynamicBorder());
|
|
344
|
+
this.ctx.chatContainer.addChild(new Text(formatContextProfile(this.ctx.session.getContextProfile()), 1, 0));
|
|
345
|
+
this.ctx.chatContainer.addChild(new DynamicBorder());
|
|
346
|
+
this.ctx.ui.requestRender();
|
|
347
|
+
}
|
|
348
|
+
|
|
336
349
|
async #handleTranscriptExport(): Promise<void> {
|
|
337
350
|
await this.ctx.handleDebugTranscriptCommand();
|
|
338
351
|
}
|
|
@@ -431,6 +444,27 @@ export class DebugSelectorComponent extends Container {
|
|
|
431
444
|
}
|
|
432
445
|
}
|
|
433
446
|
|
|
447
|
+
export function formatContextProfile(profile: ContextProfile): string {
|
|
448
|
+
const lines = [
|
|
449
|
+
"Context Profile",
|
|
450
|
+
`Loading: ${profile.loadingMode}`,
|
|
451
|
+
`System prompt: ${profile.systemPromptBytes.toLocaleString()} bytes (~${profile.estimatedSystemPromptTokens.toLocaleString()} tokens)`,
|
|
452
|
+
`Initial tools: ${profile.initialToolBytes.toLocaleString()} bytes; deferred: ${profile.deferredToolBytes.toLocaleString()} bytes`,
|
|
453
|
+
];
|
|
454
|
+
for (const call of profile.providerCalls) {
|
|
455
|
+
const prompt = call.providerPromptTokens?.toLocaleString() ?? "pending";
|
|
456
|
+
const cacheRead = call.providerCacheReadTokens?.toLocaleString() ?? "pending";
|
|
457
|
+
const output = call.providerOutputTokens?.toLocaleString() ?? "pending";
|
|
458
|
+
const percentage = call.windowPercentage === undefined ? "pending" : `${call.windowPercentage.toFixed(3)}%`;
|
|
459
|
+
lines.push(
|
|
460
|
+
`Call ${call.call}: ${call.provider}/${call.model} — prompt ${prompt}, cache read ${cacheRead}, output ${output}, window ${percentage}`,
|
|
461
|
+
` Payload: ${call.payloadBytes.toLocaleString()} bytes (${call.toolCount} tools, ${call.messageCount} messages)`,
|
|
462
|
+
);
|
|
463
|
+
}
|
|
464
|
+
if (profile.providerCalls.length === 0) lines.push("Provider calls: none yet");
|
|
465
|
+
return lines.join("\n");
|
|
466
|
+
}
|
|
467
|
+
|
|
434
468
|
/**
|
|
435
469
|
* Show the debug selector.
|
|
436
470
|
*/
|
|
@@ -17,17 +17,17 @@ export interface BuildInfo {
|
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
export const BUILD_INFO: BuildInfo = {
|
|
20
|
-
"version": "21.
|
|
21
|
-
"commit": "
|
|
22
|
-
"shortCommit": "
|
|
20
|
+
"version": "21.6.0",
|
|
21
|
+
"commit": "3c4117bc7ac3253a8faf13a50641787d01ac11be",
|
|
22
|
+
"shortCommit": "3c4117b",
|
|
23
23
|
"branch": "main",
|
|
24
|
-
"tag": "v21.
|
|
25
|
-
"commitDate": "2026-09-
|
|
26
|
-
"buildDate": "2026-09-
|
|
24
|
+
"tag": "v21.6.0",
|
|
25
|
+
"commitDate": "2026-09-02T04:05:59+00:00",
|
|
26
|
+
"buildDate": "2026-09-02T04:35:09.450Z",
|
|
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/
|
|
32
|
-
"releaseUrl": "https://github.com/f5-sales-demo/xcsh/releases/tag/v21.
|
|
31
|
+
"commitUrl": "https://github.com/f5-sales-demo/xcsh/commit/3c4117bc7ac3253a8faf13a50641787d01ac11be",
|
|
32
|
+
"releaseUrl": "https://github.com/f5-sales-demo/xcsh/releases/tag/v21.6.0"
|
|
33
33
|
};
|
|
@@ -96,6 +96,11 @@ function buildSearchDocument(tool: DiscoverableMCPTool): DiscoverableMCPSearchDo
|
|
|
96
96
|
|
|
97
97
|
export function getDiscoverableMCPTool(tool: AgentTool): DiscoverableMCPTool | null {
|
|
98
98
|
if (!isMCPToolName(tool.name)) return null;
|
|
99
|
+
return getDiscoverableTool(tool);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** Build safe search metadata for any registered tool source. */
|
|
103
|
+
export function getDiscoverableTool(tool: AgentTool): DiscoverableMCPTool {
|
|
99
104
|
const toolRecord = tool as AgentTool & {
|
|
100
105
|
label?: string;
|
|
101
106
|
description?: string;
|
|
@@ -113,6 +118,10 @@ export function getDiscoverableMCPTool(tool: AgentTool): DiscoverableMCPTool | n
|
|
|
113
118
|
};
|
|
114
119
|
}
|
|
115
120
|
|
|
121
|
+
export function collectDiscoverableTools(tools: Iterable<AgentTool>): DiscoverableMCPTool[] {
|
|
122
|
+
return Array.from(tools, getDiscoverableTool);
|
|
123
|
+
}
|
|
124
|
+
|
|
116
125
|
export function collectDiscoverableMCPTools(tools: Iterable<AgentTool>): DiscoverableMCPTool[] {
|
|
117
126
|
const discoverable: DiscoverableMCPTool[] = [];
|
|
118
127
|
for (const tool of tools) {
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { type Container, Text } from "@f5-sales-demo/pi-tui";
|
|
2
|
+
import { copyToClipboard } from "../../utils/clipboard";
|
|
3
|
+
import { theme } from "../theme/theme";
|
|
4
|
+
|
|
5
|
+
type AuthLinkContainer = Pick<Container, "addChild">;
|
|
6
|
+
|
|
7
|
+
export interface AuthLinkPresenterOptions {
|
|
8
|
+
copy?: (url: string) => void | Promise<void>;
|
|
9
|
+
platform?: NodeJS.Platform;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/** Present a browser authorization target without exposing its long URL as visible terminal text. */
|
|
13
|
+
export function presentAuthLink(
|
|
14
|
+
container: AuthLinkContainer,
|
|
15
|
+
url: string,
|
|
16
|
+
options: AuthLinkPresenterOptions = {},
|
|
17
|
+
): void {
|
|
18
|
+
const platform = options.platform ?? process.platform;
|
|
19
|
+
const clickHint = platform === "darwin" ? "Cmd+click to open" : "Ctrl+click to open";
|
|
20
|
+
const hyperlink = `\x1b]8;;${url}\x07Open sign-in page\x1b]8;;\x07`;
|
|
21
|
+
container.addChild(new Text(`${theme.fg("accent", hyperlink)} ${theme.fg("dim", `(${clickHint})`)}`, 1, 0));
|
|
22
|
+
container.addChild(
|
|
23
|
+
new Text(
|
|
24
|
+
theme.fg("dim", "Sign-in URL copied when supported. Clipboard availability depends on terminal support."),
|
|
25
|
+
1,
|
|
26
|
+
0,
|
|
27
|
+
),
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
const copy = options.copy ?? copyToClipboard;
|
|
31
|
+
try {
|
|
32
|
+
void Promise.resolve(copy(url)).catch(() => undefined);
|
|
33
|
+
} catch {
|
|
34
|
+
// Clipboard access is best-effort; the OSC 8 link remains available.
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -2,8 +2,14 @@ import { getOAuthProviders } from "@f5-sales-demo/pi-ai";
|
|
|
2
2
|
import { Container, getKeybindings, Input, Spacer, Text, type TUI } from "@f5-sales-demo/pi-tui";
|
|
3
3
|
import { theme } from "../../modes/theme/theme";
|
|
4
4
|
import { openPath } from "../../utils/open";
|
|
5
|
+
import { presentAuthLink } from "./auth-link-presenter";
|
|
5
6
|
import { DynamicBorder } from "./dynamic-border";
|
|
6
7
|
|
|
8
|
+
interface LoginDialogDependencies {
|
|
9
|
+
openUrl?: (url: string) => void;
|
|
10
|
+
presentLink?: typeof presentAuthLink;
|
|
11
|
+
}
|
|
12
|
+
|
|
7
13
|
/**
|
|
8
14
|
* Login dialog component - replaces editor during OAuth login flow
|
|
9
15
|
*/
|
|
@@ -19,6 +25,7 @@ export class LoginDialogComponent extends Container {
|
|
|
19
25
|
tui: TUI,
|
|
20
26
|
providerId: string,
|
|
21
27
|
private onComplete: (success: boolean, message?: string) => void,
|
|
28
|
+
private dependencies: LoginDialogDependencies = {},
|
|
22
29
|
) {
|
|
23
30
|
super();
|
|
24
31
|
this.#tui = tui;
|
|
@@ -73,11 +80,7 @@ export class LoginDialogComponent extends Container {
|
|
|
73
80
|
showAuth(url: string, instructions?: string): void {
|
|
74
81
|
this.#contentContainer.clear();
|
|
75
82
|
this.#contentContainer.addChild(new Spacer(1));
|
|
76
|
-
this
|
|
77
|
-
|
|
78
|
-
const clickHint = process.platform === "darwin" ? "Cmd+click to open" : "Ctrl+click to open";
|
|
79
|
-
const hyperlink = `\x1b]8;;${url}\x07${clickHint}\x1b]8;;\x07`;
|
|
80
|
-
this.#contentContainer.addChild(new Text(theme.fg("dim", hyperlink), 1, 0));
|
|
83
|
+
(this.dependencies.presentLink ?? presentAuthLink)(this.#contentContainer, url);
|
|
81
84
|
|
|
82
85
|
if (instructions) {
|
|
83
86
|
this.#contentContainer.addChild(new Spacer(1));
|
|
@@ -85,7 +88,7 @@ export class LoginDialogComponent extends Container {
|
|
|
85
88
|
}
|
|
86
89
|
|
|
87
90
|
// Open browser (best-effort)
|
|
88
|
-
openPath(url);
|
|
91
|
+
(this.dependencies.openUrl ?? openPath)(url);
|
|
89
92
|
|
|
90
93
|
this.#tui.requestRender();
|
|
91
94
|
}
|