@co0ontty/wand 1.1.5 → 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.
@@ -435,12 +435,11 @@ export class ClaudePtyBridge extends EventEmitter {
435
435
  /\bgrant\b.*\bpermission\b/i.test(normalized) ||
436
436
  /\bhaven't granted\b/i.test(normalized) ||
437
437
  /\benter to confirm\b/i.test(normalized) ||
438
- /\bwould you like to proceed\b/i.test(normalized) ||
439
- /❯/.test(normalized));
438
+ /\bwould you like to proceed\b/i.test(normalized));
440
439
  }
441
440
  extractPromptText(normalized) {
442
441
  // Return a snippet around the permission prompt
443
- const match = normalized.match(/.{0,100}(?:do you want to|permission|grant|enter to confirm|would you like to proceed|❯).{0,100}/i);
442
+ const match = normalized.match(/.{0,100}(?:do you want to|permission|grant|enter to confirm|would you like to proceed).{0,100}/i);
444
443
  return match?.[0] ?? normalized.slice(-100);
445
444
  }
446
445
  extractPermissionTarget(normalized) {
@@ -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
  }
@@ -40,8 +40,7 @@ const PROMPT_PATTERNS = [
40
40
  /\bwould you like to\b/i,
41
41
  /\bshall i\b/i,
42
42
  /\bcan i\b/i,
43
- /\bgrant\b.*\bpermission\b/i,
44
- /❯/
43
+ /\bgrant\b.*\bpermission\b/i
45
44
  ];
46
45
  const REAL_CONVERSATION_MIN_LINES = 2;
47
46
  const REAL_CONVERSATION_MIN_MESSAGES = 2;
@@ -1143,16 +1142,14 @@ export class ProcessManager extends EventEmitter {
1143
1142
  continue;
1144
1143
  const jsonlPath = path.join(getClaudeProjectDir(cwd), `${claudeSessionId}.jsonl`);
1145
1144
  try {
1146
- if (existsSync(jsonlPath)) {
1147
- unlinkSync(jsonlPath);
1148
- deleted++;
1149
- }
1145
+ unlinkSync(jsonlPath);
1146
+ deleted++;
1150
1147
  }
1151
1148
  catch {
1152
- // Best-effort — Claude cache cleanup is non-critical
1149
+ // Best-effort — file may already be gone
1153
1150
  }
1154
1151
  }
1155
- if (deleted > 0) {
1152
+ if (sessions.length > 0) {
1156
1153
  this.claudeHistoryCache = null;
1157
1154
  }
1158
1155
  return deleted;
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
  }
@@ -165,6 +165,14 @@
165
165
  }
166
166
  }
167
167
 
168
+ function getConfigCwd() {
169
+ return (state.config && state.config.defaultCwd) || "/tmp";
170
+ }
171
+
172
+ function getEffectiveCwd() {
173
+ return state.workingDir || getConfigCwd();
174
+ }
175
+
168
176
  // PWA install prompt handling
169
177
  window.addEventListener('beforeinstallprompt', function(e) {
170
178
  e.preventDefault();
@@ -249,6 +257,24 @@
249
257
  }
250
258
  startPolling();
251
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
+ }
252
278
  // Auto-load claude history since section defaults to expanded
