@agentuity/coder-tui 3.0.0-alpha.7 → 3.0.0-beta.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/dist/aigateway.d.ts +4 -0
- package/dist/aigateway.d.ts.map +1 -0
- package/dist/aigateway.js +178 -0
- package/dist/aigateway.js.map +1 -0
- package/dist/footer.d.ts +3 -2
- package/dist/footer.d.ts.map +1 -1
- package/dist/footer.js +50 -16
- package/dist/footer.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +10 -2
- package/dist/index.js.map +1 -1
- package/dist/startup-logo.d.ts +3 -0
- package/dist/startup-logo.d.ts.map +1 -0
- package/dist/startup-logo.js +212 -0
- package/dist/startup-logo.js.map +1 -0
- package/package.json +5 -5
- package/src/aigateway.ts +256 -0
- package/src/footer.ts +62 -15
- package/src/index.ts +13 -2
- package/src/startup-logo.ts +255 -0
package/src/aigateway.ts
ADDED
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agentuity AI Gateway Custom Provider Extension
|
|
3
|
+
*
|
|
4
|
+
* Registers models from the Agentuity AI Gateway using the appropriate API type
|
|
5
|
+
* based on model ID patterns. Models are loaded dynamically from the gateway's /models endpoint.
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* Use /model to switch to aigateway models
|
|
9
|
+
*/
|
|
10
|
+
import { delimiter, join } from 'node:path';
|
|
11
|
+
import { existsSync } from 'node:fs';
|
|
12
|
+
import { execFileSync } from 'node:child_process';
|
|
13
|
+
import type { ExtensionAPI, ProviderModelConfig } from '@mariozechner/pi-coding-agent';
|
|
14
|
+
|
|
15
|
+
export type KnownApi =
|
|
16
|
+
| 'openai-completions'
|
|
17
|
+
| 'mistral-conversations'
|
|
18
|
+
| 'openai-responses'
|
|
19
|
+
| 'azure-openai-responses'
|
|
20
|
+
| 'openai-codex-responses'
|
|
21
|
+
| 'anthropic-messages'
|
|
22
|
+
| 'bedrock-converse-stream'
|
|
23
|
+
| 'google-generative-ai'
|
|
24
|
+
| 'google-gemini-cli'
|
|
25
|
+
| 'google-vertex';
|
|
26
|
+
|
|
27
|
+
const MODEL_CATALOG_TIMEOUT_MS = 5_000;
|
|
28
|
+
|
|
29
|
+
const KNOWN_APIS = new Set<string>([
|
|
30
|
+
'openai-completions',
|
|
31
|
+
'mistral-conversations',
|
|
32
|
+
'openai-responses',
|
|
33
|
+
'azure-openai-responses',
|
|
34
|
+
'openai-codex-responses',
|
|
35
|
+
'anthropic-messages',
|
|
36
|
+
'bedrock-converse-stream',
|
|
37
|
+
'google-generative-ai',
|
|
38
|
+
'google-gemini-cli',
|
|
39
|
+
'google-vertex',
|
|
40
|
+
] satisfies KnownApi[]);
|
|
41
|
+
|
|
42
|
+
interface AIGatewayModels {
|
|
43
|
+
[key: string]: AIGatewayModel[];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
interface AIGatewayModelResponse {
|
|
47
|
+
success: boolean;
|
|
48
|
+
data: AIGatewayModels;
|
|
49
|
+
message?: string;
|
|
50
|
+
error?: string;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
interface AIGatewayModel {
|
|
54
|
+
id: string;
|
|
55
|
+
name: string;
|
|
56
|
+
api: KnownApi;
|
|
57
|
+
reasoning: boolean;
|
|
58
|
+
input_modalities?: ('text' | 'image')[];
|
|
59
|
+
context_window?: number;
|
|
60
|
+
max_output_tokens?: number;
|
|
61
|
+
pricing?: {
|
|
62
|
+
input: number;
|
|
63
|
+
output: number;
|
|
64
|
+
cached_input: number;
|
|
65
|
+
unit: 'per_million_tokens';
|
|
66
|
+
currency: 'USD';
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function getEnv(...keys: string[]): string | undefined {
|
|
71
|
+
for (const key of keys) {
|
|
72
|
+
if (process.env[key]) {
|
|
73
|
+
return process.env[key];
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function normalizeCredential(value: unknown): string | undefined {
|
|
79
|
+
if (value === undefined || value === null) {
|
|
80
|
+
return undefined;
|
|
81
|
+
}
|
|
82
|
+
const normalized = String(value).trim();
|
|
83
|
+
return normalized.length > 0 ? normalized : undefined;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function isKnownApi(api: unknown): api is KnownApi {
|
|
87
|
+
return typeof api === 'string' && KNOWN_APIS.has(api);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function getRegion(): string {
|
|
91
|
+
return getEnv('AGENTUITY_REGION') ?? 'usc';
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function getBaseUrl(): string {
|
|
95
|
+
const region = getRegion();
|
|
96
|
+
return `https://aigateway-${region}.agentuity.cloud`;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async function fetchModels(): Promise<AIGatewayModels> {
|
|
100
|
+
const baseUrl = getBaseUrl();
|
|
101
|
+
let apiKey = normalizeCredential(
|
|
102
|
+
getEnv(
|
|
103
|
+
'AGENTUITY_CODER_API_KEY',
|
|
104
|
+
'AGENTUITY_SDK_KEY',
|
|
105
|
+
'AGENTUITY_CLI_API_KEY',
|
|
106
|
+
'AGENTUITY_CLI_KEY'
|
|
107
|
+
)
|
|
108
|
+
);
|
|
109
|
+
let orgId = normalizeCredential(
|
|
110
|
+
getEnv('AGENTUITY_ORGID', 'AGENTUITY_CLOUD_ORG_ID', 'AGENTUITY_ORG_ID')
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
if (!apiKey) {
|
|
114
|
+
let found = false;
|
|
115
|
+
const path = process.env.PATH?.split(delimiter) ?? [];
|
|
116
|
+
for (const dir of path) {
|
|
117
|
+
const fn = join(dir, 'agentuity');
|
|
118
|
+
if (existsSync(fn)) {
|
|
119
|
+
try {
|
|
120
|
+
const res = execFileSync(fn, ['auth', 'apikey', '--json']);
|
|
121
|
+
const apiKeyResult = JSON.parse(res.toString()) as { apiKey: string };
|
|
122
|
+
apiKey = normalizeCredential(apiKeyResult.apiKey);
|
|
123
|
+
found = true;
|
|
124
|
+
if (!orgId) {
|
|
125
|
+
const ores = execFileSync(fn, ['auth', 'org', 'current']);
|
|
126
|
+
orgId = normalizeCredential(ores);
|
|
127
|
+
if (!orgId) {
|
|
128
|
+
console.warn(
|
|
129
|
+
'Cannot determine the org id. Use `agentuity auth org select` to select a default organization'
|
|
130
|
+
);
|
|
131
|
+
return {};
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
break;
|
|
135
|
+
} catch (_ex) {
|
|
136
|
+
//
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
if (!found) {
|
|
141
|
+
console.warn(
|
|
142
|
+
'AGENTUITY_SDK_KEY, AGENTUITY_CLI_API_KEY or AGENTUITY_CLI_KEY not set and cannot find the agentuit cli, cannot fetch models from AI Gateway'
|
|
143
|
+
);
|
|
144
|
+
return {};
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (!apiKey) {
|
|
149
|
+
console.warn('Cannot determine the API key, cannot fetch models from AI Gateway');
|
|
150
|
+
return {};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
process.env.AGENTUITY_AIGATEWAY_KEY = apiKey;
|
|
154
|
+
if (orgId) {
|
|
155
|
+
process.env.AGENTUITY_AIGATEWAY_ORGID = orgId;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const controller = new AbortController();
|
|
159
|
+
const timeout = setTimeout(() => controller.abort(), MODEL_CATALOG_TIMEOUT_MS);
|
|
160
|
+
|
|
161
|
+
try {
|
|
162
|
+
const response = await fetch(`${baseUrl}/models`, { signal: controller.signal });
|
|
163
|
+
|
|
164
|
+
if (!response.ok) {
|
|
165
|
+
console.warn(
|
|
166
|
+
`Failed to fetch models from AI Gateway: ${response.status} ${response.statusText}`
|
|
167
|
+
);
|
|
168
|
+
return {};
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const payload = (await response.json()) as AIGatewayModelResponse;
|
|
172
|
+
|
|
173
|
+
if (!payload.success) {
|
|
174
|
+
console.warn(`Failed to load models. ${payload.message} ${payload}`);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return payload.data;
|
|
178
|
+
} catch (error) {
|
|
179
|
+
if (error instanceof Error && error.name === 'AbortError') {
|
|
180
|
+
console.warn(
|
|
181
|
+
`Timed out fetching models from AI Gateway after ${MODEL_CATALOG_TIMEOUT_MS}ms`
|
|
182
|
+
);
|
|
183
|
+
return {};
|
|
184
|
+
}
|
|
185
|
+
console.warn('Failed to fetch models from AI Gateway:', error);
|
|
186
|
+
return {};
|
|
187
|
+
} finally {
|
|
188
|
+
clearTimeout(timeout);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function toPiModel(m: AIGatewayModel): ProviderModelConfig {
|
|
193
|
+
return {
|
|
194
|
+
id: m.id,
|
|
195
|
+
name: m.name,
|
|
196
|
+
reasoning: m.reasoning,
|
|
197
|
+
input: m.input_modalities as ('text' | 'image')[],
|
|
198
|
+
contextWindow: m.context_window ?? 40000,
|
|
199
|
+
maxTokens: m.max_output_tokens ?? 64000,
|
|
200
|
+
cost: {
|
|
201
|
+
input: m.pricing?.input ?? 0,
|
|
202
|
+
output: m.pricing?.output ?? 0,
|
|
203
|
+
cacheRead: m.pricing?.cached_input ?? 0,
|
|
204
|
+
cacheWrite: 0,
|
|
205
|
+
},
|
|
206
|
+
compat: {
|
|
207
|
+
supportsDeveloperRole: false,
|
|
208
|
+
},
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
export async function setupAIGateway(pi: ExtensionAPI) {
|
|
213
|
+
const models = await fetchModels();
|
|
214
|
+
const baseUrl = getBaseUrl();
|
|
215
|
+
|
|
216
|
+
const allModels: AIGatewayModel[] = [];
|
|
217
|
+
for (const providerModels of Object.values(models)) {
|
|
218
|
+
if (providerModels) {
|
|
219
|
+
allModels.push(...providerModels);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
if (allModels.length === 0) {
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const modelsByApi = new Map<KnownApi, ProviderModelConfig[]>();
|
|
227
|
+
|
|
228
|
+
for (const m of allModels) {
|
|
229
|
+
const apiType = m.api;
|
|
230
|
+
if (!isKnownApi(apiType)) {
|
|
231
|
+
continue; // THIS SHOULD NEVER HAPPEN BUT JUST IN CASE
|
|
232
|
+
}
|
|
233
|
+
const existing = modelsByApi.get(apiType) ?? [];
|
|
234
|
+
existing.push(toPiModel(m));
|
|
235
|
+
modelsByApi.set(apiType, existing);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const headers: Record<string, string> = {};
|
|
239
|
+
if (process.env.AGENTUITY_AIGATEWAY_ORGID) {
|
|
240
|
+
headers['x-agentuity-orgid'] = process.env.AGENTUITY_AIGATEWAY_ORGID;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
for (const [apiType, providerModels] of modelsByApi) {
|
|
244
|
+
const apitok = apiType.split('-');
|
|
245
|
+
const name = apitok.length >= 2 ? apitok.slice(0, 2).join('-') : apitok[0];
|
|
246
|
+
const providerName = `agentuity/${name}`;
|
|
247
|
+
pi.registerProvider(providerName, {
|
|
248
|
+
baseUrl,
|
|
249
|
+
apiKey: 'AGENTUITY_AIGATEWAY_KEY',
|
|
250
|
+
headers,
|
|
251
|
+
authHeader: true,
|
|
252
|
+
api: apiType,
|
|
253
|
+
models: providerModels,
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
}
|
package/src/footer.ts
CHANGED
|
@@ -11,7 +11,11 @@
|
|
|
11
11
|
* [3] SwiftRaven — 3 observers watching, session label "SwiftRaven"
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
-
import type {
|
|
14
|
+
import type {
|
|
15
|
+
ExtensionAPI,
|
|
16
|
+
ExtensionContext,
|
|
17
|
+
ReadonlyFooterDataProvider,
|
|
18
|
+
} from '@mariozechner/pi-coding-agent';
|
|
15
19
|
|
|
16
20
|
const RESET = '\x1b[0m';
|
|
17
21
|
const SEP = '>';
|
|
@@ -185,42 +189,85 @@ function formatCost(n: number): string {
|
|
|
185
189
|
*
|
|
186
190
|
* Includes a braille spinner animation when an agent is actively working.
|
|
187
191
|
*
|
|
192
|
+
* @param pi Extension API
|
|
188
193
|
* @param ctx Extension context with UI access
|
|
189
194
|
* @param getHubStatus Callback that returns current Hub connection status
|
|
190
195
|
* @param getObserverState Optional callback that returns observer count + session label
|
|
191
196
|
*/
|
|
192
197
|
export function setupCoderFooter(
|
|
198
|
+
pi: ExtensionAPI,
|
|
193
199
|
ctx: ExtensionContext,
|
|
194
200
|
getHubStatus: () => HubStatus,
|
|
195
201
|
getObserverState?: () => ObserverState
|
|
196
202
|
): void {
|
|
197
203
|
if (!ctx.hasUI) return;
|
|
198
204
|
|
|
205
|
+
ctx.ui.setWorkingIndicator({ frames: [] }); // turn off the working indicator since we use our own spinner
|
|
206
|
+
|
|
207
|
+
let pendingRequests = 0;
|
|
208
|
+
let spinnerTimer: ReturnType<typeof setInterval> | null = null;
|
|
209
|
+
let spinnerFrame = 0;
|
|
210
|
+
let activeAgentRunning = false;
|
|
211
|
+
let requestFooterRender: (() => void) | undefined;
|
|
212
|
+
|
|
213
|
+
const startSpinner = () => {
|
|
214
|
+
if (spinnerTimer) return;
|
|
215
|
+
spinnerTimer = setInterval(() => {
|
|
216
|
+
spinnerFrame = (spinnerFrame + 1) % SPINNER_FRAMES.length;
|
|
217
|
+
requestFooterRender?.();
|
|
218
|
+
}, 80);
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
const stopSpinner = () => {
|
|
222
|
+
if (!spinnerTimer) return;
|
|
223
|
+
clearInterval(spinnerTimer);
|
|
224
|
+
spinnerTimer = null;
|
|
225
|
+
spinnerFrame = 0;
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
const syncSpinner = () => {
|
|
229
|
+
if (pendingRequests > 0 || activeAgentRunning) {
|
|
230
|
+
startSpinner();
|
|
231
|
+
} else {
|
|
232
|
+
stopSpinner();
|
|
233
|
+
}
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
pi.on('before_provider_request', () => {
|
|
237
|
+
pendingRequests += 1;
|
|
238
|
+
if (pendingRequests === 1) {
|
|
239
|
+
startSpinner();
|
|
240
|
+
}
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
pi.on('after_provider_response', () => {
|
|
244
|
+
pendingRequests = Math.max(0, pendingRequests - 1);
|
|
245
|
+
syncSpinner();
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
pi.on('session_shutdown', () => {
|
|
249
|
+
pendingRequests = 0;
|
|
250
|
+
activeAgentRunning = false;
|
|
251
|
+
stopSpinner();
|
|
252
|
+
});
|
|
253
|
+
|
|
199
254
|
ctx.ui.setFooter((tui, _theme, footerData) => {
|
|
200
|
-
|
|
201
|
-
let spinnerTimer: ReturnType<typeof setInterval> | null = null;
|
|
202
|
-
let spinnerFrame = 0;
|
|
255
|
+
requestFooterRender = () => tui.requestRender();
|
|
203
256
|
|
|
204
257
|
const getText = (width: number): string => {
|
|
205
258
|
// Detect active agent
|
|
206
259
|
const activeAgent = footerData.getExtensionStatuses().get('active_agent');
|
|
207
260
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
spinnerTimer = setInterval(() => {
|
|
211
|
-
spinnerFrame = (spinnerFrame + 1) % SPINNER_FRAMES.length;
|
|
212
|
-
tui.requestRender();
|
|
213
|
-
}, 80);
|
|
214
|
-
} else if (!activeAgent && spinnerTimer) {
|
|
215
|
-
clearInterval(spinnerTimer);
|
|
216
|
-
spinnerTimer = null;
|
|
217
|
-
spinnerFrame = 0;
|
|
218
|
-
}
|
|
261
|
+
activeAgentRunning = !!activeAgent;
|
|
262
|
+
syncSpinner();
|
|
219
263
|
|
|
220
264
|
// Token stats from session messages
|
|
221
265
|
let inputTokens = 0;
|
|
222
266
|
let outputTokens = 0;
|
|
223
267
|
let totalCost = 0;
|
|
268
|
+
|
|
269
|
+
// TODO (jhaynie): we need to think about how to handle our AI Gateway costs vs whats coming from pi
|
|
270
|
+
|
|
224
271
|
for (const entry of ctx.sessionManager.getBranch()) {
|
|
225
272
|
if (entry.type === 'message') {
|
|
226
273
|
const msg = entry.message as {
|
package/src/index.ts
CHANGED
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
ExtensionCommandContext,
|
|
7
7
|
ToolDefinition,
|
|
8
8
|
} from '@mariozechner/pi-coding-agent';
|
|
9
|
-
import { Type, type TSchema } from 'typebox';
|
|
9
|
+
import { Type, type TSchema } from '@sinclair/typebox';
|
|
10
10
|
import { createRequire } from 'node:module';
|
|
11
11
|
import { HubClient } from './client.ts';
|
|
12
12
|
import type { ConnectionState } from './client.ts';
|
|
@@ -14,6 +14,7 @@ import { processActions } from './handlers.ts';
|
|
|
14
14
|
import { getToolRenderers } from './renderers.ts';
|
|
15
15
|
import { setupCoderFooter, type ObserverState } from './footer.ts';
|
|
16
16
|
import { setupTitlebar } from './titlebar.ts';
|
|
17
|
+
import { setupStartupLogo } from './startup-logo.ts';
|
|
17
18
|
import { registerAgentCommands } from './commands.ts';
|
|
18
19
|
import { AgentManagerOverlay } from './overlay.ts';
|
|
19
20
|
import { ChainEditorOverlay, type ChainResult } from './chain-preview.ts';
|
|
@@ -24,6 +25,7 @@ import { handleRemoteUiRequest } from './remote-ui-handler.ts';
|
|
|
24
25
|
import { buildInboundRpcPromptText, getInboundRpcDeliverAs } from './inbound-rpc.ts';
|
|
25
26
|
import { applyCoderAuthHeaders, getCoderAuthCurlArgs } from './auth.ts';
|
|
26
27
|
import { formatToolDisplay } from './agentuity-cli.ts';
|
|
28
|
+
import { setupAIGateway } from './aigateway.ts';
|
|
27
29
|
import { adaptInitMessageForLocalTui } from './local-init-filter.ts';
|
|
28
30
|
import { selectSubAgentToolNames } from './subagent-tool-selection.ts';
|
|
29
31
|
import type {
|
|
@@ -279,6 +281,15 @@ async function fetchInitMessage(hubUrl: string, agentRole?: string): Promise<Ini
|
|
|
279
281
|
}
|
|
280
282
|
|
|
281
283
|
export function agentuityCoderHub(pi: ExtensionAPI) {
|
|
284
|
+
process.env.AGENTUITY_AGENT_MODE = 'coder'; // let the agentuity cli know we're inside coder
|
|
285
|
+
|
|
286
|
+
// Register the startup header before Hub bootstrap so `pi -e .` works for
|
|
287
|
+
// local visual testing without Agentuity Coder environment variables.
|
|
288
|
+
setupStartupLogo(pi);
|
|
289
|
+
|
|
290
|
+
// Register the AI Gateway
|
|
291
|
+
setupAIGateway(pi);
|
|
292
|
+
|
|
282
293
|
const hubUrl = process.env[HUB_URL_ENV];
|
|
283
294
|
if (!hubUrl) return;
|
|
284
295
|
|
|
@@ -1469,7 +1480,7 @@ export function agentuityCoderHub(pi: ExtensionAPI) {
|
|
|
1469
1480
|
}
|
|
1470
1481
|
|
|
1471
1482
|
// Set up Coder footer (powerline: model or active agent > branch > status + observer count)
|
|
1472
|
-
setupCoderFooter(ctx, getHubUiStatus, getObserverState);
|
|
1483
|
+
setupCoderFooter(pi, ctx, getHubUiStatus, getObserverState);
|
|
1473
1484
|
|
|
1474
1485
|
// Fire-and-forget: fetch session snapshot for label + initial observer count.
|
|
1475
1486
|
// Uses the Hub REST endpoint — non-blocking, best-effort.
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
import type { ExtensionAPI, ExtensionContext, Theme } from '@mariozechner/pi-coding-agent';
|
|
2
|
+
|
|
3
|
+
type TuiRenderer = {
|
|
4
|
+
requestRender(): void;
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
type LogoCell = {
|
|
8
|
+
ri: number;
|
|
9
|
+
ci: number;
|
|
10
|
+
sr: number;
|
|
11
|
+
sc: number;
|
|
12
|
+
delay: number;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
const LOGO = [
|
|
16
|
+
' ## ',
|
|
17
|
+
' #### ',
|
|
18
|
+
' ###### ',
|
|
19
|
+
' ########## ',
|
|
20
|
+
' ##### ##### ',
|
|
21
|
+
' ##### ##### ',
|
|
22
|
+
' ##### ##### ',
|
|
23
|
+
' ##### ##### ',
|
|
24
|
+
' ##### ##### ',
|
|
25
|
+
' ############################# ',
|
|
26
|
+
' ############################## ',
|
|
27
|
+
' ',
|
|
28
|
+
' ',
|
|
29
|
+
' ##################################### ',
|
|
30
|
+
'######################################## ',
|
|
31
|
+
' ##### ##### ',
|
|
32
|
+
' ##### ##### ',
|
|
33
|
+
' ##### ##### ',
|
|
34
|
+
' ##### ##### ',
|
|
35
|
+
' ############################################ ',
|
|
36
|
+
'############################################## ',
|
|
37
|
+
];
|
|
38
|
+
|
|
39
|
+
const ASSEMBLE_FRAMES = 30;
|
|
40
|
+
const SHIMMER_FRAMES = 56;
|
|
41
|
+
const PULSE_FRAMES = 40;
|
|
42
|
+
const FRAME_INTERVAL_MS = 35;
|
|
43
|
+
const CHARS = '░▒▓█#@%&*+=-:.';
|
|
44
|
+
const TITLE = 'Agentuity Coder';
|
|
45
|
+
const SANDBOX_ID_ENV = 'AGENTUITY_SANDBOX_ID';
|
|
46
|
+
|
|
47
|
+
const LOGO_ROWS = LOGO.length;
|
|
48
|
+
const LOGO_COLS = LOGO[0]?.length ?? 0;
|
|
49
|
+
const CELLS = LOGO.flatMap((row, ri) =>
|
|
50
|
+
[...row].flatMap((ch, ci) => (ch === '#' ? [{ ri, ci }] : []))
|
|
51
|
+
);
|
|
52
|
+
const TOTAL_FRAMES = ASSEMBLE_FRAMES + SHIMMER_FRAMES + PULSE_FRAMES;
|
|
53
|
+
const TITLE_OFFSET = 3;
|
|
54
|
+
|
|
55
|
+
function mulberry32(seed: number): () => number {
|
|
56
|
+
return () => {
|
|
57
|
+
seed += 0x6d2b79f5;
|
|
58
|
+
let value = seed;
|
|
59
|
+
value = Math.imul(value ^ (value >>> 15), value | 1);
|
|
60
|
+
value ^= value + Math.imul(value ^ (value >>> 7), value | 61);
|
|
61
|
+
return ((value ^ (value >>> 14)) >>> 0) / 4294967296;
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function center(line: string, width: number): string {
|
|
66
|
+
if (width <= LOGO_COLS) return line;
|
|
67
|
+
const padding = Math.max(0, Math.floor((width - LOGO_COLS) / 2));
|
|
68
|
+
return `${' '.repeat(padding)}${line}`;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function centerText(line: string, width: number): string {
|
|
72
|
+
const padding = Math.max(0, Math.floor((width - line.length) / 2) - TITLE_OFFSET);
|
|
73
|
+
return `${' '.repeat(padding)}${line}`;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function randomChar(rand: () => number): string {
|
|
77
|
+
return CHARS[Math.floor(rand() * CHARS.length)] ?? '#';
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function makeParticles(seed: number): LogoCell[] {
|
|
81
|
+
const rand = mulberry32(seed);
|
|
82
|
+
return CELLS.map((cell) => ({
|
|
83
|
+
...cell,
|
|
84
|
+
sr: Math.floor(rand() * LOGO_ROWS),
|
|
85
|
+
sc: Math.floor(rand() * LOGO_COLS),
|
|
86
|
+
delay: rand() * 0.6,
|
|
87
|
+
}));
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function makeGrid(): string[][] {
|
|
91
|
+
return LOGO.map((row) => [...row].map(() => ' '));
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function phaseForFrame(
|
|
95
|
+
frame: number
|
|
96
|
+
):
|
|
97
|
+
| { type: 'assemble'; frame: number }
|
|
98
|
+
| { type: 'shimmer'; frame: number }
|
|
99
|
+
| { type: 'pulse'; frame: number }
|
|
100
|
+
| { type: 'final' } {
|
|
101
|
+
if (frame < ASSEMBLE_FRAMES) return { type: 'assemble', frame };
|
|
102
|
+
if (frame < ASSEMBLE_FRAMES + SHIMMER_FRAMES) {
|
|
103
|
+
return { type: 'shimmer', frame: frame - ASSEMBLE_FRAMES };
|
|
104
|
+
}
|
|
105
|
+
if (frame < TOTAL_FRAMES) {
|
|
106
|
+
return { type: 'pulse', frame: frame - ASSEMBLE_FRAMES - SHIMMER_FRAMES };
|
|
107
|
+
}
|
|
108
|
+
return { type: 'final' };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
class StartupLogoHeader {
|
|
112
|
+
#frame = 0;
|
|
113
|
+
#timer: ReturnType<typeof setInterval> | null = null;
|
|
114
|
+
readonly #particles = makeParticles(Date.now());
|
|
115
|
+
|
|
116
|
+
constructor(
|
|
117
|
+
private readonly tui: TuiRenderer,
|
|
118
|
+
private readonly theme: Theme
|
|
119
|
+
) {
|
|
120
|
+
this.#timer = setInterval(() => {
|
|
121
|
+
this.#frame++;
|
|
122
|
+
if (this.#frame >= TOTAL_FRAMES) this.#stop();
|
|
123
|
+
this.tui.requestRender();
|
|
124
|
+
}, FRAME_INTERVAL_MS);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
render(width: number): string[] {
|
|
128
|
+
const grid = this.#renderGrid();
|
|
129
|
+
return [
|
|
130
|
+
'',
|
|
131
|
+
...grid.map((row) => center(row.join(''), width)),
|
|
132
|
+
'',
|
|
133
|
+
this.#renderTitle(width),
|
|
134
|
+
'',
|
|
135
|
+
];
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
invalidate(): void {
|
|
139
|
+
this.tui.requestRender();
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
dispose(): void {
|
|
143
|
+
this.#stop();
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
#renderGrid(): string[][] {
|
|
147
|
+
const phase = phaseForFrame(this.#frame);
|
|
148
|
+
if (phase.type === 'assemble') return this.#renderAssemble(phase.frame);
|
|
149
|
+
if (phase.type === 'shimmer') return this.#renderShimmer(phase.frame);
|
|
150
|
+
if (phase.type === 'pulse') return this.#renderPulse(phase.frame);
|
|
151
|
+
return this.#renderFinal();
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
#renderAssemble(frame: number): string[][] {
|
|
155
|
+
const rand = mulberry32(frame + 1);
|
|
156
|
+
const grid = makeGrid();
|
|
157
|
+
const t = frame / Math.max(1, ASSEMBLE_FRAMES - 1);
|
|
158
|
+
|
|
159
|
+
for (const particle of this.#particles) {
|
|
160
|
+
const pt = Math.max(0, (t - particle.delay) / (1 - particle.delay));
|
|
161
|
+
const eased = 1 - (1 - Math.min(pt, 1)) ** 3;
|
|
162
|
+
const ri = Math.round(particle.sr + (particle.ri - particle.sr) * eased);
|
|
163
|
+
const ci = Math.round(particle.sc + (particle.ci - particle.sc) * eased);
|
|
164
|
+
|
|
165
|
+
if (pt >= 1) {
|
|
166
|
+
grid[particle.ri]![particle.ci] = this.#cyan('#');
|
|
167
|
+
} else if (ri >= 0 && ri < LOGO_ROWS && ci >= 0 && ci < LOGO_COLS) {
|
|
168
|
+
grid[ri]![ci] = this.#dim(randomChar(rand));
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return grid;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
#renderShimmer(frame: number): string[][] {
|
|
176
|
+
const wave = (frame / SHIMMER_FRAMES) * (LOGO_ROWS + LOGO_COLS) * 1.5;
|
|
177
|
+
return LOGO.map((row, ri) =>
|
|
178
|
+
[...row].map((ch, ci) => {
|
|
179
|
+
if (ch !== '#') return ' ';
|
|
180
|
+
const dist = Math.abs(ri + ci * 0.5 - wave);
|
|
181
|
+
if (dist < 1.5) return this.#brightCyan('█');
|
|
182
|
+
if (dist < 3.5) return this.#cyan('#');
|
|
183
|
+
if (dist < 5) return this.#dim('#');
|
|
184
|
+
return this.#cyan('#');
|
|
185
|
+
})
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
#renderPulse(frame: number): string[][] {
|
|
190
|
+
const step = frame % 20;
|
|
191
|
+
const t = step / 20;
|
|
192
|
+
const bright = t < 0.5 ? t * 2 : (1 - t) * 2;
|
|
193
|
+
|
|
194
|
+
return LOGO.map((row) =>
|
|
195
|
+
[...row].map((ch) => {
|
|
196
|
+
if (ch !== '#') return ' ';
|
|
197
|
+
if (bright > 0.5) return this.#brightCyan('#');
|
|
198
|
+
if (bright > 0.2) return this.#cyan('#');
|
|
199
|
+
return this.#dim('#');
|
|
200
|
+
})
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
#renderFinal(): string[][] {
|
|
205
|
+
return LOGO.map((row) => [...row].map((ch) => (ch === '#' ? this.#cyan('#') : ' ')));
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
#renderTitle(width: number): string {
|
|
209
|
+
if (this.#frame < ASSEMBLE_FRAMES) return '';
|
|
210
|
+
|
|
211
|
+
const titleFrame = Math.min(this.#frame - ASSEMBLE_FRAMES, SHIMMER_FRAMES);
|
|
212
|
+
const visibleChars = Math.min(
|
|
213
|
+
TITLE.length,
|
|
214
|
+
Math.floor((titleFrame / Math.max(1, SHIMMER_FRAMES - 1)) * TITLE.length)
|
|
215
|
+
);
|
|
216
|
+
const title = TITLE.slice(0, visibleChars);
|
|
217
|
+
|
|
218
|
+
if (this.#frame >= ASSEMBLE_FRAMES + SHIMMER_FRAMES) {
|
|
219
|
+
return this.#brightCyan(centerText(TITLE, width));
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
return this.#cyan(centerText(title, width));
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
#cyan(text: string): string {
|
|
226
|
+
return `\x1b[36m${text}\x1b[0m`;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
#brightCyan(text: string): string {
|
|
230
|
+
return `\x1b[96m${text}\x1b[0m`;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
#dim(text: string): string {
|
|
234
|
+
return this.theme.fg('dim', this.theme.fg('accent', text));
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
#stop(): void {
|
|
238
|
+
if (!this.#timer) return;
|
|
239
|
+
clearInterval(this.#timer);
|
|
240
|
+
this.#timer = null;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function installStartupLogo(ctx: ExtensionContext): void {
|
|
245
|
+
if (!ctx.hasUI) return;
|
|
246
|
+
ctx.ui.setHeader((tui, theme) => new StartupLogoHeader(tui, theme));
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
export function setupStartupLogo(pi: ExtensionAPI): void {
|
|
250
|
+
if (process.env[SANDBOX_ID_ENV]) return;
|
|
251
|
+
|
|
252
|
+
pi.on('session_start', async (_event, ctx) => {
|
|
253
|
+
installStartupLogo(ctx);
|
|
254
|
+
});
|
|
255
|
+
}
|