@co0ontty/wand 1.1.7 → 1.2.0

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.
@@ -2,7 +2,7 @@ import { EventEmitter } from "node:events";
2
2
  import { WandStorage } from "./storage.js";
3
3
  import { ExecutionMode, SessionSnapshot, WandConfig } from "./types.js";
4
4
  export interface ProcessEvent {
5
- type: "output" | "status" | "started" | "ended" | "usage" | "task";
5
+ type: "output" | "status" | "started" | "ended" | "usage" | "task" | "notification";
6
6
  sessionId: string;
7
7
  data?: unknown;
8
8
  }
package/dist/server.js CHANGED
@@ -21,6 +21,8 @@ const PKG_REPO_URL = "https://github.com/co0ontty/wand";
21
21
  let cachedLatestVersion = null;
22
22
  let cacheTimestamp = 0;
23
23
  const CACHE_TTL_MS = 10 * 60 * 1000; // 10 minutes
24
+ /** Cached update result broadcast to new clients on connect. */
25
+ let cachedUpdateInfo = null;
24
26
  async function checkNpmLatestVersion(forceRefresh = false) {
25
27
  const now = Date.now();
26
28
  if (forceRefresh || !cachedLatestVersion || (now - cacheTimestamp > CACHE_TTL_MS)) {
@@ -384,6 +386,9 @@ export async function startServer(config, configPath) {
384
386
  defaultMode: config.defaultMode,
385
387
  defaultCwd: config.defaultCwd,
386
388
  commandPresets: config.commandPresets,
389
+ updateAvailable: cachedUpdateInfo?.updateAvailable ?? false,
390
+ latestVersion: cachedUpdateInfo?.latest ?? null,
391
+ currentVersion: PKG_VERSION,
387
392
  });
388
393
  });
389
394
  // ── Settings endpoints ──
@@ -1163,9 +1168,16 @@ export async function startServer(config, configPath) {
1163
1168
  // Start configured background sessions after the server is already reachable.
1164
1169
  processes.runStartupCommands();
1165
1170
  // Background update check on startup
1166
- checkNpmLatestVersion().then(({ current, latest, updateAvailable }) => {
1167
- if (updateAvailable) {
1168
- process.stdout.write(`[wand] 发现新版本 ${latest}(当前 ${current})。运行 npm install -g ${PKG_NAME}@latest 进行更新。\n`);
1171
+ checkNpmLatestVersion().then((info) => {
1172
+ cachedUpdateInfo = info;
1173
+ if (info.updateAvailable) {
1174
+ process.stdout.write(`[wand] 发现新版本 ${info.latest}(当前 ${info.current})。运行 npm install -g ${PKG_NAME}@latest 进行更新。\n`);
1175
+ // Broadcast update notification to all connected WS clients
1176
+ wsManager.emitEvent({
1177
+ type: "notification",
1178
+ sessionId: "__system__",
1179
+ data: { kind: "update", current: info.current, latest: info.latest },
1180
+ });
1169
1181
  }
1170
1182
  }).catch(() => { });
1171
1183
  }
@@ -257,6 +257,24 @@
257
257
  }
258
258
  startPolling();
259
259
  refreshAll();
260
+ // Request browser notification permission after login
261
+ requestNotificationPermission();
262
+ // Show update bubble if server reports an available update
263
+ if (config.updateAvailable && config.latestVersion) {
264
+ showNotificationBubble({
265
+ title: "\u53d1\u73b0\u65b0\u7248\u672c",
266
+ body: "\u5f53\u524d " + (config.currentVersion || "-") + " \u2192 \u6700\u65b0 " + config.latestVersion,
267
+ type: "info",
268
+ icon: "\u2191",
269
+ duration: 0,
270
+ actionLabel: "\u53bb\u66f4\u65b0",
271
+ action: function() {
272
+ var settingsBtn = document.getElementById("open-settings-btn") || document.querySelector("[data-action='settings']");
273
+ if (settingsBtn) settingsBtn.click();
274
+ }
275
+ });
276
+ sendBrowserNotification("Wand \u53d1\u73b0\u65b0\u7248\u672c", "\u5f53\u524d " + (config.currentVersion || "-") + " \u2192 \u6700\u65b0 " + config.latestVersion, { tag: "wand-update" });
277
+ }
260
278
  // Auto-load claude history since section defaults to expanded
261
279
  if (state.claudeHistoryExpanded && !state.claudeHistoryLoaded) {
262
280
  loadClaudeHistory();
@@ -631,6 +649,18 @@
631
649
  '</div>' +
632
650
  '<p id="update-message" class="hint hidden"></p>' +
633
651
  '</div>' +
652
+ '<div class="settings-notification-section">' +
653
+ '<div class="settings-section-title">\u901a\u77e5\u72b6\u6001</div>' +
654
+ '<div class="settings-about-row">' +
655
+ '<span class="settings-label">\u6d4f\u89c8\u5668\u901a\u77e5</span>' +
656
+ '<span class="settings-value" id="notification-permission-status">-</span>' +
657
+ '</div>' +
658
+ '<div class="settings-update-actions">' +
659
+ '<button id="notification-request-btn" class="btn btn-ghost btn-sm hidden">\u6388\u6743\u901a\u77e5</button>' +
660
+ '<button id="notification-test-btn" class="btn btn-ghost btn-sm">\u53d1\u9001\u6d4b\u8bd5\u901a\u77e5</button>' +
661
+ '</div>' +
662
+ '<p id="notification-test-message" class="hint hidden"></p>' +
663
+ '</div>' +
634
664
  '</div>' +
635
665
 
636
666
  // General config tab
@@ -1886,6 +1916,16 @@
1886
1916
  if (checkUpdateBtn) checkUpdateBtn.addEventListener("click", checkForUpdate);
1887
1917
  var doUpdateBtn = document.getElementById("do-update-button");
1888
1918
  if (doUpdateBtn) doUpdateBtn.addEventListener("click", performUpdate);
1919
+ // Notification test section
1920
+ var notifRequestBtn = document.getElementById("notification-request-btn");
1921
+ if (notifRequestBtn) notifRequestBtn.addEventListener("click", function() {
1922
+ if (typeof Notification !== "undefined") {
1923
+ Notification.requestPermission().then(function() { updateNotificationStatus(); });
1924
+ }
1925
+ });
1926
+ var notifTestBtn = document.getElementById("notification-test-btn");
1927
+ if (notifTestBtn) notifTestBtn.addEventListener("click", testNotification);
1928
+ updateNotificationStatus();
1889
1929
  var newSessBtn = document.getElementById("topbar-new-session-button");
1890
1930
  if (newSessBtn) newSessBtn.addEventListener("click", openSessionModal);
1891
1931
  var drawerNewSessBtn = document.getElementById("drawer-new-session-button");
@@ -3913,6 +3953,101 @@
3913
3953
  });
3914
3954
  }