253
279
  if (state.claudeHistoryExpanded && !state.claudeHistoryLoaded) {
254
280
  loadClaudeHistory();
@@ -464,14 +490,14 @@
464
490
  '</div>' +
465
491
  '<div class="file-side-panel-body">' +
466
492
  '<div class="file-explorer-header">' +
467
- '<span class="file-explorer-path" id="file-explorer-cwd">' + escapeHtml(selectedSession && selectedSession.cwd ? selectedSession.cwd : (state.config && state.config.defaultCwd ? state.config.defaultCwd : "")) + '</span>' +
493
+ '<span class="file-explorer-path" id="file-explorer-cwd">' + escapeHtml(selectedSession && selectedSession.cwd ? selectedSession.cwd : getConfigCwd()) + '</span>' +
468
494
  '<button class="file-explorer-refresh" id="file-explorer-refresh" title="刷新" aria-label="刷新文件列表">↻</button>' +
469
495
  '</div>' +
470
496
  '<div class="file-search-box">' +
471
497
  '<input type="text" id="file-search-input" class="file-search-input" placeholder="搜索文件..." autocomplete="off" />' +
472
498
  '<button class="file-search-clear" id="file-search-clear" type="button" aria-label="清除搜索">×</button>' +
473
499
  '</div>' +
474
- '<div class="file-explorer" id="file-explorer">' + renderFileExplorer(selectedSession && selectedSession.cwd ? selectedSession.cwd : (state.config && state.config.defaultCwd ? state.config.defaultCwd : "")) + '</div>' +
500
+ '<div class="file-explorer" id="file-explorer">' + renderFileExplorer(selectedSession && selectedSession.cwd ? selectedSession.cwd : getConfigCwd()) + '</div>' +
475
501
  '</div>' +
476
502
  '</div>' +
477
503
  '<div id="output" class="terminal-container' + (state.selectedId ? "" : " hidden") + ' active">' +
@@ -498,7 +524,7 @@
498
524
  '<div class="blank-chat-cwd-wrap">' +
499
525
  '<div class="blank-chat-cwd" id="blank-chat-cwd" role="button" tabindex="0" title="点击切换工作目录">' +
500
526
  '<span class="blank-chat-cwd-icon">📁</span>' +
501
- '<span class="blank-chat-cwd-path" id="blank-chat-cwd-path">' + escapeHtml(state.workingDir || (state.config && state.config.defaultCwd ? state.config.defaultCwd : "/tmp")) + '</span>' +
527
+ '<span class="blank-chat-cwd-path" id="blank-chat-cwd-path">' + escapeHtml(getEffectiveCwd()) + '</span>' +
502
528
  '<span class="blank-chat-cwd-arrow" id="blank-chat-cwd-arrow">▼</span>' +
503
529
  '</div>' +
504
530
  '<div class="blank-chat-cwd-dropdown hidden" id="blank-chat-cwd-dropdown"></div>' +
@@ -623,6 +649,18 @@
623
649
  '</div>' +
624
650
  '<p id="update-message" class="hint hidden"></p>' +
625
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>' +
626
664
  '</div>' +
627
665
 
628
666
  // General config tab
@@ -1103,7 +1141,7 @@
1103
1141
  function updateFilePanelCwd(session) {
1104
1142
  var cwdEl = document.getElementById("file-explorer-cwd");
1105
1143
  if (!cwdEl) return;
1106
- var cwd = session && session.cwd ? session.cwd : (state.config && state.config.defaultCwd ? state.config.defaultCwd : "");
1144
+ var cwd = session && session.cwd ? session.cwd : getConfigCwd();
1107
1145
  cwdEl.textContent = cwd;
1108
1146
  }
1109
1147
 
@@ -1142,7 +1180,7 @@
1142
1180
  }
1143
1181
 
1144
1182
  function renderFileExplorer(cwd) {
1145
- var root = cwd || (state.config && state.config.defaultCwd) || "";
1183
+ var root = cwd || getConfigCwd();
1146
1184
  if (!root) {
1147
1185
  return '<div class="file-explorer empty">No working directory configured.</div>';
1148
1186
  }
@@ -1161,8 +1199,8 @@
1161
1199
  var session = state.sessions.find(function(s) { return s.id === state.selectedId; });
1162
1200
  if (session) cwd = session.cwd || "";
1163
1201
  }
