@co0ontty/wand 1.3.3 → 1.3.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.
@@ -499,7 +499,7 @@ function shouldBackfillClaudeSessionId(record) {
499
499
  function snapshotMessages(record) {
500
500
  return record.ptyBridge?.getMessages() ?? record.messages;
501
501
  }
502
- const MAX_SESSIONS = 50;
502
+ const MAX_SESSIONS = 200;
503
503
  const ARCHIVE_AFTER_MS = 1000 * 60 * 60 * 24;
504
504
  const CONFIRM_WINDOW_SIZE = 800;
505
505
  // Claude 会话 ID 格式:UUID v4
@@ -651,24 +651,33 @@ export class ProcessManager extends EventEmitter {
651
651
  this.emit("process", event);
652
652
  }
653
653
  cleanupOldSessions() {
654
- // Remove oldest finished sessions if we're at the limit
654
+ // Only clean up when well over the limit
655
655
  if (this.sessions.size < MAX_SESSIONS)
656
656
  return;
657
- const finishedIds = [];
657
+ const now = Date.now();
658
+ const STALE_MS = 7 * 24 * 60 * 60 * 1000; // 7 days
659
+ const removable = [];
658
660
  for (const [id, record] of this.sessions) {
659
- if (record.status !== "running") {
660
- finishedIds.push(id);
661
+ // Only remove archived, non-running sessions older than 7 days
662
+ if (record.status === "running")
663
+ continue;
664
+ if (!record.archived)
665
+ continue;
666
+ const ref = record.endedAt ?? record.startedAt;
667
+ const refMs = Date.parse(ref);
668
+ if (Number.isFinite(refMs) && now - refMs > STALE_MS) {
669
+ removable.push(id);
661
670
  }
662
671
  }
663
- // Remove oldest finished sessions first
664
- finishedIds
672
+ // Sort oldest first and remove enough to get back under the limit
673
+ const toRemove = removable
665
674
  .sort((a, b) => {
666
675
  const ra = this.sessions.get(a);
667
676
  const rb = this.sessions.get(b);
668
677
  return (ra?.endedAt || "").localeCompare(rb?.endedAt || "");
669
678
  })
670
- .slice(0, this.sessions.size - MAX_SESSIONS + 1)
671
- .forEach((id) => {
679
+ .slice(0, this.sessions.size - MAX_SESSIONS + 1);
680
+ for (const id of toRemove) {
672
681
  const record = this.sessions.get(id);
673
682
  if (record) {
674
683
  this.logger.deleteSession(id);
@@ -677,7 +686,7 @@ export class ProcessManager extends EventEmitter {
677
686
  this.sessions.delete(id);
678
687
  this.lastPersistedMessageCount.delete(id);
679
688
  this.storage.deleteSession(id);
680
- });
689
+ }
681
690
  }
682
691
  start(command, cwd, mode, initialInput, opts) {
683
692
  this.assertCommandAllowed(command);
@@ -989,8 +998,10 @@ export class ProcessManager extends EventEmitter {
989
998
  get(id) {
990
999
  this.archiveExpiredSessions();
991
1000
  const record = this.sessions.get(id);
992
- if (!record)
993
- return null;
1001
+ if (!record) {
1002
+ // Fallback: check SQLite for sessions that were evicted from memory
1003
+ return this.storage.getSession(id) ?? null;
1004
+ }
994
1005
  // For sessions loaded from storage on startup, in-memory output starts empty.
995
1006
  // Prefer in-memory output (live PTY data), fall back to stored output.
996
1007
  if (!record.output && record.storedOutput) {
@@ -3561,6 +3561,15 @@
3561
3561
  return fetch("/api/sessions/" + id, { credentials: "same-origin" })
3562
3562
  .then(function(res) { return res.json(); })
3563
3563
  .then(function(data) {
3564
+ if (data.error) {
3565
+ // Session no longer exists — deselect and refresh list
3566
+ if (state.selectedId === id) {
3567
+ state.selectedId = null;
3568
+ persistSelectedId();
3569
+ }
3570
+ loadSessions();
3571
+ return;
3572
+ }
3564
3573
  updateSessionSnapshot(data);
3565
3574
  updateShellChrome();
3566
3575
 
@@ -5825,11 +5834,26 @@
5825
5834
 
5826
5835
  function handleInputBoxBlur() {
5827
5836
  resetInputPanelViewportSpacing();
5828
- // Restore app container height when keyboard closes
5829
- var appContainer = document.querySelector('.app-container');
5830
- if (appContainer) {
5831
- appContainer.style.height = '';
5832
- }
5837
+ // Restore app container height when keyboard closes.
5838
+ // Use a short delay because on iOS the visualViewport may not
5839
+ // have updated yet at the moment blur fires.
5840
+ setTimeout(function() {
5841
+ var appContainer = document.querySelector('.app-container');
5842
+ if (appContainer) {
5843
+ // Only clear if keyboard is actually closed now
5844
+ var vv = window.visualViewport;
5845
+ if (vv) {
5846
+ var offsetBottom = window.innerHeight - vv.height - vv.offsetTop;
5847
+ if (offsetBottom <= 50) {
5848
+ appContainer.style.height = '';
5849
+ }
5850
+ } else {
5851
+ appContainer.style.height = '';
5852
+ }
5853
+ }
5854
+ // Scroll the window back to top to fix any residual offset
5855
+ window.scrollTo(0, 0);
5856
+ }, 100);
5833
5857
  }
5834
5858
 
5835
5859
  function adjustInputBoxSelection(inputBox) {
@@ -6542,13 +6566,16 @@
6542
6566
  var isKeyboardOpen = offsetBottom > 50;
6543
6567
  var heightChanged = Math.abs(vv.height - lastHeight) > 8;
6544
6568
 
6545
- // In PWA standalone mode, dynamically resize the app container
6546
- // because 100dvh does NOT shrink when keyboard appears in standalone PWA
6569
+ // Dynamically resize the app container to match visible viewport.
6570
+ // This is needed because 100dvh does NOT shrink when the keyboard
6571
+ // appears in PWA standalone mode, and on some browsers the layout
6572
+ // viewport doesn't update on keyboard dismiss without this.
6547
6573
  var appContainer = document.querySelector('.app-container');
6548
6574
  if (appContainer) {
6549
6575
  if (isKeyboardOpen) {
6550
6576
  appContainer.style.height = vv.height + 'px';
6551
- } else {
6577
+ } else if (keyboardOpen) {
6578
+ // Keyboard just closed — clear forced height
6552
6579
  appContainer.style.height = '';
6553
6580
  }
6554
6581
  }
@@ -6571,6 +6598,9 @@
6571
6598
  }
6572
6599
 
6573
6600
  vv.addEventListener('resize', debouncedUpdate);
6601
+ // Also listen to scroll — on iOS, keyboard dismiss sometimes only
6602
+ // fires a scroll event (viewport scrolls back) without a resize event.
6603
+ vv.addEventListener('scroll', debouncedUpdate);
6574
6604
 
6575
6605
  updateViewport();
6576
6606
  }
@@ -7856,8 +7886,12 @@
7856
7886
  if (!state.terminalDomView || !state.serializeAddon) return;
7857
7887
 
7858
7888
  try {
7889
+ // Serialize the entire buffer including scrollback history
7890
+ var buf = state.terminal.buffer.active;
7891
+ var totalRows = buf.length;
7859
7892
  var html = state.serializeAddon.serializeAsHTML({
7860
- includeGlobalBackground: true
7893
+ includeGlobalBackground: true,
7894
+ range: { start: 0, end: totalRows }
7861
7895
  });
7862
7896
 
7863
7897
  // Extract the <pre>...</pre> portion
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@co0ontty/wand",
3
- "version": "1.3.3",
3
+ "version": "1.3.4",
4
4
  "description": "A web terminal for local CLI tools like Claude.",
5
5
  "type": "module",
6
6
  "bin": {