3915
3955
 
3956
+ // ── Notification Settings Helpers ──
3957
+
3958
+ function updateNotificationStatus() {
3959
+ var statusEl = document.getElementById("notification-permission-status");
3960
+ var requestBtn = document.getElementById("notification-request-btn");
3961
+ var testMsgEl = document.getElementById("notification-test-message");
3962
+ if (!statusEl) return;
3963
+
3964
+ if (typeof Notification === "undefined") {
3965
+ statusEl.textContent = "\u4e0d\u652f\u6301";
3966
+ statusEl.style.color = "var(--fg-muted)";
3967
+ if (requestBtn) requestBtn.classList.add("hidden");
3968
+ return;
3969
+ }
3970
+
3971
+ var perm = Notification.permission;
3972
+ if (perm === "granted") {
3973
+ statusEl.textContent = "\u5df2\u6388\u6743 \u2713";
3974
+ statusEl.style.color = "var(--success)";
3975
+ if (requestBtn) requestBtn.classList.add("hidden");
3976
+ } else if (perm === "denied") {
3977
+ statusEl.textContent = "\u5df2\u62d2\u7edd";
3978
+ statusEl.style.color = "var(--danger)";
3979
+ if (requestBtn) requestBtn.classList.add("hidden");
3980
+ if (testMsgEl) {
3981
+ testMsgEl.textContent = "\u6d4f\u89c8\u5668\u5df2\u62d2\u7edd\u901a\u77e5\u6743\u9650\uff0c\u8bf7\u5728\u6d4f\u89c8\u5668\u8bbe\u7f6e\u4e2d\u624b\u52a8\u5f00\u542f";
3982
+ testMsgEl.style.color = "var(--fg-muted)";
3983
+ testMsgEl.classList.remove("hidden");
3984
+ }
3985
+ } else {
3986
+ statusEl.textContent = "\u672a\u6388\u6743";
3987
+ statusEl.style.color = "var(--warning)";
3988
+ if (requestBtn) requestBtn.classList.remove("hidden");
3989
+ }
3990
+ }
3991
+
3992
+ function testNotification() {
3993
+ var testMsgEl = document.getElementById("notification-test-message");
3994
+
3995
+ // Always show in-app bubble
3996
+ showNotificationBubble({
3997
+ title: "\u6d4b\u8bd5\u901a\u77e5",
3998
+ body: "\u8fd9\u662f\u4e00\u6761\u6d4b\u8bd5\u901a\u77e5\uff0c\u5e94\u7528\u5185\u6c14\u6ce1\u5df2\u6b63\u5e38\u5de5\u4f5c\u3002",
3999
+ type: "info",
4000
+ icon: "\u266a",
4001
+ duration: 5000,
4002
+ });
4003
+
4004
+ // Test browser notification
4005
+ if (typeof Notification === "undefined") {
4006
+ if (testMsgEl) {
4007
+ testMsgEl.textContent = "\u6d4f\u89c8\u5668\u4e0d\u652f\u6301\u901a\u77e5 API\uff0c\u4ec5\u53ef\u4f7f\u7528\u5e94\u7528\u5185\u6c14\u6ce1\u901a\u77e5\u3002";
4008
+ testMsgEl.style.color = "var(--fg-muted)";
4009
+ testMsgEl.classList.remove("hidden");
4010
+ }
4011
+ return;
4012
+ }
4013
+
4014
+ if (Notification.permission === "granted") {
4015
+ try {
4016
+ var n = new Notification("Wand \u6d4b\u8bd5\u901a\u77e5", {
4017
+ body: "\u6d4f\u89c8\u5668\u901a\u77e5\u5df2\u6b63\u5e38\u5de5\u4f5c\u3002",
4018
+ icon: "/favicon.ico",
4019
+ tag: "wand-test",
4020
+ });
4021
+ setTimeout(function() { n.close(); }, 5000);
4022
+ if (testMsgEl) {
4023
+ testMsgEl.textContent = "\u2713 \u6d4f\u89c8\u5668\u901a\u77e5 + \u5e94\u7528\u5185\u6c14\u6ce1\u5747\u5df2\u53d1\u9001";
4024
+ testMsgEl.style.color = "var(--success)";
4025
+ testMsgEl.classList.remove("hidden");
4026
+ }
4027
+ } catch (_e) {
4028
+ if (testMsgEl) {
4029
+ testMsgEl.textContent = "\u6d4f\u89c8\u5668\u901a\u77e5\u53d1\u9001\u5931\u8d25\uff0c\u53ef\u80fd\u9700\u8981 HTTPS";
4030
+ testMsgEl.style.color = "var(--warning)";
4031
+ testMsgEl.classList.remove("hidden");
4032
+ }
4033
+ }
4034
+ } else {
4035
+ // permission is "default" or "denied" — always try requesting
4036
+ Notification.requestPermission().then(function(result) {
4037
+ updateNotificationStatus();
4038
+ if (result === "granted") {
4039
+ testNotification();
4040
+ } else if (result === "denied") {
4041
+ if (testMsgEl) {
4042
+ testMsgEl.textContent = "\u6d4f\u89c8\u5668\u5df2\u62d2\u7edd\u901a\u77e5\u6743\u9650\uff0c\u8bf7\u70b9\u51fb\u5730\u5740\u680f\u5de6\u4fa7\u9501\u56fe\u6807\u6216\u5728\u6d4f\u89c8\u5668\u8bbe\u7f6e\u4e2d\u624b\u52a8\u5f00\u542f";
4043
+ testMsgEl.style.color = "var(--fg-muted)";
4044
+ testMsgEl.classList.remove("hidden");
4045
+ }
4046
+ }
4047
+ });
4048
+ }
4049
+ }
4050
+
3916
4051
  function quickStartSession() {
3917
4052
  var command = getPreferredTool();
3918
4053
  var defaultCwd = getEffectiveCwd();
@@ -6681,6 +6816,33 @@
6681
6816
  }