1164
- if (!cwd && state.config && state.config.defaultCwd) {
1165
- cwd = state.config.defaultCwd;
1202
+ if (!cwd) {
1203
+ cwd = getConfigCwd();
1166
1204
  }
1167
1205
  if (!cwd) {
1168
1206
  explorer.innerHTML = '<div class="file-explorer empty">No working directory.</div>';
@@ -1504,7 +1542,7 @@
1504
1542
  }
1505
1543
 
1506
1544
  function renderFolderPicker(state) {
1507
- var currentDir = state.workingDir || (state.config && state.config.defaultCwd ? state.config.defaultCwd : "/tmp");
1545
+ var currentDir = getEffectiveCwd();
1508
1546
 
1509
1547
  // 如果有选中的会话,不显示单独的工作目录标签(已嵌入输入框内部)
1510
1548
  if (state.selectedId) {
@@ -1530,7 +1568,7 @@
1530
1568
 
1531
1569
  // 渲染内嵌到输入框的工作目录指示器
1532
1570
  function renderWorkingDirIndicator(state) {
1533
- var currentDir = state.workingDir || (state.config && state.config.defaultCwd ? state.config.defaultCwd : "/tmp");
1571
+ var currentDir = getEffectiveCwd();
1534
1572
  var displayDir = currentDir;
1535
1573
 
1536
1574
  // 如果有选中的会话,使用会话的工作目录
@@ -1653,7 +1691,7 @@
1653
1691
  '<div class="field">' +
1654
1692
  '<label class="field-label" for="cwd">工作目录</label>' +
1655
1693
  '<div class="suggestions-wrap">' +
1656
- '<input id="cwd" type="text" class="field-input" autocomplete="off" placeholder="' + escapeHtml(state.workingDir || (state.config && state.config.defaultCwd ? state.config.defaultCwd : "/tmp")) + '" />' +
1694
+ '<input id="cwd" type="text" class="field-input" autocomplete="off" placeholder="' + escapeHtml(getEffectiveCwd()) + '" />' +
1657
1695
  '<div id="cwd-suggestions" class="suggestions hidden"></div>' +
1658
1696
  '</div>' +
1659
1697
  '<p class="field-hint">留空则使用上方目录,支持路径自动补全。</p>' +
@@ -1878,6 +1916,16 @@
1878
1916
  if (checkUpdateBtn) checkUpdateBtn.addEventListener("click", checkForUpdate);
1879
1917
  var doUpdateBtn = document.getElementById("do-update-button");
1880
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();
1881
1929
  var newSessBtn = document.getElementById("topbar-new-session-button");
1882
1930
  if (newSessBtn) newSessBtn.addEventListener("click", openSessionModal);
1883
1931
  var drawerNewSessBtn = document.getElementById("drawer-new-session-button");
@@ -2221,7 +2269,7 @@
2221
2269
 
2222
2270
  if (folderPickerInput) {
2223
2271
  // Load initial folders from saved or default path
2224
- var initialPath = state.workingDir || (state.config && state.config.defaultCwd ? state.config.defaultCwd : "/tmp");
2272
+ var initialPath = getEffectiveCwd();
2225
2273
  loadFolderSuggestions(initialPath);
2226
2274
 
2227
2275
  folderPickerInput.addEventListener("focus", function() {
@@ -2397,16 +2445,14 @@
2397
2445
  folderPickerModal.classList.remove("hidden");
2398
2446
  // Set initial path in input
2399
2447
  if (folderPickerInput) {
2400
- folderPickerInput.value = state.workingDir || (state.config && state.config.defaultCwd ? state.config.defaultCwd : "/tmp");
2448
+ folderPickerInput.value = getEffectiveCwd();
2401
2449
  }
2402
2450
  // Load initial folders
2403
- var initialPath = state.workingDir || (state.config && state.config.defaultCwd ? state.config.defaultCwd : "/tmp");
2451
+ var initialPath = getEffectiveCwd();
2404
2452
  loadFolderSuggestions(initialPath);
2405
2453
  renderBreadcrumb(initialPath);
2406
2454
  }
2407
2455
 
2408
- // Welcome screen folder button (legacy, now handled by initBlankChatCwd)
2409
-
2410
2456
  if (closeFolderPicker && folderPickerModal) {
2411
2457
  closeFolderPicker.addEventListener("click", function() {
2412
2458
  folderPickerModal.classList.add("hidden");
@@ -2426,6 +2472,16 @@
2426
2472
  setupVisualViewportHandlers();
2427
2473
  }
2428
2474
 
2475
+ function activateSessionItem(sessionId) {
2476
+ var session = state.sessions.find(function(s) { return s.id === sessionId; });
2477
+ if (session && session.status !== "running") {
2478
+ resumeSessionFromList(sessionId);
2479
+ } else {
2480
+ selectSession(sessionId);
2481
+ }
2482
+ closeSessionsDrawer();
2483
+ }
2484
+
2429
2485
  function handleSessionItemClick(event) {
2430
2486
  var target = event.target;
2431
2487
  if (!target || !(target instanceof Element)) return;
@@ -2502,13 +2558,22 @@
2502
2558
  }
2503
2559
  if (_swipeState) return;
2504
2560
  if (item.dataset.sessionId) {
2505
- var clickedSession = state.sessions.find(function(s) { return s.id === item.dataset.sessionId; });
2506
- if (clickedSession && clickedSession.status !== "running") {
2507
- resumeSessionFromList(item.dataset.sessionId);
2508
- } else {
2509
- selectSession(item.dataset.sessionId);
2510
- }
2511
- closeSessionsDrawer();
2561
+ activateSessionItem(item.dataset.sessionId);
2562
+ } else if (item.dataset.claudeHistoryId) {
2563
+ var claudeSessionId = item.dataset.claudeHistoryId;
2564
+ var cwd = item.dataset.cwd;
2565
+ resumeClaudeHistorySession(claudeSessionId, cwd)
2566
+ .then(function(data) {
2567
+ if (data && data.id) {
2568
+ state.selectedId = data.id;
2569
+ persistSelectedId();
2570
+ state.drafts[data.id] = "";
2571
+ loadSessions().then(function() {
2572
+ selectSession(data.id);
2573
+ closeSessionsDrawer();
2574
+ });
2575
+ }
2576
+ });
2512
2577
  }
2513
2578
  }
2514
2579
  }
@@ -2527,13 +2592,22 @@
2527
2592
  return;
2528
2593
  }
2529
2594
  if (item.dataset.sessionId) {
2530
- var keySession = state.sessions.find(function(s) { return s.id === item.dataset.sessionId; });
2531
- if (keySession && keySession.status !== "running") {
2532
- resumeSessionFromList(item.dataset.sessionId);
2533
- } else {
2534
- selectSession(item.dataset.sessionId);
2535
- }
2536
- closeSessionsDrawer();
2595
+ activateSessionItem(item.dataset.sessionId);
2596
+ } else if (item.dataset.claudeHistoryId) {
2597
+ var claudeSessionId = item.dataset.claudeHistoryId;
2598
+ var cwd = item.dataset.cwd;
2599
+ resumeClaudeHistorySession(claudeSessionId, cwd)
2600
+ .then(function(data) {
2601
+ if (data && data.id) {
2602
+ state.selectedId = data.id;
2603
+ persistSelectedId();
2604
+ state.drafts[data.id] = "";
2605
+ loadSessions().then(function() {
2606
+ selectSession(data.id);
2607
+ closeSessionsDrawer();
2608
+ });
2609
+ }
2610
+ });
2537
2611
  }
2538
2612
  }
2539
2613
 
@@ -2968,15 +3042,9 @@
2968
3042
  state.terminalViewportSize = { width: 0, height: 0 };
2969
3043
  state.terminalAutoFollow = true;
2970
3044
  clearTerminalScrollIdleTimer();
2971
- // Double-rAF: wait for browser to complete layout before measuring and fitting
3045
+ // Retry-based fit: wait for browser to complete layout before measuring and fitting
2972
3046
  if (state.fitAddon) {
2973
- requestAnimationFrame(function() {
2974
- requestAnimationFrame(function() {
2975
- if (state.fitAddon && shouldResizeTerminalViewport()) {
2976
- state.fitAddon.fit();
2977
- }
2978
- });
2979
- });
3047
+ ensureTerminalFit();
2980
3048
  }
2981
3049
 
2982
3050
  var viewport = getTerminalViewport();
@@ -3403,11 +3471,7 @@
3403
3471
  updateShellChrome();
3404
3472
 
3405
3473
  var selectedSession = state.sessions.find(function(s) { return s.id === id; });
3406
- if (data.messages && data.messages.length > 0) {
3407
- state.currentMessages = data.messages;
3408
- } else {
3409
- state.currentMessages = [];
3410
- }
3474
+ state.currentMessages = data.messages || [];
3411
3475
 
3412
3476
  if (state.terminal) {
3413
3477
  syncTerminalBuffer(id, data.output || "", { mode: "replace" });
@@ -3889,9 +3953,104 @@
3889
3953
  });
3890
3954
  }
3891
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
+
3892
4051
  function quickStartSession() {
3893
4052
  var command = getPreferredTool();
3894
- var defaultCwd = state.workingDir || (state.config && state.config.defaultCwd ? state.config.defaultCwd : "");
4053
+ var defaultCwd = getEffectiveCwd();
3895
4054
  var defaultMode = (state.config && state.config.defaultMode) ? state.config.defaultMode : "default";
3896
4055
  state.preferredCommand = command;
3897
4056
  state.chatMode = getSafeModeForTool(command, state.chatMode);
@@ -3928,7 +4087,7 @@
3928
4087
 
3929
4088
  hideError(errorEl);
3930
4089
 
3931
- var defaultCwd = state.workingDir || (state.config && state.config.defaultCwd ? state.config.defaultCwd : "");
4090
+ var defaultCwd = getEffectiveCwd();
3932
4091
  var selectedMode = getSafeModeForTool(command, state.modeValue);
3933
4092
  state.modeValue = selectedMode;
3934
4093
  state.chatMode = selectedMode;
@@ -4006,7 +4165,7 @@
4006
4165
  }
4007
4166
 
4008
4167
  function loadBlankChatCwdDropdown(dropdown) {
4009
- var defaultCwd = state.config && state.config.defaultCwd ? state.config.defaultCwd : "/tmp";
4168
+ var defaultCwd = getConfigCwd();
4010
4169
  dropdown.innerHTML = '<div class="blank-chat-cwd-loading">加载中...</div>';
4011
4170
  fetch("/api/recent-paths", { credentials: "same-origin" })
4012
4171
  .then(function(res) { return res.json(); })
@@ -4364,7 +4523,7 @@
4364
4523
  welcomeInput.placeholder = "正在启动会话...";
4365
4524
  welcomeInput.disabled = true;
4366
4525
  var mode = state.chatMode || "full-access";
4367
- var defaultCwd = state.workingDir || (state.config && state.config.defaultCwd ? state.config.defaultCwd : "");
4526
+ var defaultCwd = getEffectiveCwd();
4368
4527
  var preferredTool = getPreferredTool();
4369
4528
  fetch("/api/commands", {
4370
4529
  method: "POST",
@@ -4426,7 +4585,7 @@
4426
4585
 
4427
4586
  // No selected session, create a new one
4428
4587
  var mode = state.chatMode || "full-access";
4429
- var defaultCwd = state.workingDir || (state.config && state.config.defaultCwd ? state.config.defaultCwd : "");
4588
+ var defaultCwd = getEffectiveCwd();
4430
4589
  var preferredTool = getPreferredTool();
4431
4590
  fetch("/api/commands", {
4432
4591
  method: "POST",
@@ -4493,9 +4652,8 @@
4493
4652
  // Init terminal if not already done
4494
4653
  if (!state.terminal) initTerminal();
4495
4654
  applyCurrentView();
4496
- if (state.currentView === "terminal") {
4497
- state.terminalViewportSize = { width: 0, height: 0 };
4498
- scheduleTerminalResize(true);
4655
+ if (state.terminal && state.fitAddon) {
4656
+ ensureTerminalFit();
4499
4657
  }
4500
4658
  // Don't call renderChat() here — loadOutput() always calls renderChat() after it resolves.
4501
4659
  // Calling renderChat() prematurely would render with stale/empty messages.
@@ -5380,7 +5538,7 @@
5380
5538
  welcomeInput.placeholder = "Claude 正在思考,请稍候...";
5381
5539
  welcomeInput.disabled = true;
5382
5540
  var mode = state.chatMode || "full-access";
5383
- var defaultCwd = state.workingDir || (state.config && state.config.defaultCwd ? state.config.defaultCwd : "");
5541
+ var defaultCwd = getEffectiveCwd();
5384
5542
  var preferredTool = getPreferredTool();
5385
5543
  fetch("/api/commands", {
5386
5544
  method: "POST",
@@ -5416,7 +5574,7 @@
5416
5574
 
5417
5575
  function createSessionFromInput(value, inputBox, welcomeInput) {
5418
5576
  var mode = state.chatMode || "full-access";
5419
- var defaultCwd = state.workingDir || (state.config && state.config.defaultCwd ? state.config.defaultCwd : "");
5577
+ var defaultCwd = getEffectiveCwd();
5420
5578
  var preferredTool = getPreferredTool();
5421
5579
  fetch("/api/commands", {
5422
5580
  method: "POST",
@@ -6463,6 +6621,34 @@
6463
6621
  updateTerminalJumpToBottomButton();
6464
6622
  }
6465
6623
 
6624
+ function ensureTerminalFit() {
6625
+ var maxAttempts = 10;
6626
+ var attempt = 0;
6627
+ function tryFit() {
6628
+ attempt++;
6629
+ state.terminalViewportSize = { width: 0, height: 0 };
6630
+ if (shouldResizeTerminalViewport() && state.fitAddon) {
6631
+ state.fitAddon.fit();
6632
+ maybeScrollTerminalToBottom("resize");
6633
+ if (state.selectedId && state.terminal) {
6634
+ var nextSize = { cols: state.terminal.cols, rows: state.terminal.rows };
6635
+ if (state.lastResize.cols !== nextSize.cols || state.lastResize.rows !== nextSize.rows) {
6636
+ state.lastResize = nextSize;
6637
+ fetch("/api/sessions/" + state.selectedId + "/resize", {
6638
+ method: "POST",
6639
+ headers: { "Content-Type": "application/json" },
6640
+ credentials: "same-origin",
6641
+ body: JSON.stringify(nextSize)
6642
+ }).catch(function() {});
6643
+ }
6644
+ }
6645
+ } else if (attempt < maxAttempts) {
6646
+ requestAnimationFrame(tryFit);
6647
+ }
6648
+ }
6649
+ requestAnimationFrame(tryFit);
6650
+ }
6651
+
6466
6652
  function scheduleTerminalResize(immediate) {
6467
6653
  if (state.resizeTimer) {
6468
6654
  clearTimeout(state.resizeTimer);
@@ -6588,7 +6774,7 @@
6588
6774
  }
6589
6775
  updateSessionSnapshot(snapshot);
6590
6776
  if (msg.sessionId === state.selectedId) {
6591
- if (msg.data.messages && msg.data.messages.length > 0) {
6777
+ if (msg.data.messages) {
6592
6778
  state.currentMessages = msg.data.messages;
6593
6779
  }
6594
6780
  updateTaskDisplay();
@@ -6630,6 +6816,33 @@
6630
6816
  }
6631
6817
  updateSessionSnapshot(endedSnapshot);
6632
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
+
6633
6846
  // Clear stale queued inputs so they cannot race with the ended session.
6634
6847
  // Each queued item's postInput will hit the server and get an error, but
6635
6848
  // clearing the queues here prevents them from growing unbounded.
@@ -6661,6 +6874,8 @@
6661
6874
  if (msg.sessionId === state.selectedId && msg.data) {
6662
6875
  if (chatRenderTimer) { clearTimeout(chatRenderTimer); chatRenderTimer = null; }
6663
6876
  updateTerminalOutput(msg.data.output || "", msg.sessionId, "replace");
6877
+ // Ensure terminal is properly fitted after receiving initial data
6878
+ scheduleTerminalResize(true);
6664
6879
  }
6665
6880
  break;
6666
6881
  case 'usage':
@@ -6685,6 +6900,35 @@
6685
6900
  target: msg.data.permissionRequest.target,
6686
6901
  reason: msg.data.permissionRequest.prompt
6687
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
+ }
6688
6932
  }
6689
6933
  if (msg.data.permissionBlocked === false) {
6690
6934
  statusUpdate.pendingEscalation = null;
@@ -6695,6 +6939,29 @@
6695
6939
  }
6696
6940
  }
6697
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;
6698
6965
  }
6699
6966
  }
6700
6967
 
@@ -6719,10 +6986,10 @@
6719
6986
  }
6720
6987
  }
6721
6988
  if (permissionActionsEl) permissionActionsEl.classList.remove("hidden");
6722
- // Also show in task bar
6723
- taskEl.textContent = pendingEscalation ? (pendingEscalation.reason || "等待 Claude 权限授权") : "等待 Claude 权限授权";
6724
- taskEl.classList.remove("hidden");
6725
- taskEl.classList.add("permission-blocked");
6989
+ // Hide top task bar — permission info is already shown in the composer
6990
+ taskEl.textContent = "";
6991
+ taskEl.classList.add("hidden");
6992
+ taskEl.classList.remove("permission-blocked");
6726
6993
  return;
6727
6994
  }
6728
6995
 
@@ -8505,6 +8772,136 @@
8505
8772
  }, type === "error" ? 4000 : 2200);
8506
8773
  }
8507
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
+
8508
8905
  function escapeHtml(value) {
8509
8906
  return String(value)
8510
8907
  .replace(/&/g, "&amp;")
@@ -1529,15 +1529,6 @@
1529
1529
  flex-shrink: 0;
1530
1530
  animation: task-pulse 1.2s ease-in-out infinite;
1531
1531
  }
1532
- .current-task.permission-blocked {
1533
- color: #d18b00;
1534
- background: rgba(240, 165, 0, 0.14);
1535
- border-color: rgba(240, 165, 0, 0.3);
1536
- }
1537
- .current-task.permission-blocked::before {
1538
- background: #f0a500;
1539
- animation: none;
1540
- }
1541
1532
  @keyframes task-pulse {
1542
1533
  0%, 100% { opacity: 1; transform: scale(1); }
1543
1534
  50% { opacity: 0.5; transform: scale(0.8); }
@@ -1559,8 +1550,8 @@
1559
1550
  padding: 10px;
1560
1551
  overflow: hidden;
1561
1552
  min-height: 0;
1562
- margin: 0 10px 10px;
1563
- border-radius: var(--radius-lg);
1553
+ margin: 0 6px 6px;
1554
+ border-radius: var(--radius-md);
1564
1555
  border: 1px solid rgba(140, 110, 85, 0.35);
1565
1556
  box-shadow:
1566
1557
  inset 0 1px 0 rgba(255, 255, 255, 0.06),
@@ -4861,7 +4852,7 @@
4861
4852
  width: min(300px, calc(100vw - 20px));
4862
4853
  top: 0;
4863
4854
  }
4864
- .terminal-container { margin: 0 12px 12px; min-height: 0; }
4855
+ .terminal-container { margin: 0 6px 6px; min-height: 0; }
4865
4856
  .btn { min-height: 40px; }
4866
4857
  .btn-sm { min-height: 36px; padding: 0 10px; font-size: 0.75rem; height: 36px; }
4867
4858
  .btn-icon { width: 36px; height: 36px; min-height: 36px; }
@@ -5839,6 +5830,109 @@
5839
5830
  to { opacity: 1; transform: translateX(-50%) translateY(0); }
5840
5831
  }
5841
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
+
5842
5936
  /* File Preview Modal */
5843
5937
  .file-preview-overlay {
5844
5938
  position: fixed;
@@ -5940,6 +6034,19 @@
5940
6034
  padding-top: 14px;
5941
6035
  }
5942
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
+
5943
6050
  .settings-update-actions {
5944
6051
  display: flex;
5945
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.5",
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",