@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 CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@f5-sales-demo/xcsh",
4
- "version": "21.5.6",
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.5.6",
65
- "@f5-sales-demo/pi-ai": "21.5.6",
66
- "@f5-sales-demo/pi-natives": "21.5.6",
67
- "@f5-sales-demo/pi-resource-management": "21.5.6",
68
- "@f5-sales-demo/pi-tui": "21.5.6",
69
- "@f5-sales-demo/pi-utils": "21.5.6",
70
- "@f5-sales-demo/xcsh-stats": "21.5.6",
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
+ }
@@ -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.5.6",
21
- "commit": "d3cadfbd84e1ec85fc4f48619ffdabe334164c90",
22
- "shortCommit": "d3cadfb",
20
+ "version": "21.6.0",
21
+ "commit": "3c4117bc7ac3253a8faf13a50641787d01ac11be",
22
+ "shortCommit": "3c4117b",
23
23
  "branch": "main",
24
- "tag": "v21.5.6",
25
- "commitDate": "2026-09-02T00:45:22+00:00",
26
- "buildDate": "2026-09-02T01:15:05.987Z",
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/d3cadfbd84e1ec85fc4f48619ffdabe334164c90",
32
- "releaseUrl": "https://github.com/f5-sales-demo/xcsh/releases/tag/v21.5.6"
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
+ }
@@ -1,5 +1,6 @@
1
1
  // UI Components barrel export
2
2
  export * from "./assistant-message";
3
+ export * from "./auth-link-presenter";
3
4
  export * from "./bash-execution";
4
5
  export * from "./bordered-loader";
5
6
  export * from "./branch-summary-message";
@@ -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.#contentContainer.addChild(new Text(theme.fg("contentAccent", url), 1, 0));
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
  }