6682
6817
  updateSessionSnapshot(endedSnapshot);
6683
6818
 
6819
+ // Notify user when a session completes (browser + in-app if backgrounded or not viewing)
6820
+ var endedSession = state.sessions.find(function(s) { return s.id === msg.sessionId; });
6821
+ var endedName = endedSession ? (endedSession.label || endedSession.command || msg.sessionId) : msg.sessionId;
6822
+ var endedExitCode = msg.data && msg.data.exitCode;
6823
+ var endedIsError = endedExitCode !== null && endedExitCode !== undefined && endedExitCode !== 0;
6824
+ sendBrowserNotification(
6825
+ endedIsError ? "\u4f1a\u8bdd\u5f02\u5e38\u7ed3\u675f" : "\u4f1a\u8bdd\u5df2\u5b8c\u6210",
6826
+ endedName,
6827
+ {
6828
+ tag: "wand-ended-" + msg.sessionId,
6829
+ onClick: function() {
6830
+ if (msg.sessionId !== state.selectedId) selectSession(msg.sessionId);
6831
+ }
6832
+ }
6833
+ );
6834
+ if (msg.sessionId !== state.selectedId || document.hidden) {
6835
+ showNotificationBubble({
6836
+ title: endedIsError ? "\u4f1a\u8bdd\u5f02\u5e38\u7ed3\u675f" : "\u4f1a\u8bdd\u5df2\u5b8c\u6210",
6837
+ body: endedName,
6838
+ type: endedIsError ? "warning" : "success",
6839
+ icon: endedIsError ? "!" : "\u2713",
6840
+ duration: 6000,
6841
+ actionLabel: "\u67e5\u770b",
6842
+ action: function() { selectSession(msg.sessionId); }
6843
+ });
6844
+ }
6845
+
6684
6846
  // Clear stale queued inputs so they cannot race with the ended session.
