pi-sdk-web 0.2.2 → 0.2.4

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/server.js CHANGED
@@ -251,7 +251,9 @@ export class PiWebServer {
251
251
  buildHistory() {
252
252
  try {
253
253
  return {
254
- entries: this.session.sessionManager.getEntries(),
254
+ // Match TUI's initial rendering: the current context view
255
+ // (compaction collapses older entries into a summary; branch-aware)
256
+ entries: this.session.sessionManager.buildContextEntries(),
255
257
  leafId: this.session.sessionManager.getLeafId(),
256
258
  };
257
259
  }
@@ -485,10 +487,68 @@ export class PiWebServer {
485
487
  await this.executeCommand(name, args);
486
488
  break;
487
489
  }
490
+ case "session":
491
+ // Read-only session info (TUI /session equivalent)
492
+ this.broadcastSessionInfo();
493
+ break;
488
494
  default:
489
495
  throw new Error(`Unsupported command: ${cmdType}`);
490
496
  }
491
497
  }
498
+ /** TUI /session equivalent: show session info as a modal (read-only). */
499
+ broadcastSessionInfo() {
500
+ try {
501
+ const stats = this.session.getSessionStats();
502
+ const sm = this.session.sessionManager;
503
+ const entries = sm.getEntries();
504
+ const model = this.session.model;
505
+ const lines = [];
506
+ lines.push("## Session Info", "");
507
+ const name = sm.getSessionName();
508
+ if (name)
509
+ lines.push(`**Name:** ${name}`);
510
+ lines.push(`**ID:** ${this.session.sessionId}`);
511
+ if (stats.sessionFile)
512
+ lines.push(`**File:** ${stats.sessionFile}`);
513
+ if (model)
514
+ lines.push(`**Model:** ${model.provider}/${model.id}`);
515
+ lines.push(`**Thinking:** ${this.session.thinkingLevel}`, "");
516
+ lines.push("### Messages");
517
+ lines.push(`- User: ${stats.userMessages}`);
518
+ lines.push(`- Assistant: ${stats.assistantMessages}`);
519
+ lines.push(`- Tool calls: ${stats.toolCalls}`);
520
+ lines.push(`- Tool results: ${stats.toolResults}`);
521
+ lines.push(`- Total: ${stats.totalMessages}`);
522
+ lines.push(`- Entries: ${entries.length}`, "");
523
+ const t = stats.tokens;
524
+ if (t) {
525
+ lines.push("### Tokens");
526
+ lines.push(`- Input: ${t.input}`);
527
+ lines.push(`- Output: ${t.output}`);
528
+ lines.push(`- Cache read: ${t.cacheRead}`);
529
+ lines.push(`- Cache write: ${t.cacheWrite}`);
530
+ lines.push(`- Total: ${t.total}`, "");
531
+ }
532
+ lines.push("### Cost");
533
+ lines.push(`$${stats.cost.toFixed(4)}`);
534
+ const cu = stats.contextUsage;
535
+ if (cu?.contextWindow) {
536
+ lines.push("", "### Context");
537
+ lines.push(`- ${cu.percent}% / ${cu.contextWindow} tokens`);
538
+ }
539
+ this.broadcast({
540
+ type: "extension_ui_request",
541
+ id: crypto.randomUUID(),
542
+ method: "notify",
543
+ title: "/session",
544
+ message: lines.join("\n"),
545
+ notifyType: "info",
546
+ });
547
+ }
548
+ catch {
549
+ // session info unavailable - skip
550
+ }
551
+ }
492
552
  /**
493
553
  * Execute an extension slash command (e.g. /ctx-status) by invoking its
494
554
  * registered handler with a command context.
@@ -8,6 +8,7 @@ const BUILTIN_COMMANDS = [
8
8
  { name: 'compact', description: 'Manually compact the session context', builtin: true, action: 'compact' },
9
9
  { name: 'reload', description: 'Reload session resources and extensions', builtin: true, action: 'reload' },
10
10
  { name: 'export', description: 'Export session to HTML (or .jsonl)', builtin: true, action: 'export' },
11
+ { name: 'session', description: 'Show session information', builtin: true, action: 'session' },
11
12
  { name: 'name', description: 'Set session display name', builtin: true, action: 'name' },
12
13
  { name: 'login', description: 'Configure provider authentication (not supported in web)', builtin: true, unsupported: true },
13
14
  { name: 'logout', description: 'Remove provider authentication (not supported in web)', builtin: true, unsupported: true },
@@ -1202,6 +1203,8 @@ class PiWebClient {
1202
1203
  } else if (cmd.action === 'export') {
1203
1204
  const path = (args || '').trim();
1204
1205
  this.send({ type: 'export', path: path });
1206
+ } else if (cmd.action === 'session') {
1207
+ this.send({ type: 'session' });
1205
1208
  } else if (cmd.action === 'name') {
1206
1209
  const newName = window.prompt('Set session display name:', '');
1207
1210
  if (newName && newName.trim()) {
@@ -1469,12 +1472,30 @@ class PiWebClient {
1469
1472
  }
1470
1473
 
1471
1474
  openExtensionNotify(req) {
1472
- this.openModal(req.title || 'Notification', 'extension-notify');
1473
- this.currentExtRequest = req;
1474
- this.modalSearch.style.display = 'none';
1475
- // Render as markdown (sanitized) so command outputs (/ctx-status etc.) look right
1476
- this.modalList.innerHTML = `<div class="modal-message body-text">${this.renderMarkdown(req.message || '')}</div>`;
1477
- // Close button in modal footer is enough
1475
+ // Command output notifications (title starts with "/", e.g. /ctx-status) keep
1476
+ // the modal. Other extension notifies are lightweight toasts (TUI shows
1477
+ // notify as a transient status message, not a dialog).
1478
+ if (req.title && String(req.title).startsWith('/')) {
1479
+ this.openModal(req.title || 'Notification', 'extension-notify');
1480
+ this.currentExtRequest = req;
1481
+ this.modalSearch.style.display = 'none';
1482
+ this.modalList.innerHTML = `<div class="modal-message body-text">${this.renderMarkdown(req.message || '')}</div>`;
1483
+ return;
1484
+ }
1485
+ this.showToast(req.message || '', req.notifyType);
1486
+ }
1487
+
1488
+ showToast(message, type) {
1489
+ let container = document.getElementById('toast-container');
1490
+ if (!container) return;
1491
+ const toast = document.createElement('div');
1492
+ toast.className = 'toast' + (type ? ` toast-${type}` : '');
1493
+ toast.textContent = message;
1494
+ container.appendChild(toast);
1495
+ setTimeout(() => {
1496
+ toast.classList.add('toast-hide');
1497
+ setTimeout(() => toast.remove(), 300);
1498
+ }, type === 'error' ? 8000 : 5000);
1478
1499
  }
1479
1500
 
1480
1501
  handleModels(models) {
@@ -1614,11 +1635,24 @@ class PiWebClient {
1614
1635
  ...((this.lastState && this.lastState.commands) || []),
1615
1636
  ...BUILTIN_COMMANDS,
1616
1637
  ];
1617
- const filtered = commands.filter((c) => {
1618
- const name = (c.name || '').replace(/^skill:/, '').toLowerCase();
1619
- const desc = (c.description || c.source || '').toLowerCase();
1620
- return name.includes(query) || desc.includes(query);
1621
- });
1638
+ const filtered = commands
1639
+ .filter((c) => {
1640
+ const name = (c.name || '').replace(/^skill:/, '').toLowerCase();
1641
+ const desc = (c.description || c.source || '').toLowerCase();
1642
+ return name.includes(query) || desc.includes(query);
1643
+ })
1644
+ .sort((a, b) => {
1645
+ // Exact-name match first, then name-prefix, then name-contains,
1646
+ // then description-contains (avoids unrelated commands flooding the list)
1647
+ const score = (c) => {
1648
+ const name = (c.name || '').replace(/^skill:/, '').toLowerCase();
1649
+ if (name === query) return 0;
1650
+ if (name.startsWith(query)) return 1;
1651
+ if (name.includes(query)) return 2;
1652
+ return 3;
1653
+ };
1654
+ return score(a) - score(b);
1655
+ });
1622
1656
 
1623
1657
  if (filtered.length === 0) {
1624
1658
  this.hideCommandMenu();
@@ -53,6 +53,7 @@
53
53
  <div id="modal-close">Close (Esc)</div>
54
54
  </div>
55
55
  </div>
56
+ <div id="toast-container"></div>
56
57
  <script src="vendor/marked.min.js"></script>
57
58
  <script src="app.js"></script>
58
59
  </body>
@@ -734,6 +734,44 @@ body {
734
734
  color: var(--text);
735
735
  }
736
736
 
737
+ /* Lightweight extension notifications (TUI shows notify as transient status) */
738
+ #toast-container {
739
+ position: fixed;
740
+ top: 12px;
741
+ right: 12px;
742
+ z-index: 200;
743
+ display: flex;
744
+ flex-direction: column;
745
+ gap: 8px;
746
+ max-width: 420px;
747
+ }
748
+
749
+ .toast {
750
+ background: var(--modal-bg);
751
+ border: 1px solid var(--border);
752
+ border-left: 3px solid var(--accent);
753
+ border-radius: 6px;
754
+ padding: 8px 12px;
755
+ color: var(--text);
756
+ font-size: 13px;
757
+ line-height: 1.5;
758
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
759
+ opacity: 1;
760
+ transition: opacity 0.3s;
761
+ }
762
+
763
+ .toast-warning {
764
+ border-left-color: var(--warning);
765
+ }
766
+
767
+ .toast-error {
768
+ border-left-color: var(--error);
769
+ }
770
+
771
+ .toast-hide {
772
+ opacity: 0;
773
+ }
774
+
737
775
  .modal-message {
738
776
  color: var(--text);
739
777
  font-size: 13px;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-sdk-web",
3
- "version": "0.2.2",
3
+ "version": "0.2.4",
4
4
  "description": "Browser Web access for Pi (AI coding agent) via the Pi SDK - standalone module, zero modification to Pi itself",
5
5
  "type": "module",
6
6
  "bin": {