@co0ontty/wand 1.1.7 → 1.2.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.
@@ -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();
@@ -331,22 +349,25 @@
331
349
  }
332
350
  }
333
351
 
352
+ function renderShortcutKeys() {
353
+ return '<button class="shortcut-key' + (state.modifiers.ctrl ? ' active' : '') + '" data-key="ctrl" type="button">Ctrl</button>' +
354
+ '<button class="shortcut-key' + (state.modifiers.alt ? ' active' : '') + '" data-key="alt" type="button">Alt</button>' +
355
+ '<span class="shortcut-sep">·</span>' +
356
+ '<button class="shortcut-key shortcut-dir" data-key="up" type="button">↑</button>' +
357
+ '<button class="shortcut-key shortcut-dir" data-key="down" type="button">↓</button>' +
358
+ '<button class="shortcut-key shortcut-dir" data-key="left" type="button">←</button>' +
359
+ '<button class="shortcut-key shortcut-dir" data-key="right" type="button">→</button>' +
360
+ '<span class="shortcut-sep">·</span>' +
361
+ '<button class="shortcut-key" data-key="enter" type="button">↵</button>' +
362
+ '<button class="shortcut-key" data-key="ctrl_enter" type="button">C-↵</button>' +
363
+ '<button class="shortcut-key" data-key="escape" type="button">Esc</button>';
364
+ }
365
+
334
366
  function renderInlineKeyboard() {
335
367
  if (!state.selectedId) return "";
336
368
  var isTerminal = state.currentView === "terminal";
337
369
  if (!isTerminal) return "";
338
- var keys =
339
- '<button class="shortcut-key' + (state.modifiers.ctrl ? ' active' : '') + '" data-key="ctrl" type="button">Ctrl</button>' +
340
- '<button class="shortcut-key' + (state.modifiers.alt ? ' active' : '') + '" data-key="alt" type="button">Alt</button>' +
341
- '<span class="shortcut-sep">·</span>' +
342
- '<button class="shortcut-key shortcut-dir" data-key="up" type="button">↑</button>' +
343
- '<button class="shortcut-key shortcut-dir" data-key="down" type="button">↓</button>' +
344
- '<button class="shortcut-key shortcut-dir" data-key="left" type="button">←</button>' +
345
- '<button class="shortcut-key shortcut-dir" data-key="right" type="button">→</button>' +
346
- '<span class="shortcut-sep">·</span>' +
347
- '<button class="shortcut-key" data-key="enter" type="button">↵</button>' +
348
- '<button class="shortcut-key" data-key="ctrl_enter" type="button">C-↵</button>' +
349
- '<button class="shortcut-key" data-key="escape" type="button">Esc</button>';
370
+ var keys = renderShortcutKeys();
350
371
  var arrow = state.shortcutsExpanded ? '›' : '‹';
351
372
  return '<div class="inline-shortcuts-wrap' + (state.shortcutsExpanded ? ' expanded' : '') + '">' +
352
373
  '<button class="shortcuts-toggle' + (state.shortcutsExpanded ? ' active' : '') + '" type="button" title="快捷键">' + arrow + '</button>' +
@@ -355,9 +376,11 @@
355
376
  '</div>';
356
377
  }
357
378
 
