@ekkos/cli 1.3.9 → 1.4.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.
@@ -87,10 +87,31 @@ const MODEL_PRICING = {
87
87
  'claude-sonnet-4': { input: 3, output: 15, cacheWrite: 3.75, cacheRead: 0.30 },
88
88
  'claude-haiku-4-5-20251001': { input: 1, output: 5, cacheWrite: 1.25, cacheRead: 0.10 },
89
89
  'claude-haiku-4-5': { input: 1, output: 5, cacheWrite: 1.25, cacheRead: 0.10 },
90
+ 'gemini-3.1-flash-lite-preview': { input: 0.05, output: 0.20, cacheWrite: 0, cacheRead: 0.05 },
91
+ 'gemini-3.1-pro-preview': { input: 1.25, output: 5.00, cacheWrite: 0, cacheRead: 1.25 },
92
+ 'gemini-3-pro-preview': { input: 1.25, output: 5.00, cacheWrite: 0, cacheRead: 1.25 },
93
+ 'gemini-3-flash-preview': { input: 0.075, output: 0.30, cacheWrite: 0, cacheRead: 0.075 },
94
+ 'gemini-2.5-flash-lite': { input: 0.05, output: 0.20, cacheWrite: 0, cacheRead: 0.05 },
95
+ 'gemini-2.5-pro': { input: 1.25, output: 5.00, cacheWrite: 0, cacheRead: 1.25 },
96
+ 'gemini-2.5-flash': { input: 0.075, output: 0.30, cacheWrite: 0, cacheRead: 0.075 },
90
97
  };
