@agentuity/coder-tui 3.0.0-alpha.7 → 3.0.0-beta.1

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.
@@ -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 { ExtensionContext, ReadonlyFooterDataProvider } from '@mariozechner/pi-coding-agent';
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
- // Spinner state
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
- // Start/stop spinner based on agent activity
209
- if (activeAgent && !spinnerTimer) {
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
+ }
@@ -1,3 +0,0 @@
1
- export * from './types.ts';
2
- export * from './store.ts';
3
- //# sourceMappingURL=index.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/todo/index.ts"],"names":[],"mappings":"AAAA,cAAc,YAAY,CAAC;AAC3B,cAAc,YAAY,CAAC"}
@@ -1,3 +0,0 @@
1
- export * from "./types.js";
2
- export * from "./store.js";
3
- //# sourceMappingURL=index.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/todo/index.ts"],"names":[],"mappings":"AAAA,cAAc,YAAY,CAAC;AAC3B,cAAc,YAAY,CAAC"}
@@ -1,6 +0,0 @@
1
- import type { Todo } from './types.ts';
2
- export declare function createTodo(text: string): Todo;
3
- export declare function listTodos(): Todo[];
4
- export declare function completeTodo(id: string): Todo;
5
- export declare function deleteTodo(id: string): Todo;
6
- //# sourceMappingURL=store.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../../src/todo/store.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAA4B,MAAM,YAAY,CAAC;AAYjE,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAa7C;AAED,wBAAgB,SAAS,IAAI,IAAI,EAAE,CAElC;AAED,wBAAgB,YAAY,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI,CAQ7C;AAED,wBAAgB,UAAU,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI,CAO3C"}