6685
6847
  // Each queued item's postInput will hit the server and get an error, but
6686
6848
  // clearing the queues here prevents them from growing unbounded.
@@ -6738,6 +6900,35 @@
6738
6900
  target: msg.data.permissionRequest.target,
6739
6901
  reason: msg.data.permissionRequest.prompt
6740
6902
  };
6903
+ // Browser notification for permission waiting (background tab)
6904
+ var permSession = state.sessions.find(function(s) { return s.id === msg.sessionId; });
6905
+ var permSessionName = permSession ? (permSession.label || permSession.command || msg.sessionId) : msg.sessionId;
6906
+ sendBrowserNotification(
6907
+ "\u4f1a\u8bdd\u7b49\u5f85\u6388\u6743",
6908
+ permSessionName + " \u2014 " + (msg.data.permissionRequest.prompt || "\u9700\u8981\u6743\u9650\u5ba1\u6279"),
6909
+ {
6910
+ tag: "wand-perm-" + msg.sessionId,
6911
+ onClick: function() {
6912
+ if (msg.sessionId !== state.selectedId) {
6913
+ selectSession(msg.sessionId);
6914
+ }
6915
+ }
6916
+ }
6917
+ );
6918
+ // In-app bubble if not currently viewing this session
6919
+ if (msg.sessionId !== state.selectedId) {
6920
+ showNotificationBubble({
6921
+ title: "\u4f1a\u8bdd\u7b49\u5f85\u6388\u6743",
6922
+ body: permSessionName + " \u2014 " + (msg.data.permissionRequest.prompt || "\u9700\u8981\u6743\u9650\u5ba1\u6279"),
6923
+ type: "warning",
6924
+ icon: "!",
6925
+ duration: 0,
6926
+ actionLabel: "\u67e5\u770b",
6927
+ action: function() {
6928
+ selectSession(msg.sessionId);
6929
+ }
6930
+ });
6931
+ }
6741
6932
  }
6742
6933
  if (msg.data.permissionBlocked === false) {
6743
6934
  statusUpdate.pendingEscalation = null;
@@ -6748,6 +6939,29 @@
6748
6939
  }
6749
6940
  }
6750
6941
  break;
6942
+ case 'notification':
6943
+ if (msg.data) {
6944
+ if (msg.data.kind === "update") {
6945
+ showNotificationBubble({
6946
+ title: "\u53d1\u73b0\u65b0\u7248\u672c",
6947
+ body: "\u5f53\u524d " + (msg.data.current || "-") + " \u2192 \u6700\u65b0 " + (msg.data.latest || "-"),
6948
+ type: "info",
6949
+ icon: "\u2191",
6950
+ duration: 0,
6951
+ actionLabel: "\u53bb\u66f4\u65b0",
6952
+ action: function() {
6953
+ var settingsBtn = document.getElementById("open-settings-btn") || document.querySelector("[data-action='settings']");
6954
+ if (settingsBtn) settingsBtn.click();
6955
+ }
6956
+ });
6957
+ sendBrowserNotification(
6958
+ "Wand \u53d1\u73b0\u65b0\u7248\u672c",
6959
+ "\u5f53\u524d " + (msg.data.current || "-") + " \u2192 \u6700\u65b0 " + (msg.data.latest || "-"),
6960
+ { tag: "wand-update" }
6961
+ );
6962
+ }
6963
+ }
6964
+ break;
6751
6965
  }
