@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.
- package/dist/process-manager.d.ts +1 -1
- package/dist/server.js +15 -3
- package/dist/web-ui/content/scripts.js +344 -0
- package/dist/web-ui/content/styles.css +116 -0
- package/dist/ws-broadcast.d.ts +1 -1
- package/package.json +3 -2
|
@@ -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((
|
|
1167
|
-
|
|
1168
|
-
|
|
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, "&")
|
|
@@ -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;
|
package/dist/ws-broadcast.d.ts
CHANGED
|
@@ -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.
|
|
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",
|