@mariozechner/pi-coding-agent 0.10.0 → 0.10.2

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.
@@ -227,11 +227,31 @@ function formatToolExecution(toolName, args, result) {
227
227
  }
228
228
  return { html, bgColor };
229
229
  }
230
+ /**
231
+ * Format timestamp for display
232
+ */
233
+ function formatTimestamp(timestamp) {
234
+ if (!timestamp)
235
+ return "";
236
+ const date = new Date(typeof timestamp === "string" ? timestamp : timestamp);
237
+ return date.toLocaleTimeString(undefined, { hour: "2-digit", minute: "2-digit", second: "2-digit" });
238
+ }
239
+ /**
240
+ * Format model change event
241
+ */
242
+ function formatModelChange(event) {
243
+ const timestamp = formatTimestamp(event.timestamp);
244
+ const timestampHtml = timestamp ? `<div class="message-timestamp">${timestamp}</div>` : "";
245
+ const modelInfo = `${event.provider}/${event.modelId}`;
246
+ return `<div class="model-change">${timestampHtml}<div class="model-change-text">Switched to model: <span class="model-name">${escapeHtml(modelInfo)}</span></div></div>`;
247
+ }
230
248
  /**
231
249
  * Format a message as HTML (matching TUI component styling)
232
250
  */