6752
6966
  }
6753
6967
 
@@ -8558,6 +8772,136 @@
8558
8772
  }, type === "error" ? 4000 : 2200);
8559
8773
  }
8560
8774
 
8775
+ // ── Notification Bubble System ──
8776
+
8777
+ var notificationStack = [];
8778
+ var notificationIdCounter = 0;
8779
+ var NOTIFICATION_GAP = 8;
8780
+ var NOTIFICATION_TOP = 24;
8781
+
8782
+ /**
8783
+ * Show an in-app notification bubble at bottom-right.
8784
+ * @param {object} opts
8785
+ * @param {string} opts.title - Notification title
8786
+ * @param {string} [opts.body] - Body text
8787
+ * @param {string} [opts.type] - "info" | "warning" | "success" (default "info")
8788
+ * @param {string} [opts.icon] - Icon character (default derived from type)
8789
+ * @param {number} [opts.duration] - Auto-dismiss ms, 0 = manual only (default 8000)
8790
+ * @param {string} [opts.actionLabel] - Action button label
8791
+ * @param {function} [opts.action] - Action button callback
8792
+ * @returns {{ dismiss: function }} handle
8793
+ */
8794
+ function showNotificationBubble(opts) {
8795
+ var id = ++notificationIdCounter;
8796
+ var type = opts.type || "info";
8797
+ var icon = opts.icon || (type === "warning" ? "!" : type === "success" ? "\u2713" : "i");
8798
+ var duration = opts.duration !== undefined ? opts.duration : 8000;
8799
+
8800
+ var bubble = document.createElement("div");
8801
+ bubble.className = "notification-bubble";
8802
+ bubble.setAttribute("data-nid", id);
8803
+
8804
+ var headerHtml =
8805
+ '<div class="notification-bubble-header">' +
8806
+ '<span class="notification-bubble-icon ' + type + '">' + icon + '</span>' +
8807
+ '<span class="notification-bubble-title">' + escapeHtml(opts.title) + '</span>' +
8808
+ '<button class="notification-bubble-close" title="\u5173\u95ed">\u00d7</button>' +
8809
+ '</div>';
8810
+
8811
+ var bodyHtml = opts.body
8812
+ ? '<div class="notification-bubble-body">' + escapeHtml(opts.body) + '</div>'
8813
+ : '';
8814
+
8815
+ var actionsHtml = opts.actionLabel
8816
+ ? '<div class="notification-bubble-actions">' +
8817
+ '<button class="primary">' + escapeHtml(opts.actionLabel) + '</button>' +
8818
+ '</div>'
8819
+ : '';
8820
+
8821
+ bubble.innerHTML = headerHtml + bodyHtml + actionsHtml;
8822
+ document.body.appendChild(bubble);
8823
+
8824
+ // Stack position
8825
+ var entry = { id: id, el: bubble };
8826
+ notificationStack.push(entry);
8827
+ repositionNotifications();
8828
+
8829
+ // Wire close button
8830
+ var closeBtn = bubble.querySelector(".notification-bubble-close");
8831
+ if (closeBtn) closeBtn.onclick = function() { dismissNotification(id); };
8832
+
8833
+ // Wire action button
8834
+ if (opts.actionLabel && opts.action) {
8835
+ var actionBtn = bubble.querySelector(".notification-bubble-actions button");
8836
+ if (actionBtn) actionBtn.onclick = function() {
8837
+ opts.action();
8838
+ dismissNotification(id);
8839
+ };
8840
+ }
8841
+
8842
+ // Auto-dismiss
8843
+ var timer = null;
8844
+ if (duration > 0) {
8845
+ timer = setTimeout(function() { dismissNotification(id); }, duration);
8846
+ }
8847
+
8848
+ return {
8849
+ dismiss: function() { dismissNotification(id); }
8850
+ };
8851
+ }
8852
+
8853
+ function dismissNotification(id) {
8854
+ var idx = -1;
8855
+ for (var i = 0; i < notificationStack.length; i++) {
8856
+ if (notificationStack[i].id === id) { idx = i; break; }
8857
+ }
8858
+ if (idx === -1) return;
8859
+ var entry = notificationStack[idx];
8860
+ entry.el.classList.add("slide-out");
8861
+ notificationStack.splice(idx, 1);
8862
+ repositionNotifications();
8863
+ setTimeout(function() {
8864
+ if (entry.el.parentNode) entry.el.parentNode.removeChild(entry.el);
8865
+ }, 300);
8866
+ }
8867
+
8868
+ function repositionNotifications() {
8869
+ var top = NOTIFICATION_TOP;
8870
+ for (var i = 0; i < notificationStack.length; i++) {
8871
+ notificationStack[i].el.style.top = top + "px";
8872
+ top += notificationStack[i].el.offsetHeight + NOTIFICATION_GAP;
8873
+ }
8874
+ }
8875
+
8876
+ // ── Browser Notification API ──
8877
+
8878
+ function requestNotificationPermission() {
8879
+ if (typeof Notification !== "undefined" && Notification.permission === "default") {
8880
+ Notification.requestPermission();
8881
+ }
8882
+ }
8883
+
8884
+ function sendBrowserNotification(title, body, opts) {
8885
+ if (typeof Notification === "undefined" || Notification.permission !== "granted") return;
8886
+ if (!document.hidden) return; // Only notify when tab is in background
8887
+ try {
8888
+ var n = new Notification(title, {
8889
+ body: body || "",
8890
+ icon: (opts && opts.icon) || "/favicon.ico",
8891
+ tag: (opts && opts.tag) || undefined,
8892
+ });
8893
+ n.onclick = function() {
8894
+ window.focus();
8895
+ n.close();
8896
+ if (opts && opts.onClick) opts.onClick();
8897
+ };
8898
+ // Auto-close after 10s
8899
+ setTimeout(function() { n.close(); }, 10000);
8900
+ } catch (_e) {
8901
+ // Notification constructor may fail in some contexts (e.g. insecure origin)
8902
+ }
8903
+ }
8904
+
8561
8905
  function escapeHtml(value) {
8562
8906
  return String(value)
8563
8907
  .replace(/&/g, "&amp;")
@@ -5830,6 +5830,109 @@
5830
5830
  to { opacity: 1; transform: translateX(-50%) translateY(0); }
5831
5831
  }
