@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.
- package/dist/claude-pty-bridge.js +2 -3
- package/dist/process-manager.d.ts +1 -1
- package/dist/process-manager.js +5 -8
- package/dist/server.js +15 -3
- package/dist/web-ui/content/scripts.js +454 -57
- package/dist/web-ui/content/styles.css +119 -12
- package/dist/ws-broadcast.d.ts +1 -1
- package/package.json +3 -2
|
@@ -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
|
|
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
|
}
|
package/dist/process-manager.js
CHANGED
|
@@ -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
|
-
|
|
1147
|
-
|
|
1148
|
-
deleted++;
|
|
1149
|
-
}
|
|
1145
|
+
unlinkSync(jsonlPath);
|
|
1146
|
+
deleted++;
|
|
1150
1147
|
}
|
|
1151
1148
|
catch {
|
|
1152
|
-
// Best-effort —
|
|
1149
|
+
// Best-effort — file may already be gone
|
|
1153
1150
|
}
|
|
1154
1151
|
}
|
|
1155
|
-
if (
|
|
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((
|
|
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
|
}
|
|
@@ -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 : (
|
|
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 : (
|
|
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(
|
|
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 : (
|
|
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 || (
|
|
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
|
|
1165
|
-
cwd =
|
|
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 =
|
|
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 =
|
|
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(
|
|
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 =
|
|
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 =
|
|
2448
|
+
folderPickerInput.value = getEffectiveCwd();
|
|
2401
2449
|
}
|
|
2402
2450
|
// Load initial folders
|
|
2403
|
-
var initialPath =
|
|
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
|
-
|
|
2506
|
-
|
|
2507
|
-
|
|
2508
|
-
|
|
2509
|
-
|
|
2510
|
-
|
|
2511
|
-
|
|
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
|
-
|
|
2531
|
-
|
|
2532
|
-
|
|
2533
|
-
|
|
2534
|
-
|
|
2535
|
-
|
|
2536
|
-
|
|
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
|
-
//
|
|
3045
|
+
// Retry-based fit: wait for browser to complete layout before measuring and fitting
|
|
2972
3046
|
if (state.fitAddon) {
|
|
2973
|
-
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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.
|
|
4497
|
-
|
|
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 =
|
|
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 =
|
|
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
|
|
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
|
-
//
|
|
6723
|
-
taskEl.textContent =
|
|
6724
|
-
taskEl.classList.
|
|
6725
|
-
taskEl.classList.
|
|
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, "&")
|
|
@@ -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
|
|
1563
|
-
border-radius: var(--radius-
|
|
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
|
|
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;
|
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",
|