233
251
  function formatMessage(message, toolResultsMap) {
234
252
  let html = "";
253
+ const timestamp = message.timestamp;
254
+ const timestampHtml = timestamp ? `<div class="message-timestamp">${formatTimestamp(timestamp)}</div>` : "";
235
255
  if (message.role === "user") {
236
256
  const userMsg = message;
237
257
  let textContent = "";
@@ -243,11 +263,12 @@ function formatMessage(message, toolResultsMap) {
243
263
  textContent = textBlocks.map((c) => c.text).join("");
244
264
  }
245
265
  if (textContent.trim()) {
246
- html += `<div class="user-message">${escapeHtml(textContent).replace(/\n/g, "<br>")}</div>`;
266
+ html += `<div class="user-message">${timestampHtml}${escapeHtml(textContent).replace(/\n/g, "<br>")}</div>`;
247
267
  }
248
268
  }
249
269
  else if (message.role === "assistant") {
250
270
  const assistantMsg = message;
271
+ html += timestampHtml ? `<div class="assistant-message">${timestampHtml}` : "";
251
272
  // Render text and thinking content
252
273
  for (const content of assistantMsg.content) {
253
274
  if (content.type === "text" && content.text.trim()) {
@@ -276,6 +297,10 @@ function formatMessage(message, toolResultsMap) {
276
297
  html += `<div class="error-text">Error: ${escapeHtml(errorMsg)}</div>`;
277
298
  }
278
299
  }
300
+ // Close the assistant message wrapper if we opened one
301
+ if (timestampHtml) {
302
+ html += "</div>";
303
+ }
279
304
  }
280
305
  return html;
281
306
  }
@@ -285,10 +310,10 @@ function formatMessage(message, toolResultsMap) {
285
310
  export function exportSessionToHtml(sessionManager, state, outputPath) {
286
311
  const sessionFile = sessionManager.getSessionFile();
287
312
  const timestamp = new Date().toISOString();
288
- // Use session filename + .html if no output path provided
313
+ // Use pi-session- prefix + session filename + .html if no output path provided
289
314
  if (!outputPath) {
290
315
  const sessionBasename = basename(sessionFile, ".jsonl");
291
- outputPath = `${sessionBasename}.html`;
316
+ outputPath = `pi-session-${sessionBasename}.html`;
292
317
  }
293
318
  // Read and parse session data
294
319
  const sessionContent = readFileSync(sessionFile, "utf8");
@@ -296,18 +321,61 @@ export function exportSessionToHtml(sessionManager, state, outputPath) {
296
321
  let sessionHeader = null;
297
322
  const messages = [];
298
323
  const toolResultsMap = new Map();
324
+ const sessionEvents = []; // Track all events including model changes
325
+ const modelsUsed = new Set(); // Track unique models used
326
+ // Cumulative token and cost stats
327
+ const tokenStats = {
328
+ input: 0,
329
+ output: 0,
330
+ cacheRead: 0,
331
+ cacheWrite: 0,
332
+ };
333
+ const costStats = {
334
+ input: 0,
335
+ output: 0,
336
+ cacheRead: 0,
337
+ cacheWrite: 0,
338
+ };
299
339
  for (const line of lines) {
300
340
  try {
301
341
  const entry = JSON.parse(line);
302
342
  if (entry.type === "session") {
303
343
  sessionHeader = entry;
344
+ // Track initial model from session header
345
+ if (entry.modelId) {
346
+ const modelInfo = entry.provider ? `${entry.provider}/${entry.modelId}` : entry.modelId;
347
+ modelsUsed.add(modelInfo);
348
+ }
304
349
  }
305
350
  else if (entry.type === "message") {
306
351
  messages.push(entry.message);
352
+ sessionEvents.push(entry);
307
353
  // Build map of tool call ID to result
308
354
  if (entry.message.role === "toolResult") {
309
355
  toolResultsMap.set(entry.message.toolCallId, entry.message);
310
356
  }
357
+ // Accumulate token and cost stats from assistant messages
358
+ if (entry.message.role === "assistant" && entry.message.usage) {
359
+ const usage = entry.message.usage;
360
+ tokenStats.input += usage.input || 0;
361
+ tokenStats.output += usage.output || 0;
362
+ tokenStats.cacheRead += usage.cacheRead || 0;
363
+ tokenStats.cacheWrite += usage.cacheWrite || 0;
364
+ if (usage.cost) {
365
+ costStats.input += usage.cost.input || 0;
366
+ costStats.output += usage.cost.output || 0;
367
+ costStats.cacheRead += usage.cost.cacheRead || 0;
368
+ costStats.cacheWrite += usage.cost.cacheWrite || 0;
369
+ }
370
+ }
371
+ }
372
+ else if (entry.type === "model_change") {
373
+ sessionEvents.push(entry);
374
+ // Track model from model change event
375
+ if (entry.modelId) {
376
+ const modelInfo = entry.provider ? `${entry.provider}/${entry.modelId}` : entry.modelId;
377
+ modelsUsed.add(modelInfo);
378
+ }
311
379
  }
312
380
  }
313
381
  catch {
@@ -327,12 +395,33 @@ export function exportSessionToHtml(sessionManager, state, outputPath) {
327
395
  toolCallsCount += assistantMsg.content.filter((c) => c.type === "toolCall").length;
328
396
  }
329
397
  }
330
- // Generate messages HTML
398
+ // Get last assistant message for context percentage calculation (skip aborted messages)
399
+ const lastAssistantMessage = messages
400
+ .slice()
401
+ .reverse()
402
+ .find((m) => m.role === "assistant" && m.stopReason !== "aborted");
403
+ // Calculate context percentage from last message (input + output + cacheRead + cacheWrite)
404
+ const contextTokens = lastAssistantMessage
405
+ ? lastAssistantMessage.usage.input +
406
+ lastAssistantMessage.usage.output +
407
+ lastAssistantMessage.usage.cacheRead +
408
+ lastAssistantMessage.usage.cacheWrite
409
+ : 0;
410
+ // Get the model info from the last assistant message
411
+ const lastModel = lastAssistantMessage?.model || state.model?.id || "unknown";
412
+ const lastProvider = lastAssistantMessage?.provider || "";
413
+ const lastModelInfo = lastProvider ? `${lastProvider}/${lastModel}` : lastModel;
414
+ const contextWindow = state.model?.contextWindow || 0;
415
+ const contextPercent = contextWindow > 0 ? ((contextTokens / contextWindow) * 100).toFixed(1) : "0.0";
416
+ // Generate messages HTML (including model changes in chronological order)
331
417
  let messagesHtml = "";
332
- for (const message of messages) {
333
- if (message.role !== "toolResult") {
418
+ for (const event of sessionEvents) {
419
+ if (event.type === "message" && event.message.role !== "toolResult") {
334
420
  // Skip toolResult messages as they're rendered with their tool calls
335
- messagesHtml += formatMessage(message, toolResultsMap);
421
+ messagesHtml += formatMessage(event.message, toolResultsMap);
422
+ }
423
+ else if (event.type === "model_change") {
424
+ messagesHtml += formatModelChange(event);
336
425
  }
337
426
  }
338
427
  // Generate HTML (matching TUI aesthetic)
@@ -350,8 +439,8 @@ export function exportSessionToHtml(sessionManager, state, outputPath) {
350
439
  }
351
440
 
352
441
  body {
353
- font-family: 'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New', monospace;
354
- font-size: 14px;
442
+ font-family: ui-monospace, 'Cascadia Code', 'Source Code Pro', Menlo, Consolas, 'DejaVu Sans Mono', monospace;
443
+ font-size: 12px;
355
444
  line-height: 1.6;
356
445
  color: ${COLORS.text};
357
446
  background: ${COLORS.bodyBg};
@@ -371,7 +460,7 @@ export function exportSessionToHtml(sessionManager, state, outputPath) {
371
460
  }
372
461
 
373
462
  .header h1 {
374
- font-size: 16px;
463
+ font-size: 14px;
375
464
  font-weight: bold;
376
465
  margin-bottom: 12px;
377
466
  color: ${COLORS.cyan};
@@ -380,8 +469,8 @@ export function exportSessionToHtml(sessionManager, state, outputPath) {
380
469
  .header-info {
381
470
  display: flex;
382
471
  flex-direction: column;
383
- gap: 6px;
384
- font-size: 13px;
472
+ gap: 3px;
473
+ font-size: 11px;
385
474
  }
386
475
 
387
476
  .info-item {
@@ -393,7 +482,7 @@ export function exportSessionToHtml(sessionManager, state, outputPath) {
393
482
  .info-label {
394
483
  font-weight: 600;
395
484
  margin-right: 8px;
396
- min-width: 80px;
485
+ min-width: 100px;
397
486
  }
398
487
 
399
488
  .info-value {
@@ -401,12 +490,24 @@ export function exportSessionToHtml(sessionManager, state, outputPath) {
401
490
  flex: 1;
402
491
  }
403
492
 
493
+ .info-value.cost {
494
+ font-family: 'SF Mono', monospace;
495
+ }
496
+
404
497
  .messages {
405
498
  display: flex;
406
499
  flex-direction: column;
407
500
  gap: 16px;
408
501
  }
409
502
 
503
+ /* Message timestamp */
504
+ .message-timestamp {
505
+ font-size: 10px;
506
+ color: ${COLORS.textDim};
507
+ margin-bottom: 4px;
508
+ opacity: 0.8;
509
+ }
510
+
410
511
  /* User message - matching TUI UserMessageComponent */
411
512
  .user-message {
412
513
  background: ${COLORS.userMessageBg};
@@ -414,6 +515,13 @@ export function exportSessionToHtml(sessionManager, state, outputPath) {
414
515
  border-radius: 4px;
415
516
  white-space: pre-wrap;
416
517
  word-wrap: break-word;
518
+ overflow-wrap: break-word;
519
+ word-break: break-word;
520
+ }
521
+
522
+ /* Assistant message wrapper */
523
+ .assistant-message {
524
+ padding: 0;
417
525
  }
418
526
 
419
527
  /* Assistant text - matching TUI AssistantMessageComponent */
@@ -421,6 +529,8 @@ export function exportSessionToHtml(sessionManager, state, outputPath) {
421
529
  padding: 12px 16px;
422
530
  white-space: pre-wrap;
423
531
  word-wrap: break-word;
532
+ overflow-wrap: break-word;
533
+ word-break: break-word;
424
534
  }
425
535
 
426
536
  /* Thinking text - gray italic */
@@ -430,6 +540,25 @@ export function exportSessionToHtml(sessionManager, state, outputPath) {
430
540
  font-style: italic;
431
541
  white-space: pre-wrap;
432
542
  word-wrap: break-word;
543
+ overflow-wrap: break-word;
544
+ word-break: break-word;
545
+ }
546
+
547
+ /* Model change */
548
+ .model-change {
549
+ padding: 8px 16px;
550
+ background: rgb(40, 40, 50);
551
+ border-radius: 4px;
552
+ }
553
+
554
+ .model-change-text {
555
+ color: ${COLORS.textDim};
556
+ font-size: 11px;
557
+ }
558
+
559
+ .model-name {
560
+ color: ${COLORS.cyan};
561
+ font-weight: bold;
433
562
  }
434
563
 
435
564
  /* Tool execution - matching TUI ToolExecutionComponent */
@@ -449,6 +578,7 @@ export function exportSessionToHtml(sessionManager, state, outputPath) {
449
578
 
450
579
  .tool-path {
451
580
  color: ${COLORS.cyan};
581
+ word-break: break-all;
452
582
  }
453
583
 
454
584
  .line-count {
@@ -457,13 +587,21 @@ export function exportSessionToHtml(sessionManager, state, outputPath) {
457
587
 
458
588
  .tool-command {
459
589
  font-weight: bold;
590
+ white-space: pre-wrap;
591
+ word-wrap: break-word;
592
+ overflow-wrap: break-word;
593
+ word-break: break-word;
460
594
  }
461
595
 
462
596
  .tool-output {
463
597
  margin-top: 12px;
464
598
  color: ${COLORS.textDim};
465
599
  white-space: pre-wrap;
600
+ word-wrap: break-word;
601
+ overflow-wrap: break-word;
602
+ word-break: break-word;
466
603
  font-family: inherit;
604
+ overflow-x: auto;
467
605
  }
468
606
 
469
607
  .tool-output > div {
@@ -474,6 +612,9 @@ export function exportSessionToHtml(sessionManager, state, outputPath) {
474
612
  margin: 0;
475
613
  font-family: inherit;
476
614
  color: inherit;
615
+ white-space: pre-wrap;
616
+ word-wrap: break-word;
617
+ overflow-wrap: break-word;
477
618
  }
478
619
 
479
620
  /* Expandable tool output */
@@ -521,7 +662,9 @@ export function exportSessionToHtml(sessionManager, state, outputPath) {
521
662
  color: ${COLORS.textDim};
522
663
  white-space: pre-wrap;
523
664
  word-wrap: break-word;
524
- font-size: 13px;
665
+ overflow-wrap: break-word;
666
+ word-break: break-word;
667
+ font-size: 11px;
525
668
  }
526
669
 
527
670
  .tools-list {
@@ -539,7 +682,7 @@ export function exportSessionToHtml(sessionManager, state, outputPath) {
539
682
 
540
683
  .tools-content {
541
684
  color: ${COLORS.textDim};
542
- font-size: 13px;
685
+ font-size: 11px;
543
686
  }
544
687
 
545
688
  .tool-item {
@@ -554,25 +697,31 @@ export function exportSessionToHtml(sessionManager, state, outputPath) {
554
697
  /* Diff styling */
555
698
  .tool-diff {
556
699
  margin-top: 12px;
557
- font-size: 13px;
558
- font-family: 'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New', monospace;
700
+ font-size: 11px;
701
+ font-family: ui-monospace, 'Cascadia Code', 'Source Code Pro', Menlo, Consolas, 'DejaVu Sans Mono', monospace;
559
702
  overflow-x: auto;
560
703
  max-width: 100%;
561
704
  }
562
705
 
563
706
  .diff-line-old {
564
707
  color: ${COLORS.red};
565
- white-space: pre;
708
+ white-space: pre-wrap;
709
+ word-wrap: break-word;
710
+ overflow-wrap: break-word;
566
711
  }
567
712
 
568
713
  .diff-line-new {
569
714
  color: ${COLORS.green};
570
- white-space: pre;
715
+ white-space: pre-wrap;
716
+ word-wrap: break-word;
717
+ overflow-wrap: break-word;
571
718
  }
572
719
 
573
720
  .diff-line-context {
574
721
  color: ${COLORS.textDim};
575
- white-space: pre;
722
+ white-space: pre-wrap;
723
+ word-wrap: break-word;
724
+ overflow-wrap: break-word;
576
725
  }
577
726
 
578
727
  /* Error text */
@@ -586,7 +735,7 @@ export function exportSessionToHtml(sessionManager, state, outputPath) {
586
735
  padding: 20px;
587
736
  text-align: center;
588
737
  color: ${COLORS.textDim};
589
- font-size: 12px;
738
+ font-size: 10px;
590
739
  }
591
740
 
592
741
  @media print {
@@ -614,8 +763,10 @@ export function exportSessionToHtml(sessionManager, state, outputPath) {
614
763
  <span class="info-value">${sessionHeader?.timestamp ? new Date(sessionHeader.timestamp).toLocaleString() : timestamp}</span>
615
764
  </div>
616
765
  <div class="info-item">
617
- <span class="info-label">Model:</span>
618
- <span class="info-value">${escapeHtml(sessionHeader?.model || state.model.id)}</span>
766
+ <span class="info-label">Models:</span>
767
+ <span class="info-value">${Array.from(modelsUsed)
768
+ .map((m) => escapeHtml(m))
769
+ .join(", ") || escapeHtml(sessionHeader?.model || state.model.id)}</span>
619
770
  </div>
620
771
  </div>
621
772
  </div>
@@ -635,21 +786,55 @@ export function exportSessionToHtml(sessionManager, state, outputPath) {
635
786
  <span class="info-label">Tool Calls:</span>
636
787
  <span class="info-value">${toolCallsCount}</span>
637
788
  </div>
789
+ </div>
790
+ </div>
791
+
792
+ <div class="header">
793
+ <h1>Tokens & Cost</h1>
794
+ <div class="header-info">
795
+ <div class="info-item">
796
+ <span class="info-label">Input:</span>
797
+ <span class="info-value">${tokenStats.input.toLocaleString()} tokens</span>
798
+ </div>
799
+ <div class="info-item">
800
+ <span class="info-label">Output:</span>
801
+ <span class="info-value">${tokenStats.output.toLocaleString()} tokens</span>
802
+ </div>
803
+ <div class="info-item">
804
+ <span class="info-label">Cache Read:</span>
805
+ <span class="info-value">${tokenStats.cacheRead.toLocaleString()} tokens</span>
806
+ </div>
638
807
  <div class="info-item">
639
- <span class="info-label">Tool Results:</span>
640
- <span class="info-value">${toolResultMessages}</span>
808
+ <span class="info-label">Cache Write:</span>
809
+ <span class="info-value">${tokenStats.cacheWrite.toLocaleString()} tokens</span>
641
810
  </div>
642
811
  <div class="info-item">
643
812
  <span class="info-label">Total:</span>
644
- <span class="info-value">${totalMessages}</span>
813
+ <span class="info-value">${(tokenStats.input + tokenStats.output + tokenStats.cacheRead + tokenStats.cacheWrite).toLocaleString()} tokens</span>
814
+ </div>
815
+ <div class="info-item">
816
+ <span class="info-label">Input Cost:</span>
817
+ <span class="info-value cost">$${costStats.input.toFixed(4)}</span>
818
+ </div>
819
+ <div class="info-item">
820
+ <span class="info-label">Output Cost:</span>
821
+ <span class="info-value cost">$${costStats.output.toFixed(4)}</span>
822
+ </div>
823
+ <div class="info-item">
824
+ <span class="info-label">Cache Read Cost:</span>
825
+ <span class="info-value cost">$${costStats.cacheRead.toFixed(4)}</span>
826
+ </div>
827
+ <div class="info-item">
828
+ <span class="info-label">Cache Write Cost:</span>
829
+ <span class="info-value cost">$${costStats.cacheWrite.toFixed(4)}</span>
645
830
  </div>
646
831
  <div class="info-item">
647
- <span class="info-label">Directory:</span>
648
- <span class="info-value">${escapeHtml(shortenPath(sessionHeader?.cwd || process.cwd()))}</span>
832
+ <span class="info-label">Total Cost:</span>
833
+ <span class="info-value cost"><strong>$${(costStats.input + costStats.output + costStats.cacheRead + costStats.cacheWrite).toFixed(4)}</strong></span>
649
834
  </div>
650
835
  <div class="info-item">
651
- <span class="info-label">Thinking:</span>
652
- <span class="info-value">${escapeHtml(sessionHeader?.thinkingLevel || state.thinkingLevel)}</span>
836
+ <span class="info-label">Context Usage:</span>
837
+ <span class="info-value">${contextTokens.toLocaleString()} / ${contextWindow.toLocaleString()} tokens (${contextPercent}%) - ${escapeHtml(lastModelInfo)}</span>
653
838
  </div>
654
839
  </div>
655
840
  </div>