358
- function renderMiniKeyboard() {
359
- // Mini keyboard is now inline, rendered in input-composer-right
360
- return "";
379
+ function renderExpandedShortcutsRow() {
380
+ if (!state.selectedId) return "";
381
+ var isTerminal = state.currentView === "terminal";
382
+ if (!isTerminal) return "";
383
+ return '<div class="inline-shortcuts-expanded-row' + (state.shortcutsExpanded ? ' visible' : '') + '">' + renderShortcutKeys() + '</div>';
361
384
  }
362
385
 
363
386
  function renderLogin() {
@@ -555,6 +578,7 @@
555
578
  '</button>' +
556
579
  '</div>' +
557
580
  '</div>' +
581
+ renderExpandedShortcutsRow() +
558
582
  // Session info bar at bottom
559
583
  '<div class="input-session-info-bar">' +
560
584
  '<span id="session-cwd-display" class="session-cwd-display">' + (selectedSession && selectedSession.cwd ? escapeHtml(selectedSession.cwd) : '未设置目录') + '</span>' +
@@ -631,6 +655,18 @@
631
655
  '</div>' +
632
656
  '<p id="update-message" class="hint hidden"></p>' +
633
657
  '</div>' +
658
+ '<div class="settings-notification-section">' +
659
+ '<div class="settings-section-title">\u901a\u77e5\u72b6\u6001</div>' +
660
+ '<div class="settings-about-row">' +
661
+ '<span class="settings-label">\u6d4f\u89c8\u5668\u901a\u77e5</span>' +
662
+ '<span class="settings-value" id="notification-permission-status">-</span>' +
663
+ '</div>' +
664
+ '<div class="settings-update-actions">' +
665
+ '<button id="notification-request-btn" class="btn btn-ghost btn-sm hidden">\u6388\u6743\u901a\u77e5</button>' +
666
+ '<button id="notification-test-btn" class="btn btn-ghost btn-sm">\u53d1\u9001\u6d4b\u8bd5\u901a\u77e5</button>' +
667
+ '</div>' +
668
+ '<p id="notification-test-message" class="hint hidden"></p>' +
669
+ '</div>' +
634
670
  '</div>' +
635
671
 
636
672
  // General config tab
@@ -1886,6 +1922,16 @@
1886
1922
  if (checkUpdateBtn) checkUpdateBtn.addEventListener("click", checkForUpdate);
1887
1923
  var doUpdateBtn = document.getElementById("do-update-button");
1888
1924
  if (doUpdateBtn) doUpdateBtn.addEventListener("click", performUpdate);
1925
+ // Notification test section
1926
+ var notifRequestBtn = document.getElementById("notification-request-btn");
1927
+ if (notifRequestBtn) notifRequestBtn.addEventListener("click", function() {
1928
+ if (typeof Notification !== "undefined") {
1929
+ Notification.requestPermission().then(function() { updateNotificationStatus(); });
1930
+ }
1931
+ });
1932
+ var notifTestBtn = document.getElementById("notification-test-btn");
1933
+ if (notifTestBtn) notifTestBtn.addEventListener("click", testNotification);
1934
+ updateNotificationStatus();
1889
1935
  var newSessBtn = document.getElementById("topbar-new-session-button");
1890
1936
  if (newSessBtn) newSessBtn.addEventListener("click", openSessionModal);
1891
1937
  var drawerNewSessBtn = document.getElementById("drawer-new-session-button");
@@ -1945,6 +1991,8 @@
1945
1991
  // Inline shortcuts click handler
1946
1992
  var inlineShortcutsWrap = document.querySelector(".inline-shortcuts-wrap");
1947
1993
  if (inlineShortcutsWrap) inlineShortcutsWrap.addEventListener("click", handleInlineKeyboardClick);
1994
+ var expandedShortcutsRow = document.querySelector(".inline-shortcuts-expanded-row");
1995
+ if (expandedShortcutsRow) expandedShortcutsRow.addEventListener("click", handleInlineKeyboardClick);
1948
1996
  // Shortcuts toggle (mobile fold/unfold)
1949
1997
  var shortcutsToggleBtn = document.querySelector(".shortcuts-toggle");
1950
1998
  if (shortcutsToggleBtn) shortcutsToggleBtn.addEventListener("click", function(e) {
@@ -1952,7 +2000,9 @@
1952
2000
  state.shortcutsExpanded = !state.shortcutsExpanded;
1953
2001
  var wrap = document.querySelector(".inline-shortcuts-wrap");
1954
2002
  var toggle = document.querySelector(".shortcuts-toggle");
2003
+ var row = document.querySelector(".inline-shortcuts-expanded-row");
1955
2004
  if (wrap) wrap.classList.toggle("expanded", state.shortcutsExpanded);
2005
+ if (row) row.classList.toggle("visible", state.shortcutsExpanded);
1956
2006
  if (toggle) {
1957
2007
  toggle.classList.toggle("active", state.shortcutsExpanded);
1958
2008
  toggle.textContent = state.shortcutsExpanded ? "\u203a" : "\u2039";
@@ -1962,9 +2012,12 @@
1962
2012
  document.addEventListener("click", function(e) {
1963
2013
  if (!state.shortcutsExpanded) return;
1964
2014
  var wrap = document.querySelector(".inline-shortcuts-wrap");
1965
- if (wrap && !wrap.contains(e.target)) {
2015
+ var expandedRow = document.querySelector(".inline-shortcuts-expanded-row");
2016
+ var clickedInsideRow = expandedRow && expandedRow.contains(e.target);
2017
+ if (wrap && !wrap.contains(e.target) && !clickedInsideRow) {
1966
2018
  state.shortcutsExpanded = false;
1967
2019
  wrap.classList.remove("expanded");
2020
+ if (expandedRow) expandedRow.classList.remove("visible");
1968
2021
  var toggle = document.querySelector(".shortcuts-toggle");
1969
2022
  if (toggle) {
1970
2023
  toggle.classList.remove("active");
@@ -3270,6 +3323,58 @@
3270
3323
  }
3271
3324
  }
3272
3325
 
3326
+ function subscribeToSession(sessionId) {
3327
+ if (!sessionId || !state.ws || state.ws.readyState !== WebSocket.OPEN) return;
3328
+ state.ws.send(JSON.stringify({ type: "subscribe", sessionId: sessionId }));
3329
+ }
3330
+
3331
+ function mergeServerSession(localSession, serverSession) {
3332
+ if (!localSession) return serverSession;
3333
+
3334
+ var merged = Object.assign({}, localSession, serverSession);
3335
+ var localOutput = localSession.output || "";
3336
+ var serverOutput = serverSession.output || "";
3337
+ var keepLocalOutput = localOutput.length > serverOutput.length;
3338
+
3339
+ if (keepLocalOutput) {
3340
+ merged.output = localOutput;
3341
+ }
3342
+
3343
+ if (localSession.id === state.selectedId) {
3344
+ if (localSession.permissionBlocked && serverSession.permissionBlocked === false) {
3345
+ // server explicitly resolved it; keep resolved state
3346
+ } else if (localSession.permissionBlocked && !serverSession.permissionBlocked) {
3347
+ merged.permissionBlocked = true;
3348
+ }
3349
+
3350
+ if (localSession.pendingEscalation && !serverSession.pendingEscalation && serverSession.permissionBlocked !== false) {
3351
+ merged.pendingEscalation = localSession.pendingEscalation;
3352
+ }
3353
+
3354
+ if (localSession.messages && localSession.messages.length > 0 && (!serverSession.messages || serverSession.messages.length === 0)) {
3355
+ merged.messages = localSession.messages;
3356
+ }
3357
+ }
3358
+
3359
+ return merged;
3360
+ }
3361
+
3362
+ function getPreferredMessages(session, fallbackOutput, allowFallback) {
3363
+ if (session && session.messages && session.messages.length > 0) {
3364
+ return session.messages;
3365
+ }
3366
+ if (!allowFallback) {
3367
+ return [];
3368
+ }
3369
+ var output = typeof fallbackOutput === "string"
3370
+ ? fallbackOutput
3371
+ : (session && session.output) || "";
3372
+ if (!output) {
3373
+ return [];
3374
+ }
3375
+ return parseMessages(output, session && session.command);
3376
+ }
3377
+
3273
3378
  function getPreferredSessionId(sessions) {
3274
3379
  if (!sessions || !sessions.length) return null;
3275
3380
  // Keep currently selected session as long as it still exists
@@ -3304,10 +3409,7 @@
3304
3409
 
3305
3410
  state.sessions = serverSessions.map(function(serverSession) {
3306
3411
  var localSession = state.sessions.find(function(s) { return s.id === serverSession.id; });
3307
- if (localSession && localSession.output && localSession.output.length > (serverSession.output || '').length) {
3308
- return localSession;
3309
- }
3310
- return serverSession;
3412
+ return mergeServerSession(localSession, serverSession);
3311
3413
  });
3312
3414
 
3313
3415
  state.selectedId = getPreferredSessionId(state.sessions);
@@ -3431,7 +3533,7 @@
3431
3533
  updateShellChrome();
3432
3534
 
3433
3535
  var selectedSession = state.sessions.find(function(s) { return s.id === id; });
3434
- state.currentMessages = data.messages || [];
3536
+ state.currentMessages = getPreferredMessages(selectedSession, data.output, false);
3435
3537
 
3436
3538
  if (state.terminal) {
3437
3539
  syncTerminalBuffer(id, data.output || "", { mode: "replace" });
@@ -3466,6 +3568,7 @@
3466
3568
  refreshFileExplorer();
3467
3569
  }
3468
3570
  loadOutput(id).then(function() { focusInputBox(true); });
3571
+ subscribeToSession(id);
3469
3572
  }
3470
3573
 
3471
3574
  function updateDrawerState() {
@@ -3913,6 +4016,101 @@
3913
4016
  });
3914
4017
  }
3915
4018
 
4019
+ // ── Notification Settings Helpers ──
4020
+
4021
+ function updateNotificationStatus() {
4022
+ var statusEl = document.getElementById("notification-permission-status");
4023
+ var requestBtn = document.getElementById("notification-request-btn");
4024
+ var testMsgEl = document.getElementById("notification-test-message");
4025
+ if (!statusEl) return;
4026
+
4027
+ if (typeof Notification === "undefined") {
4028
+ statusEl.textContent = "\u4e0d\u652f\u6301";
4029
+ statusEl.style.color = "var(--fg-muted)";
4030
+ if (requestBtn) requestBtn.classList.add("hidden");
4031
+ return;
4032
+ }
4033
+
4034
+ var perm = Notification.permission;
4035
+ if (perm === "granted") {
4036
+ statusEl.textContent = "\u5df2\u6388\u6743 \u2713";
4037
+ statusEl.style.color = "var(--success)";
4038
+ if (requestBtn) requestBtn.classList.add("hidden");
4039
+ } else if (perm === "denied") {
4040
+ statusEl.textContent = "\u5df2\u62d2\u7edd";
4041
+ statusEl.style.color = "var(--danger)";
4042
+ if (requestBtn) requestBtn.classList.add("hidden");
4043
+ if (testMsgEl) {
4044
+ 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";
4045
+ testMsgEl.style.color = "var(--fg-muted)";
4046
+ testMsgEl.classList.remove("hidden");
4047
+ }
4048
+ } else {
4049
+ statusEl.textContent = "\u672a\u6388\u6743";
4050
+ statusEl.style.color = "var(--warning)";
4051
+ if (requestBtn) requestBtn.classList.remove("hidden");
4052
+ }
4053
+ }
4054
+
4055
+ function testNotification() {
4056
+ var testMsgEl = document.getElementById("notification-test-message");
4057
+
4058
+ // Always show in-app bubble
4059
+ showNotificationBubble({
4060
+ title: "\u6d4b\u8bd5\u901a\u77e5",
4061
+ body: "\u8fd9\u662f\u4e00\u6761\u6d4b\u8bd5\u901a\u77e5\uff0c\u5e94\u7528\u5185\u6c14\u6ce1\u5df2\u6b63\u5e38\u5de5\u4f5c\u3002",
4062
+ type: "info",
4063
+ icon: "\u266a",
4064
+ duration: 5000,
4065
+ });
4066
+
4067
+ // Test browser notification
4068
+ if (typeof Notification === "undefined") {
4069
+ if (testMsgEl) {
4070
+ testMsgEl.textContent = "\u6d4f\u89c8\u5668\u4e0d\u652f\u6301\u901a\u77e5 API\uff0c\u4ec5\u53ef\u4f7f\u7528\u5e94\u7528\u5185\u6c14\u6ce1\u901a\u77e5\u3002";
4071
+ testMsgEl.style.color = "var(--fg-muted)";
4072
+ testMsgEl.classList.remove("hidden");
4073
+ }
4074
+ return;
4075
+ }
4076
+
4077
+ if (Notification.permission === "granted") {
4078
+ try {
4079
+ var n = new Notification("Wand \u6d4b\u8bd5\u901a\u77e5", {
4080
+ body: "\u6d4f\u89c8\u5668\u901a\u77e5\u5df2\u6b63\u5e38\u5de5\u4f5c\u3002",
4081
+ icon: "/favicon.ico",
4082
+ tag: "wand-test",
4083
+ });
4084
+ setTimeout(function() { n.close(); }, 5000);
4085
+ if (testMsgEl) {
4086
+ testMsgEl.textContent = "\u2713 \u6d4f\u89c8\u5668\u901a\u77e5 + \u5e94\u7528\u5185\u6c14\u6ce1\u5747\u5df2\u53d1\u9001";
4087
+ testMsgEl.style.color = "var(--success)";
4088
+ testMsgEl.classList.remove("hidden");
4089
+ }
4090
+ } catch (_e) {
4091
+ if (testMsgEl) {
4092
+ testMsgEl.textContent = "\u6d4f\u89c8\u5668\u901a\u77e5\u53d1\u9001\u5931\u8d25\uff0c\u53ef\u80fd\u9700\u8981 HTTPS";
4093
+ testMsgEl.style.color = "var(--warning)";
4094
+ testMsgEl.classList.remove("hidden");
4095
+ }
4096
+ }
4097
+ } else {
4098
+ // permission is "default" or "denied" — always try requesting
4099
+ Notification.requestPermission().then(function(result) {
4100
+ updateNotificationStatus();
4101
+ if (result === "granted") {
4102
+ testNotification();
4103
+ } else if (result === "denied") {
4104
+ if (testMsgEl) {
4105
+ 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";
4106
+ testMsgEl.style.color = "var(--fg-muted)";
4107
+ testMsgEl.classList.remove("hidden");
4108
+ }
4109
+ }
4110
+ });
4111
+ }
4112
+ }
4113
+
3916
4114
  function quickStartSession() {
3917
4115
  var command = getPreferredTool();
3918
4116
  var defaultCwd = getEffectiveCwd();
@@ -4418,9 +4616,7 @@
4418
4616
  switchToSessionView(data.id);
4419
4617
  updateSessionSnapshot(data);
4420
4618
  updateSessionsList();
4421
- if (state.ws && state.ws.readyState === WebSocket.OPEN) {
4422
- state.ws.send(JSON.stringify({ type: "subscribe", sessionId: data.id }));
4423
- }
4619
+ subscribeToSession(data.id);
4424
4620
  loadOutput(data.id).then(function() {
4425
4621
  focusInputBox(true);
4426
4622
  });
@@ -4481,9 +4677,7 @@
4481
4677
  updateSessionSnapshot(data);
4482
4678
  updateSessionsList();
4483
4679
  // Subscribe to new session via WebSocket
4484
- if (state.ws && state.ws.readyState === WebSocket.OPEN) {
4485
- state.ws.send(JSON.stringify({ type: 'subscribe', sessionId: data.id }));
4486
- }
4680
+ subscribeToSession(data.id);
4487
4681
  return loadOutput(data.id);
4488
4682
  })
4489
4683
  .catch(function(error) {
@@ -4656,9 +4850,7 @@
4656
4850
  updateSessionSnapshot(data);
4657
4851
  updateSessionsList();
4658
4852
  switchToSessionView(data.id);
4659
- if (state.ws && state.ws.readyState === WebSocket.OPEN) {
4660
- state.ws.send(JSON.stringify({ type: "subscribe", sessionId: data.id }));
4661
- }
4853
+ subscribeToSession(data.id);
4662
4854
  return loadOutput(data.id).then(function() {
4663
4855
  focusInputBox(true);
4664
4856
  return data;
@@ -5371,9 +5563,7 @@
5371
5563
  switchToSessionView(data.id);
5372
5564
  updateSessionSnapshot(data);
5373
5565
  updateSessionsList();
5374
- if (state.ws && state.ws.readyState === WebSocket.OPEN) {
5375
- state.ws.send(JSON.stringify({ type: "subscribe", sessionId: data.id }));
5376
- }
5566
+ subscribeToSession(data.id);
5377
5567
  return loadOutput(data.id).then(function() {
5378
5568
  focusInputBox(true);
5379
5569
  });
@@ -6587,9 +6777,7 @@
6587
6777
  state.ws = ws;
6588
6778
  state.wsConnected = true;
6589
6779
  // Subscribe to current session if any
6590
- if (state.selectedId) {
6591
- ws.send(JSON.stringify({ type: 'subscribe', sessionId: state.selectedId }));
6592
- }
6780
+ subscribeToSession(state.selectedId);
6593
6781
  // Flush pending messages after reconnection
6594
6782
  flushPendingMessages();
6595
6783
  };
@@ -6639,9 +6827,7 @@
6639
6827
  }
6640
6828
  updateSessionSnapshot(snapshot);
6641
6829
  if (msg.sessionId === state.selectedId) {
6642
- if (msg.data.messages) {
6643
- state.currentMessages = msg.data.messages;
6644
- }
6830
+ state.currentMessages = getPreferredMessages(snapshot, msg.data.output, false);
6645
6831
  updateTaskDisplay();
6646
6832
  scheduleChatRender();
6647
6833
  }
@@ -6681,6 +6867,33 @@
6681
6867
  }
6682
6868
  updateSessionSnapshot(endedSnapshot);
6683
6869
 
6870
+ // Notify user when a session completes (browser + in-app if backgrounded or not viewing)
6871
+ var endedSession = state.sessions.find(function(s) { return s.id === msg.sessionId; });
6872
+ var endedName = endedSession ? (endedSession.label || endedSession.command || msg.sessionId) : msg.sessionId;
6873
+ var endedExitCode = msg.data && msg.data.exitCode;
6874
+ var endedIsError = endedExitCode !== null && endedExitCode !== undefined && endedExitCode !== 0;
6875
+ sendBrowserNotification(
6876
+ endedIsError ? "\u4f1a\u8bdd\u5f02\u5e38\u7ed3\u675f" : "\u4f1a\u8bdd\u5df2\u5b8c\u6210",
6877
+ endedName,
6878
+ {
6879
+ tag: "wand-ended-" + msg.sessionId,
6880
+ onClick: function() {
6881
+ if (msg.sessionId !== state.selectedId) selectSession(msg.sessionId);
6882
+ }
6883
+ }
6884
+ );
6885
+ if (msg.sessionId !== state.selectedId || document.hidden) {
6886
+ showNotificationBubble({
6887
+ title: endedIsError ? "\u4f1a\u8bdd\u5f02\u5e38\u7ed3\u675f" : "\u4f1a\u8bdd\u5df2\u5b8c\u6210",
6888
+ body: endedName,
6889
+ type: endedIsError ? "warning" : "success",
6890
+ icon: endedIsError ? "!" : "\u2713",
6891
+ duration: 6000,
6892
+ actionLabel: "\u67e5\u770b",
6893
+ action: function() { selectSession(msg.sessionId); }
6894
+ });
6895
+ }
6896
+
6684
6897
  // Clear stale queued inputs so they cannot race with the ended session.
6685
6898
  // Each queued item's postInput will hit the server and get an error, but
6686
6899
  // clearing the queues here prevents them from growing unbounded.
@@ -6738,6 +6951,35 @@
6738
6951
  target: msg.data.permissionRequest.target,
6739
6952
  reason: msg.data.permissionRequest.prompt
6740
6953
  };
6954
+ // Browser notification for permission waiting (background tab)
6955
+ var permSession = state.sessions.find(function(s) { return s.id === msg.sessionId; });
6956
+ var permSessionName = permSession ? (permSession.label || permSession.command || msg.sessionId) : msg.sessionId;
6957
+ sendBrowserNotification(
6958
+ "\u4f1a\u8bdd\u7b49\u5f85\u6388\u6743",
6959
+ permSessionName + " \u2014 " + (msg.data.permissionRequest.prompt || "\u9700\u8981\u6743\u9650\u5ba1\u6279"),
6960
+ {
6961
+ tag: "wand-perm-" + msg.sessionId,
6962
+ onClick: function() {
6963
+ if (msg.sessionId !== state.selectedId) {
6964
+ selectSession(msg.sessionId);
6965
+ }
6966
+ }
6967
+ }
6968
+ );
6969
+ // In-app bubble if not currently viewing this session
6970
+ if (msg.sessionId !== state.selectedId) {
6971
+ showNotificationBubble({
6972
+ title: "\u4f1a\u8bdd\u7b49\u5f85\u6388\u6743",
6973
+ body: permSessionName + " \u2014 " + (msg.data.permissionRequest.prompt || "\u9700\u8981\u6743\u9650\u5ba1\u6279"),
6974
+ type: "warning",
6975
+ icon: "!",
6976
+ duration: 0,
6977
+ actionLabel: "\u67e5\u770b",
6978
+ action: function() {
6979
+ selectSession(msg.sessionId);
6980
+ }
6981
+ });
6982
+ }
6741
6983
  }
6742
6984
  if (msg.data.permissionBlocked === false) {
6743
6985
  statusUpdate.pendingEscalation = null;
@@ -6748,6 +6990,29 @@
6748
6990
  }
6749
6991
  }
6750
6992
  break;
6993
+ case 'notification':
6994
+ if (msg.data) {
6995
+ if (msg.data.kind === "update") {
6996
+ showNotificationBubble({
6997
+ title: "\u53d1\u73b0\u65b0\u7248\u672c",
6998
+ body: "\u5f53\u524d " + (msg.data.current || "-") + " \u2192 \u6700\u65b0 " + (msg.data.latest || "-"),
6999
+ type: "info",
7000
+ icon: "\u2191",
7001
+ duration: 0,
7002
+ actionLabel: "\u53bb\u66f4\u65b0",
7003
+ action: function() {
7004
+ var settingsBtn = document.getElementById("open-settings-btn") || document.querySelector("[data-action='settings']");
7005
+ if (settingsBtn) settingsBtn.click();
7006
+ }
7007
+ });
7008
+ sendBrowserNotification(
7009
+ "Wand \u53d1\u73b0\u65b0\u7248\u672c",
7010
+ "\u5f53\u524d " + (msg.data.current || "-") + " \u2192 \u6700\u65b0 " + (msg.data.latest || "-"),
7011
+ { tag: "wand-update" }
7012
+ );
7013
+ }
7014
+ }
7015
+ break;
6751
7016
  }
6752
7017
  }
6753
7018
 
@@ -6905,12 +7170,7 @@
6905
7170
  // Re-parse messages from the latest session output (fallback for edge cases)
6906
7171
  var selectedSession = state.sessions.find(function(s) { return s.id === state.selectedId; });
6907
7172
  if (selectedSession) {
6908
- // Prefer structured messages from JSON chat mode
6909
- if (selectedSession.messages && selectedSession.messages.length > 0) {
6910
- state.currentMessages = selectedSession.messages;
6911
- } else if (selectedSession.output) {
6912
- state.currentMessages = parseMessages(selectedSession.output, selectedSession.command);
6913
- }
7173
+ state.currentMessages = getPreferredMessages(selectedSession, selectedSession.output, true);
6914
7174
  }
6915
7175
  renderChat();
6916
7176
  }, 30);