91
98
  function getModelPricing(modelId) {
92
99
  if (MODEL_PRICING[modelId])
93
100
  return MODEL_PRICING[modelId];
101
+ if (modelId.includes('gemini-3.1-pro'))
102
+ return MODEL_PRICING['gemini-3.1-pro-preview'];
103
+ if (modelId.includes('gemini-3-pro'))
104
+ return MODEL_PRICING['gemini-3-pro-preview'];
105
+ if (modelId.includes('gemini-3.1-flash-lite'))
106
+ return MODEL_PRICING['gemini-3.1-flash-lite-preview'];
107
+ if (modelId.includes('gemini-3-flash'))
108
+ return MODEL_PRICING['gemini-3-flash-preview'];
109
+ if (modelId.includes('gemini-2.5-pro'))
110
+ return MODEL_PRICING['gemini-2.5-pro'];
111
+ if (modelId.includes('gemini-2.5-flash-lite'))
112
+ return MODEL_PRICING['gemini-2.5-flash-lite'];
113
+ if (modelId.includes('gemini-2.5-flash'))
114
+ return MODEL_PRICING['gemini-2.5-flash'];
94
115
  if (modelId.includes('opus'))
95
116
  return MODEL_PRICING['claude-opus-4-6'];
96
117
  if (modelId.includes('sonnet'))
@@ -130,12 +151,15 @@ function getModelCtxSize(model, contextTierHint) {
130
151
  const normalized = (model || '').toLowerCase();
131
152
  if (contextTierHint === '1m')
132
153
  return 1000000;
154
+ if (contextTierHint === '200k')
155
+ return 200000;
133
156
  // Claude 4.6 has GA 1M context in Claude Code/API.
134
157
  if (/^claude-(?:opus|sonnet)-4-6(?:$|-)/.test(normalized))
135
158
  return 1000000;
136
- // Keep Gemini on the 1M class in dashboard context math.
159
+ if (normalized.startsWith('gemini-3.1-pro') || normalized.startsWith('gemini-3-pro'))
160
+ return 2097152;
137
161
  if (normalized.startsWith('gemini-'))
138
- return 1000000;
162
+ return 1048576;
139
163
  if (normalized.includes('1m'))
140
164
  return 1000000;
141
165
  if (normalized.includes('opus'))
@@ -146,6 +170,100 @@ function getModelCtxSize(model, contextTierHint) {
146
170
  return 200000;
147
171
  return 200000; // Default Anthropic context
148
172
  }
173
+ function parseExplicitContextWindowSize(value) {
174
+ if (typeof value === 'number' && Number.isFinite(value) && value > 0) {
175
+ return Math.floor(value);
176
+ }
177
+ if (typeof value === 'string') {
178
+ const parsed = Number(value.trim());
179
+ if (Number.isFinite(parsed) && parsed > 0) {
180
+ return Math.floor(parsed);
181
+ }
182
+ }
183
+ return undefined;
184
+ }
185
+ function resolveDashboardContextSize(model, contextTierHint, launchMetadata, explicitContextWindow) {
186
+ const explicitSize = parseExplicitContextWindowSize(explicitContextWindow);
187
+ if (explicitSize)
188
+ return explicitSize;
189
+ if (typeof launchMetadata?.claudeContextSize === 'number' && launchMetadata.claudeContextSize > 0) {
190
+ return launchMetadata.claudeContextSize;
191
+ }
192
+ const launchWindowHint = normalizeLaunchContextWindow(launchMetadata?.claudeContextWindow);
193
+ return getModelCtxSize(model, contextTierHint || launchWindowHint);
194
+ }
195
+ function humanizeModelName(model, includeVendor = false) {
196
+ if (!model)
197
+ return null;
198
+ const normalized = model.toLowerCase();
199
+ const prefix = includeVendor ? 'Claude ' : '';
200
+ if (normalized.includes('claude-opus-4-6'))
201
+ return `${prefix}Opus 4.6`;
202
+ if (normalized.includes('claude-opus-4-5'))
203
+ return `${prefix}Opus 4.5`;
204
+ if (normalized.includes('claude-sonnet-4-6'))
205
+ return `${prefix}Sonnet 4.6`;
206
+ if (normalized.includes('claude-sonnet-4-5'))
207
+ return `${prefix}Sonnet 4.5`;
208
+ if (normalized.includes('claude-haiku-4-5'))
209
+ return `${prefix}Haiku 4.5`;
210
+ if (normalized.includes('gemini-3.1-pro'))
211
+ return includeVendor ? 'Gemini 3.1 Pro' : 'Gemini 3.1 Pro';
212
+ if (normalized.includes('gemini-3-pro'))
213
+ return includeVendor ? 'Gemini 3 Pro' : 'Gemini 3 Pro';
214
+ if (normalized.includes('gemini-3.1-flash-lite'))
215
+ return includeVendor ? 'Gemini 3.1 Flash Lite' : 'Gemini 3.1 Flash Lite';
216
+ if (normalized.includes('gemini-3-flash'))
217
+ return includeVendor ? 'Gemini 3 Flash' : 'Gemini 3 Flash';
218
+ if (normalized.includes('gemini-2.5-pro'))
219
+ return includeVendor ? 'Gemini 2.5 Pro' : 'Gemini 2.5 Pro';
220
+ if (normalized.includes('gemini-2.5-flash-lite'))
221
+ return includeVendor ? 'Gemini 2.5 Flash Lite' : 'Gemini 2.5 Flash Lite';
222
+ if (normalized.includes('gemini-2.5-flash'))
223
+ return includeVendor ? 'Gemini 2.5 Flash' : 'Gemini 2.5 Flash';
224
+ if (normalized.startsWith('gemini-'))
225
+ return includeVendor ? 'Gemini' : 'Gemini';
226
+ if (normalized.startsWith('gpt-'))
227
+ return model;
228
+ return model;
229
+ }
230
+ function formatCompactLaunchModel(metadata) {
231
+ const model = metadata?.claudeModel || metadata?.claudeLaunchModel;
232
+ const label = humanizeModelName(model, false);
233
+ if (!label)
234
+ return null;
235
+ const window = normalizeLaunchContextWindow(metadata?.claudeContextWindow);
236
+ if (window === '200k')
237
+ return `${label} [200K]`;
238
+ if (window === '1m')
239
+ return `${label} [1M]`;
240
+ return label;
241
+ }
242
+ function formatActiveRoutedModel(model) {
243
+ return humanizeModelName(model, false);
244
+ }
245
+ function formatContextLaneCompact(contextSize, metadata) {
246
+ const explicitSize = parseExplicitContextWindowSize(contextSize);
247
+ const launchSize = typeof metadata?.claudeContextSize === 'number' ? metadata.claudeContextSize : undefined;
248
+ const resolvedSize = explicitSize || launchSize;
249
+ const maxOutput = metadata?.claudeMaxOutputTokens
250
+ || (resolvedSize && resolvedSize >= 1000000 ? 128000 : 32768);
251
+ if (resolvedSize && resolvedSize >= 1000000) {
252
+ return `1M / out ${fmtK(maxOutput)}`;
253
+ }
254
+ if (resolvedSize && resolvedSize > 0) {
255
+ return `200K / out ${fmtK(maxOutput)}`;
256
+ }
257
+ const launchWindow = normalizeLaunchContextWindow(metadata?.claudeContextWindow);
258
+ if (launchWindow === '1m')
259
+ return `1M / out ${fmtK(maxOutput)}`;
260
+ if (launchWindow === '200k')
261
+ return `200K / out ${fmtK(maxOutput)}`;
262
+ return null;
263
+ }
264
+ function buildRuntimeSignal(label, color, value) {
265
+ return `{${color}-fg}[${label}]{/${color}-fg} ${value}`;
266
+ }
149
267
  /** Model tag for dashboard display */
150
268
  function modelTag(model) {
151
269
  if (model.includes('opus'))
@@ -154,6 +272,8 @@ function modelTag(model) {
154
272
  return 'Sonnet';
155
273
  if (model.includes('haiku'))
156
274
  return 'Haiku';
275
+ if (model.includes('gemini'))
276
+ return 'Gem';
157
277
  return '?';
158
278
  }
159
279
  function parseCacheHintValue(cacheHint, key) {
@@ -169,14 +289,15 @@ function parseCacheHintValue(cacheHint, key) {
169
289
  }
170
290
  return null;
171
291
  }
172
- function parseJsonlFile(jsonlPath, sessionName) {
292
+ function parseJsonlFile(jsonlPath, sessionName, launchMetadata) {
173
293
  const content = fs.readFileSync(jsonlPath, 'utf-8');
174
294
  const lines = content.trim().split('\n');
175
295
  const turnsByMsgId = new Map();
176
296
  const msgIdOrder = [];
177
297
  let startedAt = '';
178
- let model = 'unknown';
298
+ let model = launchMetadata?.claudeModel || launchMetadata?.claudeLaunchModel || 'unknown';
179
299
  const toolsByMessage = new Map();
300
+ const fallbackContextWindow = normalizeLaunchContextWindow(launchMetadata?.claudeContextWindow);
180
301
  for (const line of lines) {
181
302
  try {
182
303
  const entry = JSON.parse(line);
@@ -226,7 +347,7 @@ function parseJsonlFile(jsonlPath, sessionName) {
226
347
  : (parseCacheHintValue(cacheHint, 'eviction') || 'unknown');
227
348
  const contextTierHint = typeof entry.message._ekkos_context_tier === 'string'
228
349
  ? entry.message._ekkos_context_tier.trim().toLowerCase()
229
- : undefined;
350
+ : fallbackContextWindow;
230
351
  const explicitExtraUsage = entry.message._ekkos_extra_usage === true
231
352
  || entry.message._ekkos_extra_usage === '1'
232
353
  || entry.message._ekkos_extra_usage === 'true';
@@ -235,7 +356,7 @@ function parseJsonlFile(jsonlPath, sessionName) {
235
356
  const cacheReadTokens = usage.cache_read_input_tokens || 0;
236
357
  const cacheCreationTokens = usage.cache_creation_input_tokens || 0;
237
358
  const contextTokens = inputTokens + cacheReadTokens + cacheCreationTokens;
238
- const modelCtxSize = getModelCtxSize(routedModel, contextTierHint);
359
+ const modelCtxSize = resolveDashboardContextSize(routedModel, contextTierHint, launchMetadata, entry.message._ekkos_context_window);
239
360
  const contextPct = (contextTokens / modelCtxSize) * 100;
240
361
  const ts = entry.timestamp || new Date().toISOString();
241
362
  if (!startedAt)
@@ -325,7 +446,7 @@ function parseJsonlFile(jsonlPath, sessionName) {
325
446
  : 0;
326
447
  const modelContextSize = lastTurn
327
448
  ? lastTurn.modelContextSize
328
- : getModelCtxSize(model);
449
+ : resolveDashboardContextSize(model, fallbackContextWindow, launchMetadata);
329
450
  return {
330
451
  sessionName,
331
452
  model,
@@ -350,6 +471,106 @@ function parseJsonlFile(jsonlPath, sessionName) {
350
471
  turns,
351
472
  };
352
473
  }
474
+ function parseGeminiSessionFile(sessionPath, sessionName, launchMetadata) {
475
+ const raw = JSON.parse(fs.readFileSync(sessionPath, 'utf-8'));
476
+ const messages = Array.isArray(raw.messages) ? raw.messages : [];
477
+ const turns = [];
478
+ const fallbackWindow = normalizeLaunchContextWindow(launchMetadata?.claudeContextWindow) || '1m';
479
+ let model = launchMetadata?.claudeModel || launchMetadata?.claudeLaunchModel || 'gemini';
480
+ const startedAt = raw.startTime || messages.find(m => typeof m.timestamp === 'string')?.timestamp || '';
481
+ for (const message of messages) {
482
+ if (message?.type !== 'gemini' || !message.tokens)
483
+ continue;
484
+ const tokenInput = Math.max(0, Number(message.tokens.input || 0));
485
+ const tokenCached = Math.max(0, Math.min(tokenInput, Number(message.tokens.cached || 0)));
486
+ const tokenOutput = Math.max(0, Number(message.tokens.output || 0));
487
+ const tokenThoughts = Math.max(0, Number(message.tokens.thoughts || 0));
488
+ const tokenTool = Math.max(0, Number(message.tokens.tool || 0));
489
+ const uncachedInput = Math.max(0, tokenInput - tokenCached);
490
+ const effectiveOutput = tokenOutput + tokenThoughts + tokenTool;
491
+ const routedModel = message.model || model;
492
+ model = routedModel;
493
+ const modelCtxSize = resolveDashboardContextSize(routedModel, fallbackWindow, launchMetadata, launchMetadata?.claudeContextSize);
494
+ const contextPct = modelCtxSize > 0 ? (tokenInput / modelCtxSize) * 100 : 0;
495
+ const toolNames = Array.isArray(message.toolCalls)
496
+ ? Array.from(new Set(message.toolCalls
497
+ .map(tool => typeof tool?.name === 'string' ? tool.name : '')
498
+ .filter(Boolean)))
499
+ : [];
500
+ const usageData = {
501
+ input_tokens: uncachedInput,
502
+ output_tokens: effectiveOutput,
503
+ cache_read_tokens: tokenCached,
504
+ cache_creation_tokens: 0,
505
+ };
506
+ turns.push({
507
+ turn: turns.length + 1,
508
+ contextPct,
509
+ modelContextSize: modelCtxSize,
510
+ input: uncachedInput,
511
+ cacheRead: tokenCached,
512
+ cacheCreate: 0,
513
+ output: effectiveOutput,
514
+ cost: calculateTurnCost(routedModel, usageData),
515
+ opusCost: calculateTurnCost(routedModel, usageData),
516
+ savings: 0,
517
+ tools: toolNames.length > 0 ? toolNames.join(',') : '-',
518
+ model: routedModel,
519
+ routedModel,
520
+ replayState: 'n/a',
521
+ replayStore: 'n/a',
522
+ evictionState: 'n/a',
523
+ timestamp: message.timestamp || raw.lastUpdated || raw.startTime || new Date().toISOString(),
524
+ });
525
+ }
526
+ const totalCost = turns.reduce((sum, turn) => sum + turn.cost, 0);
527
+ const totalInput = turns.reduce((sum, turn) => sum + turn.input, 0);
528
+ const totalCacheRead = turns.reduce((sum, turn) => sum + turn.cacheRead, 0);
529
+ const totalCacheCreate = turns.reduce((sum, turn) => sum + turn.cacheCreate, 0);
530
+ const totalOutput = turns.reduce((sum, turn) => sum + turn.output, 0);
531
+ const maxContextPct = turns.length > 0 ? Math.max(...turns.map(turn => turn.contextPct)) : 0;
532
+ const currentContextPct = turns.length > 0 ? turns[turns.length - 1].contextPct : 0;
533
+ const avgCostPerTurn = turns.length > 0 ? totalCost / turns.length : 0;
534
+ const cacheHitRate = tokenSafePercent(totalCacheRead, totalInput + totalCacheRead);
535
+ let duration = '0m';
536
+ if (startedAt && turns.length > 0) {
537
+ const start = new Date(startedAt).getTime();
538
+ const end = new Date(turns[turns.length - 1].timestamp).getTime();
539
+ const mins = Math.max(0, Math.round((end - start) / 60000));
540
+ duration = mins >= 60 ? `${Math.floor(mins / 60)}h${mins % 60}m` : `${mins}m`;
541
+ }
542
+ const lastTurn = turns.length > 0 ? turns[turns.length - 1] : null;
543
+ return {
544
+ sessionName,
545
+ model,
546
+ turnCount: turns.length,
547
+ totalCost,
548
+ totalTokens: totalInput + totalCacheRead + totalCacheCreate + totalOutput,
549
+ totalInput,
550
+ totalCacheRead,
551
+ totalCacheCreate,
552
+ totalOutput,
553
+ avgCostPerTurn,
554
+ maxContextPct,
555
+ currentContextPct,
556
+ currentContextTokens: lastTurn ? lastTurn.input + lastTurn.cacheRead + lastTurn.cacheCreate : 0,
557
+ modelContextSize: lastTurn
558
+ ? lastTurn.modelContextSize
559
+ : resolveDashboardContextSize(model, fallbackWindow, launchMetadata, launchMetadata?.claudeContextSize),
560
+ cacheHitRate,
561
+ replayAppliedCount: 0,
562
+ replaySkippedSizeCount: 0,
563
+ replaySkipStoreCount: 0,
564
+ startedAt,
565
+ duration,
566
+ turns,
567
+ };
568
+ }
569
+ function tokenSafePercent(numerator, denominator) {
570
+ if (!Number.isFinite(denominator) || denominator <= 0)
571
+ return 0;
572
+ return (numerator / denominator) * 100;
573
+ }
353
574
  function readJsonFile(filePath) {
354
575
  try {
355
576
  if (!fs.existsSync(filePath))
@@ -360,6 +581,94 @@ function readJsonFile(filePath) {
360
581
  return null;
361
582
  }
362
583
  }
584
+ function normalizeLaunchContextWindow(value) {
585
+ const normalized = (value || '').trim().toLowerCase();
586
+ if (!normalized)
587
+ return undefined;
588
+ if (normalized === 'auto')
589
+ return 'auto';
590
+ if (normalized === '200k' || normalized === '200000')
591
+ return '200k';
592
+ if (normalized === '1m' || normalized === '1000000')
593
+ return '1m';
594
+ return undefined;
595
+ }
596
+ function normalizeDashboardProvider(value) {
597
+ const normalized = typeof value === 'string' ? value.trim().toLowerCase() : '';
598
+ if (normalized === 'gemini' || normalized === 'google')
599
+ return 'gemini';
600
+ if (normalized === 'claude' || normalized === 'anthropic')
601
+ return 'claude';
602
+ return undefined;
603
+ }
604
+ function readSessionLaunchMetadata(sessionName) {
605
+ const candidates = [];
606
+ const active = (0, state_js_1.getActiveSessions)()
607
+ .filter((session) => session.sessionName === sessionName)
608
+ .sort((a, b) => (b.lastHeartbeat || '').localeCompare(a.lastHeartbeat || ''));
609
+ if (active.length > 0)
610
+ candidates.push(active[0]);
611
+ const sessionHint = readJsonFile(path.join(state_js_1.EKKOS_DIR, 'session-hint.json'));
612
+ if (!sessionHint || !sessionHint.session_name || sessionHint.session_name === sessionName) {
613
+ if (sessionHint)
614
+ candidates.push(sessionHint);
615
+ }
616
+ const current = readJsonFile(path.join(state_js_1.EKKOS_DIR, 'current-session.json'));
617
+ if (!current || !current.session_name || current.session_name === sessionName) {
618
+ if (current)
619
+ candidates.push(current);
620
+ }
621
+ for (const candidate of candidates) {
622
+ if (candidate.provider
623
+ || candidate.geminiProjectId
624
+ || candidate.claudeModel
625
+ || candidate.claudeLaunchModel
626
+ || candidate.claudeProfile
627
+ || candidate.claudeContextWindow
628
+ || candidate.claudeContextSize) {
629
+ return {
630
+ provider: normalizeDashboardProvider(candidate.provider),
631
+ claudeModel: typeof candidate.claudeModel === 'string' ? candidate.claudeModel : undefined,
632
+ claudeLaunchModel: typeof candidate.claudeLaunchModel === 'string' ? candidate.claudeLaunchModel : undefined,
633
+ claudeProfile: typeof candidate.claudeProfile === 'string' ? candidate.claudeProfile : undefined,
634
+ claudeContextWindow: normalizeLaunchContextWindow(typeof candidate.claudeContextWindow === 'string' ? candidate.claudeContextWindow : undefined),
635
+ claudeContextSize: typeof candidate.claudeContextSize === 'number' ? candidate.claudeContextSize : undefined,
636
+ claudeMaxOutputTokens: typeof candidate.claudeMaxOutputTokens === 'number' ? candidate.claudeMaxOutputTokens : undefined,
637
+ claudeCodeVersion: typeof candidate.claudeCodeVersion === 'string' ? candidate.claudeCodeVersion : undefined,
638
+ geminiProjectId: typeof candidate.geminiProjectId === 'string' ? candidate.geminiProjectId : undefined,
639
+ dashboardEnabled: candidate.dashboardEnabled === true,
640
+ bypassEnabled: candidate.bypassEnabled === true,
641
+ };
642
+ }
643
+ }
644
+ return null;
645
+ }
646
+ function formatLaunchModelLabel(metadata) {
647
+ const model = metadata?.claudeModel || metadata?.claudeLaunchModel;
648
+ if (!model)
649
+ return null;
650
+ const window = normalizeLaunchContextWindow(metadata?.claudeContextWindow);
651
+ const label = humanizeModelName(model, true) || model;
652
+ if (window === '200k')
653
+ return `${label} [200K]`;
654
+ if (window === '1m')
655
+ return `${label} [1M]`;
656
+ return label;
657
+ }
658
+ function formatLaunchWindowLabel(metadata) {
659
+ const window = normalizeLaunchContextWindow(metadata?.claudeContextWindow);
660
+ const exactSize = typeof metadata?.claudeContextSize === 'number' && metadata.claudeContextSize > 0
661
+ ? metadata.claudeContextSize
662
+ : undefined;
663
+ if (window === '200k')
664
+ return `Window ${fmtK(exactSize || 200000)} · out ${fmtK(metadata?.claudeMaxOutputTokens || 32768)}`;
665
+ if (window === '1m')
666
+ return `Window ${fmtK(exactSize || 1000000)} · out ${fmtK(metadata?.claudeMaxOutputTokens || 128000)}`;
667
+ if (typeof metadata?.claudeContextSize === 'number' && metadata.claudeContextSize > 0) {
668
+ return `Window ${fmtK(metadata.claudeContextSize)}`;
669
+ }
670
+ return null;
671
+ }
363
672
  function resolveSessionAlias(sessionId) {
364
673
  const normalized = sessionId.toLowerCase();
365
674
  // Project-local hook state (most reliable for the active session)
@@ -410,7 +719,73 @@ function isPendingSessionId(sessionId) {
410
719
  return true;
411
720
  return normalized === 'pending' || normalized === '_pending' || normalized.startsWith('_pending-');
412
721
  }
413
- // ── Resolve session to JSONL path ──
722
+ function inferDashboardProvider(explicitProvider, launchMetadata, transcriptPath) {
723
+ if (explicitProvider)
724
+ return explicitProvider;
725
+ const metadataProvider = normalizeDashboardProvider(launchMetadata?.provider);
726
+ if (metadataProvider)
727
+ return metadataProvider;
728
+ if (transcriptPath?.endsWith('.json'))
729
+ return 'gemini';
730
+ return 'claude';
731
+ }
732
+ function resolveGeminiProjectId(projectPath) {
733
+ const registry = readJsonFile(path.join(os.homedir(), '.gemini', 'projects.json'));
734
+ const projectId = registry?.projects?.[projectPath];
735
+ return typeof projectId === 'string' && projectId.length > 0 ? projectId : null;
736
+ }
737
+ function findLatestGeminiSession(projectPath, createdAfterMs) {
738
+ const projectId = resolveGeminiProjectId(projectPath);
739
+ if (!projectId)
740
+ return null;
741
+ const chatsDir = path.join(os.homedir(), '.gemini', 'tmp', projectId, 'chats');
742
+ if (!fs.existsSync(chatsDir))
743
+ return null;
744
+ try {
745
+ const files = fs.readdirSync(chatsDir)
746
+ .filter(file => file.startsWith('session-') && file.endsWith('.json'))
747
+ .map(file => {
748
+ const filePath = path.join(chatsDir, file);
749
+ const stat = fs.statSync(filePath);
750
+ return { filePath, createdMs: stat.birthtimeMs || stat.mtimeMs };
751
+ })
752
+ .filter(entry => !createdAfterMs || entry.createdMs >= createdAfterMs)
753
+ .sort((a, b) => b.createdMs - a.createdMs);
754
+ return files[0]?.filePath || null;
755
+ }
756
+ catch {
757
+ return null;
758
+ }
759
+ }
760
+ function resolveGeminiSessionPath(sessionName, createdAfterMs, launchCwd) {
761
+ const active = (0, state_js_1.getActiveSessions)()
762
+ .filter(session => session.sessionName === sessionName && normalizeDashboardProvider(session.provider) === 'gemini')
763
+ .sort((a, b) => (b.lastHeartbeat || '').localeCompare(a.lastHeartbeat || ''));
764
+ const sessionHint = readJsonFile(path.join(state_js_1.EKKOS_DIR, 'session-hint.json'));
765
+ const hintedProjectPath = !sessionHint?.session_name || sessionHint.session_name === sessionName
766
+ ? sessionHint?.project_path
767
+ : undefined;
768
+ const candidateProjectPath = active[0]?.projectPath
769
+ || hintedProjectPath
770
+ || launchCwd
771
+ || null;
772
+ if (!candidateProjectPath)
773
+ return null;
774
+ return findLatestGeminiSession(candidateProjectPath, createdAfterMs);
775
+ }
776
+ function resolveTranscriptPath(provider, sessionName, createdAfterMs, launchCwd) {
777
+ if (provider === 'gemini') {
778
+ return resolveGeminiSessionPath(sessionName, createdAfterMs, launchCwd);
779
+ }
780
+ return resolveJsonlPath(sessionName, createdAfterMs);
781
+ }
782
+ function parseTranscriptFile(transcriptPath, provider, sessionName, launchMetadata) {
783
+ if (provider === 'gemini') {
784
+ return parseGeminiSessionFile(transcriptPath, sessionName, launchMetadata);
785
+ }
786
+ return parseJsonlFile(transcriptPath, sessionName, launchMetadata);
787
+ }
788
+ // ── Resolve session to transcript path ──
414
789
  function resolveJsonlPath(sessionName, createdAfterMs) {
415
790
  // 1) Try standard resolution (works when sessionId is a real UUID)
416
791
  const resolved = (0, usage_parser_js_1.resolveSessionName)(sessionName);
@@ -651,9 +1026,11 @@ function sleep(ms) {
651
1026
  return new Promise(resolve => setTimeout(resolve, ms));
652
1027
  }
653
1028
  // ── TUI Dashboard ──
654
- async function launchDashboard(initialSessionName, initialJsonlPath, refreshMs, launchCwd, launchTs) {
1029
+ async function launchDashboard(initialSessionName, initialJsonlPath, refreshMs, launchCwd, launchTs, initialProvider) {
655
1030
  let jsonlPath = initialJsonlPath;
656
1031
  let sessionName = displaySessionName(initialSessionName);
1032
+ let launchMetadata = readSessionLaunchMetadata(sessionName) || readSessionLaunchMetadata(initialSessionName);
1033
+ let provider = inferDashboardProvider(initialProvider, launchMetadata, initialJsonlPath);
657
1034
  const blessed = require('blessed');
658
1035
  const contrib = require('blessed-contrib');
659
1036
  const inTmux = process.env.TMUX !== undefined;
@@ -784,7 +1161,7 @@ async function launchDashboard(initialSessionName, initialJsonlPath, refreshMs,
784
1161
  const CONTEXT_H = 5;
785
1162
  const USAGE_H = 4;
786
1163
  const FOOTER_H = 3;
787
- const CLAWD_W = 16; // Width reserved for Clawd mascot in context box
1164
+ const MASCOT_W = 16; // Width reserved for runtime mascot in context box
788
1165
  const FIXED_H = HEADER_H + CONTEXT_H + USAGE_H + FOOTER_H; // 15
789
1166
  function resolveChartRatio(height) {
790
1167
  if (height >= 62)
@@ -824,7 +1201,7 @@ async function launchDashboard(initialSessionName, initialJsonlPath, refreshMs,
824
1201
  tags: true,
825
1202
  style: { fg: 'white', border: { fg: 'cyan' } },
826
1203
  border: { type: 'line' },
827
- label: ' ekkOS_ ',
1204
+ label: ' ekkOS_PULSE ',
828
1205
  });
829
1206
  // Explicit header message row: with HEADER_H=3 and a border, this is the
830
1207
  // single inner content row (visual line 2 of the widget).
@@ -845,24 +1222,41 @@ async function launchDashboard(initialSessionName, initialJsonlPath, refreshMs,
845
1222
  tags: true,
846
1223
  style: { fg: 'white', border: { fg: 'cyan' } },
847
1224
  border: { type: 'line' },
848
- label: ' Context ',
1225
+ label: ' PULSE // Runtime ',
849
1226
  });
850
- // Clawd mascot — official Claude Code mascot (redBright / rgb(215,119,87))
851
- const clawdBox = blessed.box({
1227
+ const mascotBox = blessed.box({
852
1228
  parent: contextBox,
853
1229
  top: 0,
854
1230
  right: 2, // Keep a small visual gap from the context border
855
1231
  width: 10,
856
1232
  height: 3,
857
- content: ' ▐▛███▜▌\n ▝▜█████▛▘\n ▘▘ ▝▝',
858
- tags: false,
859
- style: { fg: 'red', bold: true }, // ansi redBright = official Clawd orange
1233
+ content: '',
1234
+ tags: true,
1235
+ style: { fg: 'cyan', bold: true },
860
1236
  });
1237
+ const GEMINI_MASCOT_COLORS = ['cyan', 'blue', 'magenta', 'yellow', 'white', 'green'];
1238
+ let geminiMascotPhase = 0;
1239
+ function renderRuntimeMascot() {
1240
+ if (provider === 'gemini') {
1241
+ const base = GEMINI_MASCOT_COLORS[geminiMascotPhase % GEMINI_MASCOT_COLORS.length];
1242
+ const wing = GEMINI_MASCOT_COLORS[(geminiMascotPhase + 2) % GEMINI_MASCOT_COLORS.length];
1243
+ const core = GEMINI_MASCOT_COLORS[(geminiMascotPhase + 4) % GEMINI_MASCOT_COLORS.length];
1244
+ mascotBox.setContent(` {${base}-fg}✦{/${base}-fg}\n` +
1245
+ ` {${wing}-fg}✦{/${wing}-fg}{${core}-fg}◆{/${core}-fg}{${wing}-fg}✦{/${wing}-fg}\n` +
1246
+ ` {${base}-fg}✦{/${base}-fg}`);
1247
+ mascotBox.style = { fg: 'cyan', bold: true };
1248
+ return;
1249
+ }
1250
+ // Clawd mascot — official Claude Code mascot.
1251
+ mascotBox.setContent(' ▐▛███▜▌\n ▝▜█████▛▘\n ▘▘ ▝▝');
1252
+ mascotBox.style = { fg: 'red', bold: true };
1253
+ }
1254
+ renderRuntimeMascot();
861
1255
  // Token chart (fills 40% of remaining)
862
1256
  function createTokenChart(top, left, width, height) {
863
1257
  return contrib.line({
864
1258
  top, left, width, height,
865
- label: ' Tokens/Turn (K) ',
1259
+ label: ' Signal // Tokens (K) ',
866
1260
  showLegend: true,
867
1261
  legend: { width: 8 },
868
1262
  style: {
@@ -893,7 +1287,7 @@ async function launchDashboard(initialSessionName, initialJsonlPath, refreshMs,
893
1287
  mouse: false, // Mouse disabled (use keyboard for scrolling, allows text selection)
894
1288
  input: !inTmux,
895
1289
  interactive: !inTmux, // Standalone only; passive in tmux split
896
- label: ' Turns (scroll: ↑↓/k/j, page: PgUp/u, home/end: g/G) ',
1290
+ label: ' Trace // Turns (scroll: ↑↓/k/j, page: PgUp/u, home/end: g/G) ',
897
1291
  border: { type: 'line', fg: 'cyan' },
898
1292
  style: { fg: 'white', border: { fg: 'cyan' } },
899
1293
  });
@@ -904,7 +1298,7 @@ async function launchDashboard(initialSessionName, initialJsonlPath, refreshMs,
904
1298
  tags: true,
905
1299
  style: { fg: 'white', border: { fg: 'magenta' } },
906
1300
  border: { type: 'line' },
907
- label: ' Rate Limit ',
1301
+ label: ' Pulse Limits ',
908
1302
  });
909
1303
  // Footer (totals + routing + keys)
910
1304
  const footerBox = blessed.box({
@@ -913,7 +1307,7 @@ async function launchDashboard(initialSessionName, initialJsonlPath, refreshMs,
913
1307
  tags: true,
914
1308
  style: { fg: 'white', border: { fg: 'cyan' } },
915
1309
  border: { type: 'line' },
916
- label: ' Session ',
1310
+ label: ' Session Bus ',
917
1311
  });
918
1312
  // Add all widgets to screen
919
1313
  screen.append(headerBox);
@@ -1082,8 +1476,13 @@ async function launchDashboard(initialSessionName, initialJsonlPath, refreshMs,
1082
1476
  }
1083
1477
  function renderPreTurnState() {
1084
1478
  try {
1479
+ const launchModelLabel = formatLaunchModelLabel(launchMetadata);
1480
+ const launchWindowLabel = formatLaunchWindowLabel(launchMetadata);
1481
+ const compactSelected = formatCompactLaunchModel(launchMetadata) || 'Awaiting profile';
1482
+ const compactLane = formatContextLaneCompact(launchMetadata?.claudeContextSize, launchMetadata) || 'Awaiting lane';
1085
1483
  contextBox.setContent(` {green-fg}Session active{/green-fg} {gray-fg}${sessionName}{/gray-fg}\n` +
1086
- ` Token and cost metrics appear after the first assistant response.`);
1484
+ ` ${buildRuntimeSignal('SEL', 'cyan', compactSelected)} {gray-fg}│{/gray-fg} ${buildRuntimeSignal('ACT', 'magenta', 'standby')} {gray-fg}│{/gray-fg} ${buildRuntimeSignal('LANE', 'yellow', compactLane)}\n` +
1485
+ ` ${[launchModelLabel, launchWindowLabel].filter(Boolean).join(' ') || 'Token and cost metrics appear after the first assistant response.'}`);
1087
1486
  turnBox.setContent(`{bold}Turns{/bold}\n` +
1088
1487
  `{gray-fg}—{/gray-fg}`);
1089
1488
  const timerStr = sessionStartMs
@@ -1108,10 +1507,15 @@ async function launchDashboard(initialSessionName, initialJsonlPath, refreshMs,
1108
1507
  const shouldUseCwdFallback = initialSessionName === 'initializing'
1109
1508
  || initialSessionName === 'session'
1110
1509
  || UUID_REGEX.test(initialSessionName);
1111
- const resolved = resolveJsonlPath(initialSessionName, launchTs)
1112
- || (shouldUseCwdFallback && launchCwd ? findLatestJsonl(launchCwd, launchTs) : null);
1510
+ const resolved = resolveTranscriptPath(provider, initialSessionName, launchTs, launchCwd)
1511
+ || (provider === 'claude'
1512
+ && shouldUseCwdFallback
1513
+ && launchCwd
1514
+ ? findLatestJsonl(launchCwd, launchTs)
1515
+ : null);
1113
1516
  if (resolved) {
1114
1517
  jsonlPath = resolved;
1518
+ provider = inferDashboardProvider(initialProvider, launchMetadata, resolved);
1115
1519
  dlog(`Lazy-resolved JSONL: ${jsonlPath}`);
1116
1520
  }
1117
1521
  else {
@@ -1131,6 +1535,8 @@ async function launchDashboard(initialSessionName, initialJsonlPath, refreshMs,
1131
1535
  const basename = path.basename(jsonlPath, '.jsonl');
1132
1536
  if (/^[0-9a-f]{8}-/.test(basename)) {
1133
1537
  sessionName = displaySessionName(basename);
1538
+ launchMetadata = readSessionLaunchMetadata(sessionName) || launchMetadata;
1539
+ provider = inferDashboardProvider(initialProvider, launchMetadata, jsonlPath);
1134
1540
  screen.title = `ekkOS - ${sessionName}`;
1135
1541
  }
1136
1542
  }
@@ -1141,6 +1547,9 @@ async function launchDashboard(initialSessionName, initialJsonlPath, refreshMs,
1141
1547
  const resolvedName = displaySessionName(sessionName);
1142
1548
  if (resolvedName !== sessionName) {
1143
1549
  sessionName = resolvedName;
1550
+ launchMetadata = readSessionLaunchMetadata(sessionName) || launchMetadata;
1551
+ provider = inferDashboardProvider(initialProvider, launchMetadata, jsonlPath);
1552
+ renderRuntimeMascot();
1144
1553
  screen.title = `ekkOS - ${sessionName}`;
1145
1554
  }
1146
1555
  }
@@ -1150,7 +1559,10 @@ async function launchDashboard(initialSessionName, initialJsonlPath, refreshMs,
1150
1559
  if (stat.size === lastFileSize && lastData)
1151
1560
  return;
1152
1561
  lastFileSize = stat.size;
1153
- data = parseJsonlFile(jsonlPath, sessionName);
1562
+ launchMetadata = readSessionLaunchMetadata(sessionName) || launchMetadata;
1563
+ provider = inferDashboardProvider(initialProvider, launchMetadata, jsonlPath);
1564
+ renderRuntimeMascot();
1565
+ data = parseTranscriptFile(jsonlPath, provider, sessionName, launchMetadata);
1154
1566
  lastData = data;
1155
1567
  if (!sessionStartMs && data.startedAt) {
1156
1568
  sessionStartMs = new Date(data.startedAt).getTime();
@@ -1173,10 +1585,17 @@ async function launchDashboard(initialSessionName, initialJsonlPath, refreshMs,
1173
1585
  const ctxColor = ctxPct < 50 ? 'green' : ctxPct < 80 ? 'yellow' : 'red';
1174
1586
  const tokensLabel = fmtK(data.currentContextTokens);
1175
1587
  const maxLabel = fmtK(data.modelContextSize);
1588
+ const lastTurn = data.turns.length > 0 ? data.turns[data.turns.length - 1] : null;
1589
+ const selectedProfile = formatCompactLaunchModel(launchMetadata)
1590
+ || humanizeModelName(data.model, false)
1591
+ || 'Auto';
1592
+ const activeModel = formatActiveRoutedModel(lastTurn?.routedModel || data.model) || 'Unknown';
1593
+ const laneLabel = formatContextLaneCompact(data.modelContextSize, launchMetadata)
1594
+ || `${maxLabel} window`;
1176
1595
  // Visual progress bar (fills available width)
1177
1596
  const contextInnerWidth = Math.max(10, contextBox.width - 2);
1178
1597
  // Extend bar slightly closer to mascot while keeping a small visual gap.
1179
- const barWidth = Math.max(10, contextInnerWidth - 4 - CLAWD_W);
1598
+ const barWidth = Math.max(10, contextInnerWidth - 4 - MASCOT_W);
1180
1599
  const filled = Math.round((ctxPct / 100) * barWidth);
1181
1600
  const bar = `{${ctxColor}-fg}${'█'.repeat(filled)}{/${ctxColor}-fg}${'░'.repeat(barWidth - filled)}`;
1182
1601
  // Cost breakdown by actual routed model per turn.
@@ -1192,12 +1611,13 @@ async function launchDashboard(initialSessionName, initialJsonlPath, refreshMs,
1192
1611
  const hitColor = data.cacheHitRate >= 80 ? 'green' : data.cacheHitRate >= 50 ? 'yellow' : 'red';
1193
1612
  const cappedMax = Math.min(data.maxContextPct, 100);
1194
1613
  contextBox.setContent(` ${bar}\n` +
1614
+ ` ${buildRuntimeSignal('SEL', 'cyan', selectedProfile)} {gray-fg}│{/gray-fg} ${buildRuntimeSignal('ACT', 'magenta', activeModel)} {gray-fg}│{/gray-fg} ${buildRuntimeSignal('LANE', 'yellow', laneLabel)}\n` +
1195
1615
  ` {${ctxColor}-fg}${ctxPct.toFixed(0)}%{/${ctxColor}-fg} ${tokensLabel}/${maxLabel}` +
1196
- ` {white-fg}Input{/white-fg} $${breakdown.input.toFixed(2)}` +
1616
+ ` {white-fg}In{/white-fg} $${breakdown.input.toFixed(2)}` +
1197
1617
  ` {green-fg}Read{/green-fg} $${breakdown.read.toFixed(2)}` +
1198
1618
  ` {yellow-fg}Write{/yellow-fg} $${breakdown.write.toFixed(2)}` +
1199
- ` {cyan-fg}Output{/cyan-fg} $${breakdown.output.toFixed(2)}\n` +
1200
- ` {${hitColor}-fg}${data.cacheHitRate.toFixed(0)}% cache{/${hitColor}-fg}` +
1619
+ ` {cyan-fg}Out{/cyan-fg} $${breakdown.output.toFixed(2)}` +
1620
+ ` {${hitColor}-fg}${data.cacheHitRate.toFixed(0)}% cache{/${hitColor}-fg}` +
1201
1621
  ` peak:${cappedMax.toFixed(0)}%` +
1202
1622
  ` avg:$${data.avgCostPerTurn.toFixed(2)}/t` +
1203
1623
  ` replay A:${data.replayAppliedCount}` +
@@ -1773,6 +2193,10 @@ async function launchDashboard(initialSessionName, initialJsonlPath, refreshMs,
1773
2193
  }
1774
2194
  // Fetch fresh usage data from API (called on interval)
1775
2195
  async function fetchAndCacheUsage() {
2196
+ if (provider === 'gemini') {
2197
+ renderWindowBox();
2198
+ return;
2199
+ }
1776
2200
  if (usageFetchInFlight)
1777
2201
  return;
1778
2202
  // Respect shared state across dashboard processes.
@@ -1819,6 +2243,35 @@ async function launchDashboard(initialSessionName, initialJsonlPath, refreshMs,
1819
2243
  // Render countdown from cached data (called every 1s)
1820
2244
  function renderWindowBox(skipRender = false) {
1821
2245
  try {
2246
+ if (provider === 'gemini') {
2247
+ windowBox.setLabel(' Gemini Runtime ');
2248
+ const runtimeModel = formatLaunchModelLabel(launchMetadata)
2249
+ || humanizeModelName(lastData?.model, true)
2250
+ || 'Gemini';
2251
+ const contextSize = fmtK(lastData?.modelContextSize
2252
+ || launchMetadata?.claudeContextSize
2253
+ || 1048576);
2254
+ const turnCount = lastData?.turnCount || 0;
2255
+ const tokenCount = fmtK(lastData?.currentContextTokens || 0);
2256
+ const projectId = launchMetadata?.geminiProjectId || resolveGeminiProjectId(launchCwd || process.cwd()) || 'unknown';
2257
+ const transcriptLabel = jsonlPath
2258
+ ? path.basename(jsonlPath)
2259
+ : 'awaiting chat transcript';
2260
+ const line1 = ` {bold}Model:{/bold} ${runtimeModel}` +
2261
+ ` {bold}Ctx:{/bold} ${contextSize}` +
2262
+ ` {bold}Turns:{/bold} ${turnCount}` +
2263
+ ` {bold}In:{/bold} ${tokenCount}`;
2264
+ const line2 = ` {bold}Project:{/bold} ${projectId}` +
2265
+ ` {bold}Chat:{/bold} ${transcriptLabel}`;
2266
+ windowBox.setContent(line1 + '\n' + line2);
2267
+ if (!skipRender)
2268
+ try {
2269
+ screen.render();
2270
+ }
2271
+ catch { }
2272
+ return;
2273
+ }
2274
+ windowBox.setLabel(' Pulse Limits ');
1822
2275
  const usage = cachedUsage;
1823
2276
  let line1 = ' {gray-fg}No usage data{/gray-fg}';
1824
2277
  let line2 = '';
@@ -2123,6 +2576,10 @@ async function launchDashboard(initialSessionName, initialJsonlPath, refreshMs,
2123
2576
  const headerAnimInterval = setInterval(() => {
2124
2577
  // Keep advancing across the full session label; wrap at a large value.
2125
2578
  waveOffset = (waveOffset + 1) % 1000000;
2579
+ if (provider === 'gemini') {
2580
+ geminiMascotPhase = (geminiMascotPhase + 1) % GEMINI_MASCOT_COLORS.length;
2581
+ renderRuntimeMascot();
2582
+ }
2126
2583
  renderHeader();
2127
2584
  }, animMs);
2128
2585
  const fortuneInterval = setInterval(() => {
@@ -2165,9 +2622,27 @@ function formatElapsed(startMs) {
2165
2622
  // ── Session picker ──
2166
2623
  async function pickSession() {
2167
2624
  const sessions = await (0, usage_parser_js_1.listEkkosSessions)(20);
2168
- if (sessions.length === 0) {
2169
- console.log(chalk_1.default.yellow('No sessions found with usage data.'));
2170
- console.log(chalk_1.default.gray('Start a session with "ekkos run" first.'));
2625
+ if (sessions.length > 0) {
2626
+ const { default: inquirer } = await Promise.resolve().then(() => __importStar(require('inquirer')));
2627
+ const answer = await inquirer.prompt([
2628
+ {
2629
+ type: 'list',
2630
+ name: 'session',
2631
+ message: 'Choose session:',
2632
+ choices: sessions.map(s => ({
2633
+ name: `${s.name} ($${s.cost.toFixed(2)}, ${s.turnCount}t)`,
2634
+ value: s.name,
2635
+ })),
2636
+ },
2637
+ ]);
2638
+ return answer.session;
2639
+ }
2640
+ const active = (0, state_js_1.getActiveSessions)()
2641
+ .filter(session => session.sessionName && !UUID_REGEX.test(session.sessionName))
2642
+ .sort((a, b) => (b.lastHeartbeat || '').localeCompare(a.lastHeartbeat || ''));
2643
+ if (active.length === 0) {
2644
+ console.log(chalk_1.default.yellow('No active sessions found.'));
2645
+ console.log(chalk_1.default.gray('Start a session with "ekkos run" or "ekkos gemini" first.'));
2171
2646
  return null;
2172
2647
  }
2173
2648
  const { default: inquirer } = await Promise.resolve().then(() => __importStar(require('inquirer')));
@@ -2175,10 +2650,10 @@ async function pickSession() {
2175
2650
  {
2176
2651
  type: 'list',
2177
2652
  name: 'session',
2178
- message: 'Choose session:',
2179
- choices: sessions.map(s => ({
2180
- name: `${s.name} ($${s.cost.toFixed(2)}, ${s.turnCount}t)`,
2181
- value: s.name,
2653
+ message: 'Choose active session:',
2654
+ choices: active.map(session => ({
2655
+ name: `${session.sessionName} [${normalizeDashboardProvider(session.provider) === 'gemini' ? 'Gemini' : 'Claude'}]`,
2656
+ value: session.sessionName,
2182
2657
  })),
2183
2658
  },
2184
2659
  ]);
@@ -2186,18 +2661,20 @@ async function pickSession() {
2186
2661
  }
2187
2662
  // ── Commander command ──
2188
2663
  exports.dashboardCommand = new commander_1.Command('dashboard')
2189
- .description('Live TUI dashboard for monitoring Claude Code session usage')
2664
+ .description('Live TUI dashboard for monitoring ekkOS session usage')
2190
2665
  .argument('[session-name]', 'ekkOS session name (e.g., dew-pod-hum)')
2191
2666
  .option('--latest', 'Auto-detect latest active session')
2192
2667
  .option('--wait-for-new', 'Wait for a new session to start (used by ekkos run --dashboard)')
2193
2668
  .option('--refresh <ms>', 'Polling interval in ms', '2000')
2194
2669
  .option('--compact', 'Minimal layout for small terminals')
2670
+ .option('--provider <name>', 'Session provider override (claude|gemini)')
2195
2671
  .action(async (sessionNameArg, options) => {
2196
2672
  const refreshMs = parseInt(options.refresh) || 2000;
2673
+ const explicitProvider = normalizeDashboardProvider(options.provider);
2197
2674
  // --wait-for-new: poll until session name appears (JSONL may not exist yet)
2198
2675
  if (options.waitForNew) {
2199
2676
  const result = await waitForNewSession();
2200
- await launchDashboard(result.sessionName, result.jsonlPath, refreshMs, result.launchCwd, result.launchTs);
2677
+ await launchDashboard(result.sessionName, result.jsonlPath, refreshMs, result.launchCwd, result.launchTs, explicitProvider);
2201
2678
  return;
2202
2679
  }
2203
2680
  let sessionName = null;
@@ -2221,10 +2698,11 @@ exports.dashboardCommand = new commander_1.Command('dashboard')
2221
2698
  // stale JSONL files from previous sessions (the lazy resolution already does
2222
2699
  // this correctly — this ensures the INITIAL resolve matches).
2223
2700
  const launchTs = Date.now();
2224
- const jsonlPath = resolveJsonlPath(sessionName, launchTs);
2701
+ const launchMetadata = readSessionLaunchMetadata(sessionName);
2702
+ const provider = inferDashboardProvider(explicitProvider, launchMetadata, null);
2703
+ const jsonlPath = resolveTranscriptPath(provider, sessionName, launchTs, null);
2225
2704
  if (!jsonlPath) {
2226
- // JSONL may not exist yet (session just started) — launch with lazy resolution
2227
- console.log(chalk_1.default.gray(`Waiting for JSONL for "${sessionName}"...`));
2705
+ console.log(chalk_1.default.gray(`Waiting for transcript for "${sessionName}"...`));
2228
2706
  }
2229
- await launchDashboard(sessionName, jsonlPath || null, refreshMs, null, launchTs);
2707
+ await launchDashboard(sessionName, jsonlPath || null, refreshMs, null, launchTs, provider);
2230
2708
  });