5832
5832
 
5833
+ /* ── Notification Bubble ── */
5834
+ .notification-bubble {
5835
+ position: fixed;
5836
+ left: 50%;
5837
+ transform: translateX(-50%);
5838
+ z-index: 10000;
5839
+ min-width: 280px;
5840
+ max-width: 380px;
5841
+ background: var(--bg-primary);
5842
+ border: 1px solid var(--border);
5843
+ border-radius: var(--radius-lg);
5844
+ box-shadow: var(--shadow-lg);
5845
+ padding: 14px 16px;
5846
+ animation: notification-slide-in 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
5847
+ transition: top 0.25s ease, opacity 0.25s ease;
5848
+ }
5849
+ .notification-bubble.slide-out {
5850
+ animation: notification-slide-out 0.25s ease forwards;
5851
+ transform: translateX(-50%);
5852
+ }
5853
+ .notification-bubble-header {
5854
+ display: flex;
5855
+ align-items: center;
5856
+ gap: 8px;
5857
+ margin-bottom: 6px;
5858
+ }
5859
+ .notification-bubble-icon {
5860
+ width: 20px;
5861
+ height: 20px;
5862
+ flex-shrink: 0;
5863
+ display: flex;
5864
+ align-items: center;
5865
+ justify-content: center;
5866
+ border-radius: var(--radius-sm);
5867
+ font-size: 12px;
5868
+ }
5869
+ .notification-bubble-icon.info { background: var(--accent); color: white; }
5870
+ .notification-bubble-icon.warning { background: var(--warning); color: white; }
5871
+ .notification-bubble-icon.success { background: var(--success); color: white; }
5872
+ .notification-bubble-title {
5873
+ font-size: 0.875rem;
5874
+ font-weight: 600;
5875
+ color: var(--fg-primary);
5876
+ flex: 1;
5877
+ min-width: 0;
5878
+ }
5879
+ .notification-bubble-close {
5880
+ background: none;
5881
+ border: none;
5882
+ color: var(--fg-muted);
5883
+ cursor: pointer;
5884
+ font-size: 16px;
5885
+ line-height: 1;
5886
+ padding: 2px 4px;
5887
+ border-radius: var(--radius-sm);
5888
+ flex-shrink: 0;
5889
+ }
5890
+ .notification-bubble-close:hover {
5891
+ background: var(--bg-hover);
5892
+ color: var(--fg-primary);
5893
+ }
5894
+ .notification-bubble-body {
5895
+ font-size: 0.8125rem;
5896
+ color: var(--fg-secondary);
5897
+ line-height: 1.45;
5898
+ margin-bottom: 0;
5899
+ }
5900
+ .notification-bubble-actions {
5901
+ margin-top: 10px;
5902
+ display: flex;
5903
+ gap: 8px;
5904
+ justify-content: flex-end;
5905
+ }
5906
+ .notification-bubble-actions button {
5907
+ font-size: 0.8125rem;
5908
+ padding: 4px 12px;
5909
+ border-radius: var(--radius-sm);
5910
+ cursor: pointer;
5911
+ border: 1px solid var(--border);
5912
+ background: var(--bg-secondary);
5913
+ color: var(--fg-primary);
5914
+ transition: background 0.15s;
5915
+ }
5916
+ .notification-bubble-actions button:hover {
5917
+ background: var(--bg-hover);
5918
+ }
5919
+ .notification-bubble-actions button.primary {
5920
+ background: var(--accent);
5921
+ color: white;
5922
+ border-color: var(--accent);
5923
+ }
5924
+ .notification-bubble-actions button.primary:hover {
5925
+ filter: brightness(1.1);
5926
+ }
5927
+ @keyframes notification-slide-in {
5928
+ from { opacity: 0; transform: translateX(-50%) translateY(-20px); }
5929
+ to { opacity: 1; transform: translateX(-50%) translateY(0); }
5930
+ }
5931
+ @keyframes notification-slide-out {
5932
+ from { opacity: 1; transform: translateX(-50%) translateY(0); }
5933
+ to { opacity: 0; transform: translateX(-50%) translateY(-20px); }
5934
+ }
5935
+
5833
5936
  /* File Preview Modal */