@@ -8558,6 +8818,136 @@
8558
8818
  }, type === "error" ? 4000 : 2200);
8559
8819
  }
8560
8820
 
8821
+ // ── Notification Bubble System ──
8822
+
8823
+ var notificationStack = [];
8824
+ var notificationIdCounter = 0;
8825
+ var NOTIFICATION_GAP = 8;
8826
+ var NOTIFICATION_TOP = 24;
8827
+
8828
+ /**
8829
+ * Show an in-app notification bubble at bottom-right.
8830
+ * @param {object} opts
8831
+ * @param {string} opts.title - Notification title
8832
+ * @param {string} [opts.body] - Body text
8833
+ * @param {string} [opts.type] - "info" | "warning" | "success" (default "info")
8834
+ * @param {string} [opts.icon] - Icon character (default derived from type)
8835
+ * @param {number} [opts.duration] - Auto-dismiss ms, 0 = manual only (default 8000)
8836
+ * @param {string} [opts.actionLabel] - Action button label
8837
+ * @param {function} [opts.action] - Action button callback
8838
+ * @returns {{ dismiss: function }} handle
8839
+ */
8840
+ function showNotificationBubble(opts) {
8841
+ var id = ++notificationIdCounter;
8842
+ var type = opts.type || "info";
8843
+ var icon = opts.icon || (type === "warning" ? "!" : type === "success" ? "\u2713" : "i");
8844
+ var duration = opts.duration !== undefined ? opts.duration : 8000;
8845
+
8846
+ var bubble = document.createElement("div");
8847
+ bubble.className = "notification-bubble";
8848
+ bubble.setAttribute("data-nid", id);
8849
+
8850
+ var headerHtml =
8851
+ '<div class="notification-bubble-header">' +
8852
+ '<span class="notification-bubble-icon ' + type + '">' + icon + '</span>' +
8853
+ '<span class="notification-bubble-title">' + escapeHtml(opts.title) + '</span>' +
8854
+ '<button class="notification-bubble-close" title="\u5173\u95ed">\u00d7</button>' +
8855
+ '</div>';
8856
+
8857
+ var bodyHtml = opts.body
8858
+ ? '<div class="notification-bubble-body">' + escapeHtml(opts.body) + '</div>'
8859
+ : '';
8860
+
8861
+ var actionsHtml = opts.actionLabel
8862
+ ? '<div class="notification-bubble-actions">' +
8863
+ '<button class="primary">' + escapeHtml(opts.actionLabel) + '</button>' +
8864
+ '</div>'
8865
+ : '';
8866
+
8867
+ bubble.innerHTML = headerHtml + bodyHtml + actionsHtml;
8868
+ document.body.appendChild(bubble);
8869
+
8870
+ // Stack position
8871
+ var entry = { id: id, el: bubble };
8872
+ notificationStack.push(entry);
8873
+ repositionNotifications();
8874
+
8875
+ // Wire close button
8876
+ var closeBtn = bubble.querySelector(".notification-bubble-close");
8877
+ if (closeBtn) closeBtn.onclick = function() { dismissNotification(id); };
8878
+
8879
+ // Wire action button
8880
+ if (opts.actionLabel && opts.action) {
8881
+ var actionBtn = bubble.querySelector(".notification-bubble-actions button");
8882
+ if (actionBtn) actionBtn.onclick = function() {
8883
+ opts.action();
8884
+ dismissNotification(id);
8885
+ };
8886
+ }
8887
+
8888
+ // Auto-dismiss
8889
+ var timer = null;
8890
+ if (duration > 0) {
8891
+ timer = setTimeout(function() { dismissNotification(id); }, duration);
8892
+ }
8893
+
8894
+ return {
8895
+ dismiss: function() { dismissNotification(id); }
8896
+ };
8897
+ }
8898
+
8899
+ function dismissNotification(id) {
8900
+ var idx = -1;
8901
+ for (var i = 0; i < notificationStack.length; i++) {
8902
+ if (notificationStack[i].id === id) { idx = i; break; }
8903
+ }
8904
+ if (idx === -1) return;
8905
+ var entry = notificationStack[idx];
8906
+ entry.el.classList.add("slide-out");
8907
+ notificationStack.splice(idx, 1);
8908
+ repositionNotifications();
8909
+ setTimeout(function() {
8910
+ if (entry.el.parentNode) entry.el.parentNode.removeChild(entry.el);
8911
+ }, 300);
8912
+ }
8913
+
8914
+ function repositionNotifications() {
8915
+ var top = NOTIFICATION_TOP;
8916
+ for (var i = 0; i < notificationStack.length; i++) {
8917
+ notificationStack[i].el.style.top = top + "px";
8918
+ top += notificationStack[i].el.offsetHeight + NOTIFICATION_GAP;
8919
+ }
8920
+ }
8921
+
8922
+ // ── Browser Notification API ──
8923
+
8924
+ function requestNotificationPermission() {
8925
+ if (typeof Notification !== "undefined" && Notification.permission === "default") {
8926
+ Notification.requestPermission();
8927
+ }
8928
+ }
8929
+
8930
+ function sendBrowserNotification(title, body, opts) {
8931
+ if (typeof Notification === "undefined" || Notification.permission !== "granted") return;
8932
+ if (!document.hidden) return; // Only notify when tab is in background
8933
+ try {
8934
+ var n = new Notification(title, {
8935
+ body: body || "",
8936
+ icon: (opts && opts.icon) || "/favicon.ico",
8937
+ tag: (opts && opts.tag) || undefined,
8938
+ });
8939
+ n.onclick = function() {
8940
+ window.focus();
8941
+ n.close();
8942
+ if (opts && opts.onClick) opts.onClick();
8943
+ };
8944
+ // Auto-close after 10s
8945
+ setTimeout(function() { n.close(); }, 10000);
8946
+ } catch (_e) {
8947
+ // Notification constructor may fail in some contexts (e.g. insecure origin)
8948
+ }
8949
+ }
8950
+
8561
8951
  function escapeHtml(value) {
8562
8952
  return String(value)
8563
8953
  .replace(/&/g, "&amp;")