5834
5937
  .file-preview-overlay {
5835
5938
  position: fixed;
@@ -5931,6 +6034,19 @@
5931
6034
  padding-top: 14px;
5932
6035
  }
5933
6036
 
6037
+ .settings-notification-section {
6038
+ border-top: 1px solid var(--border-subtle);
6039
+ padding-top: 14px;
6040
+ margin-top: 4px;
6041
+ }
6042
+ .settings-section-title {
6043
+ font-size: 0.8125rem;
6044
+ font-weight: 600;
6045
+ color: var(--fg-primary);
6046
+ margin-bottom: 10px;
6047
+ letter-spacing: 0.02em;
6048
+ }
6049
+
5934
6050
  .settings-update-actions {
5935
6051
  display: flex;
5936
6052
  gap: 8px;
@@ -5,7 +5,7 @@
5
5
  import { WebSocketServer } from "ws";
6
6
  import type { SessionSnapshot } from "./types.js";
7
7
  export interface ProcessEvent {
8
- type: "output" | "status" | "started" | "ended" | "usage" | "task";
8
+ type: "output" | "status" | "started" | "ended" | "usage" | "task" | "notification";
9
9
  sessionId: string;
10
10
  data?: unknown;
11
11
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@co0ontty/wand",
3
- "version": "1.1.7",
3
+ "version": "1.2.0",
4
4
  "description": "A web terminal for local CLI tools like Claude.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -19,7 +19,8 @@
19
19
  "build": "tsc -p tsconfig.json && npm run build:copy-content",
20
20
  "build:copy-content": "cp -r src/web-ui/content dist/web-ui/",
21
21
  "dev": "tsx src/cli.ts web",
22
- "check": "tsc --noEmit -p tsconfig.json"
22
+ "check": "tsc --noEmit -p tsconfig.json",
23
+ "prepublishOnly": "TAG=$(git tag --sort=-v:refname --list 'v*' | head -1) && VER=${TAG#v} && npm version $VER --no-git-tag-version --allow-same-version && npm run build"
23
24
  },
24
25
  "keywords": [
25
26
  "cli",